一步一步自定義SpringMVC參數解析器

爲所欲爲,自定義參數解析器綁定數據。前端

題圖:from Zoommygit

乾貨

  1. SpringMVC解析器用於解析request請求參數並綁定數據到Controller的入參上。
  2. 自定義一個參數解析器須要實現HandlerMethodArgumentResolver接口,重寫supportsParameterresolveArgument方法,配置文件中加入resolver配置。
  3. 若是須要多個解析器同時生效須要在一個解析器中對其餘解析器作兼容

緣起

爲何要自定義一個解析器呢?github

源於須要對前端請求參數進行手動URLDecode,也即除了Web容器自動decode一次,代碼內還須要再decode一次。web

針對這種需求,首先想到的是filter或者interceptor實現,可是因爲HttpServletRequest對象自己是不提供setParameter()方法的,所以想要修改request中的參數值爲decode後的值是不易達到的。spring

SpringMVC的HandlerMethodArgumentResolver,解析器;其功能就是解析request請求參數並綁定數據到Controller的入參上。所以自定義解析器加入URLDecode邏輯便可徹底知足需求。mvc

下面,就一步一步的完成一個解析器由簡到繁的實現過程。ide

實現一個極其簡單的參數解析器

具體如何自定義一個參數解析器呢?學習

其實很簡單,一句話——實現HandlerMethodArgumentResolver接口,重寫supportsParameterresolveArgument方法,配置文件中加入resolver配置。ui

示例代碼以下:this

  • 自定義解析器實現

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class MyArgumentsResolver implements HandlerMethodArgumentResolver {
    /**
    * 解析器是否支持當前參數
    */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    // 指定參數若是被應用MyParam註解,則使用該解析器。
    // 若是直接返回true,則表明將此解析器用於全部參數
    return parameter.hasParameterAnnotation(MyParam.class);
    }

    /**
    * 將request中的請求參數解析到當前Controller參數上
    * @param parameter 須要被解析的Controller參數,此參數必須首先傳給{@link #supportsParameter}並返回true
    * @param mavContainer 當前request的ModelAndViewContainer
    * @param webRequest 當前request
    * @param binderFactory 生成{@link WebDataBinder}實例的工廠
    * @return 解析後的Controller參數
    */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    return null;
    }
    }
  • 自定義註解

    1
    2
    3
    4
    5
    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MyParam {
    }
  • 在springmvc配置文件中註冊解析器

    1
    2
    3
    4
    5
    6
    <mvc:annotation-driven>
    <!--MyArgumentsResolver-->
    <mvc:argument-resolvers>
    <bean class="xxx.xxx.xxx.MyArgumentsResolver"/>
    </mvc:argument-resolvers>
    </mvc:annotation-driven>

好了,如今解析器會把全部應用了@MyParam註解的參數都賦值爲null

實現一個解析原始類型的參數解析器

對於如何解析原始類型參數,SpringMVC已經有了一個內置的實現——RequestParamMethodArgumentResolver,所以徹底能夠參考這個實現來自定義咱們本身的解析器。

如上所述,解析器邏輯的主要部分都在resolveArgument方法內,這裏就說說自定義該方法的實現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

// 解析器中的自定義邏輯——urldecode
Object arg = URLDecoder.decode(webRequest.getParameter(parameter.getParameterName()), "UTF-8");

// 將解析後的值綁定到對應的Controller參數上,利用DataBinder提供的方法便捷的實現類型轉換
if (binderFactory != null) {

// 生成參數綁定器,第一個參數爲request請求對象,第二個參數爲須要綁定的目標對象,第三個參數爲須要綁定的目標對象名
WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getParameterName());

try {

// 將參數轉到預期類型,第一個參數爲解析後的值,第二個參數爲綁定Controller參數的類型,第三個參數爲綁定的Controller參數
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);

} catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
parameter.getParameterName(), parameter, ex.getCause());
} catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
parameter.getParameterName(), parameter, ex.getCause());
}
}
return arg;
}

添加解析對象類型參數的功能

對於如何解析對象類型參數,SpringMVC內也有了一個內置的實現——ModelAttributeMethodProcessor,咱們也是參考這個實現來自定義咱們本身的解析器。

一樣,resolveArgument方法示例以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

