SpringBoot系列之从0到1实现自定义web参数映射器
在使用SpringMVC进行开发时,接收请求参数属于基本功,当我们希望将传参与项目中的对象关联起来时,最常见的做法是默认的case(即传参name与我们定义的name保持一致),当存在不一致,需要手动指定时,通常是借助注解@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
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 {
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); }
@Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(ParamName.class) && BeanUtils.isSimpleProperty(parameter.getParameterType()); }
@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); }
@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; }
@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); }
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; }
@GetMapping(path = "getV5") public ViewDo getV5(ViewDo viewDo) { System.out.println("v5: " + viewDo); return viewDo; }
@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或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

打赏
如果觉得我的文章对您有帮助,请随意打赏。
微信打赏
支付宝打赏