SpringBoot自定义json参数解析注入

自定义请求参数注入逻辑,允许将json解析到多个参数

背景

SpringBoot中接收json参数一般使用@RequestBody注解,基本样式如下

1
public Result<String> batchAdd(@RequestBody List<Product> data) 

但是这样有个问题,那就是 @RequestBody 只能出现一次,也就是所有json参数必须封装到一个bean里,大多数情况下这都不是什么问题,但是如果接口很多,每个接口参数都不同的话,就会有很多个类,外带我个人不怎么喜欢使用单个类来接收参数,我更习惯使用多个参数,这样接口需要的参数更加明确

原理

Spring中实现参数注入使用的是org.springframework.web.method.support.HandlerMethodArgumentResolver类,只需要实现该类即可

实现

最早我找了一篇博客,地址找不到了,总之内容是使用fastjson手动获取内容并转换类型,较为繁琐,而且对于容器类参数没有兼容,所以我参照 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters的逻辑重写了一份。

另外我还兼容了 query 传参,参数不再仅限于http body里的json格式,k=v形式一样能获取

代码如下

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.DataBinder;
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
* 参数解析器,支持将json body注入到不同参数中,同时支持普通的form、query传参
*
* @author 明明如月
* @date 2018/08/27
*/
@Component
public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {

private static final String JSONBODY_ATTRIBUTE = "JSON_REQUEST_BODY";
protected final List<HttpMessageConverter<?>> messageConverters;

public MultiRequestBodyArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}

/**
* 设置支持的方法参数类型
*
* @param parameter 方法参数
* @return 支持的类型
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 支持带@MultiRequestBody注解的参数
return parameter.hasParameterAnnotation(MultiRequestBody.class);
}

private Type getHttpEntityType(MethodParameter parameter) {
return parameter.nestedIfOptional().getNestedGenericParameterType();
}

/**
* 驼峰转下划线
*
* @param input
* @return
*/
private static String humpToUnderline(String input) {
if (input == null) return null; // garbage in, garbage out
int length = input.length();
StringBuilder result = new StringBuilder(length * 2);
int resultLength = 0;
boolean wasPrevTranslated = false;
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (i > 0 || c != '_') // skip first starting underscore
{
if (Character.isUpperCase(c)) {
if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') {
result.append('_');
resultLength++;
}
c = Character.toLowerCase(c);
wasPrevTranslated = true;
} else {
wasPrevTranslated = false;
}
result.append(c);
resultLength++;
}
}
return resultLength > 0 ? result.toString() : input;
}

private String getParameterName(MethodParameter parameter, MultiRequestBody parameterAnnotation) {
//注解的value是JSON的key
String key = parameterAnnotation.value();
// 如果@MultiRequestBody注解没有设置value,则取参数名FrameworkServlet作为json解析的key
if (!StringUtils.hasText(key)) {
// 注解为设置value则用参数名当做json的key
key = parameter.getParameterName();
// 由于整体使用下划线法,所以参数名也要转换
key = humpToUnderline(key);
}
return key;
}

@SuppressWarnings({"all"})
public <T> Object doResolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType = inputMessage.getHeaders().getContentType();

MultiRequestBody parameterAnnotation = parameter.getParameterAnnotation(MultiRequestBody.class);
String key = getParameterName(parameter, parameterAnnotation);

Object body = null;
if (contentType.toString().contains("application/x-www-form-urlencoded") || contentType.toString().contains("multipart")) {
body = getFromQuery(key, mavContainer, servletRequest, binderFactory, parameter, webRequest);
} else {
Type targetType = getHttpEntityType(parameter);
Class<T> targetClass = (targetType instanceof Class clazz ? clazz : null);
Class<?> contextClass = parameter.getContainingClass();

StringHttpInputMessage message = null;

String jsonBody = getRequestBody(webRequest);
String v = null;
if (jsonBody.startsWith("[") && jsonBody.endsWith("]")) {
// 此时为一个 array,因此不支持 key,只能整个传入
v = jsonBody;
} else if ("".equals(jsonBody)) {
v = null;
} else {
JSONObject jsonObject = JSON.parseObject(jsonBody);
if (jsonObject == null) {
v = null;
} else
// 注明了 key 的取特定json值,否则使用整个json字符串
v = "".equals(key) ? jsonBody : jsonObject.get(key).toString();
}
if (v != null) {
message = new StringHttpInputMessage(inputMessage.getHeaders(), v);
body = convertValue(targetClass, targetType, contextClass, contentType, message);
}
}

if (body == null) {
if (parameterAnnotation.required())
throw new IllegalArgumentException("require " + key);

}
return body;
}

@SuppressWarnings({"all"})
public <T> Object convertValue(Class<T> targetClass, Type targetType, Class<?> contextClass, MediaType contentType, StringHttpInputMessage msgToUse) throws IOException {
Object body = null;
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (msgToUse.hasBody()) {
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
}
break;
}
}
return body;
}


@Override
@SuppressWarnings({"all"})
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return doResolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

@SuppressWarnings({"all"})
private static class StringHttpInputMessage implements HttpInputMessage {

private final HttpHeaders headers;
private final String json;

public StringHttpInputMessage(HttpHeaders headers, String json) {
this.headers = headers;
this.json = json;
}

public boolean hasBody() {
return this.json != null;
}

@Override
public InputStream getBody() throws IOException {
return json == null ? InputStream.nullInputStream() : new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
}

@Override
public HttpHeaders getHeaders() {
return headers;
}
}

private <T> Object getFromQuery(String key,
ModelAndViewContainer mavContainer, HttpServletRequest servletRequest,
WebDataBinderFactory binderFactory, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {

Object v = null;
if (mavContainer.containsAttribute(key)) {
v = mavContainer.getModel().get(key);
} else {
v = servletRequest.getParameter(key);
}
if (v == null) {
return null;
}
DataBinder binder = binderFactory.createBinder(webRequest, null, key);

ConversionService conversionService = binder.getConversionService();
if (conversionService != null) {
TypeDescriptor source = TypeDescriptor.valueOf(String.class);
TypeDescriptor target = new TypeDescriptor(parameter);
if (conversionService.canConvert(source, target)) {
return binder.convertIfNecessary(v, parameter.getParameterType(), parameter);
}
}
return null;
}


/**
* 获取请求体JSON字符串
*/
private String getRequestBody(NativeWebRequest webRequest) throws IOException {
HttpServletRequest servletRequest = ((HttpServletRequest) webRequest.getNativeRequest());

// 有就直接获取
String jsonBody = (String) webRequest.getAttribute(JSONBODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
// 没有就从请求中读取
if (jsonBody == null) {
jsonBody = IoUtil.read(servletRequest.getReader());
webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
}
return jsonBody;
}
}

注解定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
/**
* 是否必须出现的参数
*/
boolean required() default true;

/**
* 参数名称,默认为参数名的下划线形式,如appId -> app_id
*/
String value() default "";
}

使用

只需要对参数使用@MultiRequestBody注解,样例如下

1
2
3
public Result<PageVO<ProductVO>> list(@MultiRequestBody PageRequest request
, @MultiRequestBody(value = "app_id", required = false) Long appId
);

SpringBoot自定义json参数解析注入
http://blog.inkroom.cn/2024/05/14/K4Q2PZ.html
作者
inkbox
发布于
2024年5月14日
许可协议