Spring boot中自定义Json参数解析器

转载请注明出处。。。

一、介绍

用过 springMVC/spring boot 的都清楚,在 controller 层接受参数,常用的都是两种接受方式,如下

 1 /**
 2      * 请求路径 http://127.0.0.1:8080/test 提交类型为 application/json
 3      * 测试参数 {"sid":1,"stuName":"里斯"}
 4      * @param str
 5      */
 6     @RequestMapping(value = "/test",method = RequestMethod.POST)
 7     public void testJsonStr(@RequestBody(required = false)String str){
 8         System.out.println(str);
 9     }
10     /**
11      * 请求路径 http://127.0.0.1:8080/testAcceptOrdinaryParam?str=123
12      * 测试参数
13      * @param str
14      */
15     @RequestMapping(value = "/testAcceptOrdinaryParam",method = {RequestMethod.GET,RequestMethod.POST})
16     public void testAcceptOrdinaryParam(String str){
17         System.out.println(str);
18     }

 

第一个就是前端传 json 参数,后台使用 RequestBody 注解来接受参数。第二个就是普通的 get/post 提交数据,后台进行接受参数的方式,当然 spring 还提供了参数在路径中的解析格式等,这里不作讨论

本文主要是围绕前端解析 Json 参数展开,那 @RequestBody 既然能接受 json 参数,那它有什么缺点呢,

原 spring 虽然提供了 @RequestBody 注解来封装 json 数据,但局限性也挺大的,对参数要么适用 jsonObject 或者 javabean 类,或者 string,

1、若使用 jsonObject 接收,对于 json 里面的参数,还要进一步获取解析,很麻烦

2、若使用 javabean 来接收, 若接口参数不一样,那么每一个接口都得对应一个 javabean 若使用 string 来接收,那么也得需要自己解析 json 参数

3、所以琢磨了一个和 get/post form-data 提交方式一样,直接在 controller 层接口写参数名即可接收对应参数值。

重点来了,那么要完成在 spring 给 controller 层方法注入参数前,拦截这些参数,做一定改变,对于此,spring 也提供了一个接口来让开发者自己进行扩展。这个接口名为HandlerMethodArgumentResolver,它呢 是一个接口,它的作用主要是用来提供 controller 层参数拦截和注入用的。spring 也提供了很多实现类,这里不作讨论,这里介绍它的一个比较特殊的实现类 HandlerMethodArgumentResolverComposite,下面列出该类的一个实现方法

 

 1 @Override
 2     @Nullable
 3     public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
 4             NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 5 
 6         HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
 7         if (resolver == null) {
 8             throw new IllegalArgumentException(
 9                     "Unsupported parameter type [" + parameter.getParameterType().getName()+ "]." +
10                             "supportsParameter should be called first.");
11         }
12         return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
13     }

 是不是感到比较惊讶,它自己不去执行自己的 resplveArgument 方法,反而去执行 HandlerMethodArgumentResolver 接口其他实现类的方法,具体原因,我不清楚,,,这个方法就是给 controller 层方法参数注入值得一个入口。具体的不多说啦!下面看代码

二、实现步骤

要拦截一个参数,肯定得给这个参数一个标记,在拦截的时候,判断有没有这个标记,有则拦截,没有则方向,这也是一种过滤器 / 拦截器原理,谈到标记,那肯定非注解莫属,于是一个注解类就产生了

 1 @Target(ElementType.PARAMETER)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 public @interface RequestJson {
 4 
 5     /**
 6      * 字段名,不填则默认参数名
 7      * @return
 8      */
 9     String fieldName() default "";
10 
11     /**
12      * 默认值,不填则默认为 null。
13      * @return
14      */
15     String defaultValue() default "";
16 }

 

这个注解也不复杂,就两个属性,一个是 fieldName,一个是 defaultValue。有了这个,下一步肯定得写该注解的解析器,而上面又谈到 HandlerMethodArgumentResolver 接口可以拦截 controller 层参数,所以这个注解的解析器肯定得写在该接口实现类里,

