文章目录
- 流程:
对密码进行加密
... = DigestUtils.md5DigestAsHex(password.getBytes());
对象拷贝
利用BeanUtils.copyProperties(... , ...)
stream流:
例如:
List<DishDto> dishDtoList = records.stream().map(item->{
DishDto dishDto = new DishDto();
// 2.2 拷贝
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category!=null){
//2.3 设置实体类DishDto的categoryName属性值
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
3.公共字段自动填充
Mybatis Plus提供的公共字段自动填充功能,也就是在插入或更新时给指定字段赋予指定的值
- 优点:统一处理这些字段,避免重复代码。
- 公共字段自动填充实现步骤:
- 3.1 在实体类的属性上添加@TableField注解,指定自动填充的策略
- 3.4 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需实现MetaObjectHandler接口;
- 元数据对象处理器类中怎样获取用户id? (此类中是不能获得HttpSession对象的)
- 使用ThreadLocal(JDK提供的一个类 )
- 获取登录用户id实现步骤:
- 3.2 编写BaseContext工具类,基于ThreadLocal封装的工具类;
- 3.3 在LoginCheckFilter中的doFilter方法中调用BaseContext来设置当前登录用户的id;
- 3.4 在MyMetaObjectHandler的方法中调用BaseContext来获取当前登录用户id
- 使用ThreadLocal(JDK提供的一个类 )
3.1实体类的属性上添加@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;
3.2 BaseContext工具类:
基于ThreadLocal封装的工具类
package com.lym.reggie.common;
//基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
public class BaseContext {
//用来存储用户id
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//设置值
public static void setCurrentId(Long id){
threadLocal.set(id);
}
// 获取值
public static Long getCurrentId(){
return threadLocal.get();
}
}
3.3 设置当前登录用户的id:
在LoginCheckFilter中的doFilter方法中调用BaseContext来设置当前登录用户的id
Long id = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(id);
3.4 元数据对象处理器:
- 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需实现MetaObjectHandler接口;
- 在方法中调用BaseContext来获取当前登录用户id
package com.lym.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
//调用BaseContext来获取当前登录用户id
Long UserId = BaseContext.getCurrentId();
log.info("UserId:{}",UserId);
metaObject.setValue("createUser",UserId);
metaObject.setValue("updateUser",UserId);
log.info("元数据 insertFill");
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
//调用BaseContext来获取当前登录用户id
Long UserId = BaseContext.getCurrentId();
log.info("UserId:{}",UserId);
metaObject.setValue("updateUser",UserId);
/* long threadId = Thread.currentThread().getId();
log.info("元数据 updateFill :ThreadLocal获得的id:{}",threadId);*/
}
}
4. 分页查询
4.1、一张表的分页查询
- 使用Mybatis-plus提供的分页插件,可简化分页查询代码量
1) 配置分页插件
config包下创建
配置MP的分页插件
package com.lym.reggie.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
2) …Controller中
使用Page 这个泛型(…mybatisplus.extension.plugins.pagination包下的)
- 1)构造分页构造器(Page)
… = new Page(页码,pageSize); - 2)构造条件构造器
2.1) LambdaQueryWrapper<实体>… = new LambdaQueryWrapper(); 或 用QueryWrapper
2.2) 再添加条件 - 3)执行查询
…Service调用page方法,不需返回,因为查询内部就会将封装给page对象
@RequestMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page:{},pageSize:{},name:{}",page,pageSize,name);
// 1.构造分页
Page<Employee> pageInfo = new Page<>(page,pageSize);
// 2.构造条件
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Employee::getName,name);
//添加一个排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 3.执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
4.2、涉及两张表的 分页查询
- 对象拷贝:利用
BeanUtils.copyProperties(... , ...)
- stream流:
records.stream().map(item->{。。。}.collect(Collectors.toList());
菜品信息分页查询:
//分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("name: {}",name);
// 1. Dish分页查询
//1.1 构造一个分页构造器对象
Page<Dish> dishPage = new Page<>(page,pageSize);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Dish::getName,name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(dishPage,queryWrapper);
Page<DishDto> dishDtoPage = new Page<>();
// 2. 对象拷贝:使用框架自带的工具类,第三个参数是不拷贝到属性 records属性是分页插件中表示分页中所有的数据的一个集合
BeanUtils.copyProperties(dishPage,dishDtoPage,"records");
// 2.1 处理 再进行拷贝
List<Dish> records = dishPage.getRecords();
List<DishDto> dishDtoList = records.stream().map(item->{
DishDto dishDto = new DishDto();
// 2.2 拷贝
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category!=null){
//2.3 设置实体类DishDto的categoryName属性值
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
// 3. 重新设置records属性值
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
6. 文件上传和下载
6.1 文件上传
- 将文件上传到服务器
- 文件上传代码逻辑:
- 0、修改yml配置文件:配置上传图片的存储位置;
- 1、生成一个新的 唯一的文件名 (利用UUID) 以防止文件名相同造成覆盖;
- 2、判断要接收文件的目录是否存在,若不存在则创建目录对象。
- 3、把前端传入的文件进行转存。
- 服务端要接收客户端页面上传的文件,通常需要使用Apache的两个组件:
- commons-fileupload
- commons-io
- Spring框架在spring-web包中对文件上传进行了封装,简化了服务端代码,我们只需要在
Controller的方法中声明MultipartFile类型的参数
就可以接收上传的文件。
- 文件上传概述:
6.2 文件下载
- 将文件从服务器传送到本地计算机。
- 步骤:
- 1。输入流。因为需要通过输入流读取服务器的文件。
- 2。设置写回去的文件类型。设置响应的数据格式;
- 3。输出流,需要通过输出流将文件写回浏览器;
- 4。定义缓存区,准备读写文件;
- 5。关流。
6.3 后端代码实现
- yml配置文件:配置上传图片的存储位置;
- 代码:上传、下载
- yml配置文件:配置上传图片的存储位置;
reggie:
path: /Users/liangyuanmeng/Desktop/JavaFile/Project/new/reggieImg/
- 代码:上传、下载
import com.lym.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
//文件上传. 将文件上传到服务器
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//1。生成一个新的 唯一的文件名 (利用UUID) 以防止文件名相同造成覆盖
//1.1 获取后缀,比如 .png .jpg
String originName = file.getOriginalFilename();//文件原始名
log.info("文件原始名:{}",originName);
String suffix = originName.substring(originName.lastIndexOf('.'));
//1.2 使用uuid生成的作为文件名的一部分,这样可以防止文件名相同造成的文件覆盖
String fileName = UUID.randomUUID().toString() + originName;
//2。判断要接收文件的目录是否存在,若不存在则创建目录对象。
File dir = new File(basePath);
if (!dir.exists()){
//文件目录不存在,直接创建一个目录
dir.mkdirs();
}
//3。把前端传入的文件进行转存。
try {
//把前端传过来的文件进行转存
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
//下载。 将文件从服务器传送到本地计算机
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//1。输入流,通过输入流读取服务器的文件。这里的name是前台用户需要下载的文件的文件名
FileInputStream inputStream = new FileInputStream(new File(basePath+name));
//2。设置写回去的文件类型。设置响应的数据格式
response.setContentType("image/jpeg");
//3。输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
//4。定义缓存区,准备读写文件
int len = 0;
byte[] buff = new byte[1024];
while ((len = inputStream.read(buff))!=-1){
outputStream.write(buff,0,len);
outputStream.flush();
}
//5。关流
outputStream.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Note:这里上传的文件的文件名要和这个地方的一样,接收文件的参数的名不能随便定义,要和下面的name的值一致;
1. 完善登录功能
token:
https://blog.csdn.net/tolode/article/details/103788487
- 如何实现?
使用过滤器或者是拦截器,在拦截器或者是过滤器中判断用户是否已经完成了登陆,如果没有登陆则跳转到登陆页面; - 方案:
- 使用过滤器
- 使用拦截器拦截器
- Spring Security权限控制 ?
1.1 利用过滤器
1.1.1 自定义过滤器 -->检查用户是否已完成登录
1)处理逻辑
过滤器的具体处理逻辑:
1.1 获取本次请求的URI request
1.2 定义不需处理的请求路径
登录、登出、页面
(只拦截除登录登出外的 Controller的请求)
1.3 判断本次请求是否需要处理
1.3.1 如果不需要处理,则直接放行
1.4 判断登陆状态
1.4.1 如果已登录,则直接放行
1.4.2 如果未登录则返回未登录结果
(如果方法没有返回值,则通过输出流给前端提供json数据)
2)过滤器代码
package com.lym.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.lym.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检查用户是否已经完成登录
* filterName过滤器名字
* urlPatterns拦截的请求,这里是拦截所有的请求
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 0 对请求和响应进行强转,我们需要的是带http的
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:{}",requestURI);
// 定义不需要处理的请求路径 登录、登出、页面(只拦截除登录登出外的 Controller的请求)(静态页面我们不需要拦截,因为此时的静态页面是没有数据的)
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//2.1 如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//3、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//4、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
//把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
1.1.2 启动类上加注解:@ServletComponentScan
开启组件扫描,这样才会去扫描@WebFilter注解,才能扫描到过滤器
2. 异常捕获
- 需要程序进行异常捕获时,有两种解决方案:
- Controller方法中加try catch进行异常捕获
- 使用异常处理器进行全局异常捕获
2.1 异常处理器方式
- 底层是基于代理
1)新增时
- 例子:employee表中对username字段加了唯一约束,在新增员工时,如果新增的账户已经存在,程序会抛出SQLIntegrityConstraintViolationException异常。这时我们就可以使用全局异常处理器来捕获,进行处理。
(common包下)
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})//表示拦截哪些类型的controller注解
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法,处理SQLIntegrityConstraintViolationException异常的方法
* @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("未知错误");
}
}
2)删除 (存在关联情况时)
- 可能需要删除的数据是与其他表关联的,所以删除之前要先判断该条数据是否与其他表中的数据关联;
- 例如:删除菜品分类时
- 需检查要删除的菜品分类是否关联了 菜品或套餐:
- 1)若未关联,则删除;
- 2)若关联了,则不删除
通过自定义异常实现:若关联则抛出异常,然后使用异常处理器进行全局异常捕获,然后把异常信息显示给前端人员。
- 实现步骤:
- 1)自定义一个异常类;
- 2)如果要删除的数据与其他表不关联,就直接删除。
- 3)如果存在关联,就抛出自定义的异常。
- 4)在
GlobalExceptionHandler
全局异常捕获器中添加这个自定义异常,统一进行异常处理,再响应给客户端。
- 需检查要删除的菜品分类是否关联了 菜品或套餐:
- 自定义异常;
package com.lym.my_reggie.common.exception;
public class CustomException extends RuntimeException {
/**
* 异常构造方法 在使用时方便传入错误码和信息
*/
public CustomException(String msg) {
super(msg);
}
}
- 3.实现类中 若无关联则删除,若关联则抛出自定义的异常;
/**
* 根据id删除 分类,删除之前需要进行判断是否有关联数据
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
int count = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经管理,直接抛出一个业务异常
if (count > 0){
//已经关联了菜品,抛出一个业务异常
throw new CustomException("当前分类项关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经管理,直接抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
int setmealCount = setmealService.count(setmealLambdaQueryWrapper);
if (setmealCount > 0){
//已经关联了套餐,抛出一个业务异常
throw new CustomException("当前分类项关联了套餐,不能删除");
}
//正常删除
super.removeById(id);
}
- 在
GlobalExceptionHandler
全局异常捕获器中添加这个自定义异常;当抛出异常时,在GlobalExceptionHandler
全局异常捕获器中捕获异常,统一进行异常处理,再响应给客户端。
//处理自定义的异常,为了让前端展示我们的异常信息,这里需要把异常进行全局捕获,然后返回给前端
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandle(CustomException ex){
log.error(ex.getMessage());
//这里拿到的message是业务类抛出的异常信息,我们把它显示到前端
return R.error(ex.getMessage());
}
怎样实现的统一捕获异常?异常捕获器 怎样实现的捕获到异常?❓❓
5. 前后端 传递id不匹配问题
基于Jackson进行Java对象到json数据的转换。
当id生成策略为ASSIGN_ID时(雪花算法),对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前端传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据;
- 解决方案:
- 5.1)使用自增ID的策略往数据库添加id
- 5.2)使用自定义消息转换器
将long型的数据统一转换为string字符串
5.2)使用自定义消息转换器
既然js对long型的数据会进行精度丢失,那么我们就对数据进行转型,我们可以在服务端(Java端)给页面响应json格式的数据时进行处理,将long型的数据统一转换为string字符串;
- 实现步骤:
- 1)创建对象(消息)转换器
JacksonObjectMapper
,基于Jackson进行Java对象到json数据的转换; - 2)在
WebMvcConfig
配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。
- 1)创建对象(消息)转换器
1)自定义消息(对象)转换类
package com.itheima.reggie.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)//将long型转为String字符串
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
2)在前面的webMvcConfig 配置类中扩展spring mvc 的消息转换器,在此消息转换器中使用spring提供的对象转换器进行Java对象到json数据的转换;
(底层本来就有转换器,但是不符合我们的需求,因此我们自己写一个转换器,但是要加入到他们的数组中才统一管理才可以生效)
(Controller方法结果转为json过程,会使用到对象转换器,底层就是使用Jackson )
- 重写
extendMessageConverters(...)
方法; - 方法中:
(1)创建一个消息转换器,并设置对象转换器为自定义的对象转换器;
(2)将此消息转换器追加到mvc框架的转换器集合中,并设置自定义的消息转换器为第一优先级。这样就会优先使用我们的转换器来进行相关数据进行转换,如果我们的转换器没有匹配到相应的数据来转换,那么就会去寻找第二个优先级的转换器,以此类推
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//log.info("扩展消息转换器...");
//创建消息转换器对象。 作用:将Controller方法的返回结果转为json,再通过输出流的方式响应给页面
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
//转换器是有优先级顺序的,这里我们把自己定义的消息转换器设置为第一优先级,所以会优先使用我们的转换器来进行相关数据进行转换,如果我们的转换器没有匹配到相应的数据来转换,那么就会去寻找第二个优先级的转换器,以此类推。所以这里第一个参数是0
converters.add(0,messageConverter);
}
7. 新增
7.1 新增时 出现异常时 ->详见2.
7.2 新增 涉及多张表时
-
例子:新增菜品,涉及两个表
- dish(菜品表):将新增页面录入的菜品信息插入到dish表;
- dish_flavor(菜品口味表):若添加了口味信息,将口味信息插入dish_flavor表。
-
解决步骤:
回显数据 及 图片上传下载此处省略- 1、创建一个
DishDto
用于封装页面提交的数据;(也可以使用map来接收) - 2、在DishService中新增一个方法:
saveWithFlavor(DishDto dishDto);
- 3、实现此方法 、
@Transactional
- 方法上加
@Transactional
(因为涉及对多张表的数据进行操作,需要加事务) - 实现步骤:
- 1)保存dish信息到dish表;
- 2)保存口味信息到dish_flavors表;
- 通过遍历将dishId设置进flavors;
- 把菜品口味的数据保存到dish_flavors口味表;
- 方法上加
- 4、在启动类开启事务: 加上
@EnableTransactionManagement
注解 - 5、写Controller层代码
- 1、创建一个
-
1、创建一个
DishDto
用于封装页面提交的数据;(也可以使用map来接收)
-
2、在DishService中新增一个方法:
//新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish dish_flavor
void saveWithFlavor(DishDto dishDto);
- 3、实现:
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品同时保存对应的口味数据
* @param dishDto
*/
@Override
@Transactional //涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {
//1。保存dish信息到dish表
this.save(dishDto);
Long dishId = dishDto.getId();
//2。保存口味信息到dish_flavors表
//2.1 通过遍历将dishId设置进flavors
List<DishFlavor> flavors = dishDto.getFlavors();
// stream流
flavors = flavors.stream().map(item->{
//拿到的这个item就是这个DishFlavor集合
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());//把返回的集合搜集起来,用来被接收
//把菜品口味的数据到口味表 dish_flavor 注意dish_flavor只是封装了name value 并没有封装dishId(从前端传过来的数据发现的,然而数据库又需要这个数据)
dishFlavorService.saveBatch(dishDto.getFlavors());//这个方法是批量保存
}
- 4、在启动类开启事务: 加上
@EnableTransactionManagement
注解 - 5、Controller中:
//新增菜品
@PostMapping
public R<String> addDish(@RequestBody DishDto dishDto){//前端提交的是json数据的话,我们在后端就要使用这个注解来接收参数,否则接收到的数据全是null
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
8. 下单
- AtomicInteger:原子整数类,保证线程安全。一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
- BigDecimal:BigDecimal类不能直接用