Spring自定義引數解析器設計

京東雲開發者發表於2023-04-14

作者:京東零售 王鵬超

1.什麼是引數解析器

@RequstBody、@RequstParam 這些註解是不是很熟悉?

我們在開發Controller介面時經常會用到此類引數註解,那這些註解的作用是什麼?我們真的瞭解嗎?

簡單來說,這些註解就是幫我們將前端傳遞的引數直接解析成直接可以在程式碼邏輯中使用的javaBean,例如@RequstBody接收json引數,轉換成java物件,如下所示:

前臺傳參 引數格式
application/json

正常程式碼書寫如下:

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
    //***
    return userInfo.getName();
}



但如果是服務接收引數的方式改變了,如下程式碼,引數就不能成功接收了,這個是為什麼呢?

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
    //***
    return userName;
}



如果上面的程式碼稍微改動一下註解的使用並且前臺更改一下傳參格式,就可以正常解析了。

前臺傳參 引數格式
http://***?userName=Alex&userId=1
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
    //***
    return userName;
}



這些這裡就不得不引出這些註解背後都對應的內容—Spring提供的引數解析器,這些引數解析器幫助我們解析前臺傳遞過來的引數,繫結到我們定義的Controller入參上,不通型別格式的傳遞引數,需要不同的引數解析器,有時候一些特殊的引數格式,甚至需要我們自定義一個引數解析器。

不論是在SpringBoot還是在Spring MVC中,一個HTTP請求會被DispatcherServlet類接收(本質是一個Servlet,繼承自HttpServlet)。Spring負責從HttpServlet中獲取並解析請求,將請求uri匹配到Controller類方法,並解析引數並執行方法,最後處理返回值並渲染檢視。

引數解析器的作用就是將http請求提交的引數轉化為我們controller處理單元的入參。原始的Servlet獲取引數的方式如下,需要手動從HttpServletRequest中獲取所需資訊。

@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        /**獲取引數開始*/
        String resourceId = req.getParameter("resourceId");
        String resourceType = req.getHeader("resourceType");
        /**獲取引數結束*/
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("resourceId " + resourceId + " resourceType " + resourceType);
    }
}



Spring為了幫助開發者解放生產力,提供了一些特定格式(header中content-type對應的型別)入參的引數解析器,我們在介面引數上只要加上特定的註解(當然不加註解也有預設解析器),就可以直接獲取到想要的引數,不需要我們自己去HttpServletRequest中手動獲取原始入參,如下所示:

@RestController
public class resourceController {

  @RequestMapping("/resource")
  public String getResource(@RequestParam("resourceId") String resourceId,
            @RequestParam("resourceType") String resourceType,
            @RequestHeader("token") String token) {
    return "resourceId" + resourceId + " token " + token;
  }
}



常用的註解類引數解析器使用方式以及與註解的對應關係對應關係如下:

註解命名 放置位置 用途
@PathVariable 放置在引數前 允許request的引數在url路徑中
@RequestParam 放置在引數前 允許request的引數直接連線在url地址後面,也是Spring預設的引數解析器
@RequestHeader 放置在引數前 從請求header中獲取引數
@RequestBody 放置在引數前 允許request的引數在引數體中,而不是直接連線在地址後面
註解命名 對應的解析器 content-type
@PathVariable PathVariableMethodArgumentResolver
@RequestParam RequestParamMethodArgumentResolver 無(get請求)和multipart/form-data
@RequestBody RequestResponseBodyMethodProcessor application/json
@RequestPart RequestPartMethodArgumentResolver multipart/form-data

2.引數解析器原理

要了解引數解析器,首先要了解一下最原始的Spring MVC的執行過程。客戶端使用者發起一個Http請求後,請求會被提交到前端控制器(Dispatcher Servlet),由前端控制器請求處理器對映器(步驟1),處理器對映器會返回一個執行鏈(Handler Execution 步驟2),我們通常定義的攔截器就是在這個階段執行的,之後前端控制器會將對映器返回的執行鏈中的Handler資訊傳送給介面卡(Handler Adapter 步驟3),介面卡會根據Handler找到並執行相應的Handler邏輯,也就是我們所定義的Controller控制單元(步驟4),Handler執行完畢會返回一個ModelAndView物件,後續再經過檢視解析器解析和檢視渲染就可以返回給客戶端請求響應資訊了。

在容器初始化的時候,RequestMappingHandlerMapping 對映器會將 @RequestMapping 註解註釋的方法儲存到快取,其中key是 RequestMappingInfo,value是HandlerMethod。HandlerMethod 是如何進行方法的引數解析和繫結,就要了解請求引數介面卡**RequestMappingHandlerAdapter,**該介面卡對應接下來的引數解析及繫結過程。原始碼路徑如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

RequestMappingHandlerAdapter大致的解析和繫結流程如下圖所示,

