第八篇:跨控制器间共享 - @ControllerAdvice

第八篇:跨控制器间共享 - @ControllerAdvice

官方文档

官方文档

参考资料

@ControllerAdvice

@ControllerAdvice 修饰的类专门用于声明在多个@Controller 中被共享的@ExceptionHandler, @InitBinder 和 @ModelAttribute 修饰的方法。

@ControllerAdvice 修饰的类也是一个 bean 组件,多个@ControllerAdvice 修饰的 bean 可以通过@Order 控制顺序。

ControllerAdviceBean 的作用是封装@ControllerAdvice 修饰的 SpringIOC 管理的 bean 的信息,而不需要实例化这个 bean

ControllerAdviceBean 的 findAnnotatedBeans(ApplicationContext) 方法可用于发现@ControllerAdvice 注解修饰 bean 并将其转化为 ControllerAdviceBean。但是 ControllerAdviceBean 可以从基于任意对象创建,包括没有@ControllerAdvice 注解修饰的类。

通过查看 ControllerAdviceBean 在哪里被引用,我们可以很快发现:

这两个方法真的非常相像。

我们会在后面的小节进行详细的代码分析。

ControllerAdviceBean 还在 RequestResponseBodyAdviceChain 中使用到,用于将 RequestBodyAdviceResponseBodyAdvice,转化为 ControllerAdviceBean 类型,先放着,以后再了解。

在 SpringMVC 启动时,RequestMappingHandlerMappingExceptionHandlerExceptionResolver 检测 controller advice 类型的 bean 并在运行时应用它们。来自 @ControllerAdvice 的全局 @ExceptionHandler 方法优先级在抛出错误的控制器方法所在的@Controller 类中的 @ExceptionHandler 方法之后。相反,来自 @ControllerAdvice 全局的 @ModelAttribute@InitBinder 方法的优先级在控制器方法所在的@Controller 类中的本地方法之前。

@ControllerAdvice 自身的配置

@ControllerAdvice 生效的范围控制

默认情况下,@ControllerAdvice 中的方法全局地应用于所有控制器(@Controller)。使用像 annotationsassignableTypesbasePackageClasses, 和 basePackages (别名为 value) 这样的属性(又叫 selectors 选择器)来定义更窄的目标控制器(@Controller)集合。如果声明了多个选择器,则应用或逻辑进行组合,这意味着 Controller 应该至少匹配一个选择器。注意,选择器检查是在运行时执行的,因此添加许多选择器可能会对性能产生负面影响并增加复杂性。

// 所有被@RestController修饰的 Controller 都是目标 Controller
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 对应 value 配置,省略了
// "org.example.controllers"包及其子包中下的所有Controller 都是目标 Controller
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 实现了 ControllerInterface 的控制器或者继承了 AbstractController 的 Controller 都是目标 Controller
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

// 标记类PackageMarker所在的包及其子包中的所有 Controller 都是目标 Controller
@ControllerAdvice(basePackageClasses = {PackageMarker.class})
public class ExampleAdvice3 {}

这个比较简单,就不写代码测试了。

简单分析一下,这些配置都在 ControllerAdviceBeancreateBeanTypePredicate 中使用,

private static HandlerTypePredicate createBeanTypePredicate(@Nullable ControllerAdvice controllerAdvice) {
    if (controllerAdvice != null) {
        return HandlerTypePredicate.builder()
                .basePackage(controllerAdvice.basePackages())
                .basePackageClass(controllerAdvice.basePackageClasses())
                .assignableType(controllerAdvice.assignableTypes())
                .annotation(controllerAdvice.annotations())
                .build();
    }
    return HandlerTypePredicate.forAnyHandlerType();
}

此方法专门生成 HandlerTypePredicate,专门用来根据选择器(前面提到的 4 个属性)判断控制器是否为目标控制器。这个 HandlerTypePredicate 也很简单,实现函数式接口 Predicate,实现 Predicate 接口唯一需要实现的 test 方法,传入控制器的类型,Class 对象,输出 boolean,true 表示是目标控制器,false 表是不是。

