当前位置: 首页 > news >正文

Spring 统一功能处理

Spring 统一功能处理


文章目录

  • Spring 统一功能处理
  • 拦截器
    • 拦截器快速入门
    • 拦截器使用
    • 拦截器详解
      • 拦截路径配置
      • 拦截器执行流程
    • 适配器模式
      • 适配器的定义
      • 适配器模式角色
      • 适配器模式的实现
  • 统一数据返回格式
    • 快速入门
    • 优点
  • 统一异常处理
  • @ControllerAdvice 源码分析
    • initHandlerAdapters(context)
    • initHandlerExceptionResolvers(context)


拦截器

  • 可以统一拦截所有请求

拦截器快速入门

什么是拦截器?

  • 拦截器是Spring框架中核心功能之一,主要用于拦截用户请求,在指定的方法前后,根据业务需要执行预先设定的代码
  • 也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行。也可以在用户请求前阻止其执行

在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息。如果有就可以放行,如果没有就进行拦截

拦截器使用

拦截器的使用步骤分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor 目标方法执行前执行...");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("LoginInterceptor 目标方法执行后执行");}public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");}
}
  • preHandle()方法:目标方法执行前执行。返回true:继续执行后续操作;返回false:中断后续操作。
  • postHandle()方法:目标方法执行后执行
  • afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,暂不了解)

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径(/** 表示拦截所有请求)}
}

启动服务,试试访问任意请求,观察后端日志

拦截器详解

接下来我们介绍一下拦截器的使用细节,我们主要介绍两个部分:

  1. 拦截路径的配置
  2. 拦截器实现原理

拦截路径配置

拦截路径是指我们定义的这个拦截器,对哪些请求生效

我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求。也可以通过 excludePathPatterns() 指定不拦截哪些请求

上述代码中,我们配置的是 / **,表示拦截所有的请求

@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/**/*.html","/pic/**","/js/**","/css/**","/blog-editormd/**","/user/login");}
}

在拦截器中除了可以设置 / ** 拦截所有资源外,还有一些常见拦截路径设置:

拦截路径含义举例
/*一级路径能匹配 /user/book/login,不能匹配 /user/login
/**任意级路径能匹配 /user/user/login/user/reg
/book/*/book下的一级路径能匹配 /book/addBook,不能匹配 /book/addBook/1/book
/book/**/book下的任意级路径能匹配 /book/book/addBook/book/addBook/2,不能匹配 /user/login

拦截器执行流程

正常流程:
在这里插入图片描述
使用拦截器的流程:
在这里插入图片描述

  1. 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住,执行 preHandle() 方法,这个方法需要返回一个布尔类型的值。如果返回true,就表示放行本次操作,继续访问controller中的方法。如果返回false,则不会放行(controller中的方法也不会执行)
  2. controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据

适配器模式

适配器的定义

  • 适配器模式,也叫包装器模式,将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间
  • 简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用,把两个不兼容的接口通过一定的方式使之兼容

举个例子:(转接头)

在这里插入图片描述

适配器模式角色

  • Target: 目标接口 (可以是抽象类或接口),客户希望直接用的接口
  • Adaptee: 适配者,但是与Target不兼容
  • Adapter: 适配器类,此模式的核心.通过继承或者引用适配者的对象,把适配者转为目标接口
  • client: 需要使用适配器的对象

适配器模式的实现

场景:前面学习的slf4j就使用了适配器模式,slf4j提供了一系列打印日志的api,底层调用的是log4j或者logback来打日志,我们作为调用者,只需要调用slf4j的api就行了。

/*** slf4j接口*/
interface Slf4jApi{void log(String message);
}/*** log4j 接口*/
class Log4j{void log4jLog(String message){System.out.println("Log4j打印:"+message);}
}/*** slf4j和log4j适配器*/
class Slf4jLog4jAdapter implements Slf4jApi{private Log4j log4j;public Slf4jLog4jAdapter(Log4j log4j) {this.log4j = log4j;}@Overridepublic void log(String message) {log4j.log4jLog(message);}
}/*** 客户端调用*/
public class Slf4jDemo {public static void main(String[] args) {Slf4jApi slf4jApi = new Slf4jLog4jAdapter(new Log4j());slf4jApi.log("使用slf4j打印日志");}
}

可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行。

适配器模式的实现并不在slf4j-core中(只定义了Logger),具体实现是在针对log4j的桥接器项目slf4j-log4j12中

  • 设计模式的使用非常灵活,一个项目中通常会含有多种设计模式

统一数据返回格式

  • 对后端数据进行封装,返回给前端

快速入门

  1. 实现ResponseBodyAdvice接口,重写supports 和 beforeBodyWriter 方法
  2. 使用@ControllerAdvice注解
import com.example.demo.model.Result;
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;@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {return Result.success(body);}
}
  • supports方法:判断是否要执行beforeBodyWrite方法。true为执行,false不执行。通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理