@Component
public class RequestJsonHandler implements HandlerMethodArgumentResolver {
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * json类型
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> String JSON_CONTENT_TYPE = "application/json"<span style="color: rgba(0, 0, 0, 1)">;


@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> supportsParameter(MethodParameter methodParameter) {
   </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">只有被reqeustJson注解标记的参数才能进入</span>
    <span style="color: rgba(0, 0, 255, 1)">return</span> methodParameter.hasParameterAnnotation(RequestJson.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
}

@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 解析requestJson注解的代码</span>
}

 

一个大致模型搭建好了。要实现的初步效果,这里也说下,如图

要去解析 json 参数,那肯定得有一些常用的转换器,把 json 参数对应的值,转换到 controller 层参数对应的类型中去,而常用的类型如 八种基本类型及其包装类,String、Date 类型,list/set,javabean 等,所有可以先去定义一个转换器接口。

 1 public interface Converter {
 2 
 3     /**
 4      * 将 value 转为 clazz 类型
 5      * @param clazz
 6      * @param value
 7      * @return
 8      */
 9     Object convert(Type clazz, Object value);
10 }

 

有了这个接口,那肯定得有几个实现类,在这里,我将这些转换器划分为 ,7 个阵营

1、Number 类型转换器,负责 Byte/Integer/Float/Double/Long/Short 及基础类型,还有 BigInteger/BigDecimal 两个类

2、Date 类型转换器,负责日期类型

3、String 类型转换器,负责 char 及包装类,还有 string 类型

4、Collection 类型转换器,负责集合类型

5、Boolean 类型转换器,负责 boolean/Boolean 类型

6、javaBean 类型转换器,负责普通的的 pojo 类

7、Map 类型转换器,负责 Map 接口

这里要需引入第三方包 google,在文章末尾会贴出来。

代码在这里就贴 Number 类型和 Date 类型,其余完整代码,会在 github 上给出,地址  github 链接

Number 类型转换器

 

 1 public class NumberConverter implements Converter{
 2 
 3     @Override
 4     public Object convert(Type type, Object value){
 5         Class<?> clazz = null;
 6         if (!(type instanceof Class)){
 7             return null;
 8         }
 9         clazz = (Class<?>) type;
10         if (clazz == null){
11             throw new RuntimeException("类型不能为空");
12         }else if (value == null){
13             return null;
14         }else if (value instanceof String && "".equals(String.valueOf(value))){
15             return null;
16         }else if (!clazz.isPrimitive() && clazz.getGenericSuperclass() != Number.class){
17             throw new ClassCastException(clazz.getTypeName() + "can not cast Number type!");
18         }
19         if (clazz == int.class || clazz == Integer.class){
20             return Integer.valueOf(String.valueOf(value));
21         }else if (clazz == short.class || clazz == Short.class){
22             return Short.valueOf(String.valueOf(value));
23         }else if (clazz == byte.class || clazz == Byte.class){
24             return Byte.valueOf(String.valueOf(value));
25         }else if (clazz == float.class || clazz == Float.class){
26             return Float.valueOf(String.valueOf(value));
27         }else if (clazz == double.class || clazz == Double.class){
28             return Double.valueOf(String.valueOf(value));
29         }else if (clazz == long.class || clazz == Long.class){
30             return Long.valueOf(String.valueOf(value));
31         }else if (clazz == BigDecimal.class){
32             return new BigDecimal(String.valueOf(value));
33         }else if (clazz == BigInteger.class){
34             return new BigDecimal(String.valueOf(value));
35         }else {
36             throw new RuntimeException("This type conversion is not supported!");
37         }
38     }
39 
40 
41 }

 

Date 类型转换器