public boolean test(Class<?> controllerType) {
    if (!hasSelectors()) {
        return true;
    }
    else if (controllerType != null) {
        for (String basePackage : this.basePackages) {
            if (controllerType.getName().startsWith(basePackage)) {
                return true;
            }
        }
        for (Class<?> clazz : this.assignableTypes) {
            if (ClassUtils.isAssignable(clazz, controllerType)) {
                return true;
            }
        }
        for (Class<? extends Annotation> annotationClass : this.annotations) {
            if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) {
                return true;
            }
        }
    }
    return false;
}

多个@ControllerAdvice 之间的顺序

这里我们只学习用 @Order 注解来设置 ControllerAdvice 之间的顺序,其他的关于顺序之间的注意点,看@ControllerAdvice 的源码注释。

@Order 注解的值越小表示越靠前,优先级越大,默认值是 Ordered.LOWEST_PRECEDENCE,只为 Integer.MAX_VALUE,int 的最大值,表示最低优先级

具体使用结合@InitBinder、@ModelAttribute、@ExceptionHandler 修饰的方法来看

@ControllerAdvice 内部的方法

@ModelAttribute 方法和@InitBinder 方法

这两个方法的初始化相关的源码分析,因为他们是一起初始化的,所以放在一起说

通过查看 ControllerAdviceBean 在哪里被引用,我们可以很快发现了 RequestMappingHandlerAdapter

首先 RequestMappingHandlerAdapter 也是一个 bean,实现了 InitializingBean 接口,实现 afterPropertiesSet 方法,在其中做一些初始化操作

public void afterPropertiesSet() {
    // 检查是否有全局的 @ModelAttribute方法和@InitBinder方法,并进行缓存,
    // 检查有没有实现了 RequestBodyAdvice 和 ResponseBodyAdvice 的 ControllerAdviceBean 类,并进行缓存
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
        // 初始化参数解析器
        // 如果没有初始化 argumentResolvers,则 调用 getDefaultArgumentResolvers添加默认的参数解析器,一般有26个
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.initBinderArgumentResolvers == null) {
        // 初始化专用于 @InitBinder方法 的参数解析器
        // 如果没有初始化 initBinderArgumentResolvers 调用 getDefaultInitBinderArgumentResolvers 添加默认的参数解析器,一般有13个
        List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
        this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.returnValueHandlers == null) {
        // 初始化返回值处理器
        // 如果没有初始化 returnValueHandlers 调用 getDefaultReturnValueHandlers 添加默认的返回值处理器,一般有15个
        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
}

这里重点看 initControllerAdviceCache 方法,@ControllerAdvice 中的 @InitBinder@ModelAttribute 修饰的方法会在 RequestMappingHandlerAdapterinitControllerAdviceCache 方法中进行缓存,存放到 modelAttributeAdviceCacheinitBinderAdviceCache 中,同时将请求体响应体的通知也进行了缓存,存放到 requestResponseBodyAdvice

@ControllerAdvice 注释的 bean 是否实现 RequestBodyAdvice 接口或者 ResponseBodyAdvice 接口,实现了就会被添加到 requestResponseBodyAdvice 中,将来在 RequestResponseBodyAdviceChain 中使用。先放着,以后再了解。TODO

RequestResponseBodyAdviceChain 的一个场景就是处理就是在处理 @ResponseBody 结果的时候,@ResponseBody 的处理类是 RequestResponseBodyMethodProcessorRequestResponseBodyMethodProcessorhandleReturnValue 方法调用其父类 AbstractMessageConverterMethodProcessorwriteWithMessageConverters 方法,在 writeWithMessageConverters 最终写入 body 之前,调用了 getAdvice() 方法,

