• 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 {
    
        /**
         * json类型
         */
        private static final String JSON_CONTENT_TYPE = "application/json";
    
    
        @Override
        public boolean supportsParameter(MethodParameter methodParameter) {
           //只有被reqeustJson注解标记的参数才能进入
            return methodParameter.hasParameterAnnotation(RequestJson.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        // 解析requestJson注解的代码
            
        }

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

    要去解析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:mm: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:mm: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:mm: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:mm:ss");
    44         PATTERN_MAP.put(REGEX_TIME,"HH:mm: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>

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

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

  • 相关阅读:
    中文和英文
    文件字符流
    Java IO File
    关于整数拆分的递归法与母函数法
    图论·Dijkstra·HDU2066
    图论·Floyd算法·HDU2544&1874 (伪)2066
    关于 图论·并查集·HDU1232&1856
    Power of Cryptography
    Y2K Accounting Bug
    整数划分
  • 原文地址:https://www.cnblogs.com/qm-article/p/10199622.html
Copyright © 2020-2023  润新知