点餐服务项目笔记

点餐服务项目笔记

前言

这是一份写给自己的笔记,主要记录瑞吉外卖项目中自己没有了解过的知识点。我将按照功能来分别解析

项目结构

除了基本的Controller,Service,Mapper,Config,Util,Entity层之外,还有我之前没了解过的common层和dto层

common层包含整个应用程序使用的公共辅助方法,和util层类似,但更有普适性,common层类中的方法对绝大多数功能都有辅助作用,如R类的作用是返回给前端通用格式的结果,能简化整个controller类的开发

dto层,DTO全称为Data Transfer Object,数据传输对象,起到数据封装与隔离的作用

common层工具

返回结果类R

此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import lombok.Data;
import java.util.HashMap;
import java.util.Map;

/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* @param <T>
*/
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据

public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
//A. 如果业务执行结果为成功, 构建R对象时, 只需要调用 success 方法; 如果需要返回数据传递 object 参数, 如果无需返回, 可以直接传递null。

//B. 如果业务执行结果为失败, 构建R对象时, 只需要调用error 方法, 传递错误提示信息即可。

业务逻辑

新增信息

以前的项目中,我在Controller方法中设置形参都是把一个个字段设置为形参,然后在DAO层分别写每个字段的添加方法,这样十分麻烦,我们可以把接收的形参设置为JSON实体类对象,再由mabatis-plus自动生成sql语句,就能省很多事了

1
public R<AddressBook> save(@RequestBody AddressBook addressBook) {}

菜品分页查询

在前端回显数据时,我们要回显菜品分类的名称,但是菜品名称不在Dish这张表中,因此Dish实体类中也没有菜品名称的字段,因此我们用DishDto来扩展Dish,来辅助后端给前端发数据

1
2
3
4
5
6
	@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName; //菜品分类名称
private Integer copies;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();

//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);

//执行分页查询
dishService.page(pageInfo,queryWrapper);//查询后的数据存在pageInfo里

//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");//records属性就是分页构造对象查询的分页数据,我们拷贝一份对象给新的分页构造器,但是我们要忽略records这个属性,因为我们要自己构建新的分页数据,也就是把菜品名称查出来并放到页面构造器中
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {//stream流遍历并构建新的list

DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);

if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;//每修改完一个新元素都要返回该元素,然后才能由collect()方法整合成新列表
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);//给新构造器设置我们自己修改过的分页数据

return R.success(dishDtoPage);
}

其他技术要点

前端知识

localStorage和SessionStorage的区别:

localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。

localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。

localStorage 属性是只读的。

如果你只想将数据保存在当前会话中,可以使用 sessionStorage 属性, 该数据对象临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。

JS对长整型数据进行处理时会损失精度

因此当我们数据库中id为长整型时,把id交给前端之前,先把id转成String类型,这样就不会损失精度了

java基础

Lambda表达式

Lambda表达式超详细总结_Code0cean的博客-CSDN博客_lambda表达式详细总结

Lambda表达式简化了函数式接口(只有一个抽象方法的接口)的实现操作,用Lambda表达式可以很快地创建函数式接口的实现对象。

当要传递给Lambda体的操作,已经有实现的方法了,就可以使用方法引用

1
2
3
Comparator<Integer> comparable=(x,y)->Integer.compare(x,y);

Comparator<Integer> integerComparable=Integer::compare;//使用方法引用实现相同效果

Stream 流

Java 8 Stream | 菜鸟教程 (runoob.com)

Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合_云深i不知处的博客-CSDN博客_java stream 分组聚合

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

全局异常处理

使用全局异常处理可以避免重复在每一个业务逻辑里面写try…catch来捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 全局异常处理
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody//用这个注解可以将返回值R对象以JSON格式响应给页面
@Slf4j
public class GlobalExceptionHandler {

/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)//声明拦截异常的类型
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}

自定义业务异常类

我们可以通过自定义业务异常类来抛出自定义的信息

1
2
3
4
5
6
7
8
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}

使用时代码如下

1
throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常

在全局异常处理器中捕获自定义异常

1
2
3
4
5
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}

Serliazeable

在Redis中存储对象,该对象是需要被序列化的,而对象要想被成功的序列化,就必须得实现 Serializable 接口.Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里(系列化),并且可以从其它地方把该Byte 流里的数据读出来(反序列化)

只要让类继承Serialzable接口就可以实现序列化

1
public class R<T> implements Serializable{

Spring 基础

接收前端发来的JSON数据

1
2
//接收前端发来的JSON数据,需要在相应形参前加@RequestBody注解,因为JSON数据在请求体中被发过来
public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){}

Springboot中 json序列化与反序列化

Springboot集成并封装了Jackson,使用Jackson来操作json,JSON的序列化与反序列化我们可以通过@Responsebody@RequestBody轻松实现

Lombok

Lombok是一个可以减少java模板代码的工具,我们可以用Lombok来简化Entity类的代码,省去了get,set及构造方法,接下来介绍lombok的常用注解

@Data

