在編寫 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 }
(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 }
(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 }
(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 })
但在運行測試時發現 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 }
(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 }
(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 }
方案二可以解決找不到構造函數的問題,運行不會報錯,也不會導致循環依賴,但卻沒法將前台的數據注入到入參對象中。也就是給方法傳入的只是一個剛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 }
回到最初的問題,導致無法傳入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 }
完成上面的代碼后,跑了一下,發現並沒有什么用,報的錯誤還是跟最開始的一樣。
由此推測,應該是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 }
值得注意的是,getResolvers()方法返回的是不可更改的List,不能直接插入。
至此,自定義參數處理器就可以解析RestController標注的類中的方法的 interface類型參數了。
如果要支持其他類型(比如抽象類、枚舉類),或者使用自定義注解標注入參,也可以通過類似的方法來實現。
本文地址:https://www.cnblogs.com/laishenghao/p/11488724.html