瑞吉外卖项目总结
一.过滤器
1.作用
- 既可以对请求进行拦截,也可以对响应进行处理。
2.常见场景
- 权限检查,日记操作、拦截请求、过滤操作、对请求字符设置编码
3.项目中用到过滤器
3.1为什么要用过滤器
- 项目中,在地址栏输入url路径,可以直接跳过登录进入项目中,我们需要对其进行拦截
3.2具体实现
-
先创建一个过滤器
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { filterChain.doFilter(request,response); }
- @WebFilter(filterName = “loginCheckFilter”,urlPatterns = “/*”):这个注解标志这是一个过滤器,第一个参数是过滤器的名字,第二个参数是对哪些url进行过滤。
- filterChain.doFilter(request,response):放行。
-
完整功能
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") 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 { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //1.获取本次请求的URI String requestURI = request.getRequestURI(); log.info("拦截到请求:{}",requestURI); //定义不需要处理的请求路径 String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**", "/common/**", "/user/sendMsg", "/user/login", "/doc.html", "/webjars/**", "/swagger-resources", "/v2/api-docs" }; //2.判断本次请求是否需要处理 boolean check = check(urls, requestURI); //3.如果不需要处理,则直接放行 if(check){ log.info("本次请求{}不需要处理",requestURI); filterChain.doFilter(request,response); return; } //4-1.判断登录状态,如果已登录,则直接放行 if(request.getSession().getAttribute("employee")!=null){ log.info("用户已登录,用户ID为:{}",request.getSession().getAttribute("employee")); Long empId = (Long) request.getSession().getAttribute("employee"); BaseContext.setCurrentId(empId); filterChain.doFilter(request,response); return; } //4-2.判断移动端用户登录状态,如果已登录,则直接放行 if(request.getSession().getAttribute("user")!=null){ log.info("用户已登录,用户ID为:{}",request.getSession().getAttribute("user")); Long userId = (Long) request.getSession().getAttribute("user"); BaseContext.setCurrentId(userId); filterChain.doFilter(request,response); return; } log.info("用户未登录"); //5.如果未登录,则返回未登录结果,通过输出流的方式向客户端页面响应数据 response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return; } /** * 路径匹配,检查本次请求是否需要放行 * @param requestURI,urls * @return */ public boolean check(String[] urls,String requestURI){ for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if(match){ return true; } } return false; }
- AntPathMatcher:这是spring提供的api,其中的match方法可以进行路径匹配。
- Spring Boot 应用中这三个注解默认是不被扫描的@WebServlet、@WebFilter、@WebListener ,需要在项目启动类上添加 @ServletComponentScan 注解, 表示对 组件扫描。所有启动类下必须加该注解才能扫描过滤器
二.全局捕获异常
1 什么是全局异常处理器
软件开发springboot项目过程中,不可避免的需要处理各种异常,spring mvc架构中各层会出现大量的try{…} catch{…} finally{…}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。
Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAtribute等注解配套使用。不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是异常处理器的意思。
2 为什么需要全局异常
- 不用强制写try-catch,由全局异常处理器统一捕获处理。
- 自定义异常,只能用全局异常来捕获。不能直接返回给客户端,客户端是看不懂的,需要接入全局异常处理器
- JSR303规范的Validator参数校验器,参数校验不通过会抛异常,是无法使用try-catch语句直接捕获,只能使用全局异常处理器。
3.项目中的全局捕获异常
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@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("未知错误");
}
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
- @ControllerAdvice(annotations = {RestController.class, Controller.class}):annotations参数意思是这个通知要作用到哪个类上,这里用到了aop实现
- @ExceptionHandler(SQLIntegrityConstraintViolationException.class):当我们使用这个@ExceptionHandler注解时,我们需要定义一个异常的处理方法,比如上面的exceptionHandler()方法,给这个方法加上@ExceptionHandler注解,这个方法就会处理类中其他方法(被@RequestMapping注解)抛出的异常。@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常,此时注解的参数是SQLIntegrityConstraintViolationException.class,表示只有方法抛出SQLIntegrityConstraintViolationException时,才会调用该方法。
三.使用mybatis-plus进行分页
-
先配置mybatis-plus的分页插件
/** * 配置mybatisplus的分页插件 * @author chuwutao * @create 2023-03-24-11:06 */ @Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }
2.使用:
@GetMapping("/page") public R<Page> page(int page,int pageSize,String name){ //这里之所以是返回page对象(mybatis-plus的page对象),是因为前端需要这些分页的数据(比如当前页,总页数) //在编写前先测试一下前端传过来的分页数据有没有被我们接受到 //log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name); //构造分页构造器 就是page对象 Page pageInfo = new Page(page,pageSize); //构造条件构造器 就是动态的封装前端传过来的过滤条件 记得加泛型 LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper(); //根据条件查询 注意这里的条件是不为空 queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name); //添加一个排序条件 queryWrapper.orderByDesc(Employee::getUpdateTime); //执行查询 这里不用封装了mybatis-plus帮我们做好了 employeeService.page(pageInfo,queryWrapper); return R.success(pageInfo); }
四.使用自定义的消息转化器
- SpringMVC使用消息转换器实现请求报文和对象、对象和响应报文之间的自动转换
1.为什么使用自定义的消息转换器
数据丢失:mybatis-plus对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前度传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据
解决:既然js对long型的数据会进行精度丢失,那么我们就对数据进行转型,我们可以在服务端(Java端)给页面响应json格式的数据时进行处理,将long型的数据统一转换为string字符串;
2.如何实现
1.提供对象转换器JacksonObjectMapper,基于Jackson进行java对象到json数据的转换
-
自定义消息转换类
/** * 对象映射器:基于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) .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配置类中扩展SpringMvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换
/**
* 扩展MVC框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将java对象转换为json
messageConverter.setObjectMapper(new JacksonObjectMapper());//主要是这里
//将上面的消息转换器对象追加到MVC框架的转换器集合中
converters.add(0,messageConverter);
}
五.公共字段填充
- 在员工管理功能 开发的时候,新增员工需要设置创建时间,创建人,修改时间,修改人等公共字段,在编辑员工时也需要操作这些字段。能不能对这些公共字段在某个地方进行公共处理。这就用到了mybatis-plus的公共字段填充功能。
1.实现步骤
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;
2.按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler 接口
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());
}
六.文件上传和下载
1.文件上传
-
是指将本地的文件上传到服务器上,可以供其他用户浏览或下载。
2.文件下载
-
是指将文件从服务器传输到本地计算机的过程
-
通过浏览器进行文件下载,通常有两种表现方式:
1.以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
2.直接在浏览器中打开显示
3.后端代码实现
/**
* 文件上传和下载
* @author chuwutao
* @create 2023-03-29-15:00
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));//.jpg
//使用UUID重新生成文件名,放置文件名重复造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix;//dsadasd.jpg
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}
try {
//将临时文件转成到指定位置
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
//通过输入流读取文件内容
try {
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//通过输出流将文件写回浏览器,在浏览器展示图片了
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
int len=0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) !=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
yml配置文件:配置上传图片的存储位置;
reggie:
path: E:\img\
int len=0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) !=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
~~~
yml配置文件:配置上传图片的存储位置;
```yml
reggie:
path: E:\img\