需求背景
咱們的團隊是一個多技術棧團隊,先前使用的是 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