RequestMappingHandlerAdapter實現了介面InitializingBean,在Spring容器初始化Bean後,呼叫方法afterPropertiesSet( ),將預設引數解析器繫結HandlerMethodArgumentResolverComposite 介面卡的引數 argumentResolvers上,其中HandlerMethodArgumentResolverComposite是介面HandlerMethodArgumentResolver的實現類。原始碼路徑如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet

@Override
public void afterPropertiesSet() {
   // Do this first, it may add ResponseBody advice beans
   initControllerAdviceCache();

   if (this.argumentResolvers == null) {
      /**  */
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.initBinderArgumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
      this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.returnValueHandlers == null) {
      List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
      this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
   }
}



透過getDefaultArgumentResolvers( )方法,可以看到Spring為我們提供了哪些預設的引數解析器,這些解析器都是HandlerMethodArgumentResolver介面的實現類。

針對不同的引數型別,Spring提供了一些基礎的引數解析器,其中有基於註解的解析器,也有基於特定型別的解析器,當然也有兜底預設的解析器,如果已有的解析器不能滿足解析要求,Spring也提供了支援使用者自定義解析器的擴充套件點,原始碼如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
   List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

   // Annotation-based argument resolution 基於註解
   /** @RequestPart 檔案注入 */
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
   /** @RequestParam 名稱解析引數 */
   resolvers.add(new RequestParamMapMethodArgumentResolver());
   /** @PathVariable url路徑引數 */
   resolvers.add(new PathVariableMethodArgumentResolver());
   /** @PathVariable url路徑引數,返回一個map */
   resolvers.add(new PathVariableMapMethodArgumentResolver());
   /** @MatrixVariable url矩陣變數引數 */
   resolvers.add(new MatrixVariableMethodArgumentResolver());
   /** @MatrixVariable url矩陣變數引數 返回一個map*/
   resolvers.add(new Matrix VariableMapMethodArgumentResolver());
   /** 兜底處理@ModelAttribute註解和無註解 */
   resolvers.add(new ServletModelAttributeMethodProcessor(false));
   /** @RequestBody body體解析引數 */
   resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestPart 使用類似RequestParam */
   resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestHeader 解析請求header */
   resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
   /** @RequestHeader 解析請求header,返回map */
   resolvers.add(new RequestHeaderMapMethodArgumentResolver());
   /** Cookie中取值注入 */
   resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
   /** @Value */
   resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
   /** @SessionAttribute */
   resolvers.add(new SessionAttributeMethodArgumentResolver());
   /** @RequestAttribute */
   resolvers.add(new RequestAttributeMethodArgumentResolver());

   // Type-based argument resolution 基於型別
   /** Servlet api 物件 HttpServletRequest 物件繫結值 */
   resolvers.add(new ServletRequestMethodArgumentResolver());
   /** Servlet api 物件 HttpServletResponse 物件繫結值 */
   resolvers.add(new ServletResponseMethodArgumentResolver());
   /** http請求中 HttpEntity RequestEntity資料繫結 */
   resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** 請求重定向 */
   resolvers.add(new RedirectAttributesMethodArgumentResolver());
   /** 返回Model物件 */
   resolvers.add(new ModelMethodProcessor());
   /** 處理入參,返回一個map */
   resolvers.add(new MapMethodProcessor());
   /** 處理錯誤方法引數,返回最後一個物件 */
   resolvers.add(new ErrorsMethodArgumentResolver());
   /** SessionStatus */
   resolvers.add(new SessionStatusMethodArgumentResolver());
   /**  */
   resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

   // Custom arguments 使用者自定義
   if (getCustomArgumentResolvers() != null) {
      resolvers.addAll(getCustomArgumentResolvers());
   }

   // Catch-all 兜底預設
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
   resolvers.add(new ServletModelAttributeMethodProcessor(true));

   return resolvers;
}



HandlerMethodArgumentResolver介面中只定義了兩個方法,分別是解析器適用範圍確定方法supportsParameter( )和引數解析方法resolveArgument(),不同用途的引數解析器的使用差異就體現在這兩個方法上,這裡就不具體展開引數的解析和繫結過程。

3.自定義引數解析器的設計

Spring的設計很好踐行了開閉原則,不僅在封裝整合了很多非常強大的能力,也為使用者留好了自定義擴充的能力,引數解析器也是這樣,Spring提供的引數解析器基本能滿足常用的引數解析能力,但很多系統的引數傳遞並不規範,比如京東color閘道器傳業務引數都是封裝在body中,需要先從body中取出業務引數,然後再針對性解析,這時候Spring提供的解析器就幫不了我們了,需要我們擴充套件自定義適配引數解析器了。

Spring提供兩種自定義引數解析器的方式,一種是實現介面卡介面HandlerMethodArgumentResolver,另一種是繼承已有的引數解析器(HandlerMethodArgumentResolver介面的現有實現類)例如AbstractNamedValueMethodArgumentResolver進行增強最佳化。如果是深度定製化的自定義引數解析器,建議實現自己實現介面進行開發,以實現介面介面卡介面自定義開發解析器為例,介紹如何自定義一個引數解析器。

