首页 > 开发 > Java > 正文

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

2019-10-21 18:41:46
字体:
来源:转载
供稿:网友

一、介绍

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

/**  * 请求路径 http://127.0.0.1:8080/test 提交类型为application/json  * 测试参数{"sid":1,"stuName":"里斯"}  * @param str  */ @RequestMapping(value = "/test",method = RequestMethod.POST) public void testJsonStr(@RequestBody(required = false) String str){  System.out.println(str); } /**  * 请求路径 http://127.0.0.1:8080/testAcceptOrdinaryParam?str=123  * 测试参数  * @param str  */ @RequestMapping(value = "/testAcceptOrdinaryParam",method = {RequestMethod.GET,RequestMethod.POST}) public void testAcceptOrdinaryParam(String str){  System.out.println(str); }

第一个就是前端传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,下面列出该类的一个实现方法

@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {  HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);  if (resolver == null) {   throw new IllegalArgumentException(     "Unsupported parameter type [" + parameter.getParameterType().getName() + "]." +       " supportsParameter should be called first.");  }  return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); }

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

二、实现步骤

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

@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface RequestJson { /**  * 字段名,不填则默认参数名  * @return  */ String fieldName() default ""; /**  * 默认值,不填则默认为null。  * @return  */ String defaultValue() default "";}

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

@Componentpublic 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注解的代码   }

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

Spring,boot,Json,参数解析器

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

public interface Converter { /**  * 将value转为clazz类型  * @param clazz  * @param value  * @return  */ Object convert(Type clazz, Object value);}

有了这个接口,那肯定得有几个实现类,在这里,我将这些转换器划分为 ,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类型转换器

public class NumberConverter implements Converter{ @Override public Object convert(Type type, Object value){  Class<?> clazz = null;  if (!(type instanceof Class)){   return null;  }  clazz = (Class<?>) type;  if (clazz == null){   throw new RuntimeException("类型不能为空");  }else if (value == null){   return null;  }else if (value instanceof String && "".equals(String.valueOf(value))){   return null;  }else if (!clazz.isPrimitive() && clazz.getGenericSuperclass() != Number.class){   throw new ClassCastException(clazz.getTypeName() + "can not cast Number type!");  }  if (clazz == int.class || clazz == Integer.class){   return Integer.valueOf(String.valueOf(value));  }else if (clazz == short.class || clazz == Short.class){   return Short.valueOf(String.valueOf(value));  }else if (clazz == byte.class || clazz == Byte.class){   return Byte.valueOf(String.valueOf(value));  }else if (clazz == float.class || clazz == Float.class){   return Float.valueOf(String.valueOf(value));  }else if (clazz == double.class || clazz == Double.class){   return Double.valueOf(String.valueOf(value));  }else if (clazz == long.class || clazz == Long.class){   return Long.valueOf(String.valueOf(value));  }else if (clazz == BigDecimal.class){   return new BigDecimal(String.valueOf(value));  }else if (clazz == BigInteger.class){   return new BigDecimal(String.valueOf(value));  }else {   throw new RuntimeException("This type conversion is not supported!");  } }}

Date类型转换器

/** * 日期转换器 * 对于日期校验,这里只是简单的做了一下,实际上还有对闰年的校验, * 每个月份的天数的校验及其他日期格式的校验 * @author: qiumin * @create: 2018-12-30 10:43 **/public class DateConverter implements Converter{ /**  * 校验 yyyy-MM-dd HH:mm:ss  */ private static final String REGEX_DATE_TIME = "^//d{4}([-]//d{2}){2}[ ]([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}$"; /**  * 校验 yyyy-MM-dd  */ private static final String REGEX_DATE = "^//d{4}([-]//d{2}){2}$"; /**  * 校验HH:mm:ss  */ private static final String REGEX_TIME = "^([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}"; /**  * 校验 yyyy-MM-dd HH:mm  */ 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]$"; /**  * 默认格式  */ private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss"; /**  * 存储数据map  */ private static final Map<String,String> PATTERN_MAP = new ConcurrentHashMap<>(); static {  PATTERN_MAP.put(REGEX_DATE,"yyyy-MM-dd");  PATTERN_MAP.put(REGEX_DATE_TIME,"yyyy-MM-dd HH:mm:ss");  PATTERN_MAP.put(REGEX_TIME,"HH:mm:ss");  PATTERN_MAP.put(REGEX_DATE_TIME_NOT_CONTAIN_SECOND,"yyyy-MM-dd HH:mm"); } @Override public Object convert(Type clazz, Object value) {  if (clazz == null){   throw new RuntimeException("type must be not null!");  }  if (value == null){   return null;  }else if ("".equals(String.valueOf(value))){   return null;  }  try {   return new SimpleDateFormat(getDateStrPattern(String.valueOf(value))).parse(String.valueOf(value));  } catch (ParseException e) {   throw new RuntimeException(e);  } } /**  * 获取对应的日期字符串格式  * @param value  * @return  */ private String getDateStrPattern(String value){  for (Map.Entry<String,String> m : PATTERN_MAP.entrySet()){   if (value.matches(m.getKey())){    return m.getValue();   }  }  return DEFAULT_PATTERN; }}

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

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

