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 进行处理。

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数据的解析。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
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);}@Overrideprotected 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 解析失败,返回nullif (requestBody == null)return null;// 正常解析 JSONObject 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 中获取 JSONObjectJSONObject 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接口。
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类,要配置才能生效的。

123456789101112131415161718192021222324252627282930313233
/* * 添加扩展@RequstParam注解的 ExtendRequestParamArgumentResolver 解析器, * 若使用 JsonServletRequestWrapper 则无需添加此类 */@Configurationpublic class ResolverConfig {@Resourceprivate RequestMappingHandlerAdapter adapter;private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;private ModelAttributeMethodProcessor modelAttributeMethodProcessor;@PostConstructpublic 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);}}// 合并表单提交处理和@RequestBodyresolvers.add(new JsonAndFormArgumentResolver(modelAttributeMethodProcessor, requestResponseBodyMethodProcessor));adapter.setArgumentResolvers(resolvers);}}}

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

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

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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
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 到 MapObject 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);}}}}@Overridepublic String getParameter(String name) {String[] results = parameterMap.get(name);if (results == null || results.length <= 0)return null;elsereturn results[0];}@Overridepublic Map<String, String[]> getParameterMap() {return parameterMap;}@Overridepublic Enumeration<String> getParameterNames() {Vector<String> vector = new Vector<>(parameterMap.keySet());return vector.elements();}@Overridepublic String[] getParameterValues(String name) {String[] results = parameterMap.get(name);if (results == null || results.length <= 0)return null;elsereturn results;}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream data = new ByteArrayInputStream(bytes);return new ServletInputStream() {@Overridepublic boolean isFinished() {return data.available() <= 0;}@Overridepublic boolean isReady() {return data.available() > 0;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() throws IOException {return data.read();}};}}

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

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)) {/* 使用自定义 JsonServletRequestWrapper 则无需扩展@RequestParam注解  */ServletRequest requestWrapper = new JsonServletRequestWrapper(request);filterChain.doFilter(requestWrapper, servletResponse);} else {filterChain.doFilter(servletRequest, servletResponse);}}@Overridepublic void destroy() {}}

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

3.结语

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

转自:

https://jiacyer.com/2019/01/23/Java-Spring-form-json-compatibility/

java form提交 form提交json_java