掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】(上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,让你的选择不仅仅只有拦截器【享学Spring MVC】(上)

前言


我们在实际的项目开发中,肯定会有这样的需求:请求时记录请求日志,返回时记录返回日志;对所有的入参解密,对所有的返回值加密…。这些都是与业务没关系的花边但又不可缺少的功能,若你全都写在Controller的方法内部,那将造成大量的代码重复且严重干扰了业务代码的可读性。

怎么破?可能你第一反应想到的是使用Spring MVC的HandlerInterceptor拦截器来做,没毛病,相信大部分公司的同学也都是这么来干的。那么本文就介绍一种更为优雅、更为简便的实现方案:使用@ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice不仅仅只有拦截器一种。


@ControllerAdvice / @RestControllerAdvice

对于这个注解你可能即熟悉,却又觉得陌生。熟悉是因为你看到很多项目都使用了@ControllerAdvice + @ExceptionHandler来实现全局异常捕获;陌生在于你除了copy代码时看到过外,自己似乎从来没有真正使用过它。

在前面关于@ModelAttribute和@InitBinder 的相关文章中其实和这个注解是打过照面的:在此注解标注的类上使用@InitBinder等注解可以使得它对"全局"生效实现统一的控制。本文将把@ControllerAdvice此注解作为重点进一步的去了解它的使用以及工作机制。


此类的命名是很有信息量的:Controller的Advice通知。关于Advice的含义,熟悉AOP相关概念的同学就不会陌生了,因此可以看到它整体上还是个AOP的设计思想,只是实现方式不太一样而已。


@ControllerAdvice使用AOP思想可以这么理解:此注解对目标Controller的通知是个环绕通知,织入的方式是注解方式,增强器是注解标注的方法。如此就很好理解@ControllerAdvice搭配@InitBinder/@ModelAttribute/@ExceptionHandler起到的效果喽~


使用示例


最简单的示例前文有过,这里摘抄出一小段:


@RestControllerAdvice
public class MyControllerAdvice {
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        //binder.setDisallowedFields("name");
        binder.registerCustomEditor(String.class, new StringTrimmerEditor());
    }
}

这样我们的@InitBinder标注的方法对所有的Controller都是生效的。(@InitBinder写在Controller内部只对当前处理器生效)


原理分析


接下来就看看这个注解到底是怎么work的,做到知其然,知其所以然。

// @since 3.2
@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 {};
}


官方doc说它可以和如上我指出的三个注解的一起使用。关于它的使用我总结有如下注意事项:


  • @ControllerAdvice只需要标注上即可,Spring MVC会在容器里自动探测到它(请确保能被扫描到,否则无效哦~)
  • 若有多个@ControllerAdvice可以使用@Order或者Ordered接口来控制顺序
  • basePackageClasses属性最终也是转换为了basePackages拿去匹配的,相关代码如下:


HandlerTypePredicate:
    // 这是packages属性本文:有一个判空的过滤器
    public Builder basePackage(String... packages) {
      Arrays.stream(packages).filter(StringUtils::hasText).forEach(this::addBasePackage);
      return this;
    }
    // packageClasses最终都是转换为了addBasePackage
    // 只是它的pachage值是:ClassUtils.getPackageName(clazz)
    // 说明:ClassUtils.getPackageName(String.class) --> java.lang
    public Builder basePackageClass(Class<?>... packageClasses) {
      Arrays.stream(packageClasses).forEach(clazz -> addBasePackage(ClassUtils.getPackageName(clazz)));
      return this;
    }
    private void addBasePackage(String basePackage) {
      this.basePackages.add(basePackage.endsWith(".") ? basePackage : basePackage + ".");
    }


  • 它的basePackages扫包不支持占位符Ant形式的匹配。对于其他几个属性的匹配可参照下面这段匹配代码(我配上了文字说明):