String name = parameter.getParameterName();

// 查找是否已有名爲name的參數值的映射,若是沒有則建立一個
Object attribute = mavContainer.containsAttribute(name)
? mavContainer.getModel().get(name)
: this.createAttribute(name, parameter, binderFactory, webRequest);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

if (binder.getTarget() != null) {
// 進行參數綁定
this.bindRequestParameters(binder, webRequest);
}

// 將參數轉到預期類型,第一個參數爲解析後的值,第二個參數爲綁定Controller參數的類型,第三個參數爲綁定的Controller參數
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}

return attribute;
}

protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) throws UnsupportedEncodingException {
// 將key-value封裝爲map,傳給bind方法進行參數值綁定
Map<String, String> map = new HashMap<>();
Map<String, String[]> params = request.getParameterMap();

for (Map.Entry<String, String[]> entry : params.entrySet()) {
String name = entry.getKey();
// 執行urldecode
String value = URLDecoder.decode(entry.getValue()[0], "UTF-8");
map.put(name, value);
}

PropertyValues propertyValues = new MutablePropertyValues(map);

// 將K-V綁定到binder.target屬性上
binder.bind(propertyValues);
}

同時支持多個參數解析器生效

到目前爲止,不論對於原始類型或者對象類型的參數,咱們均可以自定義一個參數解析器了,可是還有一個很嚴重的問題存在——沒法讓自定義解析器和現有解析器同時生效。

舉個例子,public String myController(@Valid @MyParam param, BindingResult result){},這個方法在執行時是會報錯的。他會提示相似以下報錯:

An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments

是SpringMVC不支持同時使用兩個解析器嗎?public String myController(@Valid @ModelAttribute param, BindingResult result){},也是兩個內置解析器,沒有任何問題。

再去看ModelAttributeMethodProcessor的實現,原來是對@Valid作了兼容處理。

所以, 若是須要多個解析器同時生效須要在一個解析器中對其餘解析器作兼容。

這裏僅以對@Valid進行兼容處理爲例,在解析對象類型的解析器實現中進行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

String name = parameter.getParameterName();

// 查找是否已有名爲name的參數值的映射,若是沒有則建立一個
Object attribute = mavContainer.containsAttribute(name)
? mavContainer.getModel().get(name)
: this.createAttribute(name, parameter, binderFactory, webRequest);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

if (binder.getTarget() != null) {
// 進行參數綁定,此方法實現再也不贅述,可到上節查看
this.bindRequestParameters(binder, webRequest);

// -----------------------------------對@Valid作兼容----------------------------------------------------

// 若是使用了validation校驗, 則進行相應校驗
if (parameter.hasParameterAnnotation(Valid.class)) {
// 若是有校驗報錯,會將結果放在binder.bindingResult屬性中
binder.validate();
}

// 若是參數中不包含BindingResult參數,直接拋出異常
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}

// 關鍵,使Controller中接下來的BindingResult參數能夠接收異常
Map bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

// -----------------------------------對@Valid作兼容----------------------------------------------------

// 將參數轉到預期類型,第一個參數爲解析後的值,第二個參數爲綁定Controller參數的類型,第三個參數爲綁定的Controller參數
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}

return attribute;
}

/**
* 檢查參數中是否包含BindingResult參數
*/
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class[] paramTypes = methodParam.getMethod().getParameterTypes();
boolean hasBindingResult = paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]);
return !hasBindingResult;
}

OK,到這裏,咱們自定義的解析器已經能夠算是一個完善的參數解析器了,若是有對其餘解析器作兼容的須要,只要參照此類方法稍做修改便可。

後記

還記得此次自定義解析器的緣由嗎——須要對前端請求參數進行手動URLDecode,也即除了Web容器自動decode一次,代碼內還須要再decode一次。

事實證實,根本不須要進行二次decode,寫出的解析器也就無疾而終了,僅存這篇整理,算是對SpringMVC解析器的一次學習總結吧。

 

http://coderec.cn/2016/08/27/%E4%B8%80%E6%AD%A5%E4%B8%80%E6%AD%A5%E8%87%AA%E5%AE%9A%E4%B9%89SpringMVC%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90%E5%99%A8/

相關文章
相關標籤/搜索