spring高级篇(六)

发布于:2024-05-07 ⋅ 阅读:(26) ⋅ 点赞:(0)

1、@ControllerAdvice、@InitBinder

       @ControllerAdvice注解是 Spring MVC 中的一个注解,它的作用是全局性地统一处理控制器(Controller)的异常,以及对控制器的全局配置。

  • 全局异常处理:可以在 @ControllerAdvice 注解标记的类中定义异常处理方法,用于捕获和处理所有控制器中抛出的异常。这些方法可以处理特定类型的异常,提供自定义的异常处理逻辑,并返回友好的错误信息或错误页面。

  • 全局数据绑定:可以在  @ControllerAdvice注解标记的类中定义全局的数据绑定规则,例如注册全局的属性编辑器、格式化器、验证器等,以应用于所有控制器中的数据绑定操作。

  • 全局模型数据:可以在 @ControllerAdvice注解标记的类中定义方法,用于在所有控制器中共享的模型数据,这些数据会自动添加到每个控制器方法的模型中,从而在视图中可用。

        上一篇中提到,@InitBinder 注解的作用是标记一个用于初始化DataBinder对象,自定义数据绑定行为的方法,它会在控制器处理请求之前被调用。

        如果@InitBinder 注解加在被@ControllerAdvice 注解标记的控制器类的方法中时,其作用范围是全局的,并且是由RequestMappingHandlerAdapter 在初始化时解析并记录。

        @InitBinder 注解加在被@Controller 标记的控制器中的方法上时,会在控制器方法首次执行时解析并记录。


        定义一个Config类,类中有三个静态内部类,MyControllerAdvice类是被@ControllerAdvice

控制的。

@Configuration
public class WebConfig {

    @ControllerAdvice
    static class MyControllerAdvice {
        @InitBinder
        public void binder3(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
        }
    }

    @Controller
    static class Controller1 {
        @InitBinder
        public void binder1(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
        }

        public void foo() {

        }
    }

    @Controller
    static class Controller2 {
        @InitBinder
        public void binder21(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
        }

        @InitBinder
        public void binder22(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
        }

        public void bar() {

        }
    }

}

        测试类中,首先创建一个 RequestMappingHandlerAdapter对象,并将其配置为使用指定的 ApplicationContext上下文。然后调用 afterPropertiesSet()方法来完成对象的初始化。

AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();

        然后定义一个方法,获取RequestMappingHandlerAdapter中的两个成员变量,这里利用了缓存的思想(不知是否可被归类为享元模式):

  • private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache:用于存储被@ControllerAdvice标记的控制器中 @InitBinder 标注的方法。
  • private final Map<Class<?>, Set<Method>> initBinderCache:用于存储@Controller标记的控制器中 @InitBinder 标注的方法。

        RequestMappingHandlerAdapter是HandlerAdapter的一个实现类:

