【WEB系列】从0到1实现自定义web参数映射器

文章目录
  1. I. 项目搭建
    1. 1. 项目依赖
  2. II. 别名映射
    1. 0. 知识点概要说明
    2. 1. 自定义注解
    3. 2. 自定义参数处理器
    4. 3. 注册与测试
    5. 4. 小结
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 微信公众号: 一灰灰Blog

SpringBoot系列之从0到1实现自定义web参数映射器

在使用SpringMVC进行开发时,接收请求参数属于基本功,当我们希望将传参与项目中的对象关联起来时,最常见的做法是默认的case(即传参name与我们定义的name保持一致),当存在不一致,需要手动指定时,通常是借助注解@RequestParam来实现,但是不知道各位小伙伴是否有发现,它的使用是有缺陷的

  • @RequestParam不支持配置在类的属性上

如果我们定义一个VO对象来接收传承,这个注解用不了,如当我们定义一个Java bean(pojo)来接收参数时,若是get请求,post表单请求时,这个时候要求传参name与pojo的属性名完全匹配,如果我们有别名的需求场景,怎么整?

最简单的如传参为: user_id=110&user_name=一灰灰

而接收参数的POJO为

1
2
3
4
public class ViewDo {
private String uesrId;
private String userName;
}

接下来本文通过从0到1,手撸一个自定义的web传参映射,带你了解SpringMVC中的参数绑定知识点

I. 项目搭建

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

1
2
3
4
5
6
7
<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

配置文件application.yml

1
2
server:
port: 8080

II. 别名映射

接下来我们的目的就是希望实现一个自定义的别名注解,来支持传参的别名绑定,核心知识点就是自定义的参数解析器 HandlerMethodArgumentResolver

0. 知识点概要说明

在下面的实现之前,先简单介绍一下我们要用到的知识点

参数处理类:HandlerMethodArgumentResolver,两个核心的接口方法

1
2
3
4
5
6
7
8
// 用于判断当前这个是否可以用来处理当前的传参
boolean supportsParameter(MethodParameter parameter);


// 实现具体的参数映射功能,从请求参数中获取对应的传参,然后设置给目标对象
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

所以我们的核心逻辑就是实现上面这个接口,然后实现上面的两个方法即可;当然直接实现原始的接口,额外需要处理的内容就稍稍有点多了,我们这里会用到SpringMVC本身提供的两个实现类,进行能力的扩展

  • ServletModelAttributeMethodProcessor:用于后续处理POJO类的属性注解
  • RequestParamMethodArgumentResolver:用于后续处理方法参数注解

1. 自定义注解

自定义的注解,支持挂在类成员上,也支持放在方法参数上

1
2
3
4
5
6
7
8
9
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamName {
/**
* new name
*/
String value();
}

2. 自定义参数处理器

接下来我们就是需要定义上面注解的解析器,鉴于方法参数注解与类的成员注解的处理逻辑的差异性(后面说为啥要区分开)

首先来看一下当方法参数上,有上面注解时,对应的解析类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ParamArgumentProcessor extends RequestParamMethodArgumentResolver {
public ParamArgumentProcessor() {
super(true);
}

// 当参数上拥有 ParanName 注解,且参数类型为基础类型时,匹配
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(ParamName.class) && BeanUtils.isSimpleProperty(parameter.getParameterType());
}


// 根据自定义的映射name,从传参中获取对应的value
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
ParamName paramName = parameter.getParameterAnnotation(ParamName.class);
String ans = request.getParameter(paramName.value());
if (ans == null) {
return request.getParameter(name);
}
return ans;
}
}

上面的实现比较简单,判断是否可以使用当前Resolver的方法实现

  • parameter.hasParameterAnnotation(ParamName.class) && BeanUtils.isSimpleProperty(parameter.getParameterType());
  • 注意上面的实现,两个要求,一是参数注解,二是要求为简单对象(非简单对象则交给下面的Resolver来处理)

其次,另外一个实现方法resolveName就很直观了,根据绑定的name获取具体的传参

  • 注意:内部还做了一个兼容,当绑定的传参name找不到时,使用变量名来取传参
  • 举例说明:
    • 参数定义如 @ParamName("user_name") String userName
    • 那么上面这个传参值,会从传参列表中,取user_name对应的值,当user_name不存在时,则取userName对应的值

