Spring boot custom Resolver support interface type parameters

 

When writing the code RestController layer, since the data entity class defines an interface and implementation class, in principle oriented programming interface, the interface I used as the reference method RestController.

Code as follows (not specific traffic section):

(1) Model Interface:

 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) implementation class model

 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 interface code

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

Request code (4) axios transmitted by the front

 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

 

But it found itself in default Spring boot does not support the interface or abstract class method as a parameter when you run the test. It reported the following error:

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
...

Create instance does not exist when the mean substantially constructor needs to abstract type configuration to the specific category.

 

Solution one:

So I search the Internet under the solution, eventually  StackOverflow  to find a solution on:

@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 { }

By adding annotations manner, the interface is mapped to the implementation class.

 

This method can solve the problem of the interface into the reference method, but would introduce a problem: the class interface and implementation refer to each other, resulting in a circular dependency. And if I have an interface and implementation class kind of thing a lot of data, each interface should write again comment.

So continue to explore. . .

 

Solution two:

Inheritance HandlerMethodArgumentResolver interface inside supportsParameter and resolveArgument method.

(1) supported in the return type supportsParameter process. Wherein the path for the package MODEL_PATH entity class, the following code types supported all default in the packet.

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

(2) In resolveArgument method, an object implementation class generated by the reflected and returned.

 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) added to the final configuration 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

 

Scheme II can not be found to solve the problem of the constructor, not being given run, it will not result in a cycle-dependent, but not injected into the reference data to the foreground object. That is just a UserImpl objects just out new methods to incoming.

 

After testing found that, although the request could not be injected into the foreground of the post data, but to get a request, or can:

Reception get method code:

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

Get back method code:

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

 

Solution three:

Since there is no good solution to find on the Internet, I finally see through Spring boot source + + debug trace write mode demo attempts, finally found a good solution.

Here to share under the general idea:

(1) Spring boot code should be associated a corresponding bag or in the vicinity of the interface HandlerMethodArgumentResolver. But this look is quite slow, because the code is still a lot.

 

(2) by breaking point, see where calls public boolean supportsParameter (MethodParameter parameter) method.

So find a place HandlerMethodArgumentResolverComposite class call:

  Can be seen from the figure, the current process is the first parameter HttpSession.

 

(3) The method of the first controller to the first reference UserImpl, i.e. implementation class, the break point corresponding code screenshot step (2),.

Continue debugging, parsing is @RequestBody find Spring boot parameters UserImpl user notes marked time, with what Resolver.

As shown below, call type information acquiring Evaluate window, click Navigate to jump to the corresponding class RequestResponseBodyMethodProcessor.

 

 (4) RequestResponseBodyMethodProcessor class resolveArgument Method source as follows:

 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

 

Guess you like

Origin www.cnblogs.com/laishenghao/p/11488724.html