private static void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
        Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
        initBinderAdviceCache.setAccessible(true);
        Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
        log.info("全局的 @InitBinder 方法 {}",
                globalMap.values().stream()
                        .flatMap(ms -> ms.stream().map(m -> m.getName()))
                        .collect(Collectors.toList())
        );

        Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
        initBinderCache.setAccessible(true);
        Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
        log.info("控制器的 @InitBinder 方法 {}",
                controllerMap.entrySet().stream()
                        .flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
                        .collect(Collectors.toList())
        );
    }

        继续通过反射调用RequestMappingHandlerAdapter中的getDataBinderFactory方法(用于模拟调用Controller1和Controller2的方法):

        log.debug("1. 刚开始...");
        showBindMethods(handlerAdapter);

        Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
        getDataBinderFactory.setAccessible(true);

        log.debug("2. 模拟调用 Controller1 的 foo 方法时 ...");
        getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
        showBindMethods(handlerAdapter);

        log.debug("3. 模拟调用 Controller2 的 bar 方法时 ...");
        getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
        showBindMethods(handlerAdapter);

        context.close();

         输出结果:

 2、控制器方法执行流程

        复习一下,DispatcherServlet 的工作流程中,首先是客户端发送请求到 DispatcherServlet然后DispatcherServlet 根据请求的 URL 找到对应的 HandlerMapping(处理器映射器),然后将请求分发给相应的 Controller(处理器)。

        RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,Map的K为路径,V就是下图中的HandlerMethodHandlerMethod又是交给HandlerAdapters执行的。

        HandlerMethod需要:

  • bean:控制器类

  • method :控制器类中的方法。

        但是只有控制器和方法不够,因为方法上还有可能存在参数或者自定义的注解,而参数有可能还需要进行类型转换。所以需要用此前提到过的参数解析器,返回值解析器,它们共同组合成了ServletInvocableHandlerMethod:

  • WebDataBinderFactory 负责对象绑定、类型转换

  • ParameterNameDiscoverer 负责参数名解析

  • HandlerMethodArgumentResolverComposite 负责解析参数

  • HandlerMethodReturnValueHandlerComposite 负责处理返回值(统一成ModelAndView类型)

         首先HandlerAdapters 会创建WebDataBinderFactory工厂,可以执行被@InitBinder 注解修饰的自定义的解析参数方法。然后会创建ModelFactory,会把标注了@ModelAttribute的数据放入ModelAndViewContainer容器中。

       

        紧接着就会获取并解析参数,通过反射调用方法,处理返回值。其中在解析参数和处理返回值时,还会涉及到RequestBodyAdvice和ResponseBodyAdvice,并且有可能会生成模型数据。最后返回ModelAndViewContainer。


        用代码演示一下图一与图二:

        首先创建一个Config类,方法foo()  的参数是一个User对象。(没有显式使用@ModelAttribute

@Configuration
public class WebConfig {


    @Controller
    static class Controller1 {
        @ResponseStatus(HttpStatus.OK)
        public ModelAndView foo(User user) {
            System.out.println("foo");
            return null;
        }
    }

    static class User {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {

            return name;
        }

        @Override
        public String toString() {
            return "User{" +
                   "name='" + name + '\'' +
                   '}';
        }
    }
}

        在测试类中创建ApplicationContext,发送模拟请求,将来name会绑定到User类的name字段上:

AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");

        创建ServletInvocableHandlerMethod对象,并且给WebDataBinderFactory,ParameterNameDiscoverer ,HandlerMethodArgumentResolverComposite赋值:

ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
                new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
//负责对象绑定、类型转换
handlerMethod.setDataBinderFactory(factory);
//负责解析参数名
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
//解析参数
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
ModelAndViewContainer container = new ModelAndViewContainer();
 handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();

         参数解析器组合类:(方法foo()  的参数User会被ServletModelAttributeMethodProcessor解析。)

public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
        composite.addResolvers(
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
                new PathVariableMethodArgumentResolver(),
                new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletRequestMethodArgumentResolver(),
                new ServletModelAttributeMethodProcessor(false),
                new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
                new ServletModelAttributeMethodProcessor(true),
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
        );
        return composite;
    }

        @ModelAttribute  注解,不仅能加在参数上,还能加在类上和方法上:

        区别在于,加在参数上,是由参数解析器进行解析的,加在类上或方法上,是由HandlerAdapter解析的,我们在Config类中加入一个静态内部类:

 @ControllerAdvice
    static class MyControllerAdvice {
        @ModelAttribute("a")
        public String aa() {
            return "aa";
        }
    }

        在测试方法上,创建RequestMappingHandlerAdapter并初始化,在初始化时,会找到加了 @ControllerAdvice类中加了 @ModelAttribute的方法并记录下来

RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

        在 handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);前还需要利用模型的工厂方法中的.initModel() 初始化模型数据:

 // 获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

        最终模型名是注解中的value,模型值为aa()方法的返回值。(即将aa()方法的返回值放入ModelAndViewContainer容器中)