public class RequestJsonFilter implements Filter { /**  * 用来对request中的Body数据进一步包装  * @param req  * @param response  * @param chain  * @throws IOException  * @throws ServletException  */ @Override public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {  ServletRequest requestWrapper = null;  if(req instanceof HttpServletRequest) {   HttpServletRequest request = (HttpServletRequest) req;   /**    * 只是为了防止一次请求中调用getReader(),getInputStream(),getParameter()    * 都清楚inputStream 并不具有重用功能,即多次读取同一个inputStream流,    * 只有第一次读取时才有数据,后面再次读取inputStream 没有数据,    * 即,getReader(),只能调用一次,但getParameter()可以调用多次,详情可见ContentCachingRequestWrapper源码    */   requestWrapper = new ContentCachingRequestWrapper(request);  }  chain.doFilter(requestWrapper == null ? req : requestWrapper, response); }

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

@Configuration@EnableWebMvcpublic class WebConfigure implements WebMvcConfigurer { @Autowired private RequestJsonHandler requestJsonHandler; // 把requestJson解析器也交给spring管理 @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {  resolvers.add(0,requestJsonHandler); } @Bean public FilterRegistrationBean filterRegister() {  FilterRegistrationBean registration = new FilterRegistrationBean();  registration.setFilter(new RequestJsonFilter());  //拦截路径  registration.addUrlPatterns("/");  //过滤器名称  registration.setName("requestJsonFilter");  //是否自动注册 false 取消Filter的自动注册  registration.setEnabled(false);  //过滤器顺序,需排在第一位  registration.setOrder(1);  return registration; } @Bean(name = "requestJsonFilter") public Filter requestFilter(){  return new RequestJsonFilter(); }}

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

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

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

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

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

@Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {  HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);  String contentType = request.getContentType();  // 不是json  if (!JSON_CONTENT_TYPE.equalsIgnoreCase(contentType)){   return null;  }  Object obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);  synchronized (RequestJsonHandler.class) {   if (obj == null) {    resolveRequestBody(request);    obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);    if (obj == null) {     return null;    }   }  }  RequestJson requestJson = methodParameter.getParameterAnnotation(RequestJson.class);  if (obj instanceof Map){   Map<String, String> map = (Map<String, String>)obj;   return dealWithMap(map,requestJson,methodParameter);  }else if (obj instanceof List){   List<Map<String,String>> list = (List<Map<String,String>>)obj;   return dealWithArray(list,requestJson,methodParameter);  }  return null; } /**  * 处理第一层json结构为数组结构的json串  * 这种结构默认就认为 为类似List<JavaBean> 结构,转json即为List<Map<K,V>> 结构,  * 其余情况不作处理,若controller层为第一种,则数组里的json,转为javabean结构,字段名要对应,  * 注意这里defaultValue不起作用  * @param list  * @param requestJson  * @param methodParameter  * @return  */ private Object dealWithArray(List<Map<String,String>> list,RequestJson requestJson,MethodParameter methodParameter){  Class<?> parameterType = methodParameter.getParameterType();  return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),JsonUtil.convertBeanToStr(list)); } /**  * 处理{"":""}第一层json结构为map结构的json串,  * @param map  * @param requestJson  * @param methodParameter  * @return  */ private Object dealWithMap(Map<String,String> map,RequestJson requestJson,MethodParameter methodParameter){  String fieldName = requestJson.fieldName();  if ("".equals(fieldName)){   fieldName = methodParameter.getParameterName();  }  Class<?> parameterType = methodParameter.getParameterType();  String orDefault = null;  if (map.containsKey(fieldName)){   orDefault = map.get(fieldName);  }else if (ConverterUtil.isMapType(parameterType)){   return map;  }else if (ConverterUtil.isBeanType(parameterType) || ConverterUtil.isCollectionType(parameterType)){   orDefault = JsonUtil.convertBeanToStr(map);  }else {   orDefault = map.getOrDefault(fieldName,requestJson.defaultValue());  }  return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),orDefault); } /**  * 解析request中的body数据  * @param request  */ private void resolveRequestBody(ServletRequest request){  BufferedReader reader = null;  try {   reader = request.getReader();   StringBuilder sb = new StringBuilder();   String line = null;   while ((line = reader.readLine()) != null) {    sb.append(line);   }   String parameterValues = sb.toString();   JsonParser parser = new JsonParser();   JsonElement element = parser.parse(parameterValues);   if (element.isJsonArray()){    List<Map<String,String>> list = new ArrayList<>();    list = JsonUtil.convertStrToBean(list.getClass(),parameterValues);    request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, list);   }else {    Map<String, String> map = new HashMap<>();    map = JsonUtil.convertStrToBean(map.getClass(), parameterValues);    request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, map);   }  } catch (IOException e) {   e.printStackTrace();  }finally {   if (reader != null){    try {     reader.close();    } catch (IOException e) {     // ignore     //e.printStackTrace();    }   }  } }

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

<dependencies>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-web</artifactId>  </dependency>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-tomcat</artifactId>   <scope>provided</scope>  </dependency>  <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-test</artifactId>   <scope>test</scope>  </dependency>  <dependency>   <groupId>com.google.code.gson</groupId>   <artifactId>gson</artifactId>   <version>2.8.4</version>  </dependency> </dependencies>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持CuoXin错新网。


注:相关教程知识阅读请移步到JAVA教程频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表