 1 /**
 2  * 日期转换器
 3  * 对于日期校验,这里只是简单的做了一下,实际上还有对闰年的校验,
 4  * 每个月份的天数的校验及其他日期格式的校验
 5  * @author: qiumin
 6  * @create: 2018-12-30 10:43
 7  **/
 8 public class DateConverter implements Converter{
 9 
10     /**
11      * 校验 yyyy-MM-dd HH🇲🇲ss
12      */
13     private static final String REGEX_DATE_TIME = "^\\d{4}([-]\\d{2}){2}[]([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}$";
14 
15     /**
16      * 校验 yyyy-MM-dd
17      */
18     private static final String REGEX_DATE = "^\\d{4}([-]\\d{2}){2}$";
19 
20     /**
21      * 校验 HH🇲🇲ss
22      */
23     private static final String REGEX_TIME = "^([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}";
24 
25     /**
26      * 校验 yyyy-MM-dd HH:mm
27      */
28     private static final String REGEX_DATE_TIME_NOT_CONTAIN_SECOND = "^\\d{4}([-]\\d{2}){2}[]([0-1][0-9]|[2][0-4]):[0-5][0-9]$";
29 
30     /**
31      * 默认格式
32      */
33     private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH🇲🇲ss";
34 
35 
36     /**
37      * 存储数据 map
38      */
39     private static final Map<String,String> PATTERN_MAP = new ConcurrentHashMap<>();
40 
41     static {
42         PATTERN_MAP.put(REGEX_DATE,"yyyy-MM-dd");
43         PATTERN_MAP.put(REGEX_DATE_TIME,"yyyy-MM-dd HH🇲🇲ss");
44         PATTERN_MAP.put(REGEX_TIME,"HH🇲🇲ss");
45         PATTERN_MAP.put(REGEX_DATE_TIME_NOT_CONTAIN_SECOND,"yyyy-MM-dd HH:mm");
46     }
47 
48     @Override
49     public Object convert(Type clazz, Object value) {
50         if (clazz == null){
51             throw new RuntimeException("type must be not null!");
52         }
53         if (value == null){
54             return null;
55         }else if ("".equals(String.valueOf(value))){
56             return null;
57         }
58         try {
59             return new SimpleDateFormat(getDateStrPattern(String.valueOf(value))).parse(String.valueOf(value));
60         } catch (ParseException e) {
61             throw new RuntimeException(e);
62         }
63     }
64 
65     /**
66      * 获取对应的日期字符串格式
67      * @param value
68      * @return
69      */
70     private String getDateStrPattern(String value){
71         for (Map.Entry<String,String> m : PATTERN_MAP.entrySet()){
72             if (value.matches(m.getKey())){
73                 return m.getValue();
74             }
75         }
76         return DEFAULT_PATTERN;
77     }
78 }

 具体分析不做过多讨论,详情看代码。

那写完转换器,那接下来,我们肯定要从 request 中拿到前端传的参数,常用的获取方式有 request.getReader(),request.getInputStream(),但值得注意的是,这两者者互斥。即在一次请求中使用了一者,然后另一个就获取不到想要的结果。具体大家可以去试下。如果我们直接在解析 requestJson 注解的时候使用这两个方法中的一个,那很大可能会出问题,因为我们也保证不了在 spring 中某个方法有使用到它,那肯定最好结果是不使用它或者包装它 (提前获取 getReader()/getInputStream()中的数据,将其存入一个 byte 数组,后续 request 使用这两个方法获取数据可以直接从 byte 数组中拿数据),不使用肯定不行,那得进一步去包装它,在 java ee 中有提供这样一个类HttpServletRequestWrapper,它就是 httpsevletRequest 的一个子实现类,也就是意味 httpservletRequest 的可以用这个来代替,具体大家可以去看看源码,spring 提供了几个 HttpServletRequestWrapper 的子类,这里就不重复造轮子,这里使用ContentCachingRequestWrapper 类。对 request 进行包装,肯定得在 filter 中进行包装