HandlerTypePredicate:
  @Override
  public boolean test(Class<?> controllerType) {
    // 1、若所有属性一个都没有指定,那就是default情况-->作用于所有的Controller
    if (!hasSelectors()) {
      return true;
    } else if (controllerType != null) {
      // 2、注意此处的basePackage只是简单的startsWith前缀匹配而已~~~
      // 说明:basePackageClasses属性最终都是转为它来匹配的,
      // 如果写了一个Controller类匹配上了,那它所在的包下所有的都是匹配的(因为同包嘛)
      for (String basePackage : this.basePackages) {
        if (controllerType.getName().startsWith(basePackage)) {
          return true;
        }
      }
      // 3、指定具体的Class类型,只会匹配数组里面的这些类型,精确匹配。
      for (Class<?> clazz : this.assignableTypes) {
        if (ClassUtils.isAssignable(clazz, controllerType)) {
          return true;
        }
      }
      // 4、根据类上的注解类型来匹配(若你想个性化灵活配置,可以使用这种方式)
      for (Class<? extends Annotation> annotationClass : this.annotations) {
        if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) {
          return true;
        }
      }
    }
    return false;
  }


这里做个说明:


  • 若注解的多个属性都给值,它们是取并集的关系(只要符合一个就成)
  • ControllerAdviceBean.findAnnotatedBeans()去找@ControllerAdvice类会被调用两次:

- RequestMappingHandlerAdapter#afterPropertiesSet() -> initControllerAdviceCache()

- ExceptionHandlerExceptionResolver#afterPropertiesSet() -> initExceptionHandlerAdviceCache()

- 因为前面说了:@ControllerAdvice它即可用于正常的(和ResponseBodyAdvice等联用),也可使用在异常上(和@RestControllerAdvice联用)

  • 若注解的属性一个都没有指定值,那它将作用于所有的@Controller们(为何是所有的Controller呢?各位可参考ControllerAdviceBean#isApplicableToBeanType()方法的调用处:只在处理RequestMapping、@RequestBody相关的类里使用到,当然处理异常时会看这个异常我到底要不要处理等等…)

- 虽然可以不用指定包名,但我个人标记比较喜欢使用basePackageClasses属性把它显示的指明出来~


针对于@RestControllerAdvice,它就类似于@RestController和@Controller之间的区别,在@ControllerAdvice的基础上带有@ResponseBody的效果。


@ControllerAdvice在容器初始化的时候被解析,伪代码如下:


所有的被标注有此注解的Bean最终都变成一个org.springframework.web.method.ControllerAdviceBean,它内部持有Bean本身,以及判断逻辑器(HandlerTypePredicate)的引用


RequestMappingHandlerAdapter:
  @Override
  public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();
    ...
  }
  private void initControllerAdviceCache() {
    // 因为它需要通过它去容器内找到所有标注有@ControllerAdvice注解的Bean们
    if (getApplicationContext() == null) {
      return;
    }
    // 关键就是在findAnnotatedBeans方法里:传入了容器上下文
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    // 注意此处是有个排序的~~~~
    AnnotationAwareOrderComparator.sort(adviceBeans);
    ...
    // 注意:找到这些标注有@ControllerAdvice后并不需要保存下来。
    // 而是一个一个的找它们里面的@InitBinder/@ModelAttribute 以及 RequestBodyAdvice和ResponseBodyAdvice
    // 说明:异常注解不在这里解析,而是在`ExceptionHandlerMethodResolver`里~~~
    for (ControllerAdviceBean adviceBean : adviceBeans) {
      ...
    }
  }
ControllerAdviceBean:
  // 找到容器内(包括父容器)所有的标注有@ControllerAdvice的Bean们~~~
  public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
    return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class))
        .filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null)
        .map(name -> new ControllerAdviceBean(name, context))
        .collect(Collectors.toList());
  }


这就是@ControllerAdvice被解析、初始化的原理。它提供一个书写Advice增强器的平台,在初始化的时候根据此类完成解析各种注解作用于各个功能上,从而在运行期直接运行即可。




相关文章
|
19天前
|
设计模式 前端开发 Java
步步深入SpringMvc DispatcherServlet源码掌握springmvc全流程原理
通过对 `DispatcherServlet`源码的深入剖析,我们了解了SpringMVC请求处理的全流程。`DispatcherServlet`作为前端控制器,负责请求的接收和分发,处理器映射和适配负责将请求分派到具体的处理器方法,视图解析器负责生成和渲染视图。理解这些核心组件及其交互原理,有助于开发者更好地使用和扩展SpringMVC框架。
31 4
|
2月前
|
监控 Java 数据安全/隐私保护
如何用Spring Boot实现拦截器:从入门到实践
如何用Spring Boot实现拦截器:从入门到实践
53 5
|
2月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
142 2
|
3月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
3月前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
41 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
4月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
3月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
66 2
|
3月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
230 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
4月前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
5月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】