private void initControllerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }

    // 在给定的ApplicationContext中查找带有@ControllerAdvice注释的bean,并将它们包装为ControllerAdviceBean实例。
    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);
        }
        // 根据筛选器选择给定目标类型上的方法。
        // MODEL_ATTRIBUTE_METHODS 用于筛选不被@RequestMapping修饰,且被@ModelAttribute修饰的方法
        // 排除@RequestMapping注解是因为@ModelAttribute注解可以跟@RequestMapping注解一起使用,具体请看《SpringMVC-第四篇:域对象共享数据》
        Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
        if (!attrMethods.isEmpty()) {
            // 添加到缓存中
            this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
        }
        // INIT_BINDER_METHODS 用于筛选被@InitBinder修饰的方法
        Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
        if (!binderMethods.isEmpty()) {
            // 添加到缓存中
            this.initBinderAdviceCache.put(adviceBean, binderMethods);
        }
        // @ControllerAdvice注释的bean是否实现 RequestBodyAdvice接口或者 ResponseBodyAdvice接口
        if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
            requestResponseBodyAdviceBeans.add(adviceBean);
        }
    }

    if (!requestResponseBodyAdviceBeans.isEmpty()) {
        // 添加到缓存中
        this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
        // 在RequestResponseBodyAdviceChain中,处理 RequestBodyAdvice 和 ResponseBodyAdvice,会判断 RequestBodyAdvice 或者 ResponseBodyAdvice,会判断是不是 ControllerAdviceBean 类型 
    }

    ......
    // 日志
}

此外,看完了 initControllerAdviceCache 方法,我们再看看 getDefaultInitBinderArgumentResolvers 方法,这个方法的作用是初始化专用于 @InitBinder 方法的参数解析器,只有添加了相应的参数解析器,才能使用特定类型的参数或者使用特定的注解修饰参数

private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);

    // 注解修饰的参数的解析器
    // @RequestParam注解
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    // @PathVariable注解
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    // @MatrixVariable注解
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    // @Value注解
    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    // @SessionAttribute注解
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    // @RequestAttribute注解
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // 特定类型的参数的解析器
    // WebRequest ServletRequest MultipartRequest HttpSession InputStream Reader HttpMethod Locale 等等
    resolvers.add(new ServletRequestMethodArgumentResolver());
    // ServletResponse OutputStream Writer
    resolvers.add(new ServletResponseMethodArgumentResolver());

    // 添加自定义的参数的解析器,即 RequestMappingHandlerAdapter 的 customArgumentResolvers,可在实例化后,注册到ICO容器前 调用setCustomArgumentResolvers方法添加
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    // Catch-all
    resolvers.add(new PrincipalMethodArgumentResolver());
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));

    return resolvers;
}

RequestMappingHandlerAdapter 又是什么时候生效呢?

DispatcherServletdoDispatch 方法中,通过 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 获取 HandlerAdapter(控制器方法适配器),

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            ......

            // 确定使用哪个 HandlerAdapter,一般都是使用 RequestMappingHandlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            ......

            // Actually invoke the handler.
            // 调用 HandlerAdapter ,最终调用 控制器方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            ......

        }
        catch (Exception ex) {
            ......
        }
        catch (Throwable err) {
            ......
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        ......
    }
    catch (Throwable err) {
        ......
    }
    finally {
        ......
    }
}

具体看 getHandlerAdapter 方法:

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        // 注意,适配器是已经事先生成好了的,默认有三种适配器
        for (HandlerAdapter adapter : this.handlerAdapters) {
            if (adapter.supports(handler)) {
                // 如果支持,则直接返回
                return adapter;
            }
        }
    }
    throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

实际调试过程中,有三种类型的适配器,其中就有我们前面提到的 RequestMappingHandlerAdapter,实际最终返回的,也是这个类型

附上 HandlerAdapter 的继承图:

RequestMappingHandlerAdapter 中缓存 modelAttributeAdviceCacheinitBinderAdviceCache 何时是如何生效的呢?那我们就继续往后看在 DispatcherServletdoDispatch 方法

DispatcherServletdoDispatch 方法中 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 中,实际上是通过调用 HandlerMethodAdapterhandle 方法来调用控制器方法

AbstractHandlerMethodAdapter 类实现了 handle 方法,将其委托给子类实现。

public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 委托为 handleInternal 方法,让子类重写
    return handleInternal(request, response, (HandlerMethod) handler);
}

RequestMappingHandlerAdapter 又将其委托给 invokeHandlerMethod 方法

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        // 根据外部@ControllerAdvice中的@InitBinder方法(排在前)和控制器方法所在的控制器内部的@InitBinder方法(排在后)初始化 WebDataBinderFactory
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        // 根据外部@ControllerAdvice中的@ModelAttribute方法(排在前)和控制器方法所在的控制器内部的@ModelAttribute方法(排在后)初始化 ModelFactory
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }
        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        // 初始化 Model,依次调用前面在初始化 ModelFactory 时传入的 @ModelAttribute方法,
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        // 异步请求的相关处理   
        ......

        // 执行控制器方法,在执行控制器方法前,如果需要的话,会依次调用WebDataBinderFactory中的 @InitBinder方法 
        invocableMethod.invokeAndHandle(webRequest, mavContainer);

        // 异步请求的相关处理   
        ......

        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally {
        webRequest.requestCompleted();
    }
}

