Spring boot 自定义 Resolver 支持 interface 类型参数
在编写 RestController 层的代码时,由于数据实体类定义了接口及实现类,本着面向接口编程的原则,我使用了接口作为 RestController 方法的入参。
代码大致如下(省略具体业务部分):
(1)模型接口:
public interface User { long getUserId(); void setUserId(long userId); String getUserName(); void setUserName(String userName); String getCategory(); void setCategory(String category); }View Code
(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; } }View Code
(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(); }View Code
(4)前台用的axios发送的请求代码
const AXIOS = axios.create({ baseURL: 'htt aec p://localhost:9999', withCredentials: false, headers: { Accept: 'application/json', 'Content-type': 'application/json' } }) AXIOS.post('/updateUser', { userName: 'testName', userId: '123456789', category: 'XX' })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为实体类的包路径,下列代码中默认支持了包内的所有类型。
@Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().getName().startsWith(MODEL_PATH); }View Code
(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; }View Code
(3)最后添加到Spring boot 的配置中
@Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new MethodInterfaceArgumentResolver()); super.addArgumentResolvers(argumentResolvers); } }; }View Code
方案二可以解决找不到构造函数的问题,运行不会报错,也不会导致循环依赖,但却没法将前台的数据注入到入参对象中。也就是给方法传入的只是一个刚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尝试的方式,终于找到了好的解决方案。 20bc
这里先分享下大致的思路:
(1)Spring boot的相关代码应该在 HandlerMethodArgumentResolver 接口对应的包里或者附近。但这样找还是比较慢,因为代码还是很多。
(2)通过打断点,看看哪里调用了 public boolean supportsParameter(MethodParameter parameter) 方法。
于是找到了HandlerMethodArgumentResolverComposite 类调用的地方:
/** * 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); }View Code 回到最初的问题,导致无法传入interface类型参数的原因是接口无法实例化。那既然如此,我们要修改的地方肯定是Spring boot 尝试实例化接口的地方,也就是实例化失败进而抛出异常的地方。
一路顺腾摸瓜,最终发现 readWithMessageConverters 方法中, 通过给 readWithMessageConverters 方法传入类型信息,最终生成参数实例。
package com.sample.demo.config; import org.springframework.core.MethodParameter; import org.springframework.http.converter.HttpMessag 576 eConverter; 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; /** 15 * @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 Strin aec g 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; } } }View Code
完成上面的代码后,跑了一下,发现并没有什么用,报的错误还是跟最开始的一样。
由此推测,应该是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; 5 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); } }View Code
值得注意的是,getResolvers()方法返回的是不可更改的List,不能直接插入。
至此,自定义参数处理器就可以解析RestController标注的类中的方法的 interface类型参数了。
如果要支持其他类型(比如抽象类、枚举类),或者使用自定义注解标注入参,也可以通过类似的方法来实现。
本文地址:https://www.cnblogs.com/laishenghao/p/11488724.html
- springboot读取XXX.properties自定义配置文件中的map和list类型配置参数
- [原创]Spring Boot + Mybatis 简易使用指南(二)多参数方法支持 与 Joda DateTime类型支持
- 如何定制支持用户自定义boot参数的基于debian os的live cd
- Spring MVC HandlerMethodArgumentResolver 自定义参数解析器
- SpringMVC第四篇【参数绑定详讲、默认支持参数类型、自定义参数绑定、RequestParam注解】
- springboot+aop+自定义注解,打造通用的全局异常处理和参数校验切面(通用版)
- SpringMVC HandlerMethodArgumentResolver自定义参数转换器 针对HashMap失效的问题
- Spring Boot 获取自定义参数
- Spring boot中自定义Json参数解析器的方法
- 用AOP拦截自定义注解并获取注解属性与上下文参数(基于Springboot框架)
- Oracle不支持在select语句中调用自定义函数时使用自定义类型作参数?
- SpringBoot2.1.3修改tomcat参数支持请求特殊符号问题
- springcloud中常用注解, spring boot常见get 、post请求参数处理、参数注解校验、参数自定义注解校验
- 关于SpringBoot自定义注解(解决post接收String参数 null(前台传递json格式))
- Spring boot+Mybatis+MySql 处理 日期类型参数 需留意
- SpringBoot自定义注解获取参数和值
- SpringBoot中自定义properties文件配置参数并带有输入提示
- SpringMVC第四篇【参数绑定详讲、默认支持参数类型、自定义参数绑定、RequestParam注解】
- Spring Boot学习笔记--自定义参数
- SpringMVC HandlerMethodArgumentResolver自定义参数转换器 针对HashMap失效的问题