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 的位置。~~~~
相关文章
相关标签/搜索