先看 getDataBinderFactory 方法,可以看到是先添加 initBinderAdviceCache 缓存中的也就是外部外部 @ControllerAdvice 中的 @InitBinder 方法,再添加控制器方法所在控制器中(即本地)的 @InitBinder 方法。

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    Class<?> handlerType = handlerMethod.getBeanType();
    // 控制器方法所在的控制器中的 @InitBinder方法的缓存(即本地@InitBinder方法)
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
        // 如果没有,那就查一下,进行缓存,查出来也没有没有也要存
        methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    // 先添加缓存中外部的 @InitBinder 方法
    this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
        if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
            Object bean = controllerAdviceBean.resolveBean();
            for (Method method : methodSet) {
                initBinderMethods.add(createInitBinderMethod(bean, method));
            }
        }
    });
    // 再添加本地的 @InitBinder 方法
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    return createDataBinderFactory(initBinderMethods);
}

其中,在填充 initBinderMethods 列表的时候,会调用 createInitBinderMethod 方法,而在 createInitBinderMethod 方法中,会使用我们前面初始化的 initBinderArgumentResolvers 作为 @InitBinder 修饰的方法的参数解析器:

private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {
    InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);
    // 如果 RequestMappingHandlerAdapter 实例化之后没有调用 setInitBinderArgumentResolvers 初始化  initBinderArgumentResolvers,
    // 则使用 getDefaultInitBinderArgumentResolvers 中初始化的 13个参数解析器
    if (this.initBinderArgumentResolvers != null) {
        // 设置方法的参数解析器
        binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);
    }
    // 设置方法的数据绑定器
    binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));
    // 设置方法的参数名发现器
    binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
    return binderMethod;
}

再看 getModelFactory 方法,基本上结构跟 getDataBinderFactory 方法是差不多的,从中可以看到是先添加 modelAttributeAdviceCache 缓存中的也就是外部外部 @ControllerAdvice 中的 @ModelAttribute 方法,再添加控制器方法所在控制器中(即本地)的 @ModelAttribute 方法。

private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
    SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
    Class<?> handlerType = handlerMethod.getBeanType();
    // 控制器方法所在的控制器中的 @ModelAttribute方法的缓存(即本地@ModelAttribute方法),注意,不包括同时被@RequestMapping注解修饰的方法
    Set<Method> methods = this.modelAttributeCache.get(handlerType);
    if (methods == null) {
        // 如果没有,那就查出来,进行缓存,没有就算了
        methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
        this.modelAttributeCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
    // 先添加外部的 @ModelAttribute方法
    this.modelAttributeAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
        if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
            Object bean = controllerAdviceBean.resolveBean();
            for (Method method : methodSet) {
                attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
            }
        }
    });
    // 再添加本地的 @ModelAttribute方法
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
    }
    // 构建 Model工厂
    return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}

再看看何时调用的 @ModelAttribute 方法,modelFactory.initModel 中的 invokeModelAttributeMethods 方法依次调用初始化时传入的 @ModelAttribute 修饰的方法。invokeModelAttributeMethods 方法内部就不深入了解了。

public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {

    // 处理@sessionAttributes注解
    Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
    container.mergeAttributes(sessionAttributes);
    // 依次调用所有@ModelAttribute方法
    invokeModelAttributeMethods(request, container);

    // 处理@sessionAttributes注解
    for (String name : findSessionAttributeArguments(handlerMethod)) {
        if (!container.containsAttribute(name)) {
            Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
            if (value == null) {
                throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
            }
            container.addAttribute(name, value);
        }
    }
}

看看何时调用的 @InitBinder 方法,在 invocableMethod.invokeAndHandle(webRequest, mavContainer); 方法中调用控制器方法前调用,具体的定位有点复杂,这里先不仔细研究了,将来学习了 SpringMVC中的类型转换SpringMVC中的数据绑定 再继续深入。

