spring mvc 自定义参数绑定,自定义数据返回格式

概述

工作中有这么个需求:前端请求后端数据时参数采用蛇形命名法,即:/xxxx?user_name=ty&user_age=12,后端返回的json数据要也要蛇形命名法,即:

1
2
3
4
5
6
7
8
{
"user_name": "tuyu",
"user_age": 12,
"address": "xxxxx"
}
需要做两件事:
1. controller的方法的参数的命名方式采用小驼峰命名,希望将前端传递的蛇形命名的参数自动绑定到对应的驼峰命名的参数上
2. 后端返回的json数据的命名方式采用蛇形命名法

参数绑定

配置参数解析器
新建一个配置类继承WebMvcConfigurationSupport类,并加上@Configuration注解,重写addArgumentResolvers方法,需要在参数解析器列表中添加一个自定义的参数解析器,该自定义的参数解析器需要实现HandlerMethodArgumentResolver接口,并实现supportsParameter方法和resolveArgument方法。

配置HttpMessageConverter

要想返回返回的json数据采用蛇形命名法,在spring boot项目中有两种方法:

  1. 在主配置文件application.properties文件中添加配置项spring.jackson.property-naming-strategy=CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES
  2. 新建一个配置类继承WebMvcConfigurationSupport类,并加上@Configuration注解,重写configureMessageConverters方法,方法中实例化一个MappingJackson2HttpMessageConverter对象加入到HttpMessageConverter列表中,并为其设置一个PropertyNamingStrategyPropertyNamingStrategy.SnakeCaseStrategy()ObjectMapper,此外为MappingJackson2HttpMessageConverter对象设置supportedMediaTypes属性为MediaType.APPLICATION_JSON_UTF8,即:
    1
    2
    3
    4
    5
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    ObjectMapper mapper = new ObjectMapper();
    mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SnakeCaseStrategy());
    converter.setObjectMapper(mapper);
    converter.setSupportedMediaTypes(Arrays.asList(MedidaType.APPLICATION_JSON_UTF8));

整个配置文件如下:

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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
@Configuration
public class WebAppConfig extends CorsConfig {

@Autowired
private Validator validator;

@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
}

/**
* 配置参数绑定解析器
* @param argumentResolvers
*/
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
System.out.println("hello word , resolver");
argumentResolvers.add(new UnderlineToCamelArgumentResolver(validator));
}

/**
* 配置converter
* @param converters
*/
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// jacksonConverter(converters);
fastJsonConverter(converters);
}

private void jacksonConverter(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(){
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof ReturnData) {
object = getUnderscoreMap(object);
}
super.writeInternal(object, type, outputMessage);
}
};
converter.setObjectMapper(new ObjectMapper().setPropertyNamingStrategy(new PropertyNamingStrategy.LowerCaseStrategy()));
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
converters.add(converter);
}

private void fastJsonConverter(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(){
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof ReturnData) {
object = getUnderscoreMap(object);
} else if (object instanceof Json) {
object = handleSwaggerWebJson(object);
}
super.writeInternal(object, outputMessage);
}
};
SerializeConfig serializeConfig = new SerializeConfig();
serializeConfig.setPropertyNamingStrategy(com.alibaba.fastjson.PropertyNamingStrategy.CamelCase);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializeConfig(serializeConfig);
converter.setFastJsonConfig(fastJsonConfig);
converters.add(converter);
}

/**
* 将swagger请求的json数据特殊处理
* <p>将系统中查询的驼峰命名的数据转为蛇形命名法</p>
* @param object
*
* @return
*/
private static Object handleSwaggerWebJson(Object object) {
Json json = (Json) object;
String jsonStr = json.value();
jsonStr = SwaggerJsonHelper.preHandle(jsonStr);
LinkedHashMap resultMap = JSON.parseObject(jsonStr, LinkedHashMap.class);
// 找所有paths
Map paths = (Map) resultMap.get("paths");
if (paths != null) {
for (Object path : paths.entrySet()) {
// 每个path都会是post或者get或者put或者delete请求
Map.Entry pathEntry = (Map.Entry) path;
Map methods = (Map) pathEntry.getValue();
for (Object method : methods.entrySet()) {
Map.Entry methodEntry = (Map.Entry) method;
Map methodMap = (Map) methodEntry.getValue();
for (Object param : methodMap.entrySet()) {
JSONArray paramArray = (JSONArray) methodMap.get("parameters");
for (Object pa : paramArray) {
Map paramMap = (Map) pa;
Object name = paramMap.get("name");
if (name != null) {
String underscore = humpToLine(name.toString());
paramMap.put("name", underscore);
}
}
}
}

}
}
Map definitions = (Map) resultMap.get("definitions");
if (definitions != null) {
for (Object po : definitions.keySet()) {
Map poMap = (Map) definitions.get(po);

JSONArray requiredArr = (JSONArray) poMap.get("required");
if (requiredArr != null) {
int arrSize = requiredArr.size();
for (int i = 0; i < arrSize; i++) {
Object key = null;
if ((key = requiredArr.get(i)) != null) {
requiredArr.set(i, humpToLine(key.toString()));
}
}
}

Map propertyMap = (Map) poMap.get("properties");
if (propertyMap != null) {
Map newPropertyMap = new HashMap(propertyMap.size());
for (Object key : propertyMap.keySet()) {
newPropertyMap.put(humpToLine(key.toString()), propertyMap.get(key));
}
poMap.put("properties", newPropertyMap);
}
}
}
String result = JSON.toJSONString(resultMap);
result = SwaggerJsonHelper.postHandle(result);
return new Json(result);
}

