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>
----------------------------------------------------------------------------------------------------华丽的分界线------------------------------------------------------------------------------------------------------------
以后就是本文全部内容,若有不足或错误之处还望指正,谢谢!
推荐阅读
-
spring boot2 修改默认json解析器Jackson为fastjson
-
Spring boot中自定义Json参数解析器
-
spring boot拦截器中获取request post请求中的参数
-
Spring Boot Admin 更换应用管理端口后在Environment中配置参数
-
spring boot在mvc中添加自定义handler Interceptor,ViewResolver,MessageConverter
-
spring boot在mvc中添加自定义handler Interceptor,ViewResolver,MessageConverter
-
spring boot2.x 后端参数校验+统一异常处理+后端自定义全局统一接口返回响应数据格式
-
Spring security 自定义过滤器实现Json参数传递并兼容表单参数(实例代码)
-
spring boot2 修改默认json解析器Jackson为fastjson
-
spring boot拦截器中获取request post请求中的参数【转】