这里只是简单涉及了一点 HandlerAdapter 的源码

关于控制器方法适配器的相关源码分析,请看《SpringMVC 控制器方法(handler)适配器 - HandlerAdapter》和《SpringMVC-RequestMappingHandlerAdapter 源码解析.md

简单实践。

被通知的控制器 AdvisedController

@RestController
@RequestMapping("/advise")
public class AdvisedController {

    @ModelAttribute
    public void setModelAttribute(Model model) {
        System.out.println("Controller local @ModelAttribute method");
        String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
        model.addAttribute("AttributeSetPath", path + "end");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        System.out.println("Controller local @initBinder method");
        System.out.println("binder AllowedFields:"+Arrays.toString(binder.getAllowedFields()));
        System.out.println("binder DisallowedFields:"+Arrays.toString(binder.getDisallowedFields()));
    }

    @RequestMapping("/target")
    public String target(Model model) {
        System.out.println(model);
        return "success";
    }

    @RequestMapping("/testInitBinder")
    public String testInitBinder(Date date, Model model) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date));
        return "success";
    }

}

第一个控制器通知@ControllerAdvice,限制目标控制器所在的包为 HelloController 所在的包,也就是 AdvisedController 所在的包

同时,在其中添加了 @ModelAttribute 方法和 @InitBinder 方法,在执行这些方法时,首先输出此方法所在 ControllerAdvice 的 Order 信息,然后输出自身的方法信息,方便控制台分析。

后续的两个 ControllerAdvice 方法也大同小异

@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(0)
public class ControllerAdvice0 {

    private int showOrder(){
        // 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
        Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
        int value = order.value();
        System.out.println("执行排序为: "+value+" 的ControllerAdvice");
        return value;
    }

    @ModelAttribute
    public void setModelAttribute(Model model) {
        int value = showOrder();
        System.out.println("执行 ModelAttribute 方法");
        String path = model.getAttribute("AttributeSetPath")!=null ? (String) model.getAttribute("AttributeSetPath") : "";
        model.addAttribute("AttributeSetPath", path+value+"-");
    }


    @InitBinder
    public void initBinder(WebDataBinder binder){
        showOrder();
        System.out.println("执行 initBinder 方法");
        binder.setAllowedFields("aaa");
    }

}

第二个 ControllerAdvice 方法

@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(1)
public class ControllerAdvice1 {

    private int showOrder(){
        // 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
        Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
        int value = order.value();
        System.out.println("执行排序为: "+value+" 的ControllerAdvice");
        return value;
    }

    @ModelAttribute
    public void setModelAttribute(Model model) {
        int value = showOrder();
        System.out.println("执行 ModelAttribute 方法");
        String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
        model.addAttribute("AttributeSetPath", path + value + "-");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder){
        int value = showOrder();
        System.out.println("执行 initBinder 方法");
        binder.setDisallowedFields("bbb");
    }


}

第三个 ControllerAdvice 方法

@ControllerAdvice(basePackageClasses = HelloController.class)
@Order(2)
public class ControllerAdvice2 {

    private int showOrder(){
        // 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
        Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
        int value = order.value();
        System.out.println("执行排序为: "+value+" 的ControllerAdvice");
        return value;
    }

    @ModelAttribute
    public void setModelAttribute(Model model) {
        int value = showOrder();
        System.out.println("执行 ModelAttribute 方法");
        String path = model.getAttribute("AttributeSetPath") != null ? (String) model.getAttribute("AttributeSetPath") : "";
        model.addAttribute("AttributeSetPath", path + value + "-");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder){
        int value = showOrder();
        System.out.println("执行 initBinder 方法");
        binder.addCustomFormatter(new DateFormatter("yyyy年MM月dd日"));
    }


}

此外我们还尝试自定义个一个 RequestResponseBodyAdvice,实现 RequestBodyAdviceResponseBodyAdvice<User>,不过这里我们不做具体操作

@ControllerAdvice(basePackageClasses= HelloController.class)
@Order(3)
public class MyRequestResponseBodyAdvice implements RequestBodyAdvice, ResponseBodyAdvice<User> {