private static class SwaggerJsonHelper {
private static final String DOLLAR = "MEIYUANFUHAO";
private static final String JING = "JINZIFU";

/**
* 将字符串中的特殊字符$、#替换为特殊的字符组合
* @param jsonStr
*
* @return
*/
public static String preHandle(String jsonStr) {
return jsonStr.replaceAll("\\$", DOLLAR).replaceAll("\\#", JING);
}

/**
* 将字符串中的特殊的字符组合替换为特殊字符$、#
* @param jsonStr
*
* @return
*/
public static String postHandle(String jsonStr) {
return jsonStr.replaceAll(DOLLAR, "\\$").replaceAll(JING, "\\#");
}
}

/**
* 下划线转驼峰参数绑定解析器
*/
private static class UnderlineToCamelArgumentResolver implements HandlerMethodArgumentResolver {

private Validator validator;

public UnderlineToCamelArgumentResolver() {
}

public UnderlineToCamelArgumentResolver(Validator validator) {
this.validator = validator;
}

/**
* 匹配下划线的格式
*/
private static Pattern pattern = Pattern.compile("_(\\w)");

private static String underLineToCamel(String source) {
Matcher matcher = pattern.matcher(source);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}

@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return true;
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return handleParameterNames(parameter, webRequest);
}

private Object handleParameterNames(MethodParameter parameter, NativeWebRequest webRequest) {
if (isSimpleType(parameter.getParameterType())) {
// 基本类型
String parameterName = parameter.getParameterName();
String snake = humpToLine(parameterName);
String real = webRequest.getParameter(snake);
if (real == null) {
real = webRequest.getParameter(parameterName);
}
return real == null ? null : getObject(parameter.getParameterType(), real);
} else {
// 自定义类型
Object obj = BeanUtils.instantiate(parameter.getParameterType());
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(obj);
Iterator<String> paramNames = webRequest.getParameterNames();
while (paramNames.hasNext()) {
String paramName = paramNames.next();
Object o = webRequest.getParameter(paramName);
try {
wrapper.setPropertyValue(underLineToCamel(paramName), o);
} catch (BeansException e) {

}
}
// 参数校验
if (parameter.hasParameterAnnotation(Valid.class)) {
Set<ConstraintViolation<Object>> validate = validator.validate(obj);
if (validate != null && validate.size() != 0) {
StringBuilder sb = new StringBuilder();
int size = validate.size();
int i = 0;
for (ConstraintViolation<Object> cv : validate) {
sb.append(cv.getMessage());
i++;
if (i < size) {
sb.append(", ");
}
}
// 校验不通过,直接报异常
throw new RuntimeException(sb.toString());
}
}
return obj;
}
}
}

