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

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

(1)模型接口:

 public interface User {

     long getUserId();

     void setUserId(long userId);

     String getUserName();

     void setUserName(String userName);

     String getCategory();

     void setCategory(String category);
}

(2)模型实现类

 public class UserImpl implements User{
private long userId;
private String userName;
private String category; @Override
public long getUserId() {
return userId;
} @Override
public void setUserId(long userId) {
this.userId = userId;
} @Override
public String getUserName() {
return userName;
} @Override
public void setUserName(String userName) {
this.userName = userName;
} @Override
public String getCategory() {
return category;
} @Override
public void setCategory(String category) {
this.category = category;
} }

(3)RestController POST接口代码

     @PostMapping(value = "/updateUser", consumes = MediaType.APPLICATION_JSON_VALUE)
public long updateUser(HttpSession session, @RequestBody User user) {
System.out.println(session.getId()); System.out.println(user.getUserName());
System.out.println(user.getUserId());
return user.getUserId();
}

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

 const AXIOS = axios.create({
baseURL: 'http://localhost:9999',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-type': 'application/json'
}
}) AXIOS.post('/updateUser', {
userName: 'testName',
userId: '123456789',
category: 'XX'
})

但在运行测试时发现 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为实体类的包路径,下列代码中默认支持了包内的所有类型。

     @Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().getName().startsWith(MODEL_PATH);
}

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

  @Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
Class<?> parameterType = parameter.getParameterType();
String implName = parameterType.getName() + SUFFIX;
Class<?> implClass = Class.forName(implName); if (!parameterType.isAssignableFrom(implClass)) {
throw new IllegalStateException("type error:" + parameterType.getName());
} Object impl = implClass.newInstance();
WebDataBinder webDataBinder = webDataBinderFactory.createBinder(nativeWebRequest, impl, parameter.getParameterName());
ServletRequest servletRequest = nativeWebRequest.getNativeRequest(ServletRequest.class);
Assert.notNull(servletRequest, "servletRequest is null."); ServletRequestDataBinder servletRequestDataBinder = (ServletRequestDataBinder) webDataBinder;
servletRequestDataBinder.bind(servletRequest);
return impl;
}

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

     @Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new MethodInterfaceArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
};
}

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

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

前台get方法代码:

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

后台get方法代码:

     @GetMapping("/getUser")
public User getUser(User user) {
System.out.println(user.getUserName());
return user;
}

解决方案三:

由于在网上没有找到好的解决方案,我最后通过看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 方法源码如下:

     /**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
} return adaptArgumentIfNecessary(arg, parameter);
}

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

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

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

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

 package com.sample.demo.config;

 import org.springframework.core.MethodParameter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List; /**
* @breaf
* @author https://cnblogs.com/laishenghao
* @date 2019/9/7
* @since 1.0
**/
public class ModelRequestBodyMethodArgumentResolver extends RequestResponseBodyMethodProcessor {
private static final String MODEL_PATH = "com.sample.demo.model";
private static final String SUFFIX = "Impl"; public ModelRequestBodyMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
super(converters);
} @Override
public boolean supportsParameter(MethodParameter methodParameter) {
return super.supportsParameter(methodParameter)
&& methodParameter.getParameterType().getName().startsWith(MODEL_PATH);
} @Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
try {
Class<?> clazz = Class.forName(paramType.getTypeName() + SUFFIX);
return super.readWithMessageConverters(webRequest, parameter, clazz);
} catch (ClassNotFoundException e) {
return null;
}
} }

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

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

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

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

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

 import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List; /**
* @breaf
* @blog https://www.cnblogs.com/laishenghao
* @date 2019/9/7
* @since 1.0
**/
@Configuration
public class CustomConfigurations {
@Autowired
private RequestMappingHandlerAdapter adapter; @PostConstruct
public void prioritizeCustomArgumentMethodHandlers () {
List<HandlerMethodArgumentResolver> allResolvers = adapter.getArgumentResolvers();
if (allResolvers == null) {
allResolvers = new ArrayList<>();
}
List<HandlerMethodArgumentResolver> customResolvers = adapter.getCustomArgumentResolvers ();
if (customResolvers == null) {
customResolvers = new ArrayList<>();
}
ModelRequestBodyMethodArgumentResolver argumentResolver = new ModelRequestBodyMethodArgumentResolver(adapter.getMessageConverters());
customResolvers.add(0,argumentResolver); List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<> (allResolvers);
argumentResolvers.removeAll (customResolvers);
argumentResolvers.addAll (0, customResolvers);
adapter.setArgumentResolvers (argumentResolvers);
}
}

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

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

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

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

最新文章

  1. C/C++语言,自学资源,滚动更新中&hellip;&hellip;
  2. FT部署图
  3. Opencv,腐蚀,膨胀,轮廓检测,轮廓外接多边形
  4. Selenium2+python自动化21-TXT数据参数化
  5. BestCoder——59
  6. 新Android学习计划
  7. C++习题 对象数组输入与输出
  8. Django之强大的Form功能
  9. 初学spring笔记
  10. python while and for
  11. 洛谷P5112 FZOUTSY
  12. 享元(FlyWeight)模式
  13. 如果没有 Android 世界会是什么样子?
  14. numpy细碎知识点
  15. 【Android】5.2 图像按钮和图片格式
  16. Vue如何引入远程JS文件
  17. App后台开发架构实践笔记
  18. 使用JDK自带的keytool工具生成证书
  19. Go语言,用原子函数atomic避免资源竞争
  20. 【BZOJ1053】[HAOI2007]反素数ant 暴力

热门文章

  1. table 表格 细边框 最简单样式
  2. Java虚拟机——Java内存区域
  3. Opengl_入门学习分享和记录_01_Graphics Pipeline(图形渲染管线)
  4. 基于.NET Core开发的个人博客发布至CentOS小计
  5. 100天搞定机器学习|day40-42 Tensorflow Keras识别猫狗
  6. 通过Blazor使用C#开发SPA单页面应用程序(4) - Ant Design Button
  7. k8s云集群混搭模式,可能帮你节省50%以上的服务成本
  8. c排序
  9. antd模糊搜索和远程数据的结合
  10. Ajax数据解析格式