Spring自定義argumentResolver參數解析器從原理到實戰

背景

在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;
}
  1. ElementType.PARAMETER可以看出,這個註解是用於修飾參數的。
  2. 該註解的保留策略要設爲RUNTIME,很顯然,因爲框架是運行時通過反射拿到註解信息的。
  3. 註解攜帶了個參數required,在這裏,是個類似接口的聲明;但是在後面,則要通過此信息決定解析器的行爲。

二、參數解析器

首先看位於HandlerMethodArgumentResolver.java的這個接口。通過實現這個接口的類,就是解析器。按照我們的期望,它中間的函數應該能得到必要信息,從而按照自定義邏輯計算並返回一個值。

 public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter); 
    Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
  1. MethodParameter是spring對被註解修飾過參數的包裝,從其中能拿到參數的反射相關信息。
  2. supportsParameter傳入一個參數,用以判斷此參數是否能夠使用該解析器。
  3. resolveArgument就是之前討論的解析函數,傳入必要信息,計算並返回一個值。
  4. 綜合來看,框架會將每一個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;
    }
}
  1. supportsParameter的實現可以看出,只有被@UserId註解修飾的參數才能被此解析器處理。
  2. 如果參數被@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());
}

微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章