returnType获取类名和方法名

//获取执行的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执行的方法
Method method = returnType.getMethod();
  • beforeBodyWrite方法:对response方法进行具体操作处理

注意⚠️:

  • 在使用 ResponseBodyAdvice 统一包装响应时,返回值为 String 类型时可能出现转换异常

解决方案:

import com.example.demo.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
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;@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {private static ObjectMapper mapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化if (body instanceof String) {return mapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}

原因分析:

  • SpringMVC 默认会注册一些自带的 HttpMessageConverter(从先后顺序排列分别为 ByteArrayHttpMessageConverter,StringHttpMessageConverter,SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter
  • 其中AllEncompassingFormHttpMessageConverter 会根据项目依赖情况 添加对应的 HttpMessageConverter
  • 在依赖中引入 jackson 包后,容器会把MappingJackson2HttpMessageConverter 自动注册到 messageConverters 链的末尾
  • Spring 会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter
  • 当返回的数据是非字符串时,使用的 MappingJackson2HttpMessageConverter 写入返回对象
  • 当返回的数据是字符串时,StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使用
  • ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中,调用父类的 write 方法
  • 由于 StringHttpMessageConverter 重写了 addDefaultHeaders 方法,所以会执行子类的方法
  • 然而子类 StringHttpMessageConverteraddDefaultHeaders 方法定义接收参数为 String,此时为 Result 类型,所以出现类型不匹配 "Result cannot be cast to java.lang.String" 的异常

优点

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的
  3. 有利于项目统一数据的维护和修改
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容

统一异常处理

  1. 使用@ControllerAdvice
  2. 使用@ExceptionHandler

注意⚠️:

  • 假如是返回数据,需要加上@ResponseBody注解

具体代码如下:

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}
}

以上代码表示,如果代码出现Exception异常(包括Exception的子类),就返回一个 Result的对象,Result对象的设置参考 Result.fail(e.getMessage())

public static Result fail(String msg) {Result result = new Result();result.setStatus(ResultStatus.FAIL);result.setErrorMessage(msg);result.setData("");return result;
}

我们可以针对不同的异常,返回不同的结果

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ResponseBody
@ControllerAdvice
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}@ExceptionHandlerpublic Object handler(NullPointerException e) {return Result.fail("发生NullPointerException:" + e.getMessage());}@ExceptionHandlerpublic Object handler(ArithmeticException e) {return Result.fail("发生ArithmeticException:" + e.getMessage());}
}

@ControllerAdvice 源码分析

统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的,通过分析 @ControllerAdvice 的源码,可以知道它们的执行流程

点击 @ControllerAdvice 实现源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {@AliasFor("basePackages")String[] value() default {};@AliasFor("value")String[] basePackages() default {};Class<?>[] basePackageClasses() default {};Class<?>[] assignableTypes() default {};Class<? extends Annotation>[] annotations() default {};
}

从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,这也就是为什么没有五大注解,ControllerAdvice 就生效的原因

下面我们看看 Spring 是怎么实现的,还是从 DispatcherServlet 的代码开始分析,DispatcherServlet 对象在创建时会初始化一系列的对象:

public class DispatcherServlet extends FrameworkServlet {//...@Overrideprotected void onRefresh(ApplicationContext context) {initStrategies(context);}/*** Initialize the strategy objects that this servlet uses.* <p>May be overridden in subclasses in order to initialize further strategy objects.*/protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}//...
}

对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个方法。

initHandlerAdapters(context)

initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的 bean 并保存起来,其中有一个类型为 RequestMappingHandlerAdapter 的 bean,这个 bean 就是 @RequestMapping 注解能起作用的关键,这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象,并做进一步处理,关键代码如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {//.../*** 添加ControllerAdvice bean的处理*/private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}//获取所有有被 @ControllerAdvice 注解标注的bean对象List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);}if (logger.isDebugEnabled()) {int modelSize = this.modelAttributeAdviceCache.size();int binderSize = this.initBinderAdviceCache.size();int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {logger.debug("ControllerAdvice beans: none");} else {logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize + " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");}}//...}
}

这个方法在执行时会查找使用所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发生某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装。至于 DispatcherServletRequestMappingHandlerAdapter 是如何交互的这就是另一个复杂的话题了

initHandlerExceptionResolvers(context)

接下来看 DispatcherServletinitHandlerExceptionResolvers(context) 方法,这个方法会取得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的 bean,这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步处理,代码如下:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {//...private void initExceptionHandlerAdviceCache() {if (getApplicationContext() == null) {return;}// 获取所有有被 @ControllerAdvice 注解标注的bean对象List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);if (resolver.hasExceptionMappings()) {this.exceptionHandlerAdviceCache.put(adviceBean, resolver);}if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {this.responseBodyAdvice.add(adviceBean);}}if (logger.isDebugEnabled()) {int handlerSize = this.exceptionHandlerAdviceCache.size();int adviceSize = this.responseBodyAdvice.size();if (handlerSize == 0 && adviceSize == 0) {logger.debug("ControllerAdvice beans: none");} else {logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");}}//...}
}

