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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM