一、背景 开发接口时经常会出现
有的接口一堆参数 得定义 Bean 用 @RequestBody 接收
有的又只有一两个 参数 用 @RequestParam 接收
快速迭代时修改接口导致 前端请求 ContentType 也要跟着改变
前端每次都要纠结参数放 param 还是放 body
就产生了 修改下参数绑定逻辑的想法
二、修改参数绑定 Spring 会从 request 中获取
parameterMap 和 body
formData 是放 parameterMap 中 表现为 key-values
x-www 是放在 body中 表现为 key=value&key=value
json 是放在 body 中 表现为 {key:value}
凭印象写的 具体得用 postMan 实测下
因为 post 和 get 会有区别
默认 get 没有 content-type 但 postMan 也能传递 x-www
首先 通过过滤器替换 request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 import java.io.IOException;import java.net.URL;import java.net.URLDecoder;import java.util.Map;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.core.Ordered;import org.springframework.core.annotation.Order;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.stereotype.Component;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import org.lqs1848.framework.enums.HttpMethod;import org.lqs1848.framework.utils.StringUtils;import org.lqs1848.framework.web.wrapper.ContentCachingRequestWrapper;import org.lqs1848.framework.web.wrapper.CryptoHttpServletResponse;import org.lqs1848.framework.web.wrapper.ResponseWrapper;import cn.hutool.core.date.DateUtil;import eu.bitwalker.useragentutils.Browser;import eu.bitwalker.useragentutils.OperatingSystem;import eu.bitwalker.useragentutils.UserAgent;import lombok.extern.slf4j.Slf4j;@Slf4j @Order(Ordered.HIGHEST_PRECEDENCE) @Component public class TraceFilter extends ActionFilter { @Override public void filter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { Long startTime = System.nanoTime(); String contentType = request.getContentType(); log.info("QueryStart_Url(" + request.getRequestURI() + ")_Method(" +request.getMethod()+")_Type(" +getContentType(contentType)+")_Ip(" +getIpAddress(request)+")" +getDevInfo(request.getHeader(HttpHeaders.USER_AGENT))); ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); log.info("QueryString:" + requestWrapper.getQueryString()); log.info("QueryParameter:" + (requestWrapper.getParameterMap().size() !=0 ? JSON.toJSONString(requestWrapper.getParameterMap()):"" )); if (requestWrapper.hasFile() && requestWrapper.getBodySize() != 0 ) { log.info("QueryBody: 内容是个文件" ); } else { String queryBody = requestWrapper.getQueryBody(); log.info("QueryBody:" + queryBody.replaceAll("\\s+" , " " )); if (HttpMethod.GET.matches(request.getMethod()) && requestWrapper.getBodySize() != 0 ) { queryBody = URLDecoder.decode(queryBody, "UTF-8" ); } if (queryBody != null && queryBody.length() > 3 && queryBody.length() < 999 ) { if (contentType == null || contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) { JSONObject json = JSONObject.parseObject(queryBody); for (String key : json.keySet()) { String value = json.getString(key); if (StringUtils.isNotEmpty(value) && !value.startsWith("{" ) && !value.startsWith("[" )) requestWrapper.put(key, value); } } if (contentType == null || contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) { String[] params = queryBody.split("&" ); for (String param : params) { String[] par = param.split("=" ); if (par.length != 2 ) break ; requestWrapper.put(par[0 ], par[1 ]); } } } if ((requestWrapper.getBodySize() == 0 || HttpMethod.GET.matches(request.getMethod())) && (contentType == null || contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE) || contentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE))) { Map<String,String> map = requestWrapper.getParameterMapEx(); if (!map.isEmpty()) { String mapJson = JSON.toJSONString(map) .replaceAll("\"\\{" ,"{" ) .replaceAll("\\}\"" , "}" ) .replaceAll("\"\\[" ,"[" ) .replaceAll("\\]\"" , "]" ) .replaceAll("\\\\\"" , "\"" ); requestWrapper.rewriteBody(mapJson); } } } boolean isMeHook = false ; ResponseWrapper proxyResponse; if (response instanceof ResponseWrapper) proxyResponse = (ResponseWrapper) response; else { proxyResponse = new ResponseWrapper(response); isMeHook = true ; } chain.doFilter(requestWrapper, proxyResponse); if (response != null && (response.getContentType() == null || response.getContentType().startsWith(MediaType.APPLICATION_JSON_VALUE))) log.info("ResponseBody Secret({}) Detail({})" , response instanceof CryptoHttpServletResponse, new String(proxyResponse.getContent())); else log.info("ResponseBody Detail({})" , "内容未知 type:" + response.getContentType()); Long elapsedTime = DateUtil.nanosToMillis(DateUtil.spendNt(startTime)); log.info("QueryEnd-Url:" + request.getRequestURI() + " elapsedTime(" + elapsedTime + ")" ); if (isMeHook) proxyResponse.finish(); } public static String getContentType (String contentType) { if (StringUtils.isEmpty(contentType)) return "NONE" ; if (contentType.indexOf(";" ) != -1 ) contentType = contentType.split(";" )[0 ]; if (contentType.indexOf("/" ) != -1 ) contentType = contentType.split("/" )[1 ]; return contentType; } public static String getDevInfo (String agent) { StringBuilder sb = new StringBuilder(); UserAgent userAgent = UserAgent.parseUserAgentString(agent); Browser browser = userAgent.getBrowser(); OperatingSystem operatingSystem = userAgent.getOperatingSystem(); sb.append("_DevType(" ).append(operatingSystem.getDeviceType()).append(")" ) .append("_Browser(" ).append(browser.getName()).append(")" ) .append("_Os(" ).append(operatingSystem.getName()).append(")" ); return sb.toString(); } public static String getIpAddress (HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for" ); if (ip != null && ip.length() != 0 && !"unknown" .equalsIgnoreCase(ip)) { if (ip.indexOf("," ) != -1 ) { ip = ip.split("," )[0 ]; } } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP" ); } if (ip == null || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.nio.charset.Charset;import java.util.Collections;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import org.apache.commons.fileupload.servlet.ServletFileUpload;import org.apache.commons.io.IOUtils;public class ContentCachingRequestWrapper extends HttpServletRequestWrapper { private byte [] body; private final Map<String, String[]> parameterMap; private BufferedReader reader; private ServletInputStream inputStream; private StringBuffer queryStringSb; private boolean hasFile; private boolean isRewriteBody = false ; public ContentCachingRequestWrapper (HttpServletRequest request) throws IOException { super (request); parameterMap = new HashMap<String, String[]>(request.getParameterMap()); queryStringSb = new StringBuffer(); if (super .getQueryString() != null ) queryStringSb.append(super .getQueryString()); loadBody(request); hasFile = ServletFileUpload.isMultipartContent(this ); } public boolean put (String key, String value) { if (!parameterMap.containsKey(key)) { parameterMap.put(key, new String[] { value }); if (queryStringSb.length() > 0 ) queryStringSb.append("&" ); queryStringSb.append(key).append("=" ).append(value); return true ; } return false ; } public Map<String, String> getParameterMapEx () { Map<String, String> map = new HashMap<>(); for (String key : parameterMap.keySet()) { map.put(key, this .getParameter(key)); } return map; } public void rewriteBody (String input) { this .body = input.getBytes(Charset.defaultCharset()); this .inputStream = new RequestCachingInputStream(this .body); this .isRewriteBody = true ; } public boolean isRewiteBody () { return this .isRewriteBody; } private void loadBody (HttpServletRequest request) throws IOException { this .body = IOUtils.toByteArray(request.getInputStream()); this .inputStream = new RequestCachingInputStream(body); } public byte [] getBody() { return this .body; } public int getBodySize () { return this .body.length; } @Override public String getContentType () { return super .getContentType(); } public boolean hasFile () { return this .hasFile; } @Override public String getQueryString () { return this .queryStringSb.toString(); } @Override public Map<String, String[]> getParameterMap() { return this .parameterMap; } @Override public String getParameter (String name) { String[] values = this .parameterMap.get(name); return values != null ? values[0 ] : null ; } @Override public String[] getParameterValues(String name) { return this .parameterMap.get(name); } @Override public Enumeration<String> getParameterNames () { return Collections.enumeration(parameterMap.keySet()); } @Override public ServletInputStream getInputStream () throws IOException { if (this .inputStream != null ) { return this .inputStream; } return super .getInputStream(); } @Override public BufferedReader getReader () throws IOException { if (reader == null ) { reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding())); } return reader; } public String getQueryBody () throws IOException { return IOUtils.toString(getBody(), getCharacterEncoding()); } }
一开始只覆盖了 getParameterMap() 以为就能让 Spring 读取到 Paramter
结果 Spring 是通过 getParameterNames() 获得 key 再去匹配 Controller 的参数名
反正 ServletRequest 中的方法最好全部都覆盖掉
到这里只是支持 body 中的数据放到 param 中
如果Controller 用 bean 去接收参数 还是有可能获取不到
用自定义 参数解析器去接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import org.springframework.core.MethodParameter;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.stereotype.Component;import org.springframework.web.bind.support.WebDataBinderFactory;import org.springframework.web.context.request.NativeWebRequest;import org.springframework.web.method.support.HandlerMethodArgumentResolver;import org.springframework.web.method.support.ModelAndViewContainer;import com.alibaba.fastjson.JSONObject;import org.lqs1848.framework.web.wrapper.ContentCachingRequestWrapper;import lombok.extern.slf4j.Slf4j;@Slf4j @Component public class DiyHandlerResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.getParameterType().getPackageName().startsWith("org.lqs1848" ); } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest nativeWebRequest, WebDataBinderFactory factory) throws Exception { ContentCachingRequestWrapper request = nativeWebRequest.getNativeRequest(ContentCachingRequestWrapper.class); String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE); if (request.getBodySize() > 0 && (request.isRewiteBody() || contentType == null || contentType.startsWith(MediaType.APPLICATION_JSON_VALUE))) { String queryBody = request.getQueryBody(); try { if (queryBody.indexOf("\"" + parameter.getParameterName() + "\"" ) != -1 ) { return JSONObject.parseObject(queryBody).getObject(parameter.getParameterName(), parameter.getParameterType()); } else { return JSONObject.parseObject(queryBody,parameter.getParameterType()); } }catch (Exception e) { log.error("参数注入增强-JSON转换失败" ); } } return null ; } }
三、总结 懒得重复写了 搬一下自己写在项目的中的说明
Controller 中可以以任何方式接收前端传递的 QueryBody 参数 封装了 ContentType JSON 自动转换为 x-www 和 fromData 的逻辑
比如 public R Test(Entity e)//不需要写 @RequestBody 注解 public R Test(String param1,String param2)//放在 body 的可以自动拆成参数名 不需要额外定义一个 vo public R Test(Entity1 e1,Entity2 e2)//可以同时接收多个数据 前端需要传递 { e1:{ … }, e2:{ … } }
甚至前端用 get请求 但是发送了x-www 的content-type 并把数据放在 requestBody 中 并以 xxx1=xxx1&xxx2=xx2 的形式传输 后台使用 JavaBean 也可以接收
写完代码
突然有点迷惑 为什么 Spring 没有去实现类似的逻辑呢