盒子
盒子
文章目录
  1. 0.前言
  2. 1.扩展@RequestParam注解方式实现JSON格式扩展
    1. 1.1.自定义 JsonAndFormArgumentResolver 类实现不同数据的 Resolver 分发
    2. 1.2.自定义 ExtendRequestParamArgumentResolver 类实现对@RequestParam注解的扩展
    3. 1.3.自定义配置 ResolverConfig 类实现以上两个类的注入
  3. 2.继承 HttpServletRequestWrapper 类方式实现JSON格式扩展
  4. 3.结语

Java Spring框架下同一接口兼容Form表单和JSON两种提交方式

0.前言

  在我目前的项目中,是使用Vue.jsJava Spring方式的前后端分离,使用JSON格式数据交互,但常常网页提交的数据是Form表单。为防止未来开放API接口或者开发APP时,使用JSON提交数据时,带来的不便,我决定尝试同一接口兼容Form表单和JSON两种提交。
  Google了解下来,发现几乎全网都是仅仅重写兼容Form表单和JSON的自动注入对象方式,或者仅仅扩展了@RequestParam注解,使之兼容解析JSON格式的数据。而常常在针对Form表单提交的接口设计时,会@RequestParam注解和对象自动注入同时使用,但很难找到相关教程,自己不断踩坑,前后花了三天左右的时间,才大致弄清楚实现的方式,并找到两套同样效果的实现方案。

1.扩展@RequestParam注解方式实现JSON格式扩展

  参看 liulu 的spring mvc 拓展 – controller 方法不加注解自动接收json参数或者from表单参数一文可知,Form表单是使用继承自ServletModelAttributeMethodProcessorServletModelAttributeMethodProcessor进行处理,而JSON数据是使用@RequestBody注释的RequestResponseBodyMethodProcessor进行处理。对于 HTTP Request 来说, 我们可以根据请求的content-type来判断请求传递的参数是什么格式的。

  • content-typeapplication/json的 Request body 是 JSON 数据。
  • content-typeapplication/x-www-form-urlencoded的 Request 是表单提交。

    1.1.自定义 JsonAndFormArgumentResolver 类实现不同数据的 Resolver 分发

      于是,我们可以通过自定义JsonAndFormArgumentResolver类实现HandlerMethodArgumentResolver接口,对不同content-type调用不同的 Resolver 进行处理。
    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
    public class JsonAndFormArgumentResolver implements HandlerMethodArgumentResolver {
    private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;
    private ModelAttributeMethodProcessor modelAttributeMethodProcessor;
    public JsonAndFormArgumentResolver(ModelAttributeMethodProcessor methodProcessor, RequestResponseBodyMethodProcessor bodyMethodProcessor){
    this.modelAttributeMethodProcessor = methodProcessor;
    this.requestResponseBodyMethodProcessor = bodyMethodProcessor;
    }
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    boolean support = modelAttributeMethodProcessor.supportsParameter(parameter)
    || requestResponseBodyMethodProcessor.supportsParameter(parameter);
    return support;
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
    if (request != null) {
    if (HttpMethod.GET.matches(request.getMethod().toUpperCase()))
    return modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE))
    return requestResponseBodyMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
    return modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
    }

1.2.自定义 ExtendRequestParamArgumentResolver 类实现对@RequestParam注解的扩展

  实际上,@RequestParam注解是使用RequestParamMethodArgumentResolver类进行解析的,自动注入到@RequestParam注解对应的字段中或者实体类中。这里,我们需要扩展这个类,以实现在按照Form表单数据解析失败后,再次尝试以JSON数据的解析。

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
public class ExtendRequestParamArgumentResolver extends RequestParamMethodArgumentResolver {
private static final String JSON_BODY_ATTRIBUTE = "JSON_REQUEST_BODY";
public ExtendRequestParamArgumentResolver(boolean useDefaultResolution) {
super(useDefaultResolution);
}
public ExtendRequestParamArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
super(beanFactory, useDefaultResolution);
}
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
// 尝试 form 格式解析
Object arg = super.resolveName(name, parameter, request);
// 解析失败时,尝试 JSON 格式解析
if (arg == null && name != null) {
JSONObject requestBody = getRequestBody(request);
// JSON 解析失败,返回null
if (requestBody == null)
return null;
// 正常解析 JSON
Object argValue = requestBody.get(name);
Class<?> parameterType = parameter.getParameterType();
if (parameterType.isPrimitive()) {
arg = parsePrimitive(parameterType.getName(), argValue);
} else if (isBasicDataTypes(parameterType)) {
arg = parseBasicTypeWrapper(parameterType, argValue);
} else {
arg = JSON.parseObject(argValue.toString(), parameterType);
}
}
return arg;
}
// 基本类型解析
private Object parsePrimitive(String parameterTypeName,Object value){
if(value == null)
return null;
final String booleanTypeName = "boolean";
if(booleanTypeName.equals(parameterTypeName))
return Boolean.valueOf(value.toString());
final String intTypeName = "int";
if(intTypeName.equals(parameterTypeName))
return Integer.valueOf(value.toString());
final String charTypeName = "char";
if(charTypeName.equals(parameterTypeName))
return value.toString().charAt(0);
final String shortTypeName = "short";
if(shortTypeName.equals(parameterTypeName))
return Short.valueOf(value.toString());
final String longTypeName ="long";
if(longTypeName.equals(parameterTypeName))
return Long.valueOf(value.toString());
final String floatTypeName = "float";
if(floatTypeName.equals(parameterTypeName))
return Float.valueOf(value.toString());
final String doubleTypeName = "double";
if(doubleTypeName.equals(parameterTypeName))
return Double.valueOf(value.toString());
final String byteTypeName = "byte";
if(byteTypeName.equals(parameterTypeName))
return Byte.valueOf(value.toString());
return null;
}
// 基本类型包装类型解析
private Object parseBasicTypeWrapper(Class<?> parameterType,Object value){
if(Number.class.isAssignableFrom(parameterType)){
Number number = (Number) value;
if(parameterType == Integer.class){
return number.intValue();
}else if(parameterType == Short.class){
return number.shortValue();
}else if(parameterType == Long.class){
return number.longValue();
}else if(parameterType == Float.class){
return number.floatValue();
}else if(parameterType == Double.class){
return number.doubleValue();
}else if(parameterType == Byte.class){
return number.byteValue();
}
}else if(parameterType == Boolean.class || parameterType == String.class){
return value.toString();
}else if(parameterType == Character.class){
return value.toString().charAt(0);
}
return null;
}
/**
* 基本数据类型直接返回
*/
private boolean isBasicDataTypes(Class clazz) {
Set<Class> classSet = new HashSet<>();
classSet.add(String.class);
classSet.add(Integer.class);
classSet.add(Long.class);
classSet.add(Short.class);
classSet.add(Float.class);
classSet.add(Double.class);
classSet.add(Boolean.class);
classSet.add(Byte.class);
classSet.add(Character.class);
return classSet.contains(clazz);
}
private JSONObject getRequestBody(NativeWebRequest webRequest) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// 尝试从 Request 中获取 JSONObject
JSONObject jsonBody = (JSONObject) webRequest.getAttribute(JSON_BODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
if (jsonBody == null && request != null) {
try {
// 需要从输入流中获取参数,Json格式数据不能用request.getParameter(name)方式获得
String jsonStr = IOUtils.toString(request.getInputStream());
if (JsonValidator.validate(jsonStr)) {
jsonBody = JSON.parseObject(jsonStr);
webRequest.setAttribute(JSON_BODY_ATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
}
} catch (IOException e) {
e.printStackTrace();
}
}
return jsonBody;
}
}

  值得注意的是,这里有两个坑。其一,getRequestBody方法中,会尝试通过request.getInputStream()获取一个InputStream输入流,而这个输入流也许会在框架某处中调用,导致获取失败。其二,通过这个InputStream输入流获取到的字符串,不是JSON格式数据,会导致解析失败,无论是报错观感不好,还是以防影响程序正常运行,这里都最好自行判断字符串格式后,再进行JSON格式解析。

  • 针对第一个问题,其实 Spring 提供了一个ContentCachingRequestWrapper类对HttpServletRequest的实例进行包装,便可解决getInputStream方法失效的问题。自定义过滤器HttpRequestFilter类实现Filter接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class HttpRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    if (request.getContentType() != null && request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
    /* 使用 ContentCachingRequestWrapper 需搭配 ExtendRequestParamArgumentResolver 扩展@RequestParam注解 */
    ServletRequest requestWrapper = new ContentCachingRequestWrapper(request);
    filterChain.doFilter(requestWrapper, servletResponse);
    } else {
    filterChain.doFilter(servletRequest, servletResponse);
    }
    }
    @Override
    public void destroy() {}
    }
  • 并在web.xml中添加如下配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!--所有请求getInputStream方法多次调用-->
    <filter>
    <filter-name>Request Context</filter-name>
    <filter-class>com.jiacyer.resolver.HttpRequestFilter</filter-class>
    </filter>
    <filter-mapping>
    <filter-name>Request Context</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>
  • 针对第二个问题,我在网上搜罗了一份检验JSON数据的代码。来自 iaiai Java JSON格式校验

    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
    /**
    * 用于校验一个字符串是否是合法的JSON格式
    */
    public class JsonValidator {
    private static CharacterIterator it;
    private static char c;
    private static int col;
    /**
    * 验证一个字符串是否是合法的JSON串
    *
    * @param input 要验证的字符串
    * @return true-合法 ,false-非法
    */
    public static boolean validate(String input) {
    if (StringUtils.isEmpty(input)) return false;
    input = input.trim();
    return valid(input);
    }
    private static boolean valid(String input) {
    boolean ret = true;
    it = new StringCharacterIterator(input);
    c = it.first();
    col = 1;
    if (!value()) {
    ret = error("value", 1);
    } else {
    skipWhiteSpace();
    if (c != CharacterIterator.DONE) {
    ret = error("end", col);
    }
    }
    it = null;
    return ret;
    }
    private static boolean value() {
    return literal("true") || literal("false") || literal("null") || string() || number() || object() || array();
    }
    private static boolean literal(String text) {
    CharacterIterator ci = new StringCharacterIterator(text);
    char t = ci.first();
    if (c != t) return false;
    int start = col;
    boolean ret = true;
    for (t = ci.next(); t != CharacterIterator.DONE; t = ci.next()) {
    if (t != nextCharacter()) {
    ret = false;
    break;
    }
    }
    nextCharacter();
    if (!ret) error("literal " + text, start);
    return ret;
    }
    private static boolean array() {
    return aggregate('[', ']', false);
    }
    private static boolean object() {
    return aggregate('{', '}', true);
    }
    private static boolean aggregate(char entryCharacter, char exitCharacter, boolean prefix) {
    if (c != entryCharacter) return false;
    nextCharacter();
    skipWhiteSpace();
    if (c == exitCharacter) {
    nextCharacter();
    return true;
    }
    for (;;) {
    if (prefix) {
    int start = col;
    if (!string()) return error("string", start);
    skipWhiteSpace();
    if (c != ':') return error("colon", col);
    nextCharacter();
    skipWhiteSpace();
    }
    if (value()) {
    skipWhiteSpace();
    if (c == ',') {
    nextCharacter();
    } else if (c == exitCharacter) {
    break;
    } else {
    return error("comma or " + exitCharacter, col);
    }
    } else {
    return error("value", col);
    }
    skipWhiteSpace();
    }
    nextCharacter();
    return true;
    }
    private static boolean number() {
    if (!Character.isDigit(c) && c != '-') return false;
    int start = col;
    if (c == '-') nextCharacter();
    if (c == '0') {
    nextCharacter();
    } else if (Character.isDigit(c)) {
    while (Character.isDigit(c))
    nextCharacter();
    } else {
    return error("number", start);
    }
    if (c == '.') {
    nextCharacter();
    if (Character.isDigit(c)) {
    while (Character.isDigit(c))
    nextCharacter();
    } else {
    return error("number", start);
    }
    }
    if (c == 'e' || c == 'E') {
    nextCharacter();
    if (c == '+' || c == '-') {
    nextCharacter();
    }
    if (Character.isDigit(c)) {
    while (Character.isDigit(c))
    nextCharacter();
    } else {
    return error("number", start);
    }
    }
    return true;
    }
    private static boolean string() {
    if (c != '"') return false;
    int start = col;
    boolean escaped = false;
    for (nextCharacter(); c != CharacterIterator.DONE; nextCharacter()) {
    if (!escaped && c == '\\') {
    escaped = true;
    } else if (escaped) {
    if (!escape()) {
    return false;
    }
    escaped = false;
    } else if (c == '"') {
    nextCharacter();
    return true;
    }
    }
    return error("quoted string", start);
    }
    private static boolean escape() {
    int start = col - 1;
    if (" \\\"/bfnrtu".indexOf(c) < 0) {
    return error("escape sequence \\\",\\\\,\\/,\\b,\\f,\\n,\\r,\\t or \\uxxxx ", start);
    }
    if (c == 'u') {
    if (!ishex(nextCharacter()) || !ishex(nextCharacter()) || !ishex(nextCharacter())
    || !ishex(nextCharacter())) {
    return error("unicode escape sequence \\uxxxx ", start);
    }
    }
    return true;
    }
    private static boolean ishex(char d) {
    return "0123456789abcdefABCDEF".indexOf(c) >= 0;
    }
    private static char nextCharacter() {
    c = it.next();
    ++col;
    return c;
    }
    private static void skipWhiteSpace() {
    while (Character.isWhitespace(c))
    nextCharacter();
    }
    private static boolean error(String type, int col) {
    System.out.printf("type: %s, col: %s%s", type, col, System.getProperty("line.separator"));
    return false;
    }
    }