3、返回值处理

        返回值处理和参数解析一样,同样可以使用组合的方式:

 public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
        HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
        composite.addHandler(new ModelAndViewMethodReturnValueHandler());
        composite.addHandler(new ViewNameMethodReturnValueHandler());
        composite.addHandler(new ServletModelAttributeMethodProcessor(false));
        composite.addHandler(new HttpEntityMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
        composite.addHandler(new HttpHeadersReturnValueHandler());
        composite.addHandler(new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
        composite.addHandler(new ServletModelAttributeMethodProcessor(true));
        return composite;
    }

        MappingJackson2HttpMessageConverter是一种消息转换器,可以把请求、响应的消息转换成JSON或xml格式。

3.1、解析ModelAndView返回值类型

        在控制器类中有如下的方法,返回值是ModelAndView类型:

 public ModelAndView test1() {
     log.debug("test1()");
     ModelAndView mav = new ModelAndView("view1");
     mav.addObject("name", "张三");
     return mav;
 }

        对其进行解析,经历的步骤:

  • 准备容器,封装HandlerMethod,创建模拟请求响应
  • 得到返回值处理器的组合,并进行匹配(ModelAndViewMethodReturnValueHandler
  • 调用匹配的返回值处理器,将ModelAndView的视图、模型提取出来,放到container容器中
  • 根据视图找到模板页面,然后用container容器中的视图模型渲染页面
 private static void test1(AnnotationConfigApplicationContext context) throws Exception {
        // 通过反射调用Controller中的test1方法 获取返回值
        Method method = Controller.class.getMethod("test1");
        Controller controller = new Controller();
        Object returnValue = method.invoke(controller);
        //封装HandlerMethod
        HandlerMethod methodHandle = new HandlerMethod(controller, method);
        //准备容器
        ModelAndViewContainer container = new ModelAndViewContainer();
        //得到返回值处理器组合
        HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
        //创建模拟请求响应
        ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
        // 检查是否支持此类型的返回值 用的处理器是ModelAndViewMethodReturnValueHandler()
        if (composite.supportsReturnType(methodHandle.getReturnType())) {
            composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
            System.out.println(container.getModel());
            System.out.println(container.getViewName());
            // 渲染视图
            renderView(context, container, webRequest);
        }
    }
3.2、解析String返回值类型

        在控制器类中有如下的方法,返回值是String类型:

   public String test2() {
            log.debug("test2()");
            return "view2";
   }

        其解析过程和3.1类似,但是用到的返回值解析器是ViewNameMethodReturnValueHandler

最后是根据test2()的返回值view2找到对应名称的视图,因为没有定义ModelAndView的视图、模型,所以是用view2中默认的数据渲染视图。

3.3、解析对象返回值类型

        在控制器类中有如下的方法,返回值是一个对象,不同的是,test3()  方法显式地加上了@ModelAttribute注解,但无论是否显式声明,最终的结果都是将返回值加入modelAndView。(是否显式声明,都是通过ServletModelAttributeMethodProcessor处理器)

         /**
         * 将返回值加入modelAndView
         * 默认是按照@RequestMapping("/test3")找视图名
         * @return
         */
        @ModelAttribute
//        @RequestMapping("/test3")
        public User test3() {
            log.debug("test3()");
            return new User("李四", 20);
        }

        public User test4() {
            log.debug("test4()");
            return new User("王五", 30);
        }

        默认是按照@RequestMapping() 中的路径找视图名,如果没有加@RequestMapping() 注解,则需要自定义视图名:

private static void test3(AnnotationConfigApplicationContext context) throws Exception {
        Method method = Controller.class.getMethod("test3");
        Controller controller = new Controller();
        Object returnValue = method.invoke(controller); // 获取返回值

        HandlerMethod methodHandle = new HandlerMethod(controller, method);

        ModelAndViewContainer container = new ModelAndViewContainer();
        HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
        MockHttpServletRequest request = new MockHttpServletRequest();
        //自定义视图名
        request.setRequestURI("/test3");
        UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
        ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
        if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
            composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
            System.out.println(container.getModel());
            System.out.println(container.getViewName());
            renderView(context, container, webRequest); // 渲染视图
        }
    }
3.4、解析HttpEntity、HttpHeaders、加上了@ResponseBody 注解的对象返回值类型

        在控制器中有如下三个方法:

  • HttpEntity代表返回响应体信息并转换成JSON,被HttpEntityMethodProcessor处理
  • HttpHeaders代表返回响应头信息,被HttpHeadersReturnValueHandler处理
  • 加上了@ResponseBody 注解的方法代表返回响应头和响应体,并且会转换成JSON格式,被RequestResponseBodyMethodProcessor 处理

        这三种返回类型的共同点在于,无需走视图解析:

        用于标记请求已经被处理,无需继续渲染视图。

        public HttpEntity<User> test5() {
            log.debug("test5()");
            return new HttpEntity<>(new User("赵六", 40));
        }

        public HttpHeaders test6() {
            log.debug("test6()");
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "text/html");
            return headers;
        }

        @ResponseBody
        public User test7() {
            log.debug("test7()");
            return new User("钱七", 50);
        }
