在编写 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

Spring boot 自定义 Resolver 支持 interface 类型参数的更多相关文章

  1. Spring Boot自定义starter必知必会条件

    前言 在目前的Spring Boot框架中,不管是Spring Boot官方还是非官方,都提供了非常多的starter系列组件,助力开发者在企业应用中的开发,提升研发人员的工作效率,Spring Bo ...

  2. Spring Boot 2.X(四):Spring Boot 自定义 Web MVC 配置

    0.准备 Spring Boot 不仅提供了相当简单使用的自动配置功能,而且开放了非常自由灵活的配置类.Spring MVC 为我们提供了 WebMvcConfigurationSupport 类和一 ...

  3. Spring Boot 之FilterRegistrationBean --支持web Filter 排序的使用(转)

    Spring Boot 之FilterRegistrationBean  --支持web Filter 排序的使用Spring 提供了FilterRegistrationBean类,此类提供setOr ...

  4. Springboot 系列(十七)迅速使用 Spring Boot Admin 监控你的 Spring Boot 程序,支持异常邮件通知

    1. Spring Boot Admin 是什么 Spring Boot Admin 是由 codecentric 组织开发的开源项目,使用 Spring Boot Admin 可以管理和监控你的 S ...

  5. 玩转Spring Boot 自定义配置、导入XML配置与外部化配置

    玩转Spring Boot 自定义配置.导入XML配置与外部化配置       在这里我会全面介绍在Spring Boot里面如何自定义配置,更改Spring Boot默认的配置,以及介绍各配置的优先 ...

  6. spring boot自定义线程池以及异步处理

    spring boot自定义线程池以及异步处理@Async:什么是线程池?线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使 ...

  7. Spring Boot 添加JSP支持【转】

    Spring Boot 添加JSP支持 大体步骤: (1)            创建Maven web project: (2)            在pom.xml文件添加依赖: (3)     ...

  8. Spring Boot自定义配置与加载

    Spring Boot自定义配置与加载 application.properties主要用来配置数据库连接.日志相关配置等.除了这些配置内容之外,还可以自定义一些配置项,如: my.config.ms ...

  9. Spring Boot自定义Redis缓存配置,保存value格式JSON字符串

    Spring Boot自定义Redis缓存,保存格式JSON字符串 部分内容转自 https://blog.csdn.net/caojidasabi/article/details/83059642 ...

随机推荐

  1. JDBC连接池-C3P0连接

    JDBC连接池-C3P0连接 c3p0连接池的学习英语好的看英文原版      c3p0 - JDBC3 Connection and Statement Pooling 使用c3p0连接池  三种方 ...

  2. 『开发技术』Windows极简安装使用face_recognition

    face_recognition是一个强大.简单.易上手的人脸识别开源项目,并且配备了完整的开发文档和应用案例,特别是兼容树莓派系统.此项目是世界上最简洁的人脸识别库,你可以使用Python和命令行工 ...

  3. Xamarin 基础知识

    Xamarin 跨平台处理: C#: if (Device.OS == TargetPlatform.Android) { Code…… } else if (Device.OS == TargetP ...

  4. weblogic 内存溢出解决 java.lang.OutOfMemoryError: PermGen space

    解决办法: 1.在idea中,运行时给weblogic server中 VM options 配置增加内存的参数:-server -XX:PermSize=1024m -XX:MaxPermSize= ...

  5. 0x33 同余

    目录 定义 同余类与剩余系 费马小定理 欧拉定理 证明: 欧拉定理的推论 证明: 应用: 定义 若整数 $a$ 和整数 $b$ 除以正整数 $m$ 的余数相等,则称 $a,b$ 模 $m$ 同余,记为 ...

  6. JAVA基础知识(九)Java 异常

    Throwable是Error和Exception的基类 Exception(异常) :是程序本身可以处理的异常. Error(错误): 是程序无法处理的错误.这些错误表示故障发生于虚拟机自身.或者发 ...

  7. python多线程同步实例分析

    进程之间通信与线程同步是一个历久弥新的话题,对编程稍有了解应该都知道,但是细说又说不清.一方面除了工作中可能用的比较少,另一方面就是这些概念牵涉到的东西比较多,而且相对较深.网络编程,服务端编程,并发 ...

  8. Go基础语法学习

    Go语言基础 Go是一门类似C的编译型语言,但是它的编译速度非常快.这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多.先让我们看一眼这些关键字都长什么样: 下面列 ...

  9. IPC机制2

    1.使用Messenger Messenger可以翻译为信使,通过它可以在不同进程中传递messenge对象,在messenge中放入我们需要传递的数据,就可以轻松实现数据在进程中传递. 服务段进程: ...

  10. Oracle 12cR1 RAC集群安装(一)--环境准备

    基本环境 操作系统版本 RedHat6.7 数据库版本 12.1.0.2 数据库名称 testdb 数据库实例 testdb1.testdb2 (一)安装服务器硬件要求 配置项目 参数要求 网卡 每台 ...