1.3.自定义配置 ResolverConfig 类实现以上两个类的注入

  自定义的JsonAndFormArgumentResolver类和ExtendRequestParamArgumentResolver类,要配置才能生效的。

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
/*
* 添加扩展@RequstParam注解的 ExtendRequestParamArgumentResolver 解析器,
* 若使用 JsonServletRequestWrapper 则无需添加此类
*/
@Configuration
public class ResolverConfig {
@Resource
private RequestMappingHandlerAdapter adapter;
private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;
private ModelAttributeMethodProcessor modelAttributeMethodProcessor;
@PostConstruct
public void injectSelfMethodArgumentResolver() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
List<HandlerMethodArgumentResolver> argumentResolvers = adapter.getArgumentResolvers();
if (argumentResolvers != null) {
resolvers.add(new ExtendRequestParamArgumentResolver(false));
for (HandlerMethodArgumentResolver resolver : argumentResolvers) {
if (resolver instanceof RequestResponseBodyMethodProcessor) {
requestResponseBodyMethodProcessor = (RequestResponseBodyMethodProcessor) resolver;
}else if (resolver instanceof ModelAttributeMethodProcessor) {
modelAttributeMethodProcessor = (ModelAttributeMethodProcessor) resolver;
} else {
resolvers.add(resolver);
}
}
// 合并表单提交处理和@RequestBody
resolvers.add(new JsonAndFormArgumentResolver(modelAttributeMethodProcessor, requestResponseBodyMethodProcessor));
adapter.setArgumentResolvers(resolvers);
}
}
}

  经过这番复杂的配置,终于实现同一接口兼容Form表单和JSON两种提交方式。

