Spring自定义参数解析器的使用


前言

最近写SpringWeb项目时,以前在写的时候都是在用form表单,Spring可以很轻松的将参数值映射到控制器方法的对应参数中(我下面就就把这种行为叫做“请求参数映射”),但是这次我想前后端都使用Json交互数据,遇到了解析Json参数的问题,就是Spring貌似只能借助Json工具包将请求内容反序列化成为一个对象或者获取一个Json工具包提供的一个Json节点对象,不能像处理form表单一样直接将参数值映射过去,多番查询下,找到自定义参数器的解决方案,学习之后,写了一个较为完善的Json的请求参数映射。下面是我实战出来的经验教程。

一、Spring的参数解析器是什么?

老惯例,先介绍一下参数解析器是什么东西,简单来说,就是参数过滤规则与映射处理方式。如何区分这些过滤的规则,一般情况下我们通常是使用区分注解的方式最为直观易读,Spring可以根据不同的注解来确定应该映射哪个参数域的参数,当然我们也通过一些复杂的规则来定义参数解析器的过滤规则。
在Spring内部提供了很多参数解析器,大致分布在这两个包内,有兴趣的小伙伴可以了解一下。
org.springframework.web.method.annotation
org.springframework.web.servlet.mvc.method.annotation
这里咱们选取一些常用作为展示,并且展示它一些部分信息。

参数过滤规则注解参数解析器
请求参数域@RequestParamRequestParamMapMethodArgumentResolver
uri 路径中的请求参数@PathVariablePathVariableMapMethodArgumentResolver
请求体中的参数@RequestBodyRequestResponseBodyMethodProcessor

具体更加详细Spring参数解析器的信息,可以参考一下 :
Spring参数解析器 - 作者: 骑个小蜗牛
了解Spring参数解析器的原理,可以参考这个:
Spring 全面解析 - 作者:securitit

二、Spring参数解析器的接口

下面就是我们创建Spring参数解析器时需要实现的接口类。

public interface HandlerMethodArgumentResolver {
	
	// 定义哪些 控制器参数 可以通过该过滤器
	boolean supportsParameter(MethodParameter parameter);
	
	// 定义通过该过滤器的 控制器参数 的映射处理方式
	Object resolveArgument(MethodParameter parameter, 
		@Nullable ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, 
		@Nullable WebDataBinderFactory binderFactory) throws Exception;
}

简单说明一下参数解析器的一些运行机制
首先来讲,Spring初始化的时候会把所有的参数解析器都放进一个集合里(ArrayList)。

当调用某个控制器方法时,方法中参数都会遍历每个参数解析器,调用supportsParameter方法,一旦某个参数解析器返回了true,则为该方法参数指定该参数解析器,之后跳出循环遍历。

对于已经指定参数解析器的方法参数,之后则不会再去遍历查找匹配的参数解析器。
所以在匹配参数解析器时,在数组中更靠前的参数解析器则享受更高的匹配优先级。

如果某个方法参数没有与之匹配的参数解析器,则会报一个运行时异常(RuntimeException)。
如果匹配成功则会调用resolveArgument方法,得到与方法参数与之映射的请求参数。

涉及到的参数类型的概述:
MethodParameter

这个类 类似于一个辅助包装类,该对象将关于参数信息获取方式封装了起来(比如参数类型,参数在方法中的位置,参数的注解,参数名等等),使用它时不用关心参数的归属是一个方法,还是构造方法。

NativeWebRequest

这个接口类是Spring 抽象出来的 web 请求的通用接口,我们平时一般的请求对象是ServletWebRequest,实现了NativeWebRequest接口,同时该类也是对Tomcat 的HttpServletRequest一个包装类 。
对于大部分接口方法,我们可以按照HttpServletRequest的习惯去使用这个接口类。

ModelAndViewContainer

这个类我个人没有接触的不多,就直接贴官方文档了。
官方描述:记录HandlerMethodArgumentResolvers和HandlerMethodReturnValueHandlers在调用控制器方法的过程中做出的与模型和视图相关的决策。
ModelAndViewContainer的概述的源码

WebDataBinderFactory

该类是Spring 数据绑定器工厂。具体详解可以参考:SpringMVC中WebDataBinder的应用及原理

三、Spring参数解析器的使用

基本使用

  1. 创建实现了HandlerMethodArgumentResolver接口的类
  2. 将该类添加到Spring的配置中。

创建实现了HandlerMethodArgumentResolver接口的类

@Component
public class ArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
    public boolean supportsParameter(MethodParameter parameter) {
        String name = parameter.getParameterName();
        if (name == null) return false;
        if (name.equals("test"))
            return true;
        return false;
    }
	@Override
    public Object resolveArgument(MethodParameter parameter, 
    		ModelAndViewContainer mavContainer, 
    		NativeWebRequest webRequest, 
    		WebDataBinderFactory binderFactory) throws Exception {
		return "TEST";
	}
}