/**
* 利用反射实例化一个对象
* @param clazz 对象类型
* @param value 值
*
* @return
*/
private static Object getObject(Class clazz, String value) {
Constructor constructor = null;
Object o1 = null;
try {
constructor = clazz.getConstructor(String.class);
o1 = constructor.newInstance(value);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return o1;
}

/**
* 判断是否为基本数据类型、简单类型
* @param clazz
*
* @return
*/
private static boolean isSimpleType(Class<?> clazz) {
String simpleName = clazz.getSimpleName();
boolean simple = false;
switch (simpleName.toLowerCase()) {
case "byte":
simple = true;
break;
case "short":
simple = true;
break;
case "int":
simple = true;
break;
case "integer":
simple = true;
break;
case "long":
simple = true;
break;
case "float":
simple = true;
break;
case "double":
simple = true;
break;
case "boolean":
simple = true;
break;
case "char":
simple = true;
break;
case "character":
simple = true;
break;
case "bigdecimal":
simple = true;
break;
case "string":
simple = true;
break;
default:
simple = false;
break;

}
return simple;
}

/**
* 下划线转驼峰
*
* @param str
*
* @return
*/
public static String lineToHump(String str) {
str = str.toLowerCase();
Matcher matcher = linePattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}

private static Pattern linePattern = Pattern.compile("_([a-z])");
private static Pattern humpPattern = Pattern.compile("[A-Z]");

/**
* 驼峰转下划线
*
* @param str
*
* @return
*/
public static String humpToLine(String str) {
Matcher matcher = humpPattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}

/**
* 反射对象字段
*
* @param obj
*
* @return
*/
public static Map<String, Object> getUnderscoreMap(Object obj) {
if (obj == null) {
return null;
}
Class<?> bindClass = obj.getClass();
Map<String, Object> objMap = new HashMap<>(0);

/*
* 得到类中的所有属性集合
*/
try {
Field[] fs = bindClass.getDeclaredFields();
for (Field f : fs) {
//设置些属性是可以访问的
f.setAccessible(true);
if (!isSimpleType(f.getType())) {
Map<String, Object> childMap = getUnderscoreMap(f.get(obj));
objMap.put(humpToLine(f.getName()), childMap);
} else {
objMap.put(humpToLine(f.getName()), f.get(obj));
}
}
} catch (IllegalAccessException e) {
System.out.println(e.getMessage());
}
objMap.remove("serial_version_u_i_d");
return objMap;
}
}

配置文件有点长,因为除了解决上面提到的两个问题之外,还解决了其他问题:

  1. 项目中使用了swagger自动生成接口文档,swagger请求的json数据需要驼峰命名,但是项目返回的是蛇形名的数据,swagger报错
  2. 项目中使用JSR-303,JSR-349做参数校验,在没有自定义参数解析器的时候,验证是生效的,自定义参数解析器后,参数验证不生效了

解决swagger需要驼峰命名的json数据问题

在查阅资料后得知,所有实现HttpMessageConverter接口的子类,真正将json数据写到response的方法是writeInteral,MappingJackson2HttpMessageConverter也不例外,那么我就重写它的writeInteral方法,如果要写入的对象是swagger查询的对象类型,我就特殊对待,如果对象是controller中方法返回的统一封装的对象,就将该对象所有属性都变为蛇形命名,并将其放在Map中,再调父类的writeInteral方法处理,其他的对象就直接调用父类的writeInteral方法即可,那么MappingJackson2HttpMessageConverter对象也没有必要把ObjectMapper的PropertyNamingStrategy设为PropertyNamingStrategy.SnakeCaseStrategy()了,因为需要转换的对象已被转为蛇形命名的Map了。

解决自定义参数解析器之后参数验证不生效问题

在查阅资料后得知,可以手动调用Validator对参数进行校验,于是我可以在参数绑定完成之后,判断参数是否有注解@Valid来决定是手动进行参数验证,如果验证失败,就抛出运行时异常,有全局异常处理器处理,如果验证通过就返回。

其实在这一系列过程中,我还遇到了另外的问题:

  1. 同一个spring mvc项目中如果有多个继承WebMvcConfigurationSupport类的配置类,spring mvc框架只会让第一个被扫描的配置类生效,如果确实有多个配置类存在的必要,比如该项目:依赖了一个jar包,它里面用拦截器配置了一些统一认证的逻辑,而项目要配置自定义参数解析器等配置,这就会出现两个配置类,而依赖的jar包中的配置类我修改不了,能改的只有本项目的配置类。

解决方法:
既然只能修改本项目的配置类,那么就可以让本项目的配置类(B)继承依赖jar包的配置类(A),并在spring boot的启动类上加上注解排除依赖类的配置类(A),即:

1
2
3
4
@SpringBootApplication(exclude=A.class)
public class WebApplication{
// ....
}

  1. swagger返回的json数据有这么一段:
    1
    2
    3
    {
    "$ref": "#/definitions/xxxxx"
    }

当我用fastjson转为Map时,由于$和#是特殊字符,转换发生了错误

解决办法是:在转换前将特殊字符替换为特定的字符串,最后又将特定的字符串替换为特殊字符,eg:
jsonStr.replaceAll("\\$", "DOLLAR").replaceAll("\\#", "JING")

参考链接

  1. spring validator手动校验
  2. Jackson 在 Spring Boot 中的使用小结 1
  3. Java Json 数据下划线与驼峰格式进行相互转换
  4. SpringMVC对象绑定时自定义名称对应关系
  5. SpringBoot 自定义方法参数解析器HandlerMethodArgumentResolver