自定義spring參數註解 - 打破@RequestBody單體限制

本文主要描述怎樣自定義相似@RequestBody這樣的參數註解來打破@RequestBody的單體限制。前端

 

目錄
1 @RequestBody的單體限制
2 自定義spring的參數註解
3 編寫spring的參數註解解析器
4 將自定義參數註解解析器設置到spring的參數解析器集合中
5 指定參數解析器的優先級java

 

1、@RequestBody的單體限制
@RequestBody的做用:將請求體中的總體數據轉化爲對象。ios

1     @RequestMapping(value = "/body", method = RequestMethod.POST)
2     public Book testCommon(@RequestBody Book book) {
3         return book;
4     }

springmvc具備一個參數解析器容器RequestMappingHandlerAdapter.argumentResolvers,該參數的初始化在RequestMappingHandlerAdapter#afterPropertiesSet()web

 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,來看一下這個解析器的主要方法:spring

 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     }

這裏注意兩點:json

一、一個參數解析器最重要的方法有兩個:
(1)supportsParameter 指定哪些參數使用該解析器進行解析
(2)resolveArgument 對參數進行真正的解析操做axios

這也是自定義參數解析器須要去實現的兩個方法(見「三」)api

二、在解析器容器中,自定義解析器是位於內置解析器以後,這個順序也是解析器的優先級,也就是說假設有一個參數同時知足兩個解析器,只有第一個解析器會生效,那麼怎麼去調整這個解析器的順序呢?(見「五」)mvc

好,如今,咱們已經大體瞭解了springmvc的參數解析器,以及@RequestBody的解析過程。那麼來看一下這個例子:app

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的解析就會拋錯。

當前,解決此類的方案有兩種:

一、@RequestBody List<Book> books

二、@RequestBody MultiObject books

不論是哪種,其實都是將衆多的對象組成一個,由於在springmvc的一個方法中只能有一個@RequestBody,這被稱爲單體限制。其實在有些場景下,我就是想實現多個@RequestBody這樣的功能,該怎麼辦?(我在實現kspringfox框架的時候,就遇到了這樣的訴求:kspringfox是一個擴展了springfox的框架,主要實現了對dubbo接口的文檔化,以及將dubbo接口透明的轉爲rest接口供咱們調用的功能)

下面咱們就來實現這樣一個功能。

 

2、自定義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指明註解應用於運行時。

 

3、編寫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格式的參數值轉化爲具體類型的對象。

 

4、將自定義參數解析器設置到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         )

 

5、指定參數解析器的優先級
經過前邊的步驟,一個自定義的參數註解就「基本」可使用了,可是還有一個問題。看這個例子,

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 }
相關文章
相關標籤/搜索