2.继承 HttpServletRequestWrapper 类方式实现JSON格式扩展

  后来,在尝试解决getInputStream方法失效问题时,看到网上有推荐重写HttpServletRequestWrapper类的getInputStream方法,同时也有文章提到,在这个类中同样可以解析这个InputStream输入流。于是,我尝试直接在HttpServletRequestWrapper类中进行解析。

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
public class JsonServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> parameterMap; // 所有参数的Map集合
private JSONObject jsonBody; // JSON 数据
private byte[] bytes; // request inputstream 字节数据
public JsonServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
jsonBody = null;
parameterMap = request.getParameterMap();
if (parameterMap == null || parameterMap.size() <= 0) {
// 非 form 数据时,判断是否为 JSON 数据
String bodyStr = IOUtils.toString(request.getInputStream());
bytes = bodyStr.getBytes();
if (JsonValidator.validate(bodyStr))
jsonBody = JSON.parseObject(bodyStr);
if (jsonBody != null) {
parameterMap = new HashMap<>();
Set<String> keySet = jsonBody.keySet();
for (String key : keySet) {
// 解析 JSON 到 Map
Object object = jsonBody.get(key);
String[] t;
if (object == null)
continue;
else if (object instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) object;
t = new String[jsonArray.size()];
for (int i=0; i<t.length; i++)
t[i] = jsonArray.get(i).toString();
} else {
t = new String[1];
t[0] = object.toString();
}
parameterMap.put(key, t);
}
}
}
}
@Override
public String getParameter(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0)
return null;
else
return results[0];
}
@Override
public Map<String, String[]> getParameterMap() {
return parameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<>(parameterMap.keySet());
return vector.elements();
}
@Override
public String[] getParameterValues(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0)
return null;
else
return results;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream data = new ByteArrayInputStream(bytes);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return data.available() <= 0;
}
@Override
public boolean isReady() {
return data.available() > 0;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return data.read();
}
};
}
}

  最后,同样在HttpRequestFilter过滤器中对HttpServletRequest类的实例进行再包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HttpRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (request.getContentType() != null && request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
/* 使用自定义 JsonServletRequestWrapper 则无需扩展@RequestParam注解 */
ServletRequest requestWrapper = new JsonServletRequestWrapper(request);
filterChain.doFilter(requestWrapper, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {}
}

  经过这两个类的简单操作,同样实现同一接口兼容Form表单和JSON两种提交方式。个人觉得,这个方式简单又优雅。

3.结语

  至此,自动接收JSON参数或者Form表单参数已经完成,可以同时实现@RequstParam注解注入和对象自动注入两个方式解析表单提交和JSON提交,都可以正常绑定参数,不必再为同一返回数据提供两个接口,以满足不同请求格式的需求了,现在同一接口搞定两种常用格式数据。

参考贴来源:
spring mvc 拓展 – controller 方法不加注解自动接收json参数或者from表单参数 作者:liulu
Java JSON格式校验 作者:iaiai

转载说明

转载请注明出处,无偿提供。

支持一下
感谢大佬们的支持