【Spring】统一功能处置惩罚
https://i-blog.csdnimg.cn/direct/200824000cc84a4e84aa083dd7b6f831.png目录
前言
拦截器
什么是拦截器?
拦截器的利用
自定义拦截器
注册并配置拦截器
拦截器详解
拦截路径
拦截器实行流程
适配器模式
统一数据返回格式
优点
统一异常处置惩罚
前言
在前面中,我们已经学习了spring中的一些常用操纵,那么本篇,我们就继续往下深入学习。
我们在做一些小项目的时间,假如我们想要判定用户是否已经登录,按照我们前面的学习,我们就需要用到seesion来举行判定。设想一下,我们有几个界面,而这几个界面都需要在用户登录后才能举行检察的,就想淘宝,如果我们未登录,那么他就跳转到登录界面。
https://i-blog.csdnimg.cn/direct/85fd84d37bc64da9bab7ac820b59d0f5.png
对于这样的操纵,我们的界面不止一个,那么我们对应的在每个页面调用后端的API,此中的方法每次都需要判定用户是否登录,这样会让代码冗余,以是,在Spring中,给我们提供了一种功能,能够让我们将这些重复的代码举行抽取——拦截器。
拦截器
什么是拦截器?
拦截器(Interceptor)是一种在哀求处置惩罚流程中,对哀求和响应举行拦截和预处置惩罚的机制。它允许开发者在哀求到达目标处置惩罚器(如控制器方法)之前或之后实行统一的逻辑,从而实现诸如权限校验、日记纪录、性能监控、哀求过滤等功能。
https://i-blog.csdnimg.cn/direct/59e52a80a5a64d1586085dd8e42a35f8.png
拦截器的利用
拦截器的利用步骤分为两步:
[*]定义拦截器
[*]注册并配置拦截器
自定义拦截器
在Spring MVC框架中,拦截器通过实现 HandlerInterceptor 接口来定义拦截逻辑。
package com.example.demo.interceptor;
import com.example.demo.Result.Results;
import com.example.demo.constant.Constants;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 登录拦截器,用于在请求处理前验证用户是否已登录
*/
@Slf4j
@Component
public class LoginInterceptorimplements HandlerInterceptor {
@Autowired
private ObjectMapper objectMapper;
/**
* 在请求处理之前进行拦截
*
* @param requestHttpServletRequest对象,用于获取请求信息
* @param response HttpServletResponse对象,用于设置响应信息
* @param handler请求处理器,可以是HandlerMethod或RequestMappingHandler等
* @return boolean 表示是否继续执行其他拦截器和目标方法。返回true表示继续执行,返回false表示中断执行。
* @throws Exception 抛出异常表示拦截器处理出现错误
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("方法执行前");
//进行登录校验
HttpSession session= request.getSession(false);
if(session!=null&&"true".equals(session.getAttribute(Constants.USER_SESSION_KEY))){
// 用户已登录,继续执行请求
return true;
}
// 用户未登录,返回未授权错误信息
Results results=Results.unLogin();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getOutputStream().write(objectMapper.writeValueAsString(results).getBytes());
response.setContentType("application/json;charset=UTF-8");
response.getOutputStream().close();
return false;
}
/**
* 在请求处理之后,视图渲染之前进行拦截
*
* @param requestHttpServletRequest对象,用于获取请求信息
* @param response HttpServletResponse对象,用于设置响应信息
* @param handler请求处理器,可以是HandlerMethod或RequestMappingHandler等
* @param modelAndView ModelAndView对象,用于添加模型数据或修改视图
* @throws Exception 抛出异常表示拦截器处理出现错误
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("方法执行后");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 在请求完成之后执行的方法
*
* @param request 传入的HTTP请求对象,包含请求相关数据
* @param response 传入的HTTP响应对象,包含响应相关数据
* @param handler 处理请求的处理器对象,可以是任何类型的对象
* @param ex 请求处理过程中发生的异常,如果没有异常,则为null
* @throws Exception 根据具体实现可能会抛出的异常
*
* 此方法主要用于在请求处理完成后进行一些清理工作,例如关闭数据库连接、释放资源等
* 它是在请求处理的最后一步调用的,确保了所有处理逻辑已经执行完毕
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
[*]preHandler():该方法是在目标方法实行前实行的。若返回true,则继续实行后续的业务逻辑,返回false,则中断后续的操纵。
[*]postHandler():该方法是在目标方法实行后再实行的。
[*]afterCompletion():该方法是在视图渲染之后实行的,在postHandler()方法之后实行,但由于现在前后端分离,以是后端根本上接触不到视图的渲染,这个方法用的少。
注册并配置拦截器
注册配置拦截器我们需要实现 WebMvcConfiguer 接口,并实现此中的 addInterceptors 方法。
package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置类用于配置Web相关的设置
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 登录拦截器,用于拦截请求以进行登录验证
*/
@Autowired
private LoginInterceptor loginInterceptor;
/**
* 添加拦截器以配置请求的预处理和后处理
*
* @param registry 拦截器注册对象,用于注册自定义拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
registry.addInterceptor(loginInterceptor)
//拦截以"/book/"开头的所有请求
.addPathPatterns("/book/**")
//排除"/user/login"请求,使其不被拦截
.excludePathPatterns("/user/login");
}
}
假如此时我们未登录,调用一下查询功能:
https://i-blog.csdnimg.cn/direct/9b830aa5c8f54fc7bd97ef2cde907687.png
可以看到,会对哀求举行拦截。
知道了拦截器的是怎样利用的,那么就来进一步了解拦截器。
拦截器详解
拦截器的利用细节我们讲以下两个部门:
[*]拦截路径配置
[*]拦截器实现原理
拦截路径
拦截路径指的是我们定义的拦截器对哪些哀求生效,我们在注册配置拦截器的时间,通过 addPathPatterns() 方法就可以来指定要拦截哪些哀求,也可以通过 excludePathPatterns() 方法来指定哪些哀求不需要拦截。
关于拦截路径设置的规则,有以下几种:
拦截路径含义举例 /*
一级路径 能匹配/user,/book,但不能匹配/book/getList等
/**恣意级路径能匹配/user,/user/login,即恣意路径都可以匹配/book/*/book下的一级路径能匹配/book/addBook,不能匹配/book/addBook/get,/book/book/**/book下的恣意级路径能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login 上面的这些拦截规则可以拦截项目中的URL,包括静态文件(如图片文件、JS和CSS等文件).
如果我们利用下面这种拦截规则,就会将前端界面的哀求也给拦截住。
https://i-blog.csdnimg.cn/direct/1c04a232f5eb45328b9d6234bfd4d223.png
https://i-blog.csdnimg.cn/direct/b992f3e025f449d39aa2750593751052.png 我们可以通过设置前端界面不拦截:
package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 配置类用于配置Web相关的设置
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 登录拦截器,用于拦截请求以进行登录验证
*/
@Autowired
private LoginInterceptor loginInterceptor;
public List<String> excludePath= Arrays.asList("/user/*",
"/css/**",
"/js/**",
"/img/**",
"/**/*.html");//放行路径
/**
* 添加拦截器以配置请求的预处理和后处理
*
* @param registry 拦截器注册对象,用于注册自定义拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
registry.addInterceptor(loginInterceptor)
//拦截以"/book/"开头的所有请求
.addPathPatterns("/**")
//排除"/user/login"请求,使其不被拦截
.excludePathPatterns(excludePath);
}
} https://i-blog.csdnimg.cn/direct/84d39478e6aa405d9857c775be6f3e51.png
这样,就可以获取到前端界面。
拦截器实行流程
在没有添加拦截器之前,我们调用顺序是:
https://i-blog.csdnimg.cn/direct/3c3e289f3e0a40ed9d8264aa2c69a449.png
在添加拦截器后:https://i-blog.csdnimg.cn/direct/d415be7c2ae946ff9e26327cadf9b93d.png
[*]在添加完拦截器后,实行Controller中的方法之前,哀求会先被拦截器拦截住,实行preHandler() 方法,这个方法会返回一个布尔范例的值。如果返回true,说明要放行,继续访问controller中的方法;如果返回false,则不会放行(controller中的方法不会实行).
[*]controller中的方法实行完后,会继续实行 postHandler() 方法以及 afterCompletion() 方法,实行完毕后,终极给浏览器响应数据。
我们通过观察日记,可以看到当Tomcat在启动之后,会有核心的类 DispatcherServlet 来控制程序的实行顺序。
https://i-blog.csdnimg.cn/direct/7a4263e8acb549df9008e35104712249.png
全部的哀求都会先进入到DispatcherServlet 中,实行 doDispatch() 调度方法,如果有拦截器,就会先实行拦截器中 preHandle() 方法中的代码,如果 preHandle() 返回true,那么就会继续访问controller中的方法,当controller中的方法实行完毕,就会再回过来实行 postHandle() 和 afterCpmpletion() 方法,返回给DispatcherServlet,终极给浏览器响应数据。
https://i-blog.csdnimg.cn/direct/bbe42d36ead746d6a804dc748d826750.png
我们观察 DispatcherServlet 中的源码,就可以看到这三个方法的实行流程:
https://i-blog.csdnimg.cn/direct/9d99ba47b76d423eb23e44ed7909a1f8.png
在 DispatcherServlet 源码中,我还可以看到利用了适配器模式:
适配器模式
https://i-blog.csdnimg.cn/direct/bc2656616d3f4e659798392c6e105350.png
适配器模式也叫包装器模式。将一个类的接口,转换成客户期望的另有一个接口,适配器让本来接口不兼容的类可以合作无间。
简单来说:就是目标类不能直接利用,通过一个新类举行包装,适配调用方利用,把两个不兼容的接口通过一定的方式使之兼容。
https://i-blog.csdnimg.cn/direct/a72389fa257f4e6bb1d7ca5f62b9cb97.png
利用适配器来使两个接口兼容:
https://i-blog.csdnimg.cn/direct/e4ed488446454f499c16b8b3cf5027d7.png
实在在我们前面学习Spring日记的时间,此中的 slf4j 就利用到了适配器模式,我们想要打印日记的时间,只需要调用slf4j的API,而它会自动去调用底层的 log4j 大概 logback 来打印日记。
示例:
package com.example.demo.config;
/**
* Slf4j接口定义了打印日志的方法
*/
public interface Slf4j {
/**
* 打印日志信息
* @param message 需要打印的日志信息
*/
void print(String message);
}
/**
* Log4j类提供了具体的日志打印实现
*/
class Log4j{
/**
* 打印日志信息
* @param message 需要打印的日志信息
*/
void log4jPrint(String message){
System.out.println("Log4j: "+message);
}
}
/**
* Slf4jAdapter类是Slf4j接口与Log4j类之间的适配器
* 它使得Log4j可以通过Slf4j接口来打印日志
*/
class Slf4jAdapterimplements Slf4j{
/**
* log4j实例用于实际的日志打印
*/
private Log4j log4j;
/**
* 构造函数,接收一个Log4j实例
* @param log4j Log4j实例,用于实际的日志打印
*/
public Slf4jAdapter(Log4j log4j) {
this.log4j = log4j;
}
/**
* 实现Slf4j接口的print方法,通过Log4j实例来打印日志
* @param message 需要打印的日志信息
*/
@Override
public void print(String message) {
log4j.log4jPrint(message);
}
}
/**
* Demo类包含主程序,用于演示Slf4jAdapter的使用
*/
class Demo{
/**
* 主函数,创建Log4j实例并通过Slf4jAdapter适配,然后打印日志信息
* @param args 命令行参数
*/
public static void main(String[] args) {
Log4j log4j = new Log4j();
Slf4j slf4j = new Slf4jAdapter(log4j);
slf4j.print("Hello World");
}
}
可以看到,我们不需要修改log4j的api,只需要利用适配器,就可以更换日记框架,维护系统。
那么为什么不直接调用Log4j呢?
适配器模式实在可以看做一种“赔偿模式”,用来调停计划上的缺陷,利用这种模式是无奈之举。如果在计划初期,能够规避接口不兼容的问题,那么就不需要利用适配器模式了。
统一数据返回格式
我们在做项目的时间,如果前端调用后端返回的数据格式都差别,后序如果修改起来,就显得有点杂乱,以是我们可以对返回的数据格式举行统一——统一数据返回格式。
在SpringBoot,如果我们想要在返回数据响应之前对数据举行一些逻辑操纵,那么我们就需要利用到注解 @ControllerAdvice 和 ResponseBodyAdvice 接口的实现。
定义下存常量的类:
package com.example.demo.constant;
public class Constants {
public static final String USER_SESSION_KEY = "user";
public static final Integer SUCCESS_CODE = 200;//成功
public static final Integer FAIL_CODE = -2;//失败
public static final Integer UNLOGIN_CODE = -1;//未登录
}
统一返回数据格式:
package com.example.demo.Result;
import com.example.demo.constant.Constants;
import lombok.Data;
@Data
public class Results<T> {
private Integer code;//200成功,-1未登录,-2程序异常
private String msg;
privateT data;
public static <T> Results success(T data){
Results results=new Results();
results.setCode(Constants.SUCCESS_CODE);
results.setMsg("");
results.setData(data);
return results;
}
public staticResults unLogin(){
Results results=new Results();
results.setCode(Constants.UNLOGIN_CODE);
results.setMsg("用户未登录");
return results;
}
public static <T> Results fail(String msg){
Results results=new Results();
results.setCode(Constants.FAIL_CODE);
results.setMsg(msg);
return results;
}
public static <T> Results fail(){
Results results=new Results();
results.setCode(Constants.FAIL_CODE);
results.setMsg("程序出现异常");
return results;
}
}
响应处置惩罚:
package com.example.demo.config;
import com.example.demo.Result.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 全局响应处理,统一处理所有响应
* 该类实现了ResponseBodyAdvice接口,用于自定义响应体
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 判断是否支持当前的返回类型和转换器类型
* 该方法始终返回true,表示支持所有类型的响应处理
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 在写入响应体前处理数据
* 该方法根据返回的数据类型,进行相应的处理和封装
* 如果返回类型是String,则将其作为成功消息封装进Results对象
* 如果返回类型已经是Results,则直接返回
* 否则,将返回值作为成功数据封装进Results对象
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof String){
returnobjectMapper.writeValueAsString(Results.success(body));
}
if(body instanceof Results){
return body;
}
return Results.success(body);
}
}
调用一下看看:
https://i-blog.csdnimg.cn/direct/662529c0662547e5962a81099a5f9832.png
https://i-blog.csdnimg.cn/direct/910fe9e459c74c5e955fc5d7b957a2e2.png
可以看到,这样的话,如果前端想要获取到我们后端的数据,以及后续修改操纵等,就更加清晰容易操纵了。
我们再来看一处地方:
https://i-blog.csdnimg.cn/direct/97bf497e98694e68b2f35d91fcea582b.png
这里为什么要这样写呢?
SpringMVC默认会注册一些自导的HttpMessageConverter(从先后顺序排序分别为ByteArrayHttpMessageConverter、StringHttpMessageConverter、SourceHttpMessageConverte、SourceHttpMessageConverterr、AllEncompassingFormHttpMessageConverter)
https://i-blog.csdnimg.cn/direct/ae32efaa24794243852b1d5a973ddaee.png
而此中的 AllEncompassingFormHttpMessageConverter 会根据项目依靠环境添加对应的HttpMessageConverter。
https://i-blog.csdnimg.cn/direct/5fd7bd25232e4613995798fda575b0a9.png
在依靠中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到
messageConverters 链的末尾。Spring会根据返回的数据范例,从 messageConverters 链选择合适的 HttpMessageConverter。当返回的数据是⾮字符串时,使⽤的MappingJackson2HttpMessageConverter 写⼊返回对象。当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会以为
StringHttpMessageConverter 可以利用。
https://i-blog.csdnimg.cn/direct/35b8f6070e514cc49418de88b80dd59e.pnghttps://i-blog.csdnimg.cn/direct/c8f307c6127b4c4bb55fa81f4e676729.png
可以看到子类StringHttpMessageConverter中的addDefaultHeaders()方法接收的参数为String,但我们需要返回的范例为Results范例,以是这里我们就需要利用SpringBoot内置提供的Jackson来实现信息的序列化。即:
https://i-blog.csdnimg.cn/direct/c7377d7153794cbe848aa5aca0411f99.png
[*]@SneakyThrows是 Lombok 提供的一个注解,用于简化 Java 中的异常处置惩罚。它允许开发者在方法中抛出受检异常(checked exceptions),而无需在方法签名中显式声明 throws,也不需要利用 try-catch块。
优点
[*]⽅便前端程序员更好的接收息争析后端数据接口返回的数据
[*]低沉前端程序员和后端程序员的沟通本钱,按照某个格式实现就可以了,由于全部接口都是这样返回的
[*]有利于项目统⼀数据的维护和修改
[*]有利于后端技术部⻔的统⼀规范的标准制定,不会出现特别古怪的返回内容
统一异常处置惩罚
当我们的程序出现异常时,我们也可以对这些异常举行统一处置惩罚。
统一异常处置惩罚利用的是 @ControllerAdvice 和 @ExceptionHandler 来实现的。
[*]@ControllerAdvice 表⽰控制器关照类
[*]@ExceptionHandler 是异常处置惩罚器,两个联合表示当出现异常的时间实行某个关照,也就是执⾏某个⽅法变乱
package com.example.demo.config;
import cn.hutool.core.io.resource.NoResourceException;
import com.example.demo.Result.Results;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
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.ResponseStatus;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/**
* 全局异常处理类
* 用于统一处理项目中的异常,提高代码的健壮性和用户体验
*/
@ControllerAdvice
@Slf4j
@ResponseBody
public class ErrorAdviceHandler {
/**
* 处理通用异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(Exception e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理数组越界异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(ArrayIndexOutOfBoundsException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理空指针异常
* @param e 异常对象
* @return 返回处理结果
*/
@ResponseStatus
@ExceptionHandler
public Object handler(NullPointerException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理算数异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(ArithmeticException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理资源未找到异常
* @param e 异常对象
* @return 返回处理结果
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public Object handler(NoResourceFoundException e){
log.error("异常信息:{}path:{}",e.getDetailMessageCode(),e.getResourcePath());
return Results.fail(e.getMessage());
}
}
测试案例:
package com.example.demo.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Demo控制器类,用于处理演示项目的HTTP请求
*/
@RestController
@RequestMapping("/demo")
public class DemoController {
/**
* 处理/t1请求的方法
* 该方法演示了未处理的除零异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发除零异常
*/
@RequestMapping("/t1")
public String t1(){
// 该行代码将产生除零异常
int a=1/0;
return "success";
}
/**
* 处理/t2请求的方法
* 该方法演示了未处理的空指针异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发空指针异常
*/
@RequestMapping("/t2")
public String t2(){
// 该行代码将产生空指针异常
String str=null;
str.length();
return "success";
}
/**
* 处理/t3请求的方法
* 该方法演示了未处理的数组越界异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发数组越界异常
*/
@RequestMapping("/t3")
public String t3(){
// 初始化一个包含三个元素的数组
int[] arr={1,2,3};
// 该行代码将产生数组越界异常
arr=4;
return "success";
}
}
https://i-blog.csdnimg.cn/direct/0184f0ac48504fb188b649ccb34a7fab.png
此外,如果发生空指针异常大概其他异常,异常处置惩罚中有Exception和NullPointerException的话,优先实行具体的异常,即实行NullPointerException异常。
具体异常优先于通用异常
https://i-blog.csdnimg.cn/direct/1a5fa8546f094609a75148e3dc6212fe.gif
以上就是本篇全部内容~
如有不敷,欢迎指正~
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]