    private int showOrder(){
        // 获取特定类型上的特定注解对象,这样我们就可以直接获取其配置,这应该是直接从Class对象中获取的
        Order order = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Order.class);
        int value = order.value();
        System.out.println("执行排序为: "+value+" 的ControllerAdvice");
        return value;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return false;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return null;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return null;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return null;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return false;
    }

    @Override
    public User beforeBodyWrite(User body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }
}

测试方法

@SpringJUnitWebConfig(locations = "classpath:SpringMVC.xml")
class AdvisedControllerTest {
    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }


    @Test
    void target() throws Exception {
        mockMvc.perform(get("/advise/target")).andDo(print());
    }

    @Test
    void testInitBinder() throws Exception {
        mockMvc.perform(get("/advise/testInitBinder").queryParam("date","2022年9月15日")).andDo(print());
    }

}

运行 testInitBinder 方法,查看日志

手动输出的日志:

执行排序为: 0 的ControllerAdvice
执行 ModelAttribute 方法
执行排序为: 1 的ControllerAdvice
执行 ModelAttribute 方法
执行排序为: 2 的ControllerAdvice
执行 ModelAttribute 方法
Controller local @ModelAttribute method
执行排序为: 0 的ControllerAdvice
执行 initBinder 方法
执行排序为: 1 的ControllerAdvice
执行 initBinder 方法
执行排序为: 2 的ControllerAdvice
执行 initBinder 方法
Controller local @initBinder method
binder AllowedFields:[aaa]
binder DisallowedFields:[bbb]
2022-09-15T00:00:00

Mock 请求和响应信息

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /advise/testInitBinder
       Parameters = {date=[2022年9月15日]}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = xyz.xiashuo.springmvccontrolleradvice.controller.AdvisedController
           Method = xyz.xiashuo.springmvccontrolleradvice.controller.AdvisedController#testInitBinder(Date, Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", Content-Length:"7"]
     Content type = application/json
             Body = success
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

从手动输出的日志我们可以看到,

因为 @ModelAttribute 方法会在任意控制器方法调用前执行,所以先按照 Order 执行 @ControllerAdvice 中的 @ModelAttribute 方法,然后再执行目标控制器中的 @ModelAttribute 方法,同时,这些 @ModelAttribute 方法使用的都是同一个 Model 域,即使用的都是同一个 Model 实例,

然后,因为我们的控制器方法需要解析请求参数,所以会调用 @InitBinder 方法,如果没有参数,是不会调用 @InitBinder 方法的,比如 AdvisedController 中的 target 方法。首先开始按照 Order 执行 @ControllerAdvice 中的 @InitBinder 方法,然后再执行目标控制器中的 @InitBinder 方法,同时,这些 @InitBinder 方法使用的都是同一个 WebDataBinder 实例。

最后执行目标控制器方法,可以看到正确地转化了时间字符串。

其实,这个处理流程也可以看作是不使用 @ControllerAdvice 类的一个普通请求的处理流程,控制器内部的 @InitBinder 方法和 @ModelAttribute 方法就是在 RequestMappingHandlerAdapter 中处理的。

PS:

MyRequestResponseBodyAdvice 单独实现了 RequestBodyAdviceResponseBodyAdvice<User>,在 RequestMappingHandlerAdapterinitControllerAdviceCache 中正常解析

@ExceptionHandler 方法

其实对@ExceptionHandler 方法的缓存的调用,在 《SpringMVC-第七篇:控制器方法异常处理》ExceptionHandlerExceptionResolver 小节中都已经详细描述了。这里就不再重复了。

最终的结论是:

先在当前控制器中的 @ExceptionHandler 方法这个范围内查找,如果没找到,就按照按照 @ControllerAdvice 的排序依次在 @ControllerAdvice 中包含的 @ExceptionHandler 方法中查找。

拿如何在一个类中查找处理当前异常呢?

先根据当前抛出的异常的类型进行匹配,按照继承路径最短原则,遍历所有的异常处理方法的 @ExceptionHandler 注解中配置的异常类型,查找异常类型就是当前爆出的异常类型或者为当前爆出的异常类型的父类,同时继承层级最短的一个,如果找不到,就查找当前抛出的异常的根异常,然后继续以这个根异常来进行类型匹配,直到找到合适的异常,如果还是没找到,那就没有了,换下一个类。

