Spring参数绑定 适应多种提交方式

一、背景

开发接口时经常会出现

有的接口一堆参数 得定义 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+", " "));

//支持 前端用 get 但是数据传递在body中
if(HttpMethod.GET.matches(request.getMethod()) && requestWrapper.getBodySize() != 0) {
queryBody = URLDecoder.decode(queryBody, "UTF-8");
}

if(queryBody != null && queryBody.length() > 3 && queryBody.length() < 999) {
//支持 前端用 JSON 时 后端用 @RequestParam 接收参数
if(contentType == null || contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) {
//对简单的 RquestBody JSON 数据 放入 RequestParam 封装
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);
}//for
}

//支持 前端用 x-www 后端用 参数 或者 JavaBean 接收
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]);
}//for
}
}//if

//支持 前端用 x-www 或 form-data 时 后端用 JavaBean 接收
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()) {
//这里主要是支持 单个参数里面有嵌套 json 意义不大
String mapJson = JSON.toJSONString(map)
.replaceAll("\"\\{","{")
.replaceAll("\\}\"", "}")
.replaceAll("\"\\[","[")
.replaceAll("\\]\"", "]")
.replaceAll("\\\\\"", "\"");
requestWrapper.rewriteBody(mapJson);
}
}
}//else

//劫持返回信息
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();
}// method


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();
//解析agent字符串
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)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实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;
}
}// class

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;
}//method getParameterMapEx

public void rewriteBody(String input) {
this.body = input.getBytes(Charset.defaultCharset());
this.inputStream = new RequestCachingInputStream(this.body);
this.isRewriteBody = true;
}//method rewriteBody

public boolean isRewiteBody() {
return this.isRewriteBody;
}

private void loadBody(HttpServletRequest request) throws IOException {
this.body = IOUtils.toByteArray(request.getInputStream());
this.inputStream = new RequestCachingInputStream(body);
}//method loadBody

public byte[] getBody() {
return this.body;
}//method getBody

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());
}
//必须重写 不然原生request getInputStream 只能调用一次
@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());
}
}//class

一开始只覆盖了 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) {
//是项目中的 JavaBean 才进行注入
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 {
//支持多个对象嵌套
/*
controller(JavaBeanA a,JavaBeanB b)
json{
a:{
xxx:xxx
},
b:{...}
}
*/
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转换失败");
}
}//if
return null;
}//method
}//class

三、总结

懒得重复写了 搬一下自己写在项目的中的说明

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 没有去实现类似的逻辑呢