接下来则是针对参数为POJO的场景,此时我们的自定义参数解析器实现类ServletModelAttributeMethodProcessor,具体的实现逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class ParamAttrProcessor extends ServletModelAttributeMethodProcessor {
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();

public ParamAttrProcessor() {
super(true);
}

// 要求参数为非基本类型,且参数的成员上存在@ParamName注解
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (!BeanUtils.isSimpleProperty(parameter.getParameterType())) {
for (Field field : parameter.getParameterType().getDeclaredFields()) {
if (field.getDeclaredAnnotation(ParamName.class) != null) {
return true;
}
}
}
return false;
}

// 主要是使用自定义的DataBinder,给传参增加一些别名映射
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamDataBinder paramNameDataBinder = new ParamDataBinder(target, binder.getObjectName(), mapping);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}


// 避免每次都去解析targetClass对应的别名定义,在实现中添加一个缓存
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<>();
for (Field field : fields) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty()) return Collections.emptyMap();
return renameMap;
}
}

虽然上面的实现相比较于第一个,代码量要长很多,但是逻辑其实也并不复杂

supportsParameter 判断是否可用

  • 参数为非基本类型
  • 参数至少有一个成员上有注解@ParamName

bindRequestParameters 请求参数绑定

  • 这个方法的核心诉求就是给传参中的key=value,添加一个别名
  • 举例说明:
    • 如原始的传参为 user_name = 一灰灰
    • 类属性定义如 @ParamName("user_name") String userName;
    • 这样,别名映射中,会有一个 user_name = userName 的kv存在
    • 然后在DataBinder中,给别名(userName)也添加进去

下面就是DataBinder的实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ParamDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public ParamDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
super(target, objectName);
this.renameMapping = renameMapping;
}

@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}

3. 注册与测试

最终也是非常重要的一点就是需要注册我们的自定义参数解析器,实现WebMvcConfigurationSupport,重载addArgumentResolvers方法即可

1
2
3
4
5
6
7
8
@SpringBootApplication
public class Application extends WebMvcConfigurationSupport {
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ParamAttrProcessor());
argumentResolvers.add(new ParamArgumentProcessor());
}
}

最后给一个基本的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RestController
public class RestDemo {

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ViewDo {
@ParamName("user_id")
private Integer userId;
@ParamName("user_name")
private String userName;
}

/**
* POJO 对应Spring中的参数转换是 ServletModelAttributeMethodProcessor | RequestParamMethodArgumentResolver
*
* @param viewDo
* @return
*/
@GetMapping(path = "getV5")
public ViewDo getV5(ViewDo viewDo) {
System.out.println("v5: " + viewDo);
return viewDo;
}

/**
* curl 'http://127.0.0.1:8080/postV1' -X POST -d 'user_id=123&user_name=一灰灰'
* 注意:非json传参,jackson的配置将不会生效,即上面这个请求是不会实现下划线转驼峰的; 但是返回结果会是下划线的
*
* @param viewDo
* @return
*/
@PostMapping(path = "postV1")
public ViewDo post(ViewDo viewDo) {
System.out.println(viewDo);
return viewDo;
}

@GetMapping(path = "ano")
public ViewDo ano(@ParamName("user_name") String userName, @ParamName("user_id") Integer userId) {
ViewDo viewDo = new ViewDo(userId, userName);
System.out.println(viewDo);
return viewDo;
}
}

上面提供了三个接口

  • ano:参数为基本类型,通过@ParamName定义别名
  • getV5: 参数为非简单类型ViewDo,类成员上通过@ParamName指定别名映射
  • post: 同上,唯一区别在于它是post请求

实测结果如下

4. 小结

本文主要通过实现自定义的参数映射解析器,来支持自定义的参数别名绑定,虽然内容不多,但其基本实现,则主要利用的是SpringMVC的参数解析这一块知识点,当然本文作为应用篇,主要只是介绍了如何实现自定义的HandlerMethodArgumentResolver,当现有的参数解析满足不了我们的诉求时,完全可以仿造上面的实现来实现自己的应用场景(相信也不会太难)

最后抽取一下本文中使用到的知识点

  • 如何判断一个类是否为基本对象:org.springframework.beans.BeanUtils#isSimpleProperty
  • 自定义参数解析器:实现接口 HandlerMethodArgumentResolver
    • 方法1:supportsParameter,判断当前这个解析器是否适用
    • 方法2:resolveArgument,具体的参数解析实现逻辑
  • RequestParamMethodArgumentResolver:默认的方法参数解析器,主要用于简单参数类型的映射,内部封装了类型适配相关逻辑
  • ServletModelAttributeMethodProcessor:用于默认的POJO/ModelAttribute参数解析

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

【WEB系列】如何支持下划线驼峰互转的传参与返回

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到