自定義spring參數註解 - 打破@RequestBody單體限制
本文主要描述怎樣自定義類似@RequestBody這樣的參數註解來打破@RequestBody的單體限制。
目錄
1 @RequestBody的單體限制
2 自定義spring的參數註解
3 編寫spring的參數註解解析器
4 將自定義參數註解解析器設置到spring的參數解析器集合中
5 指定參數解析器的優先級
一、@RequestBody的單體限制
@RequestBody的作用:將請求體中的整體數據轉化為對象。
1 @RequestMapping(value = "/body", method = RequestMethod.POST) 2 public Book testCommon(@RequestBody Book book) {3 return book; 4 }
springmvc具有一個參數解析器容器RequestMappingHandlerAdapter.argumentResolvers,該參數的初始化在RequestMappingHandlerAdapter#afterPropertiesSet()
1 public void afterPropertiesSet() { 2 ...... 3 if (this.argumentResolvers == null) { 4 List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();5 this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); 6 } 7 ...... 8 } 9 10 /** 11 * Return the list of argument resolvers to use including built-in resolvers 12 * and custom resolvers provided via {@link #setCustomArgumentResolvers}.13 */ 14 private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 15 List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); 16 17 // Annotation-based argument resolution 18 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); 19 resolvers.add(new RequestParamMapMethodArgumentResolver()); 20 resolvers.add(new PathVariableMethodArgumentResolver()); 21 resolvers.add(new PathVariableMapMethodArgumentResolver()); 22 resolvers.add(new MatrixVariableMethodArgumentResolver()); 23 resolvers.add(new MatrixVariableMapMethodArgumentResolver()); 24 resolvers.add(new ServletModelAttributeMethodProcessor(false)); 25 resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); 26 resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); 27 resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); 28 resolvers.add(new RequestHeaderMapMethodArgumentResolver()); 29 resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); 30 resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); 31 resolvers.add(new SessionAttributeMethodArgumentResolver()); 32 resolvers.add(new RequestAttributeMethodArgumentResolver()); 33 34 // Type-based argument resolution 35 resolvers.add(new ServletRequestMethodArgumentResolver()); 36 resolvers.add(new ServletResponseMethodArgumentResolver()); 37 resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); 38 resolvers.add(new RedirectAttributesMethodArgumentResolver()); 39 resolvers.add(new ModelMethodProcessor()); 40 resolvers.add(new MapMethodProcessor()); 41 resolvers.add(new ErrorsMethodArgumentResolver()); 42 resolvers.add(new SessionStatusMethodArgumentResolver()); 43 resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); 44 45 // Custom arguments 46 if (getCustomArgumentResolvers() != null) { 47 resolvers.addAll(getCustomArgumentResolvers()); 48 } 49 50 // Catch-all 51 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); 52 resolvers.add(new ServletModelAttributeMethodProcessor(true)); 53 54 return resolvers; 55 }
可以看出springmvc的參數解析器容器中存放著內置的參數解析器 + 自定義解析器,這裏邊就包括@RequestBody的解析器RequestResponseBodyMethodProcessor,來看一下這個解析器的主要方法:
1 @Override 2 public boolean supportsParameter(MethodParameter parameter) { 3 return parameter.hasParameterAnnotation(RequestBody.class); 4 } 5 6 @Override 7 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 8 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 9 // 這裏使用MappingJackson2HttpMessageConverter將輸入流body體中的轉化為Book對象 10 }
這裏註意兩點:
1、一個參數解析器最重要的方法有兩個:
(1)supportsParameter 指定哪些參數使用該解析器進行解析
(2)resolveArgument 對參數進行真正的解析操作
這也是自定義參數解析器需要去實現的兩個方法(見“三”)
2、在解析器容器中,自定義解析器是位於內置解析器之後,這個順序也是解析器的優先級,也就是說假設有一個參數同時滿足兩個解析器,只有第一個解析器會生效,那麽怎麽去調整這個解析器的順序呢?(見“五”)
好,現在,我們已經大致了解了springmvc的參數解析器,以及@RequestBody的解析過程。那麽來看一下這個例子:
1 @RequestMapping(value = "/two-body", method = RequestMethod.POST) 2 public Book testCommon(@RequestBody Book book1, @RequestBody Book book2) { 3 Book book = new Book(); 4 book.setId(Optional.ofNullable(book1).orElse(book2).getId()); 5 book.setName(Optional.ofNullable(book1).orElse(book2).getName()); 6 return book; 7 }
有兩個@RequestBody,一執行,結果拋錯:
1 { 2 "status": 400, 3 "error": "Bad Request", 4 "exception": "org.springframework.http.converter.HttpMessageNotReadableException", 5 "message": "I/O error while reading input message; nested exception is java.io.IOException: Stream closed", 6 }
400通常是輸入參數錯誤,錯誤原因:從上文對@RequestBody的解析過程的分析來看,這個參數實際上是將輸入流的body體作為一個整體進行轉換,而body整體只有一份,解析完成之後會關閉輸入流,所以第二個參數book2的解析就會拋錯。
當前,解決此類的方案有兩種:
1、@RequestBody List<Book> books
2、@RequestBody MultiObject books
不管是哪一種,其實都是將眾多的對象組成一個,因為在springmvc的一個方法中只能有一個@RequestBody,這被稱為單體限制。其實在有些場景下,我就是想實現多個@RequestBody這樣的功能,該怎麽辦?(我在實現kspringfox框架的時候,就遇到了這樣的訴求:kspringfox是一個擴展了springfox的框架,主要實現了對dubbo接口的文檔化,以及將dubbo接口透明的轉為rest接口供我們調用的功能)
下面我們就來實現這樣一個功能。
二、自定義spring的參數註解
首先自定義一個類似於@RequestBody的註解:@RequestModel
1 @Target(ElementType.PARAMETER) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface RequestModel { 4 String value() default ""; 5 boolean required() default false; 6 }
自定義註解很簡單:@Target指明註解應用於參數上;@Retention指明註解應用於運行時。
三、編寫spring的參數註解解析器
1 public class RequestModelArgumentResolver implements HandlerMethodArgumentResolver { 2 @Override 3 public boolean supportsParameter(MethodParameter parameter) { 4 return parameter.hasParameterAnnotation(RequestModel.class); 5 } 6 7 @Override 8 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 9 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 10 final String parameterJson = webRequest.getParameter(parameter.getParameterName()); 11 12 //parameter.getGenericParameterType() 返回參數的完整類型(帶泛型) 13 final Type type = parameter.getGenericParameterType(); 14 final Object o = JSON.parseObject(parameterJson, type); 15 return o; 16 } 17 }
註意:
1 supportsParameter方法指明RequestModelArgumentResolver只處理帶有@RequestModel註解的參數;
2 resolveArgument方法對入參進行解析:首先獲取參數值(json串),然後獲取參數的完整類型(帶泛型),最後使用fastjson解析器將json格式的參數值轉化為具體類型的對象。
四、將自定義參數解析器設置到spring的參數解析器集合中
1 @Configuration 2 public class WebConfig extends WebMvcConfigurerAdapter { 3 @Override 4 public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { 5 argumentResolvers.add(new RequestModelArgumentResolver()); 6 } 7 }
通過上述這種方式,我們就將自定義的RequestModelArgumentResolver解析器添加到了spring的自定義參數解析器集合中。
此時,一個自定義的參數註解就可以基本使用在我們的項目中了。簡單的做個測試:
1 @RequestMapping(value = "/two-model", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) 2 public Book testModel(@RequestModel(value = "book1") Book book1, @RequestModel(value = "book2") Book book2) { 3 Book book = new Book(); 4 book.setId(book1.getId()); 5 book.setName(book2.getName()); 6 return book; 7 }
前端調用:(有錯誤跳過)
1 const params = new URLSearchParams() 2 params.append(‘book1‘, ‘{"id": 1,"name": "11"}‘) 3 params.append(‘book2‘, ‘{"id": 2,"name": "22"}‘) 4 return axios.post(‘http://localhost:8080/dubbo-api/two-model‘, params) 5 .then(res => { 6 ... 7 }).catch( 8 err => ... 9 )
五、指定參數解析器的優先級
通過前邊的步驟,一個自定義的參數註解就“基本”可以使用了,但是還有一個問題。看這個例子,
1 @RequestMapping(value = "/map", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) 2 public Map<String, Book> testMap(@RequestModel(value = "title2Book") Map<String, Book> title2Book) { 3 return title2Book; 4 }
我們在“三”中的RequestModelArgumentResolver#supportsParameter方法中打斷點來debug一下,發現上邊這個例子根本不會走進去,也就是說此時我們自定義的RequestModelArgumentResolver不再起作用了。
原因:在springmvc的解析器容器中,自定義解析器是放在內置解析器之後的,這個順序也是解析器的優先級,也就是說假設有一個參數同時滿足兩個解析器,只有第一個解析器會生效。而springmvc對Map是專門有一個內置解析器的,這個解析器位於我們的RequestModelArgumentResolver之前,所以springmvc會使用Map解析器進行解析,而不再使用RequestModelArgumentResolver。
具體源碼我們再翻回頭看一下“一”中的getDefaultArgumentResolvers:
1 /** 2 * Return the list of argument resolvers to use including built-in resolvers 3 * and custom resolvers provided via {@link #setCustomArgumentResolvers}. 4 */ 5 private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 6 List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); 7 ... 8 //Map解析器 9 resolvers.add(new MapMethodProcessor()); 10 ... 11 // 自定義解析器 12 if (getCustomArgumentResolvers() != null) { 13 resolvers.addAll(getCustomArgumentResolvers()); 14 } 15 return resolvers; 16 }
看一下MapMethodProcessor#supportsParameter
1 @Override 2 public boolean supportsParameter(MethodParameter parameter) { 3 return Map.class.isAssignableFrom(parameter.getParameterType()); 4 }
原因明了了以後,就要去想解決方案。(如果spring可以提供為參數解析器設置order的能力,那麽就好了,但是spring沒有提供)
第一種方案
在服務啟動時,動態替換掉MapMethodProcessor#supportsParameter的字節碼。
1 @Override 2 public boolean supportsParameter(MethodParameter parameter) { 3 if(parameter.hasParameterAnnotation(RequestModel.class)){ 4 return false; 5 } 6 return Map.class.isAssignableFrom(parameter.getParameterType()); 7 }
使用javassist可以實現這一點,但是這樣去做,代碼復雜性較高。“任何一個功能的實現,都要想辦法降低代碼復雜性”
第二種方案
首先刪除"四"中的WebConfig,讓spring不再自動的將自定義解析器加到RequestMappingHandlerAdapter的解析器容器中;然後我們通過下面的方式手動的將RequestModelArgumentResolver加載到RequestMappingHandlerAdapter的解析容器中。(通過這樣的方式,我們可以任意的指定解析器的順序)
1 @Configuration 2 public class MethodArgumentResolver { 3 @Autowired 4 private RequestMappingHandlerAdapter adapter; 5 6 @PostConstruct 7 public void injectSelfMethodArgumentResolver() { 8 List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(); 9 argumentResolvers.add(new RequestModelArgumentResolver()); 10 argumentResolvers.addAll(adapter.getArgumentResolvers()); 11 adapter.setArgumentResolvers(argumentResolvers); 12 } 13 }
自定義spring參數註解 - 打破@RequestBody單體限制