所以,只要在当前类中能找到一个能处理当前爆出的异常的 @ExceptionHandler 方法,都不会继续从下一个类中查找,也就是说,只有当前类中完全不包含能处理当前爆出的异常的 @ExceptionHandler 方法,才会去下一个类中查找。

我们最开始从爆出异常的控制器方法所在的控制器中查找,然后按照 Order,从匹配的 @ControllerAdvice 中查找。

简单实践

自定义异常 MyException

public class MyException extends Exception {
    private static final long serialVersionUID = -2957544073971720547L;

    public MyException(String message,Exception cause) {
        super(message, cause);
    }
}

AdvisedController 中添加抛出异常的请求和异常处理类

// 匹配当前抛出的异常类型的cause异常
@ExceptionHandler(ArithmeticException.class)
public String exceptionHandler(ArithmeticException exception) {
    String message = exception.toString();
    System.out.println("ArithmeticException exceptionHandler:" + message);
    return message;
}

// 匹配当前抛出的异常类型的父类
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception exception) {
    String message = exception.toString();
    System.out.println("Exception exceptionHandler:" + message);
    return message;
}

// 直接匹配当前抛出的异常
@ExceptionHandler(MyException.class)
public String exceptionHandler(MyException exception) {
    String message = exception.toString();
    System.out.println("MyException exceptionHandler:" + message);
    return message;
}

@RequestMapping("/testException")
public String testException(Model model) throws Exception {
    Exception nullPointerException = new ArithmeticException("customize NullPointer exception");
    Exception myException = new MyException("customize my exception", nullPointerException);
    throw myException;
    //return "success";
}

测试方法

@Test
void testException() throws Exception {
    mockMvc.perform(get("/advise/testException")).andDo(print());
}

直接运行测试,输出

MyException exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception

说明此时触发的是 @ExceptionHandler(MyException.class) 修饰的方法

注释掉 @ExceptionHandler(MyException.class) 修饰的方法,再次运行测试,输出

Exception exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception

说明此时触发的是 @ExceptionHandler(Exception.class) 修饰的方法

再次注释掉 @ExceptionHandler(Exception.class) 修饰的方法,再次运行测试,输出

ArithmeticException exceptionHandler:java.lang.ArithmeticException: customize NullPointer exception

说明此时触发的是 @ExceptionHandler(ArithmeticException.class) 修饰的方法。

这也验证了我们之前总结的 @ExceptionHandler 方法查找的顺序

如果找不到,开始把当前爆出的异常类型的 cause 异常当作当前爆出的异常,重复上面的步骤,还是没有呢?就从下一个类来查找,比如从 @ControllerAdvice 中查找,我们来试试

我们在 ControllerAdvice0 中添加异常处理方法,

// 直接匹配当前抛出的异常
@ExceptionHandler(MyException.class)
@ResponseBody
public String exceptionHandler(MyException exception) {
    String message = exception.toString();
    System.out.println("ControllerAdvice0 MyException exceptionHandler:" + message);
    return message;
}

此时,@ExceptionHandler(MyException.class) 修饰的方法和 @ExceptionHandler(Exception.class) 修饰的方法都被注释,@ExceptionHandler(ArithmeticException.class) 修饰的方法没有被注释,运行测试方法,输出的依然是

ArithmeticException exceptionHandler:java.lang.ArithmeticException: customize NullPointer exception

只有当我们连 AdvisedController 中的 @ExceptionHandler(ArithmeticException.class) 修饰的方法也注释掉,AdvisedController 没有任何 @ExceptionHandler 方法能够处理当前爆出的异常的时候才会生效

ControllerAdvice0 MyException exceptionHandler:xyz.xiashuo.springmvccontrolleradvice.exceptions.MyException: customize my exception

所以,只要在当前类中能找到一个能处理当前爆出的异常的 @ExceptionHandler 方法,都不会继续从下一个类中查找,也就是说,只有当前类中完全不包含能处理当前爆出的异常的 @ExceptionHandler 方法,才会去下一个类中查找。

我们最开始从爆出异常的控制器方法所在的控制器中查找,然后按照 Order,从匹配的 @ControllerAdvice 中查找。