转载请注明出处。。。

一、介绍

用过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>

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

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

Spring boot中自定义Json参数解析器的更多相关文章

  1. 一步一步自定义SpringMVC参数解析器

    随心所欲,自定义参数解析器绑定数据. 题图:from Zoommy 干货 SpringMVC解析器用于解析request请求参数并绑定数据到Controller的入参上. 自定义一个参数解析器需要实现 ...

  2. Spring Boot2 系列教程(十八)Spring Boot 中自定义 SpringMVC 配置

    用过 Spring Boot 的小伙伴都知道,我们只需要在项目中引入 spring-boot-starter-web 依赖,SpringMVC 的一整套东西就会自动给我们配置好,但是,真实的项目环境比 ...

  3. 自定义HandlerMethodArgumentResolver参数解析器和源码分析

    在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如HttpServletRequest或HttpServletRe ...

  4. 自定义springmvc参数解析器

    实现spring HandlerMethodArgumentResolver接口 通过使用@JsonArg自定义注解来解析json数据(通过fastjson的jsonPath),支持多个参数(@Req ...

  5. Spring Boot中自定义注解+AOP实现主备库切换

    摘要: 本篇文章的场景是做调度中心和监控中心时的需求,后端使用TDDL实现分表分库,需求:实现关键业务的查询监控,当用Mybatis查询数据时需要从主库切换到备库或者直接连到备库上查询,从而减小主库的 ...

  6. 实现自定义的参数解析器——HandlerMethodArgumentResolver

    1.为什么需要自己实现参数解析器 我们都知道在有注解的接口方法中加上@RequestBody等注解,springMVC会自动的将消息体等地方的里面参数解析映射到请求的方法参数中. 如果我们想要的信息不 ...

  7. 探究Spring Boot中的接收参数问题与客户端发送请求传递数据

    结合此篇参考Spring框架学习笔记(9)--API接口设计相关知识及具体编码实现 在使用Spring Boot进行接收参数的时候,发现了许多问题,之前一直都很忙,最近才稍微有空研究一下此问题. 网上 ...

  8. Spring Boot 中自定义 SpringMVC 配置,到底继承谁哪一个类或则接口?

    看了这篇文章,写的非常的言简意赅,特此记录下: 1.Spring Boot 1.x 中,自定义 SpringMVC 配置可以通过继承 WebMvcConfigurerAdapter 来实现. 2.Sp ...

  9. Spring Boot(二):Spring Boot中的配置参数

    Spring Boot 配置参数 Spring Boot 帮助我们完成了许许多多的自动化配置 如果我们需要根据自己的需求修改配置 也是可以的 可以使用.properties 和 .yml 格式配置 这 ...

随机推荐

  1. Confluence 6 管理插件和组件

    一个 组件(add-on)是与 Confluence 分开安装的功能,能够加强 Confluence 的功能和使用.插件(plugin)和 组件(add-on)这 2 个词通常是一起使用的. 一共有 ...

  2. Linux基础实操二

    实操一: 1) 新建用户natasha uid为1000,gid为555,备注信息为“master” 2) 修改natasha用户的家目录为/Natasha 3) 查看用户信息配置文件的最后一行 ca ...

  3. C#关于线程的问题

    1.通过System.threading.Thread类可以创建新的线程,并在线程堆栈中运行静态和动态的实例,可以通过Thread类的构造方法传递一个无参数,并且不返回的委托, class Progr ...

  4. bzoj 2761

    神题... 其实这题巨水,用各种诡异的方法都能A,包括STL等等 我之所以写题解,是因为我发现了一个bug:bz和luogu时限有问题! 这题我用了两种做法: ①:直接使用STL-map(不能直接用数 ...

  5. linux学习笔记之 basename, dirname

    前言: basename: 用于打印目录或者文件的基本名称 dirname: 去除文件名中的非目录部分,仅显示与目录有关的内容.dirname命令读取指定路径名保留最后一个/及其后面的字符,删除其他部 ...

  6. AI学习吧-登录注册

    登录注册注销 如果需要给表设置权限,没有登录就不可以查看,只需要在每个视图函数之前加上Auth_classes=[ ]即可! 增加两张表,做登录认证 #models.py #做登录验证 class U ...

  7. Python函数之递归函数

    递归函数的定义:在这个函数里再调用这个函数本身 最大递归深度默认是997或者998,python从内存角度做的限制 优点:代码变简单 缺点:占内存 一:推导年龄 问a的值是多少: a 比 b 小2,b ...

  8. A.Ocean的礼物线段树

    A: Ocean的礼物  Time Limit: 2 s Memory Limit: 128 MB  Submit My Status  Problem Description  皇家理工存在一段很神 ...

  9. TFS 生成任务报错:目录不是空的

    转到代理目录下,将生成文件夹清空,重新启动生成任务即可

  10. [转] Mongoose初使用总结

    连接mongoose mongoose连接数据库有两种方式 第一种: 'use strict'; const mongoose = require('mongoose'); mongoose.conn ...