背景
在web程序中,一個HTTP請求進來時,會被容器處理進而轉換成一個servlet請求。http請求所攜帶的數據,雖然是格式化的但是無類型;而java作爲強類型語言,同時爲了健壯性考慮,必然要有完善的類型約束。那麼,將數據從servlet請求中轉換到java中,一個很原始的方式是手動處理。幸好,Spring MVC通過以註解往函數添加額外信息的方式,使得上述的數據轉換過程能夠交由框架自動處理。從一個角度去看,Controller中的函數聲明及註解定義了此HTTP請求的數據格式和類型,也即規定了對外部暴露的以http協議展現的接口。不過,有些時候內置註解無法滿足需求的情況。這個時候,就需要自定義自己的註解以聲明參數的格式。
一、自定義註解
現在假設我們需要自定義一種數據,叫做userId。當一個http請求進入時,我們期望的效果是框架從session取數據,並且放入到controller對應的參數中。現在,定義了一個叫做UserId的註解:
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserId {
boolean required() default true;
}
- 從
ElementType.PARAMETER
可以看出,這個註解是用於修飾參數的。 - 該註解的保留策略要設爲RUNTIME,很顯然,因爲框架是運行時通過反射拿到註解信息的。
- 註解攜帶了個參數required,在這裏,是個類似接口的聲明;但是在後面,則要通過此信息決定解析器的行爲。
二、參數解析器
首先看位於HandlerMethodArgumentResolver.java
的這個接口。通過實現這個接口的類,就是解析器。按照我們的期望,它中間的函數應該能得到必要信息,從而按照自定義邏輯計算並返回一個值。
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
MethodParameter
是spring對被註解修飾過參數的包裝,從其中能拿到參數的反射相關信息。supportsParameter
傳入一個參數,用以判斷此參數是否能夠使用該解析器。resolveArgument
就是之前討論的解析函數,傳入必要信息,計算並返回一個值。- 綜合來看,框架會將每一個
MethodParameter
傳入supportsParameter
測試是否能夠被處理,如果能夠,就使用resolveArgument
處理。
對於我們的userId解析器,如下:
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {
public static final String SESSION_USER_CLIENT_ID = "_session_user_clientId";
@Getter
@Setter
private String noLoginMessage = "未登錄!";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserId.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Annotation[] annotations = parameter.getParameterAnnotations();
// 逐一處理
for (Annotation annotation : annotations) {
// userId
if (annotation instanceof UserId) {
return resolveUserId((UserId) annotation, parameter, mavContainer, webRequest, binderFactory);
}
}
return null;
}
private Object resolveUserId(ClientId annotation, MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
Object attribute = webRequest.getAttribute(SESSION_USER_CLIENT_ID, RequestAttributes.SCOPE_SESSION);
if (attribute == null && annotation.required()) {
throw new NoLoginException(noLoginMessage);
}
return attribute;
}
}
- 從
supportsParameter
的實現可以看出,只有被@UserId
註解修飾的參數才能被此解析器處理。 - 如果參數被
@UserId
修飾,就會由resolveArgument
計算出參數的值。從resolveArgument
的實現看出,它從session
中取數據作爲參數的值。
這個NativeWebRequest
是一個關鍵點,看名字像是能拿到http請求的數據,粗略查詢資料可知道,可從中拿到HttpServletRequest
:
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
三、註冊解析器
最後,該把我們的解析器設置到spring MVC中去了:
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UserIdArgumentResolver());
}
};
}
}
通過定義WebMvcConfigurer
這個Bean能夠自定義配置。看起來像是會把默認配置覆蓋似的,不過實際上只是會和默認配置合併,大膽使用。最後,我們就能使用@UserId了
:
@RequestMapping(value = "/api/user", method = RequestMethod.POST)
@ResponseBody
public ResponseDTO user(@UserId String userId) {
return new ResponseDTO(new User());
}