zoukankan      html  css  js  c++  java
  • Spring boot 自定义 Resolver 支持 interface 类型参数

    在编写 RestController 层的代码时,由于数据实体类定义了接口及实现类,本着面向接口编程的原则,我使用了接口作为 RestController 方法的入参。

    代码大致如下(省略具体业务部分):

    (1)模型接口:

     1 public interface User {
     2 
     3     long getUserId();
     4 
     5     void setUserId(long userId);
     6 
     7     String getUserName();
     8 
     9     void setUserName(String userName);
    10 
    11     String getCategory();
    12 
    13     void setCategory(String category);
    14 }
    View Code

    (2)模型实现类

     1 public class UserImpl implements User{
     2     private long userId;
     3     private String userName;
     4     private String category;
     5     
     6     @Override
     7     public long getUserId() {
     8         return userId;
     9     }
    10 
    11     @Override
    12     public void setUserId(long userId) {
    13         this.userId = userId;
    14     }
    15 
    16     @Override
    17     public String getUserName() {
    18         return userName;
    19     }
    20 
    21     @Override
    22     public void setUserName(String userName) {
    23         this.userName = userName;
    24     }
    25 
    26     @Override
    27     public String getCategory() {
    28         return category;
    29     }
    30 
    31     @Override
    32     public void setCategory(String category) {
    33         this.category = category;
    34     }
    35 
    36 }
    View Code

    (3)RestController POST接口代码

    1     @PostMapping(value = "/updateUser", consumes = MediaType.APPLICATION_JSON_VALUE)
    2     public long updateUser(HttpSession session, @RequestBody User user) {
    3         System.out.println(session.getId());
    4 
    5         System.out.println(user.getUserName());
    6         System.out.println(user.getUserId());
    7         return user.getUserId();
    8     }
    View Code

    (4)前台用的axios发送的请求代码

     1 const AXIOS = axios.create({
     2   baseURL: 'http://localhost:9999',
     3   withCredentials: false,
     4   headers: {
     5     Accept: 'application/json',
     6     'Content-type': 'application/json'
     7   }
     8 })
     9 
    10 AXIOS.post('/updateUser', {
    11   userName: 'testName',
    12   userId: '123456789',
    13   category: 'XX'
    14 })
    View Code

    但在运行测试时发现 Spring boot 本身的默认中并不支持将interface或抽象类作为方法的参数。报了如下错误:

    2019-09-08 19:32:22.290 ERROR 12852 --- [nio-9999-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]    
    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
    [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException:
    Type definition error: [simple type, class com.sample.demo.model.User]; nested exception is com.fasterxml.jackson.databind
    .exc.InvalidDefinitionException: Cannot construct instance of `com.sample.demo.model.User` (no Creators, like default
    construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer,
    or contain additional type information at [Source: (PushbackInputStream); line: 1, column: 1]] with root cause
    ...

    大致意思时不存在创建实例的构造函数,抽象类型需要配置映射到具体的实现类。

    解决方案一:

    于是我上网搜了下解决方法,最终在 StackOverflow 上找到一种解决方案:

    @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
    @JsonSubTypes({@JsonSubTypes.Type(value = A.class, name = "A"),
            @JsonSubTypes.Type(value = B.class, name = "B")})
    public interface MyInterface {
    
    }

    通过添加注解的方式,将接口映射到实现类。

    这种方法可以解决方法入参为接口的问题,但同时又会引入一个问题:接口和实现类相互引用,导致循环依赖。而且如果我有很多数据类的接口及实现类的话,每个接口都要写一遍注解。

    于是继续探索。。。

    解决方案二:

    继承 HandlerMethodArgumentResolver  接口实现里面的 supportsParameter  和  resolveArgument 方法。

    (1)在supportsParameter 方法中返回支持的类型。其中MODEL_PATH为实体类的包路径,下列代码中默认支持了包内的所有类型。

    1     @Override
    2     public boolean supportsParameter(MethodParameter parameter) {
    3         return parameter.getParameterType().getName().startsWith(MODEL_PATH);
    4     }
    View Code

    (2)在 resolveArgument 方法中,通过反射生成一个实现类的对象并返回。

     1  @Override
     2     public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer,
     3                                   NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
     4         Class<?> parameterType = parameter.getParameterType();
     5         String implName = parameterType.getName() + SUFFIX;
     6         Class<?> implClass = Class.forName(implName);
     7 
     8         if (!parameterType.isAssignableFrom(implClass)) {
     9             throw new IllegalStateException("type error:" + parameterType.getName());
    10         }
    11 
    12         Object impl = implClass.newInstance();
    13         WebDataBinder webDataBinder = webDataBinderFactory.createBinder(nativeWebRequest, impl, parameter.getParameterName());
    14         ServletRequest servletRequest = nativeWebRequest.getNativeRequest(ServletRequest.class);
    15         Assert.notNull(servletRequest, "servletRequest is null.");
    16 
    17         ServletRequestDataBinder servletRequestDataBinder = (ServletRequestDataBinder) webDataBinder;
    18         servletRequestDataBinder.bind(servletRequest);
    19         return impl;
    20     }
    View Code

    (3)最后添加到Spring boot 的配置中

     1     @Bean
     2     public WebMvcConfigurer webMvcConfigurer() {
     3         return new WebMvcConfigurerAdapter() {
     4             @Override
     5             public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
     6                 argumentResolvers.add(new MethodInterfaceArgumentResolver());
     7                 super.addArgumentResolvers(argumentResolvers);
     8             }
     9         };
    10     }
    View Code

    方案二可以解决找不到构造函数的问题,运行不会报错,也不会导致循环依赖,但却没法将前台的数据注入到入参对象中。也就是给方法传入的只是一个刚new出来的UserImpl 对象。

    经过测试发现,虽然对post请求无法注入前台数据,但对于get请求,还是可以的:

    前台get方法代码:

    AXIOS.get('/getUser?userName=Haoye&userId=123456789&category=XX')

    后台get方法代码:

    1     @GetMapping("/getUser")
    2     public User getUser(User user) {
    3         System.out.println(user.getUserName());
    4         return user;
    5     }

    解决方案三:

    由于在网上没有找到好的解决方案,我最后通过看Spring boot 源码 + 调试跟踪 + 写demo尝试的方式,终于找到了好的解决方案。

    这里先分享下大致的思路:

    (1)Spring boot的相关代码应该在 HandlerMethodArgumentResolver 接口对应的包里或者附近。但这样找还是比较慢,因为代码还是很多。

    (2)通过打断点,看看哪里调用了 public boolean supportsParameter(MethodParameter parameter) 方法。

    于是找到了HandlerMethodArgumentResolverComposite 类调用的地方:

      从上图可以看到,当前处理的是第一个参数HttpSession。

    (3)先将controller方法的入参先改为UserImpl,也就是实现类,在步骤(2)的截图对应的代码中打断点。

    继续调试,找到Spring boot 解析被@RequestBody 注解标注的参数UserImpl user 的时候,用的是什么Resolver。

    如下图所示,调用Evaluate窗口获取类型信息,点击 Navigate 跳转到对应的类 RequestResponseBodyMethodProcessor。

     (4) RequestResponseBodyMethodProcessor 类中的 resolveArgument 方法源码如下:

     1     /**
     2      * Throws MethodArgumentNotValidException if validation fails.
     3      * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
     4      * is {@code true} and there is no body content or if there is no suitable
     5      * converter to read the content with.
     6      */
     7     @Override
     8     public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
     9             NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    10 
    11         parameter = parameter.nestedIfOptional();
    12         Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    13         String name = Conventions.getVariableNameForParameter(parameter);
    14 
    15         if (binderFactory != null) {
    16             WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
    17             if (arg != null) {
    18                 validateIfApplicable(binder, parameter);
    19                 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    20                     throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
    21                 }
    22             }
    23             if (mavContainer != null) {
    24                 mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
    25             }
    26         }
    27 
    28         return adaptArgumentIfNecessary(arg, parameter);
    29     }
    View Code

    回到最初的问题,导致无法传入interface类型参数的原因是接口无法实例化。那既然如此,我们要修改的地方肯定是Spring boot 尝试实例化接口的地方,也就是实例化失败进而抛出异常的地方。

    一路顺腾摸瓜,最终发现 readWithMessageConverters 方法中, 通过给 readWithMessageConverters 方法传入类型信息,最终生成参数实例。

    (5) 从(4)中可以看到,相关方法的访问级别为 protected,也就是我们可以通过继承 RequestResponseBodyMethodProcessor 并覆写 readWithMessageConverters 即可。

    通过反射,注入 User 接口的实现类型 UserImpl 的class:

     1 package com.sample.demo.config;
     2 
     3 import org.springframework.core.MethodParameter;
     4 import org.springframework.http.converter.HttpMessageConverter;
     5 import org.springframework.http.converter.HttpMessageNotReadableException;
     6 import org.springframework.web.HttpMediaTypeNotSupportedException;
     7 import org.springframework.web.context.request.NativeWebRequest;
     8 import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
     9 
    10 import java.io.IOException;
    11 import java.lang.reflect.Type;
    12 import java.util.List;
    13 
    14 /**
    15  * @breaf
    16  * @author https://cnblogs.com/laishenghao
    17  * @date 2019/9/7
    18  * @since 1.0
    19  **/
    20 public class ModelRequestBodyMethodArgumentResolver extends RequestResponseBodyMethodProcessor {
    21     private static final String MODEL_PATH = "com.sample.demo.model";
    22     private static final String SUFFIX = "Impl";
    23 
    24     public ModelRequestBodyMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
    25         super(converters);
    26     }
    27 
    28     @Override
    29     public boolean supportsParameter(MethodParameter methodParameter) {
    30         return super.supportsParameter(methodParameter)
    31             && methodParameter.getParameterType().getName().startsWith(MODEL_PATH);
    32     }
    33 
    34     @Override
    35     protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
    36             throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    37         try {
    38             Class<?> clazz = Class.forName(paramType.getTypeName() + SUFFIX);
    39             return super.readWithMessageConverters(webRequest, parameter, clazz);
    40         } catch (ClassNotFoundException e) {
    41             return null;
    42         }
    43     }
    44 
    45 }
    View Code

     完成上面的代码后,跑了一下,发现并没有什么用,报的错误还是跟最开始的一样。

    由此推测,应该是Spring boot 默认配置的 Resolver的优先级比较高,导致我们自定义的并没有生效。

    于是继续查找原因,发现自定义的Resolver的优先级几乎垫底了,在远未调用到之前就被它的父类抢了去。

    (6)提高自定义 Resolver的优先级。

    一个可行的方法是:在Spring boot 框架初始化完成后,获取到所有的Resolver,然后将自定义的加在ArrayList的前面。

     1 import org.springframework.beans.factory.annotation.Autowired;
     2 import org.springframework.context.annotation.Bean;
     3 import org.springframework.context.annotation.Configuration;
     4 import org.springframework.web.method.support.HandlerMethodArgumentResolver;
     5 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
     6 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
     7 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
     8 
     9 import javax.annotation.PostConstruct;
    10 import java.util.ArrayList;
    11 import java.util.List;
    12 
    13 /**
    14  * @breaf
    15  * @blog https://www.cnblogs.com/laishenghao
    16  * @date 2019/9/7
    17  * @since 1.0
    18  **/
    19 @Configuration
    20 public class CustomConfigurations {
    21     @Autowired
    22     private RequestMappingHandlerAdapter adapter;
    23 
    24     @PostConstruct
    25     public void prioritizeCustomArgumentMethodHandlers () {
    26         List<HandlerMethodArgumentResolver> allResolvers = adapter.getArgumentResolvers();
    27         if (allResolvers == null) {
    28             allResolvers = new ArrayList<>();
    29         }
    30         List<HandlerMethodArgumentResolver> customResolvers = adapter.getCustomArgumentResolvers ();
    31         if (customResolvers == null) {
    32             customResolvers = new ArrayList<>();
    33         }
    34         ModelRequestBodyMethodArgumentResolver argumentResolver = new ModelRequestBodyMethodArgumentResolver(adapter.getMessageConverters());
    35         customResolvers.add(0,argumentResolver);
    36 
    37         List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<> (allResolvers);
    38         argumentResolvers.removeAll (customResolvers);
    39         argumentResolvers.addAll (0, customResolvers);
    40         adapter.setArgumentResolvers (argumentResolvers);
    41     }
    42 }
    View Code

    值得注意的是,getResolvers()方法返回的是不可更改的List,不能直接插入。

    至此,自定义参数处理器就可以解析RestController标注的类中的方法的 interface类型参数了。

    如果要支持其他类型(比如抽象类、枚举类),或者使用自定义注解标注入参,也可以通过类似的方法来实现。

    本文地址:https://www.cnblogs.com/laishenghao/p/11488724.html

  • 相关阅读:
    构建一个真实的应用电子商务SportsStore3
    关于计算程序运行时间的方法汇总
    系统分析员备考之CMM篇
    大数据时代的技术hive:hive的数据类型和数据模型
    图片滚动图片的效果
    React初探
    所有的分页查询一览及数据导出(easyui + knouckoutjs + mvc4.0)
    golang微信公众平台之人脸识别
    测试rest接口的两个工具使用详解(restclient+soapUI)
    Linux系统服务基础
  • 原文地址:https://www.cnblogs.com/laishenghao/p/11488724.html
Copyright © 2011-2022 走看看