当 Controller 抛出异常时,DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常,而ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常,ExceptionHandlerMethodResolver 最终解析异常找到适用的 @ExceptionHandler 标注的方法是这里:

public class ExceptionHandlerMethodResolver {//...private Method getMappedMethod(Class<? extends Throwable> exceptionType) {List<Class<? extends Throwable>> matches = new ArrayList<>();//根据异常类型,查找匹配的异常处理方法://比如initExceptionHandlerAdviceCache会处理两个异常处理方法://handler(Exception e) 和 handler(NullPointerException e)for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {if (mappedException.isAssignableFrom(exceptionType)) {matches.add(mappedException);}}//如果是找到多个匹配,就进行排序,找到最使用的方法,排序的规则依据抛出异常相对于声明异常的深度//比如抛出的是NullPointerException(继承于RuntimeException),而RuntimeException又继承于Exception//相对于handler(Exception e) 声明的NullPointerException深度为0,//相对于handler(NullPointerException e)标注的方法会排在前面if (!matches.isEmpty()) {if (matches.size() > 1) {matches.sort(new ExceptionDepthComparator(exceptionType));}return this.mappedMethods.get(matches.get(0));} else {return NO_MATCHING_EXCEPTION_HANDLER_METHOD;}}//...
}

文章转载自:

http://LDB8Aryt.kfLdw.cn
http://nzlLbczh.kfLdw.cn
http://6FCKuQtS.kfLdw.cn
http://T0sHasg3.kfLdw.cn
http://zjJNLH4Z.kfLdw.cn
http://CDgc8qKs.kfLdw.cn
http://81DwBBaN.kfLdw.cn
http://n7agUjXI.kfLdw.cn
http://jsQJKP9A.kfLdw.cn
http://RZHxnvhY.kfLdw.cn
http://sc3iD1Gx.kfLdw.cn
http://CxrfIbYt.kfLdw.cn
http://e02dUrr6.kfLdw.cn
http://Wd9OK5YQ.kfLdw.cn
http://eDvAgkEV.kfLdw.cn
http://LR0YgIaj.kfLdw.cn
http://VKIuDhJV.kfLdw.cn
http://0JN4IAAp.kfLdw.cn
http://pYzzNJLb.kfLdw.cn
http://SZiX8B2V.kfLdw.cn
http://bckldoSo.kfLdw.cn
http://MlKq20av.kfLdw.cn
http://vYhGruIf.kfLdw.cn
http://3pZMgBOl.kfLdw.cn
http://6N9QowRc.kfLdw.cn
http://SCnmuxIG.kfLdw.cn
http://XzAgMTYA.kfLdw.cn
http://hwed5f9i.kfLdw.cn
http://xRYyk6SY.kfLdw.cn
http://dFxACLl2.kfLdw.cn
http://www.dtcms.com/a/377398.html

相关文章:

  • ES6基础入门教程(80问答)
  • 第3讲 机器学习入门指南
  • InnoDB 逻辑存储结构:好似 “小区管理” 得层级结构
  • copyparty 是一款使用单个 Python 文件实现的内网文件共享工具,具有跨平台、低资源占用等特点,适合需要本地化文件管理的场景
  • C# 哈希查找算法实操
  • 一个C#开发的Windows驱动程序管理工具!
  • 环境变量
  • Codeforces Round 1049 (Div. 2)
  • Eclipse下载安装图文教程(非常详细,适合新手)
  • vue2迁移到vite[保姆级教程]
  • 基于webpack的场景解决
  • Vite 中的 import.meta.env 与通用 process.env.NODE_ENV 的区别与最佳实践
  • 除了Webpack,还有哪些构建工具可以实现不同环境使用不同API地址?
  • sklearn聚类
  • I.MX6UL:汇编LED驱动实验
  • 计算机毕设 java 高校机房综合管控系统 基于 SSM+Vue 的高校机房管理平台 Java+MySQL 的设备与预约全流程系统
  • 设计模式-建造者观察者抽象工厂状态
  • 第5讲 机器学习生态构成
  • JAVA秋招面经
  • LVS群集
  • 半导体功率器件IGBT工艺全流程
  • Q3.1 PyQt 中的控件罗列
  • 深入解析ReentrantLock:可重入锁
  • ARM处理器总线架构解析:iCode、D-code与S-Bus
  • Qoder 前端UI/UE升级改造实践:从传统界面到现代化体验的华丽蜕变
  • Flutter多线程
  • 如何在高通跃龙QCS6490 Arm架构上使用Windows 11 IoT企业版?
  • JavaScript 对象说明
  • CMake目标依赖关系解析
  • 小型企业常用的元数据管理工具