0.前言
在我目前的项目中,是使用Vue.js
和Java 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表单是使用继承自ServletModelAttributeMethodProcessor
的ServletModelAttributeMethodProcessor
进行处理,而JSON数据是使用@RequestBody
注释的RequestResponseBodyMethodProcessor
进行处理。对于 HTTP Request 来说, 我们可以根据请求的content-type
来判断请求传递的参数是什么格式的。
content-type
是application/json
的 Request body 是 JSON 数据。content-type
是application/x-www-form-urlencoded
的 Request 是表单提交。1.1.自定义 JsonAndFormArgumentResolver 类实现不同数据的 Resolver 分发
于是,我们可以通过自定义
JsonAndFormArgumentResolver
类实现HandlerMethodArgumentResolver
接口,对不同content-type
调用不同的 Resolver 进行处理。123456789101112131415161718192021222324252627282930
public class JsonAndFormArgumentResolver implements HandlerMethodArgumentResolver {private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;private ModelAttributeMethodProcessor modelAttributeMethodProcessor;public JsonAndFormArgumentResolver(ModelAttributeMethodProcessor methodProcessor, RequestResponseBodyMethodProcessor bodyMethodProcessor){this.modelAttributeMethodProcessor = methodProcessor;this.requestResponseBodyMethodProcessor = bodyMethodProcessor;}@Overridepublic boolean supportsParameter(MethodParameter parameter) {boolean support = modelAttributeMethodProcessor.supportsParameter(parameter)|| requestResponseBodyMethodProcessor.supportsParameter(parameter);return support;}@Overridepublic 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数据的解析。
|
|
值得注意的是,这里有两个坑。其一,getRequestBody
方法中,会尝试通过request.getInputStream()
获取一个InputStream输入流,而这个输入流也许会在框架某处中调用,导致获取失败。其二,通过这个InputStream输入流获取到的字符串,不是JSON格式数据,会导致解析失败,无论是报错观感不好,还是以防影响程序正常运行,这里都最好自行判断字符串格式后,再进行JSON格式解析。
针对第一个问题,其实 Spring 提供了一个
ContentCachingRequestWrapper
类对HttpServletRequest
的实例进行包装,便可解决getInputStream
方法失效的问题。自定义过滤器HttpRequestFilter
类实现Filter
接口。1234567891011121314151617181920
public class HttpRequestFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic 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);}}@Overridepublic void destroy() {}}
并在web.xml中添加如下配置。
123456789
<!--所有请求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格式校验
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
/** * 用于校验一个字符串是否是合法的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
类,要配置才能生效的。
|
|
经过这番复杂的配置,终于实现同一接口兼容Form表单和JSON两种提交方式。
2.继承 HttpServletRequestWrapper 类方式实现JSON格式扩展
后来,在尝试解决getInputStream
方法失效问题时,看到网上有推荐重写HttpServletRequestWrapper
类的getInputStream
方法,同时也有文章提到,在这个类中同样可以解析这个InputStream输入流。于是,我尝试直接在HttpServletRequestWrapper
类中进行解析。
|
|
最后,同样在HttpRequestFilter
过滤器中对HttpServletRequest
类的实例进行再包装。
|
|
经过这两个类的简单操作,同样实现同一接口兼容Form表单和JSON两种提交方式。个人觉得,这个方式简单又优雅。
3.结语
至此,自动接收JSON参数或者Form表单参数已经完成,可以同时实现@RequstParam注解注入和对象自动注入两个方式解析表单提交和JSON提交,都可以正常绑定参数,不必再为同一返回数据提供两个接口,以满足不同请求格式的需求了,现在同一接口搞定两种常用格式数据。
转自:
https://jiacyer.com/2019/01/23/Java-Spring-form-json-compatibility/