 1 public class RequestJsonFilter implements Filter {
 2 
 3 
 4     /**
 5      * 用来对 request 中的 Body 数据进一步包装
 6      * @param req
 7      * @param response
 8      * @param chain
 9      * @throws IOException
10      * @throws ServletException
11      */
12     @Override
13     public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
14         ServletRequest requestWrapper = null;
15         if(req instanceof HttpServletRequest) {
16             HttpServletRequest request = (HttpServletRequest) req;
17             /**
18              * 只是为了防止一次请求中调用 getReader(),getInputStream(),getParameter()
19              * 都清楚 inputStream 并不具有重用功能,即多次读取同一个 inputStream 流,
20              * 只有第一次读取时才有数据,后面再次读取 inputStream 没有数据,
21              * 即,getReader(),只能调用一次,但 getParameter() 可以调用多次,详情可见 ContentCachingRequestWrapper 源码
22               */
23             requestWrapper = new ContentCachingRequestWrapper(request);
24         }
25         chain.doFilter(requestWrapper == null ? req : requestWrapper, response);
26     }

 

实现了过滤器,那肯定得把过滤器注册到 spring 容器中,

 1 @Configuration
 2 @EnableWebMvc
 3 public class WebConfigure implements WebMvcConfigurer {
 4 
 5 
 6     @Autowired
 7     private RequestJsonHandler requestJsonHandler;
 8 
 9    // 把 requestJson 解析器也交给 spring 管理
10     @Override
11     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
12         resolvers.add(0,requestJsonHandler);
13     }
14 
15     @Bean
16     public FilterRegistrationBean filterRegister() {
17         FilterRegistrationBean registration = new FilterRegistrationBean();
18         registration.setFilter(new RequestJsonFilter());
19         //拦截路径
20         registration.addUrlPatterns("/");
21         //过滤器名称
22         registration.setName("requestJsonFilter");
23         //是否自动注册 false 取消 Filter 的自动注册
24         registration.setEnabled(false);
25         //过滤器顺序, 需排在第一位
26         registration.setOrder(1);
27         return registration;
28     }
29 
30     @Bean(name = "requestJsonFilter")
31     public Filter requestFilter(){
32         return new RequestJsonFilter();
33     }
34 }

 

万事具备,就差解析器的代码了。

对于前端参数的传过来的 json 参数格式,大致有两种。

一、{"name":"张三"}

二、[{"name":"张三"},{"name":"张三 1"}]

