Spring 统一功能处理
文章目录
拦截器
- 可以统一拦截所有请求
拦截器快速入门
什么是拦截器?
- 拦截器是Spring框架中核心功能之一,主要用于拦截用户请求,在指定的方法前后,根据业务需要执行预先设定的代码
- 也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行。也可以在用户请求前阻止其执行
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息。如果有就可以放行,如果没有就进行拦截
拦截器使用
拦截器的使用步骤分为两步:
- 定义拦截器
- 注册配置拦截器
自定义拦截器:实现HandlerInterceptor
接口,并重写其所有方法
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行...");
return true;
}
@Override
public 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 {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");//设置拦截器拦截的请求路径(/** 表示拦截所有请求)
}
}
启动服务,试试访问任意请求,观察后端日志
拦截器详解
接下来我们介绍一下拦截器的使用细节,我们主要介绍两个部分:
- 拦截路径的配置
- 拦截器实现原理
拦截路径配置
拦截路径是指我们定义的这个拦截器,对哪些请求生效
我们在注册配置拦截器的时候,通过 addPathPatterns()
方法指定要拦截哪些请求。也可以通过 excludePathPatterns()
指定不拦截哪些请求
上述代码中,我们配置的是 / **
,表示拦截所有的请求
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public 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 |
拦截器执行流程
正常流程:
使用拦截器的流程:
- 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住,执行
preHandle()
方法,这个方法需要返回一个布尔类型的值。如果返回true
,就表示放行本次操作,继续访问controller中的方法。如果返回false
,则不会放行(controller中的方法也不会执行) - 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;
}
@Override
public 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中
- 设计模式的使用非常灵活,一个项目中通常会含有多种设计模式
统一数据返回格式
- 对后端数据进行封装,返回给前端
快速入门
- 实现ResponseBodyAdvice接口,重写supports 和 beforeBodyWriter 方法
- 使用@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 {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public 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();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public 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
方法,所以会执行子类的方法 - 然而子类
StringHttpMessageConverter
的addDefaultHeaders
方法定义接收参数为String
,此时为Result
类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"
的异常
优点
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的
- 有利于项目统一数据的维护和修改
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容
统一异常处理
- 使用@ControllerAdvice
- 使用@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 {
@ExceptionHandler
public 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 {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发生NullPointerException:" + e.getMessage());
}
@ExceptionHandler
public 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 {
//...
@Override
protected 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 方法,比如返回数据前调用统一数据封装。至于 DispatcherServlet
和 RequestMappingHandlerAdapter
是如何交互的这就是另一个复杂的话题了
initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet
的 initHandlerExceptionResolvers(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;
}
}
//...
}