将该类添加到Spring的配置中
创建一个实现WebMvcConfigurer的类。

@Configuration
public class ArgumentWebMvcConfig implements WebMvcConfigurer {
	
	@Resource
    private TestArgumentResolver testArgumentResolver;
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(testArgumentResolver);
    }
}

之后我们就可以创建Cotroller类进行测试了。

@RequestMapping("/demo7")
public String demo(int num,String test) {
    System.out.println(num);
    System.out.println(test);
    return "";
}
-- 输入 -- 
num=1&test=ffff
-- 结果 --
1
TEST

搭配注解使用

下面是一个通过注解来匹配参数的参数解析器。
该参数解析器在匹配到RA注解之后,然后尝试从RequestAttribute 域中匹配相应的 value

注解 - RA

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RA {
	// 别名
    String value() default "";
}

参数解析器 - RequestAttributeArgumentResolver

@Component
public class RequestAttributeArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        RA ra = parameter.getParameterAnnotation(RA.class);
        return ra != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        RA anno = parameter.getParameterAnnotation(RA.class);
        if (anno == null) throw new NullPointerException();
        String pName = anno.value();
        if (pName == null || pName.equals(""))
            pName = parameter.getParameter().getName();
        Object value = webRequest.getAttribute(pName, NativeWebRequest.SCOPE_REQUEST);
        return value;
    }

}

四、利用自定义参数解析器解析JSON

Spring 默认的处理JSON参数的方式

从目前网上得到信息来看,让Spring 去处理前端传来的JSON参数有以下几种方式:

接收一个实体

// {"name": "user","age": 20 }
@RequestMapping("/xxx")
public void springDemo1(@RequestBody User user) throws Exception{
    System.out.println("user:" + user);    
}

将整体接收为一个 Map

// {"name": "map","age": 20 }
@RequestMapping("/xxx")
public void springDemo2(@RequestBody Map<String, String> map) throws Exception{
    System.out.println("name:"+map.get("name"));
}

将整体接收为一个 List

@RequestMapping("/xxx")
public void springDemo3(@RequestBody List<User> list) throws Exception{
    for(User user:list) {
        System.out.println("user:" + user);
    }
}

接收原始的JSON对象

    @RequestMapping("/test5")
    public void springDemo4(@RequestBody JSONObject json) throws Exception{
        System.out.println("name:"+json.getString("name"));
    }

出现的问题

你会发现这些接受的方式都是将整体作为一个对象传入控制器方法,倘若有时候你想将其中的一个参数一一的写在控制器方法上,比如下面的场景
假如前端传来这样的JSON参数

{
  "str": "里胡经理立刻就会",
  "number": 123456,
  "number1": 12.212,
  "is" : true
}

如果你想这样处理json里的参数:

    @RequestMapping("/demo")
public String demo(String str, int number, double number1, boolean is) {
    System.out.println("str:" + str);
    System.out.println("number:" + number);
    System.out.println("number1:" + number1);
    System.out.println("is:" + is);
    return "";
}

显而易见的,Spring没法单独处理Json其中的参数,Spring 只能将Json处理成整体。
那么这个时候我们可以借助自定义参数解析器来更加优雅去处理参数。

优雅处理JSON参数的解决方案

该章节只给出关键部分的代码,我个人写了一个Demo项目,用来帮助各位小伙伴理解。
Demo项目的下载地址:https://gitee.com/iZYH/JsonParamParserDemo

首先创建一个过滤器,该过滤器在Content-Type为application/json 时,提前将JsonNode放入RequestAttribute 中。

ParamFilter的代码

@Component
public class ParamFilter implements Filter {

    public static final String JSON_NAME = ParamFilter.class.getName() + "#JSON_NAME";

    private final ObjectMapper mapper = new ObjectMapper();

    public void doFilter(ServletRequest request, ServletResponse response, 
    						FilterChain chain) throws IOException, ServletException {
        Charset charset = Charset.forName(request.getCharacterEncoding());
        if (!request.getContentType().trim().equalsIgnoreCase("application/json"))
            chain.doFilter(request, response);
        else // 这里使用一个 RequestWrapper
            doFilter(new RequestWrapper((HttpServletRequest) request,charset),response,chain);
    }

    public void doFilter(RequestWrapper requestWrapper, ServletResponse response, 
    						FilterChain chain) throws IOException, ServletException {
        JsonNode node = mapper.readTree(requestWrapper.getReader());
        requestWrapper.setAttribute(JSON_NAME,node);
        chain.doFilter(requestWrapper,response);
    }
}

注意: 你需要注意到这里使用到一个RequestWrapper,由于Request中的 InputStream 只能读取一次,所以在输入流使用前,须将输入流读取出来并存储起来,以供后续Spring 内部使用。