所以解析的时候,要对这两种情况分情况解析。

  1 @Override
  2     public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
  3 
  4         HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
  5         String contentType = request.getContentType();
  6         // 不是 json
  7         if (!JSON_CONTENT_TYPE.equalsIgnoreCase(contentType)){
  8             return null;
  9         }
 10         Object obj =  request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);
 11         synchronized (RequestJsonHandler.class) {
 12             if (obj == null) {
 13                 resolveRequestBody(request);
 14                 obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);
 15                 if (obj == null) {
 16                     return null;
 17                 }
 18             }
 19         }
 20         RequestJson requestJson = methodParameter.getParameterAnnotation(RequestJson.class);
 21         if (obj instanceof Map){
 22             Map<String, String> map = (Map<String, String>)obj;
 23             return dealWithMap(map,requestJson,methodParameter);
 24         }else if (obj instanceof List){
 25             List<Map<String,String>> list = (List<Map<String,String>>)obj;
 26             return dealWithArray(list,requestJson,methodParameter);
 27         }
 28         return null;
 29     }
 30 
 31     /**
 32      * 处理第一层 json 结构为数组结构的 json 串
 33      * 这种结构默认就认为 为类似 List<JavaBean> 结构,转 json 即为 List<Map<K,V>> 结构,
 34      * 其余情况不作处理,若 controller 层为第一种,则数组里的 json,转为 javabean 结构,字段名要对应,
 35      * 注意这里 defaultValue 不起作用
 36      * @param list
 37      * @param requestJson
 38      * @param methodParameter
 39      * @return
 40      */
 41     private Object dealWithArray(List<Map<String,String>> list,RequestJson requestJson,MethodParameter methodParameter){
 42         Class<?> parameterType = methodParameter.getParameterType();
 43         return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),JsonUtil.convertBeanToStr(list));
 44     }
 45     /**
 46      * 处理 {"":""} 第一层 json 结构为 map 结构的 json 串,
 47      * @param map
 48      * @param requestJson
 49      * @param methodParameter
 50      * @return
 51      */
 52     private Object dealWithMap(Map<String,String> map,RequestJson requestJson,MethodParameter methodParameter){
 53         String fieldName = requestJson.fieldName();
 54         if ("".equals(fieldName)){
 55             fieldName = methodParameter.getParameterName();
 56         }
 57         Class<?> parameterType = methodParameter.getParameterType();
 58         String orDefault = null;
 59         if (map.containsKey(fieldName)){
 60             orDefault = map.get(fieldName);
 61         }else if (ConverterUtil.isMapType(parameterType)){
 62             return map;
 63         }else if (ConverterUtil.isBeanType(parameterType) || ConverterUtil.isCollectionType(parameterType)){
 64             orDefault = JsonUtil.convertBeanToStr(map);
 65         }else {
 66             orDefault = map.getOrDefault(fieldName,requestJson.defaultValue());
 67         }
 68         return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),orDefault);
 69     }
 70 
 71     /**
 72      * 解析 request 中的 body 数据
 73      * @param request
 74      */
 75     private void resolveRequestBody(ServletRequest request){
 76         BufferedReader reader = null;
 77         try {
 78             reader = request.getReader();
 79             StringBuilder sb = new StringBuilder();
 80             String line = null;
 81             while ((line = reader.readLine()) != null) {
 82                 sb.append(line);
 83             }
 84             String parameterValues = sb.toString();
 85             JsonParser parser = new JsonParser();
 86             JsonElement element = parser.parse(parameterValues);
 87             if (element.isJsonArray()){
 88                 List<Map<String,String>> list = new ArrayList<>();
 89                 list = JsonUtil.convertStrToBean(list.getClass(),parameterValues);
 90                 request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, list);
 91             }else {
 92                 Map<String, String> map = new HashMap<>();
 93                 map = JsonUtil.convertStrToBean(map.getClass(), parameterValues);
 94                 request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, map);
 95             }
 96         } catch (IOException e) {
 97             e.printStackTrace();
 98         }finally {
 99             if (reader != null){
100                 try {
101                     reader.close();
102                 } catch (IOException e) {
103                     // ignore
104                     //e.printStackTrace();
105                 }
106             }
107         }
108     }

 

整个代码结构就是上面博文,完整代码在 github 上,有感兴趣的博友,可以看看地址  github 链接,最后贴下 maven 依赖包

 1 <dependencies>
 2         <dependency>
 3             <groupId>org.springframework.boot</groupId>
 4             <artifactId>spring-boot-starter-web</artifactId>
 5         </dependency>
 6 
 7         <dependency>
 8             <groupId>org.springframework.boot</groupId>
 9             <artifactId>spring-boot-starter-tomcat</artifactId>
10             <scope>provided</scope>
11         </dependency>
12         <dependency>
13             <groupId>org.springframework.boot</groupId>
14             <artifactId>spring-boot-starter-test</artifactId>
15             <scope>test</scope>
16         </dependency>
17         <dependency>
18             <groupId>com.google.code.gson</groupId>
19             <artifactId>gson</artifactId>
20             <version>2.8.4</version>
21         </dependency>
22     </dependencies>

 

---------------------------------------------------------------------------------------------------- 华丽的分界线 ------------------------------------------------------------------------------------------------------------

以后就是本文全部内容,若有不足或错误之处还望指正,谢谢!