在Entity类中使用该注解,在项目编译时,会帮我们自动加上set,get以及toString方法

@Slf4j

在类上使用该注解,我们可以在类中的方法使用log函数来输出日志信息

MybatisPlus

条件构造器

顾名思义,作用就是封装查询条件,生成sql的where条件

在项目中,查询数据库用到了LambdaQuaryWrapper

1
2
3
4
//2、根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());//这里通过Lambda表达式来获取User实体类中username的字段名,省去了查数据库的步骤,这就是lambda条件构造器的优点
Employee emp = employeeService.getOne(queryWrapper);//相当于里面放了一个查询语句,通过查询语句获取结果

公共字段填充

数据库里经常需要填充一些公共字段,如用户id,更新时间等,我们可以把这些操作交给mybatis-plus自动完成

第一步去实体类给要自动填充的字段加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过@tablefield声明要自动填充的注解,并指定填充策略
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private Long updateUser;

第二步在common层添加自定义元数据对象处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {//实现插入和更新对应的方法
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());

metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}

/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());

long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);

metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}

但是在MyMetaObjectHandler中,我们不能直接获取HttpSession对象,也就不能直接获取session中的用户ID,此项目采用ThreadLocal解决

ThreadLocal

ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。

我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。 如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}

然后我们在filter中判断用户的登录情况,放行前给ThreadLocal赋值

1
2
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

然后我们就可以在各个地方获取ThreadLocal变量了

项目优化

SpringCache

Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口。

针对不同的缓存技术需要实现不同的CacheManager:

CacheManager 描述
EhCacheCacheManager 使用EhCache作为缓存技术
GuavaCacheManager 使用Google的GuavaCache作为缓存技术
RedisCacheManager 使用Redis作为缓存技术

注解

在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

注解 说明
@EnableCaching 开启缓存注解功能
@Cacheable 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

@CachePut注解

@CachePut 说明:

​ 作用: 将方法返回值,放入缓存

​ value: 缓存的名称, 每个缓存名称下面可以有很多key

​ key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut

当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* CachePut:将方法返回值放入缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CachePut(value = "userCache", key = "#user.id")
@PostMapping
public User save(User user){
userService.save(user);
return user;
}

key的写法如下:

​ #user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

​ #user.name: #user指的是方法形参的名称, name指的是user的name属性 ,也就是使用user的name属性作为key ;

@CacheEvict注解

@CacheEvict 说明:

​ 作用: 清理指定缓存

​ value: 缓存的名称,每个缓存名称下面可以有多个key

​ key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在 delete 方法上加注解@CacheEvict

当我们在删除数据库user表的数据的时候,我们需要删除缓存中对应的数据,此时就可以使用@CacheEvict注解, 具体的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* CacheEvict:清理指定缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CacheEvict(value = "userCache",key = "#p0") //#p0 代表第一个参数
//@CacheEvict(value = "userCache",key = "#root.args[0]") //#root.args[0] 代表第一个参数
//@CacheEvict(value = "userCache",key = "#id") //#id 代表变量名为id的参数
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}

在更新数据的时候也需要删除缓存,以免数据不同步

@Cacheable注解

@Cacheable 说明:

​ 作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

​ value: 缓存的名称,每个缓存名称下面可以有多个key

​ key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在getById上加注解@Cacheable

1
2
3
4
5
6
7
8
9
10
11
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@Cacheable(value = "userCache",key = "#id")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}

缓存非null值

在@Cacheable注解中,提供了两个属性分别为: condition, unless 。

condition : 表示满足什么条件, 再进行缓存 ;

unless : 表示满足条件则不缓存 ; 与上述的condition是反向的 ;

具体实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
* condition:条件,满足条件时才缓存数据
* unless:满足条件则不缓存
*/
@Cacheable(value = "userCache",key = "#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}

==注意: 此处,我们使用的时候只能够使用 unless, 因为在condition中,我们是无法获取到结果 #result的。==

MySQL主从数据库

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。 ——瑞吉外卖PPT

主从复制

以下内容摘自CSDN博客 要不一起ci个饭的博客-CSDN博客_主从复制

主从复制的定义

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库。在赋值过程中,一个服务器充当主服务器,而另外一台服务器充当从服务器。
当一台从服务器连接到主服务器时,从服务器会通知主服务器从服务器的日志文件中读取最后一次成功更新的位置。然后从服务器会接收从哪个时刻起发生的任何更新,然后锁住并等到主服务器通知新的更新

做主从复制的好处

做数据的热备
作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。

架构的扩展
业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的评率,提高单个机器的I/O性能。

读写分离(重点)
使数据库能支持更大的并发。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。

读写分离

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

ShardingJDBC介绍

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。

Sharding-JDBC具有以下几个特点:

1). 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。

2). 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。

3). 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。

Nginx,Swagger

重构项目的时候会详细学一下,目前看这些都是纸上谈兵。


本文作者: Xu Yuhuan
本文链接: https://xuyuhuan.com/article/c8278629/
版权声明: 转载本博客的文章请注明原始出处和作者,谢谢。