跳到主要内容

自定义XXXModelAttributeResolver解决请求下划线,但是接收对象为驼峰形式

1. 前言

在SpringBoot中@ModelAttribute 可以绑定请求参数到 Java 对象上,例如:

@PostMapping("add")
public ResponseMessage add(@ModelAttribute UserInfo userInfo){

但是如果请求header是Content-Type:application/x-www-form-urlencoded, 且请求参数是下划线的形式,比如: user_id:1

此时@ModelAttribute则没法把user_id:1 这个值绑定到你的对象 Integer userId;

那么为了解决这个问题,我们可以自定义一个XXXModelAttributeResolver,然后判断自定义的注解,把注解映射的下划线参数绑定到对象参数上

下面则是具体的操作步骤

2. 自定义注解@SnakeModelAttribute@SnakeParam

@SnakeModelAttribute

@SnakeModelAttribute 的作用是代替@ModelAttribute, 当我们拿到值和对象进行绑定时,通过这个注解进行判定。

import java.lang.annotation.*;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SnakeModelAttribute {
}

@SnakeParam

@SnakeParam的作用是,进行绑定的时候,拿到驼峰属性对应下划线请求参数。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SnakeParam {

String name() default "";


}

使用时像这样:

@SnakeParam(name = "name")
private String topicId;

3. 自定义一个SnakeModelAttributeResolver

SnakeModelAttributeResolver继承了ModelAttributeMethodProcessor,其作用是为了实现自定义的属性绑定逻辑。

代码执行时大致的流程像这样:

代码如下,等等会说明关键高亮部分代码的含义:

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* SnakeModelAttributeResolver
* application/x-www-form-urlencoded 请求时,下划线参数转驼峰
*/
public class SnakeModelAttributeResolver extends ModelAttributeMethodProcessor {

public SnakeModelAttributeResolver() {
super(false);
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
//有ModelAttribute 注解时,会进入到resolveArgument转换
return parameter.hasParameterAnnotation(SnakeModelAttribute.class);
}

@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(servletRequest);

Object target = binder.getTarget();
if (target != null) {
Class<?> targetClass = target.getClass();
Map<String, String> snakeToCamel = new HashMap<>();

for (Field field : targetClass.getDeclaredFields()) {
SnakeParam annotation = field.getAnnotation(SnakeParam.class);
if (annotation != null) {
String snakeName = annotation.name();
if (!snakeName.isEmpty()) {
snakeToCamel.put(snakeName, field.getName());
}
}
}

List<PropertyValue> updated = new ArrayList<>();
for (PropertyValue pv : mpvs.getPropertyValueList()) {
String originalName = pv.getName();
if (snakeToCamel.containsKey(originalName)) {
updated.add(new PropertyValue(snakeToCamel.get(originalName), pv.getValue()));
} else {
updated.add(pv);
}
}

mpvs = new MutablePropertyValues(updated);
}

binder.bind(mpvs);
}
}

3.1 SnakeModelAttributeResolver

super(false); 意味着初始化SnakeModelAttributeResolver的时候,对内部属性:annotationNotRequired 设置为false,

org.springframework.web.method.annotation.ModelAttributeMethodProcessor#annotationNotRequired
	private final boolean annotationNotRequired;

这个属性在父类用来判断是否要使用这个类进行处理

org.springframework.web.method.annotation.ModelAttributeMethodProcessor#supportsParameter
	@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));

}

3.2 supportsParameter

这个比较简单,就是判断绑定的这个对象上是否使用了自定义注解@SnakeModelAttribute

3.3 bindRequestParameters

这里做的处理就是,判断对象的属性上是否使用了@SnakeParam注解,如果有且不为空,那么则增加对应的绑定关系到snakeToCamel

4. 对WebMvcConfigurer增加自定义的参数解析器

实现WebMvcConfigurer接口,然后重写addArgumentResolvers,增加SnakeModelAttributeResolver即刻

@Configuration
public class ApplicationResolverConfig implements WebMvcConfigurer {


@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new SnakeModelAttributeResolver());
}

}

5. Q&A

5.1 为什么要自定义注解@SnakeModelAttribute,使用原有的@ModelAttribute判断不信吗?

不行的,因为项目在启动后,默认的ServletModelAttributeMethodProcessor优先级更高,会使用这个进行参数绑定,从而代码不会执行到你自定义的SnakeModelAttributeResolver

5.2 spring什么时候判断用哪个xxxModelAttributeMethodProcessor?

代码位置如下:

org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#getArgumentResolver
	@Nullable
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}

可以看到,如果拿到某个resolver后,执行完就直接break了,不会给后面的resolver机会

5.3 那我这样resolvers.add(0, new SnakeModelAttributeResolver());也不行吗?

比如这样:

   @Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(0, new SnakeModelAttributeResolver());
}

也不行的,框架初始化的ServletModelAttributeMethodProcessor优先级更高