背景
在SpringMVC项目中,对于HTTP的GET请求,我们常在Controller方法的入参中,用@RequestParam
指定请求参数名。但当请求参数较多时,会导致参数列表很长,一般建议用POJO将请求参数封装起来。然而,@RequestParam
并不支持标注在类属性上,无法给POJO中封装的请求参数指定别名,请求参数名只能与属性名相同。这在实际项目中有很大不便,比如HTTP接口格式通常约定使用下划线命名,但Java变量规范使用驼峰命名,需要对POJO封装的请求参数取别名,如下所示。
@Data
public class Pet {
// @RequestParam("nick_name") 不支持
private String nickName;
}
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/pet")
public String testPet(Pet pet) {
// "/test/pet?nickName=Tom" -> pet.nickName is "Tom"
// "/test/pet?nick_name=Tom" -> pet.nickName is null
return pet.toString();
}
}
思路
通过调试可知,当Controller方法的参数为自定义类型(POJO)时,对应的参数解析器为ServletModelAttributeMethodProcessor
。它通过一个WebDataBinder
(实际上用的是子类ExtendedServletRequestDataBinder
)将请求参数的值绑定到POJO中对应的属性里去。ExtendedServletRequestDataBinder
的继承关系如下。
WebDataBinder
└── ServletRequestDataBinder
└── ExtendedServletRequestDataBinder
其中,ServletRequestDataBinder
中有一个钩子方法addBindValues()
,允许在真正执行绑定操作前添加绑定值(添加到入参mpvs
中)。我们可以在这个方法中,将POJO的属性名作为绑定值添加进去,这样Spring就能识别到该属性并将请求参数与之绑定。例如,在上面的例子中,我们可以通过自定义注解将nick_name
与POJO的属性nickName
相关联,当请求参数中有nick_name=Tom
时,mpvs
中会记录nick_name->Tom
对应关系,我们将nickName->Tom
也添加到mpvs
中,这样Spring就能将Tom
与POJO的属性nickName
绑定了。
public class ServletRequestDataBinder extends WebDataBinder {
// ...
/**
* Extension point that subclasses can use to add extra bind values for a
* request. Invoked before {@link #doBind(MutablePropertyValues)}.
* The default implementation is empty.
* @param mpvs the property values that will be used for data binding
* @param request the current request
*/
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
// ...
}
代码实现
以下是上述思路的具体代码实现。
/**
* 自定义注解,用于标注请求参数名
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamName {
/**
* 请求参数名
*/
String value();
}
/**
* 自定义数据绑定器,用于处理 {@link ParamName} 标注的请求参数名
*/
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
public ParamNameDataBinder(Object target, String objectName) {
super(target, objectName);
}
@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
// 获取要绑定的目标对象,即封装了请求参数的实体
Object target = getTarget();
if (target == null) {
return;
}
Class<?> targetClass = target.getClass();
// 通过反射获取该实体类的所有属性
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
ParamName paramName = field.getAnnotation(ParamName.class);
// 如果属性上标注了请求参数名,并且请求中确实传了该参数的值(mpvs中有),
// 则将该属性的名称加入mpvs中,这样在后续处理中就能将参数值正确绑定到该属性了
if (paramName != null) {
Object paramValue = mpvs.get(paramName.value());
if (paramValue != null) {
mpvs.add(field.getName(), paramValue);
}
}
}
}
}
/**
* 自定义数据绑定器的创建工厂
*/
public class ParamNameDataBinderFactory extends ServletRequestDataBinderFactory {
public ParamNameDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
@Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request)
throws Exception {
// 使用自定义数据绑定器
return new ParamNameDataBinder(target, objectName);
}
}
/**
* 自定义 HandlerAdapter
*/
public class ParamNameHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
// 使用自定义数据绑定器的创建工厂
return new ParamNameDataBinderFactory(binderMethods, getWebBindingInitializer());
}
}
/**
* 自定义 Web MVC 配置
*/
@Configuration
public class CustomWebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
// 使用自定义的 HandlerAdapter
return new ParamNameHandlerAdapter();
}
}
使用上也很简单,直接用自定义注解@ParamName
在POJO的属性上标注别名,即可生效。
@Data
public class Pet {
@ParamName("nick_name")
private String nickName;
}
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/pet")
public String testPet(Pet pet) {
// "/test/pet?nickName=Tom" -> pet.nickName is "Tom"
// "/test/pet?nick_name=Tom" -> pet.nickName is "Tom"
return pet.toString();
}
}
总结
SpringMVC中,当请求参数封装成POJO时,参数名必须与POJO属性名相同,无法通过@RequestParam
取别名。我们可以通过自定义注解以及自定义数据绑定器的方式,实现给请求参数取别名,以满足实际项目需求。