需求背景
咱们的团队是一个多技术栈团队,先前使用的是 PHP 技术栈,后来组件了 Java 技术栈团队。实现这个功能的背景是 PHP 技术栈的 API 命名风格是下划线风格,咱们须要 Flow 这套命名风格来写 API,可是若是使用下划线风格的 API 会致使 Spring MVC 的数据映射失效。
为了解决这个问题,咱们一共经历了三套方案。java
实现方案
这里先说下最终是怎么作的。最后咱们加了一个拦截器,给 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); } }
基本内容就是修改了 getParameterNames
和 getParameterValues
的返回值。框架
方案原理
经过 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)); } }
发现调用的是 WebUtils
的 getParametersStartingWith
方法,继续观察里面的实现。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 的点就是修改 getParameterNames
和 getParameterValues
的返回值。这个方法返回的 Map
的 key 将会用来寻找映射对象的属性来赋值,若是是下划线风格的毫无疑问将会没法匹配而致使赋值失败。因此加 Wrapper 修改 getParameterNames
的返回值,让获取到的 key 都是驼峰风格的,同步的还要修改 getParameterValues
保证传入驼峰风格的 key 时也能正常的获取到 value。url