RequestWrapper 的部分代码

public class RequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    private Charset charset;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        body = inputStreamToByte(request);
        charset = StandardCharsets.UTF_8;
    }

    public RequestWrapper(HttpServletRequest request,Charset charset) {
        super(request);
        body = inputStreamToByte(request);
        this.charset = charset;
    }

    @Override
    public final BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), charset));
    }

    @Override
    public final ServletInputStream getInputStream() throws IOException {
        return new ByteArrayServletInputStream(body);
    }

    private byte[] inputStreamToByte(HttpServletRequest request) {
        byte[] buffer = new byte[1024];
        int len;
        try (ServletInputStream inputStream = request.getInputStream()) {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            while ( (len = inputStream.read(buffer)) > -1)
                outputStream.write(buffer, 0, len);

            outputStream.flush();
            return outputStream.toByteArray();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static class ByteArrayServletInputStream extends ServletInputStream {

        private final ByteArrayInputStream inputStream;
        
        public ByteArrayServletInputStream(byte[] body) {
            inputStream = new ByteArrayInputStream(body);
        }
        @Override
        public int read() throws IOException { return inputStream.read(); }
        @Override
        public boolean isFinished() { return false; }
        @Override
        public boolean isReady() { return false; }
        @Override
        public void setReadListener(ReadListener listener) {}
    }

}

在这之后就可以创建参数解析器

JSON参数解析器 - ArgumentResolver

@Component
public class ArgumentResolver implements HandlerMethodArgumentResolver {
    private final ObjectMapper mapper = new ObjectMapper();

    public boolean supportsParameter(MethodParameter parameter) {
        Json arg = parameter.getParameterAnnotation(Json.class);
        return arg != null;
    }
    
    public Object resolveArgument(MethodParameter parameter, 
    						ModelAndViewContainer mavContainer, 
    						NativeWebRequest webRequest, 
    						WebDataBinderFactory binderFactory) throws Exception {
        // 获取参数名
        Json anno = parameter.getParameterAnnotation(Json.class);
        if (anno == null) throw new NullPointerException();
        // 优先使用注解中的值作为参数名 检查注解中的值是否不为空或空串
        String pName = anno.value();
        if (pName == null || pName.equals(""))
            pName = parameter.getParameter().getName();
        return handleParam(webRequest,parameter,pName,
        		ResolverUtils.canParamNull(parameter,anno.canNull()) );
    }
    
    private Object handleParam(NativeWebRequest request, MethodParameter parameter,
    					String pName, boolean canNull) {
        // 在这里拿到在过滤器中放入到RequestAttribute的JsonNode
        Object node = request.getAttribute(ParamFilter.JSON_NAME, NativeWebRequest.SCOPE_REQUEST);
        if (node == null) 
            throw new NullPointerException("confirm whether " 
                    + "[content-type] is [application/json] !");
        // 处理Json参数
        Object value = handleParamFromJson(parameter,(JsonNode) node,pName);
        if (!canNull && value == null) // 非空检查
            throw new NullPointerException(ResolverUtils.invokeRef(parameter) 
            	+ " can't be null");
        return value;
    }
    
    private Object handleParamFromJson(MethodParameter param, JsonNode node, String pName) {
        // 处理引用名称 例如: users.list.name
        String[] refs = pName.split("\\.");
        JsonNode value = recurseGetJsonNode(node, refs, 0);
        if (value == null) return null;
        try {
        	// 构造类型
            JavaType type = constructJavaType(param.getGenericParameterType());
            return mapper.convertValue(value,type);
        } catch (Exception e) {
            throw new IllegalArgumentException(ResolverUtils.invokeRef(param) +
                    ": can't match the request value  <" + pName + "> " + "Error: " + e.getMessage());
        }
    }
    
    public JavaType constructJavaType(Type type) {
        // 不是参数化类型,递归结束
        if (!(type instanceof ParameterizedType))
            return mapper.getTypeFactory().constructType(type);
        ParameterizedType pType = (ParameterizedType) type;
        Type[] genericTypes = pType.getActualTypeArguments();
        if (genericTypes == null || genericTypes.length == 0)
            return mapper.getTypeFactory().constructType(type);
        // 分支判断 可以参数化嵌套的Json类型 为 Collection 或 Map
        if (Collection.class.isAssignableFrom((Class<?>)pType.getRawType()))
            return mapper.getTypeFactory().constructCollectionType( (Class<? extends Collection<?>>)pType.getRawType(),
                    constructJavaType(genericTypes[0])
            );
        else if (Map.class.isAssignableFrom( (Class<?>)pType.getRawType()) )
            return mapper.getTypeFactory().constructMapType( (Class<? extends Map<?,?>>)pType.getRawType(),
                    constructJavaType(genericTypes[0]),
                    constructJavaType(genericTypes[1])
            );
        else // 其他参数化类型的参数 不能受到支持
            return mapper.getTypeFactory().constructType(type);
    }
    
	private JsonNode recurseGetJsonNode(JsonNode node,String[] refs,int i) {
        if (node == null) return null;
        else if (i == refs.length - 1)
            return node.get(refs[i].trim());
        else return recurseGetJsonNode(node.get(refs[i].trim()),refs,i+1);
    }
    
}

至此重要部分的代码就介绍完了,在博客上看肯定会阅读困难,个人还是建议有时间的小伙伴可以抽空把demo下载下来阅读。效果会更好。

五、解析JSON的效果

以下测试使用的content-type均为application/json

接收 标准数据类型

@RequestMapping("/demo")
public String demo(@Json String str, @Json int number, @Json double number1, 
						@Json boolean is) {
    System.out.println("\ndemo: 接收 标准数据类型");
    System.out.println("str:" + str);
    System.out.println("number:" + number);
    System.out.println("number1:" + number1);
    System.out.println("is:" + is);
    return "";
}
---- 测试数据 ----
{
  "str": "里胡经理立刻就会",
  "number": 123456,
  "number1": 12.212,
  "is" : true
}
---- 结果 ----
demo: 接收 标准数据类型
str:里胡经理立刻就会
number:123456
number1:12.212
is:true

接收 对象

@RequestMapping("/demo1")
public String demo1(@Json User user) {
    System.out.println("\ndemo1: 接收 对象 -> User");
    System.out.println(user.getClass());
    System.out.println(user);
    return "";
}
---- 测试数据 ----
{
  "str": "里胡经理立刻就会",
  "number": 123456,
  "number1": 12.212,
  "is" : true
}
---- 结果 ----
class com.example.jsonparamparserdemo.User
User(name=zyh, age=20)

接收泛型数组

@RequestMapping("/demo2")
public String demo2(@Json List<String> list) {
    System.out.println("\ndemo2: 接收 泛型数组");
    System.out.println(list);
    return "";
}
---- 测试数据 ----
{
  "list": [
    "123",
    "456",
    "789"
  ]
}
---- 结果 ----
[123, 456, 789]

接收泛型Map

@RequestMapping("/demo3")
public String demo3(@Json Map<String,String> map) {
    System.out.println("\ndemo3: 接收 泛型Map -> Map<String,String>");
    System.out.println(map);
    return "";
}
---- 测试数据 ----
{
  "map": {
    "aaa": "123",
    "bbb": "456",
    "ccc": "789"
  }
}
---- 结果 ----
{aaa=123, bbb=456, ccc=789}

泛型嵌套

@RequestMapping("/demo4")
public String demo4(@Json Map<String,List<User>> userListMap) {
     System.out.println("\ndemo4: 接收 泛型嵌套 -> Map<String,List<User>>");
     userListMap.forEach((key,value) -> {
         System.out.println(key + " -> " + value);
     });
     return "";
 }
---- 测试数据 ----
{
  "userListMap": {
    "aaa": [
      {"name": "zyh", "age": 14},
      {"name": "zfg", "age": 31},
      {"name": "dfg", "age": 45}
    ],
    "bbb": [
      {"name": "erg", "age": 20},
      {"name": "jnm", "age": 31},
      {"name": "uij", "age": 24}
    ]
  }
}
---- 结果 ----
aaa -> [User(name=zyh, age=14), User(name=zfg, age=31), User(name=dfg, age=45)]
bbb -> [User(name=erg, age=20), User(name=jnm, age=31), User(name=uij, age=24)]

使用引用

@RequestMapping("/demo5")
public String demo5(@Json("data.lang.map") Map<String,List<User>> map) {
    System.out.println("\ndemo5: 使用引用取值 -> Map<String,List<User>>");
    map.forEach((key,value) -> {
        System.out.println(key + " -> " + value);
    });
    return "";
}
---- 测试数据 ----
{
  "data": {
    "lang": {
      "map": {
        "aaa": [
          {"name": "zyh", "age": 14},
          {"name": "zfg", "age": 31},
          {"name": "dfg", "age": 45}
        ],
        "bbb": [
          {"name": "erg", "age": 20},
          {"name": "jnm", "age": 31},
          {"name": "uij", "age": 24}
        ]
      },
      "do": "do some"
    }
  }
}
---- 结果 ----
aaa -> [User(name=zyh, age=14), User(name=zfg, age=31), User(name=dfg, age=45)]
bbb -> [User(name=erg, age=20), User(name=jnm, age=31), User(name=uij, age=24)]

这下基本上都可以直接在控制方法直接写参数了,爽了,我的强迫症内耗终于结束了! ╰(°▽°)╯

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值