• Spring Data JPA 之 自定义 HandlerMethod-ArgumentResolver


    16 自定义 HandlerMethod-ArgumentResolver

    上⼀讲我们介绍了 SpringDataWebConfiguration 类的⽤法,那么这次我们来看⼀下这个类是如何被加载的,PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver ⼜是如何⽣效的,以及如何定义⾃⼰的 HandlerMethodArgumentResolvers 类,还有没有其他 Web 场景需要我们⾃定义呢?

    16.1 Page 和 Sort 参数

    想要知道分⻚和排序参数的加载原理,我们可以通过源码发现是 @EnableSpringDataWebSupport 将这个类加载进去的,其关键代码如下图所示:

    在这里插入图片描述

    其中,@EnableSpringDataWebSupport 注解是上⼀讲讲解的核⼼,即 Spring Data JPA 对 Web ⽀持需要开启的⼊⼝,由于我们使⽤的是 Spring Boot,所以 @EnableSpringDataWebSupport 不需要我们⼿动去指定。

    这是由于 Spring Boot 有⾃动加载的机制,我们会发现 org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration 类⾥⾯引⽤了 @EnableSpringDataWebSupport 的注解,所以也不需要我们⼿动去引⽤了。这⾥⾯的关键代码如下图所示:

    在这里插入图片描述

    ⽽ Spring Boot 的⾃动加载的核⼼⽂件就是 spring.factories ⽂件,那么我们打开 spring-boot-autoconfigure-2.3.3.jar 包,看⼀下 spring.factories ⽂件内容,可以找到 SpringDataWebAutoConfiguration 这个配置类,如下:

    在这里插入图片描述

    所以可以得出结论:只要是 Spring Boot 项⽬,我们什么都不需要做,它就会天然地让 Spring Data JPA ⽀持 Web 相关的操作。

    ⽽ PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver 两个类是通过 SpringDataWebConfiguration 加载进去的,所以我们基本可以知道 Spring Data JPA 的 Page 和 Sort 参数是因为 SpringDataWebConfiguration ⾥⾯ @Bean 的注⼊才⽣效的。

    在这里插入图片描述

    通过 PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver 这两个类的源码,我们可以分析出它们分别实现了 Spring MVC Web 框架⾥⾯的 org.springframework.web.method.support.HandlerMethodArgumentResolver 这个接⼝,从⽽对 Request ⾥⾯的 Page 和 Sort 的参数做了处理逻辑和解析逻辑。

    那么在实际⼯作中,可能存在特殊情况需要对其进⾏扩展,⽐如 Page 的参数可能需要⽀持多种 Key 的情况,那么我们应该怎么做呢?下⾯通过 HandlerMethodArgumentResolver 的⽤法来学习⼀下。

    16.2 HandlerMethodArgumentResolver 的用法

    16.2.1 HandlerMethodArgument-Resolver 详解

    熟悉 MVC 的⼈都知道,HandlerMethodArgumentResolvers 在 Spring MVC 中的主要作⽤是对 Controller ⾥⾯的⽅法参数做解析,即可以把 Request ⾥⾯的值映射到⽅法的参数中。我们打开此类的源码会发现只有两个⽅法,如下所示:

    public interface HandlerMethodArgumentResolver {
        /**
         * 检查⽅法的参数是否⽀持处理和转化
         */
        boolean supportsParameter(MethodParameter parameter);
    
        /**
         * 根据 request 上下⽂,解析⽅法的参数
         */
        @Nullable
        Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                               NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    此接⼝的应⽤场景⾮常⼴泛,我们可以看到其⼦类⾮常多,如下图所示:

    在这里插入图片描述

    其中⼏个类的作⽤如下:

    • PathVariableMapMethodArgumentResolver 专⻔解析 @PathVariable ⾥⾯的值;
    • RequestResponseBodyMethodProcessor 专⻔解析带 @RequestBody 注解的⽅法参数的值;
    • RequestParamMethodArgumentResolver 专⻔解析 @RequestParam 的注解参数的值,当⽅法的参数中没有任何注解的时候,默认是 @RequestParam;
    • 以及我们上⼀讲提到的 PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver。

    到这⾥你会发现,我们上⼀讲还讲解了 HttpMessageConverter,那么它和 HandlerMethodArgumentResolvers 是什么关系呢?我们接着看。

    16.2.2 与 HttpMessageConverter 的关系

    我们打开 RequestResponseBodyMethodProcessor 就会发现,这个类中主要处理的是,⽅法⾥⾯带 @RequestBody 注解的参数,如下图所示:

    在这里插入图片描述

    其中的 readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()) ⽅法,如果我们点进去继续观察,发现⾥⾯会根据 Http 请求的 MediaType,来选择不同的 HttpMessageConverter 进⾏转化。所以到这⾥你可以很清楚 HandlerMethodArgumentResolvers 与 HttpMessageConverter 的关系了,即不同的 HttpMessageConverter 都是由 RequestResponseBodyMethodProcessor 进⾏调⽤的。

    那么调⽤关系我们知道了,如此多的 HttpMessageConverter 之间是通过什么顺序执⾏的呢?

    16.2.3 HttpMessageConverter 的执行顺序

    当我们⾃定义 HandlerMethodArgumentResolver 时,通过下⾯的⽅法加载进去。

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(myPageableHandlerMethodArgumentResolver);
    }
    
    • 1
    • 2
    • 3
    • 4

    List ⾥⾯⾃定义的 resolver 的优先级是最⾼的,也就是会优先执⾏ HandlerMethodArgumentResolver 之后,才会按照顺序执⾏系统⾥⾯⾃带的那⼀批 HttpMessageConverter,按照 List 的循环顺序⼀个⼀个执⾏。

    Spring ⾥⾯有个执⾏效率问题,就是⼀旦⼀次执⾏找到了需要的 HandlerMethodArgumentResolver 的时候,利⽤ Spring 中的缓存机制,执⾏过程中就不会再遍历 List 了,⽽是直接⽤上次找到的 HandlerMethodArgumentResolver,这样提升了执⾏效率。

    如果想要了解更多的 Resolver,你可以看 org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver

    16.3 HandlerMethodArgumentResolver 实战

    16.3.1 自定义 HandlerMethod-ArgumentResolver

    在实际的⼯作中,你可能会遇到对⽼项⽬进⾏改版的⼯作,如果要我们把旧的 API 接⼝改造成 JPA 的技术实现,那么可能会出现需要新、⽼参数的问题。假设在实际场景中,我们 Page 的参数是 page[number],⽽ page size 的参数是 page[size],看看应该怎么做。

    第⼀步:新建 MyPageableHandlerMethodArgumentResolver。

    这个类的作⽤有两个:

    1. ⽤来兼容 ?page[size]=2&page[number]=0 的参数情况;
    2. ⽀持 JPA 新的参数形式 ?size=2&page=0。

    我们通过⾃定义的 MyPageableHandlerMethodArgumentResolver 来实现这个需求,请看下⾯这段代码。

    @Component
    public class MyPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
        /**
         * 我们假设 sort 的参数没有发⽣变化,采⽤ PageableHandlerMethodArgumentResolver ⾥⾯的写法
         */
        private static final SortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new SortHandlerMethodArgumentResolver();
    
        /**
         * 给定两个默认值
         */
        private static final Integer DEFAULT_PAGE = 0;
        private static final Integer DEFAULT_SIZE = 10;
    
        /**
         * 兼容新版,引⼊JPA的分⻚参数
         */
        private static final String JPA_PAGE_PARAMETER = "page";
        private static final String JPA_SIZE_PARAMETER = "size";
    
        /**
         * 兼容原来⽼的分⻚参数
         */
        private static final String DEFAULT_PAGE_PARAMETER = "page[number]";
        private static final String DEFAULT_SIZE_PARAMETER = "page[size]";
        private final SortArgumentResolver sortResolver;
    
    
        /**
         * 模仿 PageableHandlerMethodArgumentResolver ⾥⾯的构造⽅法
         */
        public MyPageableHandlerMethodArgumentResolver(@Nullable SortArgumentResolver sortResolver) {
            this.sortResolver = sortResolver == null ? DEFAULT_SORT_RESOLVER : sortResolver;
        }
    
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            // 假设⽤我们⾃⼰的类 MyPageRequest 接收参数
            return MyPageRequest.class.equals(parameter.getParameterType());
            // 同时我们也可以⽀持通过 Spring Data JPA ⾥⾯的 Pageable 参数进⾏接收,两种效果是⼀样的
            // return Pageable.class.equals(parameter.getParameterType());
        }
    
        /**
         * 参数封装逻辑 page 和 sort,JPA 参数的优先级⾼于 page[number] 和 page[size] 参数
         */
        @Override
        public MyPageRequest resolveArgument(MethodParameter parameter,
                                             ModelAndViewContainer mavContainer,
                                             NativeWebRequest webRequest,
                                             WebDataBinderFactory binderFactory) {
            String jpaPageString = webRequest.getParameter(JPA_PAGE_PARAMETER);
            String jpaSizeString = webRequest.getParameter(JPA_SIZE_PARAMETER);
            // 我们分别取参数⾥⾯ page、sort 和 page[number]、page[size] 的值
            String pageString = webRequest.getParameter(DEFAULT_PAGE_PARAMETER);
            String sizeString = webRequest.getParameter(DEFAULT_SIZE_PARAMETER);
            // 当两个都有值时候的优先级,及其默认值的逻辑
            Integer page = jpaPageString != null ? Integer.valueOf(jpaPageString) : pageString != null ? Integer.valueOf(pageString) : DEFAULT_PAGE;
            // 在这⾥同时可以计算 page+1 的逻辑;如:page=page+1;
            Integer size = jpaSizeString != null ? Integer.valueOf(jpaSizeString) : sizeString != null ? Integer.valueOf(sizeString) : DEFAULT_SIZE;
            // 我们假设,sort排序的取值⽅法先不发⽣改变
            Sort sort = sortResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
            // 如果使⽤ Pageable 参数接收值,我们也可以不⽤⾃定义 MyPageRequest 对象,直接返回 PageRequest;
            // return PageRequest.of(page,size,sort);
            // 将 page 和 size 计算出来的记过封装到我们⾃定义的 MyPageRequest 类⾥⾯去
            // 返回 controller ⾥⾯的参数需要的对象;
            return new MyPageRequest(page, size, sort);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    你可以通过代码⾥⾯的注释仔细看⼀下其中的逻辑,其实这个类并不复杂,就是取 Request 的 Page 相关的参数,封装到对象中返回给 Controller 的⽅法参数⾥⾯。其中 MyPageRequest 不是必需的,我只是为了给你演示不同的做法。

    第⼆步:新建 MyPageRequest。

    /**
     * 继承⽗类,可以省掉很多计算 page 和 index 的逻辑
     */
    public class MyPageRequest extends PageRequest {
        public MyPageRequest(int page, int size, Sort sort) {
            super(page, size, sort);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    此类,我们⽤来接收 Page 相关的参数值,也不是必需的。

    第三步:implements WebMvcConfigurer 加载 myPageableHandlerMethodArgumentResolver。

    public class MyWebMvcConfigurer implements WebMvcConfigurer {
        @Autowired
        private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
    
        /**
         * 覆盖这个⽅法,把我们⾃定义的 myPageableHandlerMethodArgumentResolver 加载到原始的 mvc 的 resolvers ⾥⾯去
         */
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(myPageableHandlerMethodArgumentResolver);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这⾥我利⽤ Spring MVC 的机制加载我们⾃定义的 myPageableHandlerMethodArgumentResolver,由于⾃定义的优先级是最⾼的,所以⽤ MyPageRequest.class 和 Pageable.class 都是可以的。

    第四步:我们看下 Controller ⾥⾯的写法。

    /** 
     * ⽤ Pageable 这种⽅式也是可以的
     */
    @GetMapping("/users")
    public Page<User> queryByPage(Pageable pageable, User user) {
        return userRepository.findAll(Example.of(user), pageable);
    }
    
    /**
     * ⽤ MyPageRequest 进⾏接收
     */
    @GetMapping("/users/mypage")
    public Page<User> queryByMyPage(MyPageRequest pageable, User user) {
        return userRepository.findAll(Example.of(user), pageable);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    你可以看到,这⾥利⽤ Pageable 和 MyPageRequest 两种⽅式都是可以的。

    第五步:启动项⽬测试⼀下。

    我们依次可以测试下⾯两种情况,发现都是可以正常⼯作的。

    ###
    GET http://127.0.0.1:8080/users?page[size]=2&page[number]=0&ages=10&sort=id,desc
    
    ###
    GET http://127.0.0.1:8080/users?size=2&page=0&ages=10&sort=id,desc
    
    ###
    GET http://127.0.0.1:8080/users/mypage?page[size]=2&page[number]=0&ages=10&sort=id,desc
    
    ###
    GET http://127.0.0.1:8080/users/mypage?size=2&page=0&ages=10&sort=id,desc
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中,你应该可以注意到,我演示的 Controller ⽅法⾥⾯有多个参数的,每个参数都各司其职,找到⾃⼰对应的 HandlerMethodArgumentResolver,这正是 Spring MVC 框架的优雅之处。

    那么除了上⾯的 Demo,⾃定义 HandlerMethodArgumentResolver 对我们的实际⼯作还有什么建议呢

    16.3.2 实际工作中的四种常见场景

    场景⼀

    当我们在 Controller ⾥⾯处理某些参数时,重复的步骤⾮常多,那么我们就可以考虑写⼀下⾃⼰的框架,来处理请求⾥⾯的参数,⽽ Controller ⾥⾯的代码就会变得⾮常优雅,不需要关⼼其他框架代码,只要知道⽅法的参数有值就可以了。

    场景⼆

    再举个例⼦,在实际⼯作中需要注意的是,默认 JPA ⾥⾯的 Page 是从 0 开始,⽽我们可能有些⽼的代码也要维护,因为⽼的代码⼤多数的 Page 都会从 1 开始。如果我们不⾃定义 HandlerMethodArgumentResolver,那么在⽤到分⻚时,每个 Controller 的⽅法⾥⾯都需要关⼼这个逻辑。那么这个时候你就应该想到上⾯列举的⾃定义 MyPageableHandlerMethodArgumentResolver 的 resolveArgument ⽅法的实现,使⽤这种⽅法我们只需要在⾥⾯修改 Page 的计算逻辑即可。

    场景三

    再举个例⼦,在实际的⼯作中,还经常会遇到“取当前⽤户”的应⽤场景。此时,普通做法是,当使⽤到当前⽤户的 UserInfo 时,每次都需要根据请求 header 的 token 取到⽤户信息,伪代码如下所示:

    @PostMapping("user/info")
    public User getUserInfo(@RequestHeader String token) {
        // 伪代码
        Long userId = redisTemplate.get(token);
        return userRepository.getById(userId);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果我们使⽤ HandlerMethodArgumentResolver 接⼝来实现,代码就会变得优雅许多。伪代码如下:

    @Component
    public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
        /**
         * 伪代码,假设我们 token 是放在 redis ⾥⾯的
         */
        private final RedisTemplate<String, Object> redisTemplate;
        private final UserInfoRepository userInfoRepository;
    
        public UserInfoArgumentResolver(RedisTemplate<String, Object> redisTemplate,
                                        UserInfoRepository userInfoRepository) {
            this.redisTemplate = redisTemplate;
            this.userInfoRepository = userInfoRepository;
        }
    
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return UserInfo.class.isAssignableFrom(parameter.getParameterType());
        }
    
        @Override
        public Object resolveArgument(MethodParameter parameter,
                                      ModelAndViewContainer mavContainer,
                                      NativeWebRequest webRequest,
                                      WebDataBinderFactory binderFactory) throws Exception {
            HttpServletRequest nativeRequest = (HttpServletRequest)
                    webRequest.getNativeRequest();
            String token = nativeRequest.getHeader("token");
            Long userId = (Long) redisTemplate.opsForValue().get(token);
            return userInfoRepository.getById(userId);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    然后我们只需要在 MyWebMvcConfigurer ⾥⾯把 userInfoArgumentResolver 添加进去即可,关键代码如下:

    @Configuration
    public class MyWebMvcConfigurer implements WebMvcConfigurer {
    
        @Autowired
        private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
        @Autowired
        private UserInfoArgumentResolver userInfoArgumentResolver;
    
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(myPageableHandlerMethodArgumentResolver);
            // 我们只需要把 userInfoArgumentResolver 加⼊ resolvers 中即可
            resolvers.add(userInfoArgumentResolver);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在 Controller 中使⽤

    @RestController
    public class UserInfoController {
        
        // 获得当前⽤户的信息
        @GetMapping("user/info")
        public UserInfo getUserInfo(UserInfo userInfo) {
            return userInfo;
        }
        
        // 给当前⽤户 say hello
        @PostMapping("sayHello")
        public String sayHello(UserInfo userInfo) {
            return "hello " + userInfo.getTelephone();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上述代码可以看到,在 Contoller ⾥⾯可以完全省掉根据 token 从 redis 取当前⽤户信息的过程,优化了操作流程。

    场景四

    有的时候我们也会更改 Pageable 的默认值和参数的名字,也可以在 application.properties 的⽂件⾥⾯通过如下的 Key 值对⾃定义进⾏配置,如下所示:

    spring:
      data:
        web:
          pageable:
            # 默认页面大小
            default-page-size: 20
            # 接受的最大页面大小
            max-page-size: 2000
            # 是否基于 1 的页码索引。默认为 false,表示请求中的页码 0 等于第一页。
            one-indexed-parameters: false
            # 页面索引参数名称
            page-parameter: page
            # 页面大小参数名称
            size-parameter: size
            # 附加到页码和页面大小参数的前缀
            prefix:
            # 在限定符与实际页码和大小属性之间使用的分隔符
            qualifier-delimiter: _
          sort:
            # 排序参数名称
            sort-parameter: sort
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    关于 Spring MVC 和 Spring Data 相关的参数处理,你通过了解上⾯的内容并动⼿操作⼀下,基本上就可以掌握了。但是实际⼯作肯定不会这么简单,还会遇到 WebMvcConfigurer ⾥⾯其他⽅法的需求,我顺带给你介绍⼀下。

    16.4 思路拓展

    16.4.1 WebMvcConfigurer 介绍

    当我们做 Spring 的 MVC 开发的时候,可能会通过实现 WebMvcConfigurer 去做⼀些公⽤的业务逻辑,下⾯我列举⼏个常⻅的⽅法,⽅便你了解。

    public interface WebMvcConfigurer {
    
    
        /**
         * 添加 Spring MVC 生命周期拦截器,用于控制器方法调用和资源处理程序请求的预处理和后处理。
         * 可以注册拦截器以应用于所有请求或仅限于 URL 模式的子集。
         */
        default void addInterceptors(InterceptorRegistry registry) {
        }
    
        /**
         * 添加处理程序以从 Web 应用程序根目录、类路径等的特定位置提供静态资源,例如图像、js 和 css 文件。
         */
        default void addResourceHandlers(ResourceHandlerRegistry registry) {
        }
    
        /**
         * 配置“全局”跨源请求处理。配置的 CORS 映射适用于带注释的控制器、功能端点和静态资源。
         */
        default void addCorsMappings(CorsRegistry registry) {
        }
    
        /**
         * 配置预先配置了响应状态代码和/或视图以呈现响应正文的简单自动化控制器。
         * 这在不需要自定义控制器逻辑的情况下很有用——例如呈现主页、执行简单的站点 URL 重定向、返回带有 HTML 内容的 404 状态、没有内容的 204 等等。
         */
        default void addViewControllers(ViewControllerRegistry registry) {
        }
    
        /**
         * 配置视图解析器以将从控制器返回的基于字符串的视图名称转换为具体的 org.springframework.web.servlet.View 实现以执行渲染。
         */
        default void configureViewResolvers(ViewResolverRegistry registry) {
        }
    
        /**
         * 添加解析器以支持自定义控制器方法参数类型。
         */
        default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        }
    
        /**
         * 添加处理程序以支持自定义控制器方法返回值类型。
         * 使用此选项不会覆盖对处理返回值的内置支持。要自定义处理返回值的内置支持,请直接配置 RequestMappingHandlerAdapter。
         */
        default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        }
    
        /**
         * 将 HttpMessageConverter 配置为从请求正文读取和写入响应正文。
         */
        default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        }
    
        /**
         * 使用默认列表configured或初始化后,扩展或修改转换器列表。
         */
        default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        }
    
        /**
         * 配置异常解析器。
         */
        default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        }
    
        /**
         * 扩展或修改默认配置的异常解析器列表。这对于插入自定义异常解析器而不干扰默认异常解析器很有用。
         */
        default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        }
    
        /**
         * 提供自定义验证器,而不是默认创建的验证器。假设 JSR-303 在类路径上,
         * 默认实现是: org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean。
         * 将返回值保留为null以保持默认值。
         */
        @Nullable
        default Validator getValidator() {
            return null;
        }
    
        /**
         * 提供自定义 MessageCodesResolver 用于从数据绑定和验证错误代码构建消息代码。将返回值保留为 null 以保持默认值。
         */
        @Nullable
        default MessageCodesResolver getMessageCodesResolver() {
            return null;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    当我们实现 Restful ⻛格的 API 协议时,会经常看到其对 json 响应结果进⾏了统⼀的封装,我们也可以采⽤ HandlerMethodReturnValueHandler 来实现,再来看⼀个例⼦。

    16.4.2 对JSON的返回结果进行统一封装

    下⾯通过五个步骤来实现⼀个通过⾃定义注解,利⽤HandlerMethodReturnValueHandler 实现 JSON 结果封装的例⼦。

    第⼀步:我们⾃定义⼀个注解 @WarpWithData,表示此注解包装的返回结果⽤ Data 进⾏包装,代码如下:

    /**
     * ⾃定义⼀个注解对返回结果进⾏包装
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface WarpWithData {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第⼆步:⾃定义 MyWarpWithDataHandlerMethodReturnValueHandler,并继承 RequestResponseBodyMethodProcessor 来实现 HandlerMethodReturnValueHandler 接⼝,⽤来处理 Data 包装的结果,代码如下:

    /**
     * ⾃定义⾃⼰的 return 的处理类,我们直接继承 RequestResponseBodyMethodProcessor,这样⽗类⾥⾯的⽅法我们直接使⽤就可以了
     */
    @Component
    public class MyWarpWithDataHandlerMethodReturnValueHandler extends
            RequestResponseBodyMethodProcessor implements HandlerMethodReturnValueHandler {
    
    
        /**
         * 参考⽗类 RequestResponseBodyMethodProcessor 的做法
         */
        @Autowired
        public MyWarpWithDataHandlerMethodReturnValueHandler(List<HttpMessageConverter<?>> converters) {
            super(converters);
        }
    
        /**
         * 只处理需要包装的注解的⽅法
         */
        @Override
        public boolean supportsReturnType(MethodParameter returnType) {
            return returnType.hasMethodAnnotation(WarpWithData.class);
        }
    
        //将返回结果包装⼀层Data
        @Override
        public void handleReturnValue(Object returnValue,
                                      MethodParameter methodParameter,
                                      ModelAndViewContainer modelAndViewContainer,
                                      NativeWebRequest nativeWebRequest) throws IOException, HttpMediaTypeNotAcceptableException {
            Map<String, Object> res = new HashMap<>();
            res.put("data", returnValue);
            super.handleReturnValue(res, methodParameter, modelAndViewContainer, nativeWebRequest);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    第三步:在 MyWebMvcConfigurer ⾥⾯直接把 myWarpWithDataHandlerMethodReturnValueHandler 加⼊ handlers ⾥⾯即可,也是通过覆盖⽗类 WebMvcConfigurer ⾥⾯的 addReturnValueHandlers ⽅法完成的,关键代码如下:

    @Configuration
    public class ReturnValueConfiguration {
        @Autowired
        private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    
        @Autowired
        private MyWarpWithDataHandlerMethodReturnValueHandler myWarpWithDataHandlerMethodReturnValueHandler;
    
        /**
         * 由于 HandlerMethodReturnValueHandler 处理的优先级问题,我们通过如下⽅法,把我们⾃定义的 myWarpWithDataHandlerMethodReturnValueHandler 放到第⼀个;
         */
        @PostConstruct
        public void init() {
            List<HandlerMethodReturnValueHandler> returnValueHandlers = Lists.newArrayList(myWarpWithDataHandlerMethodReturnValueHandler);
            // 取出原始列表,重新覆盖进去;
            returnValueHandlers.addAll(requestMappingHandlerAdapter.getReturnValueHandlers());
            requestMappingHandlerAdapter.setReturnValueHandlers(returnValueHandlers);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这⾥需要注意的是,我们利⽤ @PostConstruct 调整了⼀下 HandlerMethodReturnValueHandler 加载的优先级,使其⽣效。

    第四步:Controller ⽅法中直接加上 @WarpWithData 注解,关键代码如下:

    @GetMapping("/user/{id}")
    @WarpWithData
    public User getUserInfoFromPath(@PathVariable("id") Long id) {
        return userRepository.getById(id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第五步:我们测试⼀下。

    GET http://127.0.0.1:8089/user/1
    
    • 1

    就会得到如下结果,你会发现我们的 JSON 结果多了⼀个 Data 的包装。

    {
      "data": {
        "id": 1,
        "version": 0,
        "deleted": false,
        "createUserId": 1333938156,
        "createdDate": "2022-08-06T17:54:32.16",
        "lastModifiedUserId": 1333938156,
        "lastModifiedDate": "2022-08-06T17:54:32.16",
        "name": "zzn",
        "email": "973536793@qq.com",
        "sex": "BOY",
        "age": 18
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我们通过五个步骤,利⽤ Spring MVC 的扩展机制,实现了对返回结果的格式统⼀处理。不知道你是否掌握了这种⽅法,希望你可以多多实践,将它运⽤得更好。

    16.5 本章小结

    以上就是这⼀讲的内容了。在这⼀讲中,我通过原理分析、语法讲解、实战经验分享,帮助你掌握了 HandlerMethodArgumentResolvers 的详细⽤法,并为你扩展了学习思路,了解了 HandlerMethodReturnValueHandler 的⽤法。

  • 相关阅读:
    书籍数组中的最长连续序列(4)0716
    自媒体人必看的9个网站,每一个都很实用,值得收藏
    【论文阅读】Uformer:A General U-Shaped Transformer for Image Restoration
    嵌入式Linux入门-代码重定位和清除bss段讲解
    软文推广效果怎么样?这篇揭晓答案
    Prim算法
    最好的蓝牙耳机是哪种?品牌蓝牙耳机排行榜
    多项式——多项式牛顿迭代
    4核16G服务器价格腾讯云PK阿里云
    二百零一、Flink——Flink配置状态后端运行后报错:Can not create a Path from an empty string
  • 原文地址:https://blog.csdn.net/qq_40161813/article/details/126322805