private static void test6(AnnotationConfigApplicationContext context) throws Exception {
        Method method = Controller.class.getMethod("test6");
        Controller controller = new Controller();
        Object returnValue = method.invoke(controller); // 获取返回值

        HandlerMethod methodHandle = new HandlerMethod(controller, method);

        ModelAndViewContainer container = new ModelAndViewContainer();
        HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
            composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
            System.out.println(container.getModel());
            System.out.println(container.getViewName());
            if (!container.isRequestHandled()) {
                renderView(context, container, webRequest); // 渲染视图
            } else {
                for (String name : response.getHeaderNames()) {
                    System.out.println(name + "=" + response.getHeader(name));
                }
                System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
            }
        }
    }

System.out.println(container.getModel());
System.out.println(container.getViewName());

获取到的都是null

小结:

        ModelAndView、String、未被@ResponseBody 注解控制的对象类型返回值(无论是否显式声明了@ModelAttribute),都要经历视图渲染的过程。

        ModelAndView找视图,是根据ModelAndView构造中的viewName寻找同名的视图,还会使用.addObject() 方法中指定的数据对视图进行渲染 。

        如果没有指定视图名,则会:

  • 根据请求路径推断视图名: 如果在处理器方法中没有显式指定视图名,Spring MVC 会根据请求路径来推断视图名。
  • 根据返回值类型推断视图名: 如果处理器方法的返回值类型是String类型,并且没有使用 @ResponseBody 注解,Spring MVC 会将返回的字符串作为视图名处理。
  • 默认视图名: 如果以上两种方式都没有找到视图名,Spring MVC 会使用默认的视图名。默认的视图名通常是处理器方法所在的类名转换而来,再加上适当的前后缀。

        String找视图,是根据返回值的名称去找同名的视图。

        未被@ResponseBody 注解控制的对象类型返回值,找视图时,如果方法上使用 @RequestMapping("/") 及其派生注解声明了路径,则按照路径的值去匹配视图。如果没有,则需要手动指定路径。

        HttpEntity、HttpHeaders、加上了@ResponseBody 注解的对象返回值类型,因为对应解析器的handleReturnValue 方法中标记了请求已经被处理,无需继续渲染视图,所以不走渲染视图流程。其区别在于返回的响应头和响应体的完整性。

4、ResponseBodyAdvice

       ResponseBodyAdvice是@ControllerAdvice三大增强中的一种,前面提到过@InitBinder@ModelAttribute。

       ResponseBodyAdvice可用于返回值的统一处理,例如在项目开发中,通常会把返回信息封装成一个统一返回类:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
    private int code;
    private String msg;
    private Object data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @JsonCreator
    private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
        this.code = code;
        this.data = data;
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public static Result ok() {
        return new Result(200, null);
    }

    public static Result ok(Object data) {
        return new Result(200, data);
    }

    public static Result error(String msg) {
        return new Result(500, "服务器内部错误:" + msg);
    }
}

        但是有些返回信息封装成了Result返回类,有一些是直接返回的,我们可以通过自定义一个类实现ResponseBodyAdvice接口做统一处理:

        实现了ResponseBodyAdvice接口需要重写其中两个方法

  • supports():判断支持转换的条件
  • beforeBodyWrite():增强的逻辑
 @ControllerAdvice
    static class MyResponseAdvice implements ResponseBodyAdvice<Object> {

        /**
         * 判断支持转换的条件
         *
         * @param returnType    返回值类型
         * @param converterType 转换类型
         * @return
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            //如果方法上加了ResponseBody注解,或者类上加了ResponseBody/RestController注解,才进行转换
            if (returnType.getMethodAnnotation(ResponseBody.class) != null
                    || AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
                return true;
            }
            return false;
        }

        /**
         * 增强的逻辑
         *
         * @param body                  返回值
         * @param returnType            返回类型
         * @param selectedContentType   所选的响应内容类型。
         * @param selectedConverterType 所选的消息转换器类型。
         * @param request               当前的请求对象。
         * @param response              当前的响应对象。
         * @return
         */
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            //如果返回值就是Result类型则直接返回
            if (body instanceof Result) {
                return body;
            }
            //否则包装成Result类型返回
            return Result.ok(body);
        }
    }

        和自定义参数解析器,返回值解析器类似。