透過檢視原始碼發現,引數解析介面卡介面留給我擴充套件的方法有兩個,分別是supportsParameter( )和resolveArgument( ),第一個方法是自定義引數解析器適用的場景,也就是如何命中引數解析器,第二個是具體解析引數的實現。

public interface HandlerMethodArgumentResolver {

   /**
    * 識別到哪些引數特徵,才使用當前自定義解析器
    */
   boolean supportsParameter(MethodParameter parameter);

   /**
    * 具體引數解析方法
    */
   Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}




現在開始具體實現一個基於註解的自定義引數解析器,這個是程式碼實際使用過程中用到的引數解析器,獲取color閘道器的body業務引數,然後解析後給Controller方法直接使用。

public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
    private static final String DEFAULT_VALUE = "body";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        /** 只有指定註解註釋的引數才會走當前自定義引數解析器 */
        return parameter.hasParameterAnnotation(RequestJsonParam.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        /** 獲取引數註解 */
        RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
        
        /** 獲取引數名 */
        String name = attribute.value();
        /** 獲取指定名字引數的值 */
        String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
        /** 獲取註解設定引數型別 */
        Class<?> targetParamType = attribute.recordClass();
        /** 獲取實際引數型別 */
        Class<?> webParamType = parameter.getParameterType()
        /** 以自定義引數型別為準 */
        Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
        if (ObjectUtils.equals(paramType, String.class) 
            || ObjectUtils.equals(paramType, Integer.class)
            || ObjectUtils.equals(paramType, Long.class) 
            || ObjectUtils.equals(paramType, Boolean.class)) {
                JSONObject object = JSON.parseObject(value);
                log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
                if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
                    //入參:Integer  目標型別:Long
                    result = paramType.cast(((Integer) object.get(paramName)).longValue());
                }else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
                    //入參:Integer  目標型別:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
                    //入參:Long  目標型別:Integer(精度丟失)
                    result = paramType.cast(((Long) object.get(paramName)).intValue());
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
                    //入參:Long  目標型別:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
                    //入參:String  目標型別:Long
                    result = Long.valueOf((String) object.get(paramName));
                } else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
                    //入參:String  目標型別:Integer
                    result = Integer.valueOf((String) object.get(paramName));
                } else {
                    result = paramType.cast(object.get(paramName));
                }
        }else if (paramType.isArray()) {
            /** 入參是陣列 */
            result = JsonHelper.fromJson(value, paramType);
            if (result != null) {
                Object[] targets = (Object[]) result;
                for (int i = 0; i < targets.length; i++) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
                   validateIfApplicable(binder, parameter, annotations);
                }
             }
       } else if (Collection.class.isAssignableFrom(paramType)) {
            /** 這裡要特別注意!!!,集合引數由於範型獲取不到集合元素型別,所以指定型別就非常關鍵了 */
            Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
            result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
            if (result != null) {
               Collection<Object> targets = (Collection<Object>) result;
               int index = 0;
               for (Object targetObj : targets) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
                   validateIfApplicable(binder, parameter, annotations);
               }
            }
        } else{
              result = JSON.parseObject(value, paramType);
        }
    
        if (result != null) {
            /** 引數繫結 */
            WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
            result = binder.convertIfNecessary(result, paramType, parameter);
            validateIfApplicable(binder, parameter, annotations);
            mavContainer.addAttribute(name, result);
        }
    }



自定義引數解析器註解的定義如下,這裡定義了一個比較特殊的屬性recordClass,後續會講到是解決什麼問題。

/**
 * 請求json引數處理註解
 * @author wangpengchao01
 * @date 2022-11-07 14:18
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
    /**
     * 繫結的請求引數名
     */
    String value() default "body";

    /**
     * 引數是否必須
     */
    boolean required() default false;

    /**
     * 預設值
     */
    String defaultValue() default ValueConstants.DEFAULT_NONE;

    /**
     * 集合json反序列化後記錄的型別
     */
    Class recordClass() default null;
}



透過配置類將自定義解析器註冊到Spring容器中

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
        return new ActMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(actMethodArgumentResolverConfigurer());
    }
}



到此,一個完整的基於註解的自定義引數解析器就完成了。

4.總結

瞭解Spring的引數解析器原理有助於正確使用Spring的引數解析器,也讓我們可以設計適用於自身系統的引數解析器,對於一些通用引數型別的解析減少重複程式碼的書寫,但是這裡有個前提是我們專案中複雜型別的入參要統一前端傳遞引數的格式也要統一,不然設計自定義引數解析器就是個災難,需要做各種複雜的相容工作。引數解析器的設計儘量要放在專案開發開始階段,歷史複雜的系統如果介面開發沒有統一規範也不建議自定義引數解析器設計。

該文章僅作為Spring引數解析器的介紹性解讀,希望對大家有所幫助,歡迎有這類需求或者興趣的同學溝通交流,批評指正,一起進步!

相關文章