一灰灰blog 一灰灰blog
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)

一灰灰blog

资深搬运工
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)
  • 基础系列

  • DB系列

  • 搜索系列

  • MQ系列

  • WEB系列

    • Request

    • Response

    • RestTemplate

    • WebClient

    • WebFlux

    • WebSocket

    • Web三剑客

    • 实例

      • 【WEB系列】Spring MVC之基于xml配置的web应用构建
      • 【WEB系列】Spring MVC之基于java config无xml配置的web应用构建
      • 【WEB系列】一个web demo应用构建全过程
      • 【WEB系列】实现后端的接口版本支持(应用篇)
      • 【WEB系列】徒手撸一个扫码登录示例工程(应用篇)
      • 【WEB系列】一步步实现一个面向接口的网络访问实例(应用篇)
      • 【WEB系列】最小成本实现REST服务扩展(应用篇)
      • 【WEB系列】从0到1实现自定义web参数映射器
        • I. 项目搭建
          • 1. 项目依赖
        • II. 别名映射
          • 0. 知识点概要说明
          • 1. 自定义注解
          • 2. 自定义参数处理器
          • 3. 注册与测试
          • 4. 小结
        • III. 不能错过的源码和相关知识点
          • 0. 项目
          • 1. 微信公众号: 一灰灰Blog
    • 其他

  • 中间件

  • 运维

  • SpringSecurity

  • SpringCloud

  • Spring系列
  • WEB系列
  • 实例
一灰灰
2022-01-23

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

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

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

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

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

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

而接收参数的POJO为

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

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

# I. 项目搭建

# 1. 项目依赖

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

开一个web服务用于测试

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

配置文件application.yml

server:
  port: 8080
1
2

# II. 别名映射

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

# 0. 知识点概要说明

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

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

// 用于判断当前这个是否可以用来处理当前的传参
boolean supportsParameter(MethodParameter parameter);


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

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

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

# 1. 自定义注解

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

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

# 2. 自定义参数处理器

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

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

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上面的实现比较简单,判断是否可以使用当前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,具体的实现逻辑如下

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;
    }
}
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

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

supportsParameter 判断是否可用

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

bindRequestParameters 请求参数绑定

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

下面就是DataBinder的实现逻辑

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());
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3. 注册与测试

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

@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

最后给一个基本的测试

@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;
    }
}
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

上面提供了三个接口

  • 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. 项目

  • 工程:https://github.com/liuyueyi/spring-boot-demo (opens new window)
  • 源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/202-web-params-camel (opens new window)

系列博文:

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

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

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

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

  • 一灰灰Blog个人博客 https://blog.hhui.top (opens new window)
  • 一灰灰Blog-Spring专题博客 http://spring.hhui.top (opens new window)

一灰灰blog

编辑 (opens new window)
#请求参数
上次更新: 2022/02/27, 11:13:23
【WEB系列】最小成本实现REST服务扩展(应用篇)
【WEB系列】SpringBoot文件上传异常之提示The temporary upload location xxx is not valid(填坑篇)

← 【WEB系列】最小成本实现REST服务扩展(应用篇) 【WEB系列】SpringBoot文件上传异常之提示The temporary upload location xxx is not valid(填坑篇)→

最近更新
01
【基础系列】基于maven多环境配置
04-25
02
【WEB系列】内嵌Tomcat配置Accesslog日志文件生成位置源码探索
04-24
03
【搜索系列】ES查询常用实例演示
04-18
更多文章>
Theme by Vdoing | Copyright © 2017-2022 一灰灰Blog
MIT License | 鄂ICP备18017282号 |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×