Spring MVC實現GET請求下劃線風格參數映射至駝峯風格的Model

  • 需求背景
    咱們的團隊是一個多技術棧團隊,先前使用的是 PHP 技術棧,後來組件了 Java 技術棧團隊。實現這個功能的背景是 PHP 技術棧的 API 命名風格是下劃線風格,咱們須要 Flow 這套命名風格來寫 API,可是若是使用下劃線風格的 API 會致使 Spring MVC 的數據映射失效。
    爲了解決這個問題,咱們一共經歷了三套方案。java

    1. 將全部接收類的屬性都使用下劃線命名,這樣就能直接映射過來。在前期咱們使用的就是這種方案,不過考慮到這實際上不符合 Java 的代碼規範。後面咱們決定採用新的方案。
    2. 將全部參數都使用 JSON 進行傳輸,這樣只須要配置 JSON 的解析方式就能夠統一轉換過來。在大多數場景這個方案能夠很好的工做,可是 PHP 技術棧因爲框架的緣由,並不支持 使用 GET 發送 JSON Body。因此咱們須要開發出兼容方案。
    3. 未使用 JSON 傳輸參數的請求,將請求的下劃線風格的 QueryString 映射至駝峯風格的類屬性上。
  • 實現方案
    這裏先說下最終是怎麼作的。最後咱們加了一個攔截器,給 Request 對象加了一個 Wrapper 來修改它方法的返回值來改變框架的映射行爲。
    增長一個 Filter,在 Filter 裏給 Request 加一個 Wrapper。~~~~api

    @WebFilter(urlPatterns = {
          "/ms-api/payment/i-trade/query-trade",
          "/ms-api/payment/i-trade/query-withdraw",
          "/ms-api/payment/i-member/query-account",
          "/ms-api/payment/i-member/query-account-by-ids",
      }, filterName = "snakeCaseQueryStringFilter")
    public class SnakeCaseQueryStringConverterFilter implements Filter {
          @Override
          public void doFilter(ServletRequest servletRequest,
                               ServletResponse servletResponse,
                               FilterChain filterChain) throws IOException, ServletException {
              filterChain.doFilter(new SnakeCaseQueryStringRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
          }
    }

    這裏是 Wrapper 的具體實現app

    public class SnakeCaseQueryStringRequestWrapper extends HttpServletRequestWrapper {
      private final Enumeration<String> parameterNames;
      private final Map<String, String[]> parameterValues = new HashMap<>();
    
      public SnakeCaseQueryStringRequestWrapper(HttpServletRequest request) {
          super(request);
          Enumeration<String> parameterNames = super.getParameterNames();
          Vector<String> names = new Vector<>();
          while (parameterNames != null && parameterNames.hasMoreElements()) {
              String name = parameterNames.nextElement();
              String[] values = super.getParameterValues(name);
              String convertName = this.convertName(name);
              names.add(convertName);
              parameterValues.put(convertName, values);
          }
          this.parameterNames = names.elements();
      }
    
      private String convertName(String snakeCaseName) {
          if (!snakeCaseName.contains("_")) {
              return snakeCaseName;
          }
          StringBuilder stringBuilder = new StringBuilder();
          String[] name = snakeCaseName.split("_");
          for (int i = 0; i < name.length; i++) {
              String s = name[i];
              if (i != 0) {
                  s = toUpperFirstChar(s);
              }
              stringBuilder.append(s);
          }
          return stringBuilder.toString();
      }
    
      private String toUpperFirstChar(String string) {
          char[] charArray = string.toCharArray();
          charArray[0] -= 32;
          return String.valueOf(charArray);
      }
    
      @Override
      public Enumeration<String> getParameterNames() {
          return this.parameterNames;
      }
    
      @Override
      public String[] getParameterValues(String name) {
          return this.parameterValues.get(name);
      }
    }

    基本內容就是修改了 getParameterNamesgetParameterValues 的返回值。框架

  • 方案原理
    經過 Debug 觀察 Spring MVC 實現數據映射的原理。發如今 ServletRequestDataBinder 類裏的 bind方法這段代碼,變量 mpvs 是後面用來進行數據映射的數據。ide

    public void bind(ServletRequest request) {
          MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
          MultipartRequest multipartRequest = (MultipartRequest)WebUtils.getNativeRequest(request, MultipartRequest.class);
          if (multipartRequest != null) {
              this.bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
          }
    
          this.addBindValues(mpvs, request);
          this.doBind(mpvs);
      }

    繼續往裏面 Debug 觀察 mpvs 是如何獲得的。ui

    public class ServletRequestParameterPropertyValues extends MutablePropertyValues {
      public static final String DEFAULT_PREFIX_SEPARATOR = "_";
    
      public ServletRequestParameterPropertyValues(ServletRequest request) {
          this(request, (String)null, (String)null);
      }
    
      public ServletRequestParameterPropertyValues(ServletRequest request, @Nullable String prefix) {
          this(request, prefix, "_");
      }
    
      public ServletRequestParameterPropertyValues(ServletRequest request, @Nullable String prefix, @Nullable String prefixSeparator) {
          super(WebUtils.getParametersStartingWith(request, prefix != null ? prefix + prefixSeparator : null));
      }
    }

    發現調用的是 WebUtilsgetParametersStartingWith 方法,繼續觀察裏面的實現。this

    public static Map<String, Object> getParametersStartingWith(ServletRequest request, @Nullable String prefix) {
          Assert.notNull(request, "Request must not be null");
          Enumeration<String> paramNames = request.getParameterNames();
          Map<String, Object> params = new TreeMap();
          if (prefix == null) {
              prefix = "";
          }
    
          while(paramNames != null && paramNames.hasMoreElements()) {
              String paramName = (String)paramNames.nextElement();
              if ("".equals(prefix) || paramName.startsWith(prefix)) {
                  String unprefixed = paramName.substring(prefix.length());
                  String[] values = request.getParameterValues(paramName);
                  if (values != null && values.length != 0) {
                      if (values.length > 1) {
                          params.put(unprefixed, values);
                      } else {
                          params.put(unprefixed, values[0]);
                      }
                  }
              }
          }
    
          return params;
      }

    這裏最終發現了能夠 hook 的點就是修改 getParameterNamesgetParameterValues 的返回值。這個方法返回的 Map 的 key 將會用來尋找映射對象的屬性來賦值,若是是下劃線風格的毫無疑問將會沒法匹配而致使賦值失敗。因此加 Wrapper 修改 getParameterNames 的返回值,讓獲取到的 key 都是駝峯風格的,同步的還要修改 getParameterValues 保證傳入駝峯風格的 key 時也能正常的獲取到 value。url

  • 總結這是第一次具體的去看框架的源碼來解決問題。整個過程並無太多的麻煩,由於看源碼的時候不是毫無目的一行行看,而是參照前人已經總結好的資料來看,很快就能找到本身須要詳細 DEBUG 的位置。~~~~
相關文章
相關標籤/搜索