纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

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

不懂是非   2021-03-30 我要评论

一、介绍

用过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层参数所以这个注解的解析器肯定得写在该接口实现类里

@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等所有可以先去定义一个转换器接口。

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
@EnableWebMvc
public 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>

相关文章

猜您喜欢

网友评论

Copyright 2020 www.Shellfishsoft.com 【贝软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式