参数解析的原理
参数的解析工作由接口 HandlerMethodArgumentResolver 完成。
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
内置了许多实现类来完成这项工作。比如
- RequestParamMethodArgumentResolver 负责解析 @RequestParam 标记的参数
- ServletRequestMethodArgumentResolver 负责解析入参为 HttpServletRequest、HttpMethod 等类型的参数
- ServletModelAttributeMethodProcessor 负责解析入参为 POJO 类的参数
- RequestResponseBodyMethodProcessor 负责解析入参为 @RequestBody 标注的参数
Spring 内部在使用时,通过门面模式设计了一个 HandlerMethodArgumentResolverComposite,同样实现了接口 HandlerMethodArgumentResolver 方便调用。通过该实现类聚合了各种各样的参数解析器来负责 controller 方法的入参解析。
正常情况下,内置的这些参数解析实现类都足够使用了。但是遗憾的是,如果我们的接口参数为「下划线」的命名规范,但我们Java Bean 的字段命名规范为驼峰的形式。此时除非使用 @RequestBody 并且通过 jackson 注解 @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)、@JsonProperty("header_url") 来转换之外,其余像 POJO 入参的方式无法完成参数到字段的映射。
需要我们自定义自己的参数解析器,并配置到 MVC 中。
自定义参数解析器
自定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SnakeCaseModel {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SnakeCaseField {
String value();
}
期望的使用方式
// 定义 controller 接口的入参 POJO 类
@Data
public class Person {
private String name;
private Integer age;
// 期望通过注解来标记该字段需要映射 HTTP 请求的参数 header_url
@SnakeCaseField("header_url")
private String headerUrl;
}
// controller 接口定义
@PostMapping("/testRequestBody")
// 期望通过注解来标记该入参需要自定义的参数解析器来处理
public Person testRequestBody(@SnakeCaseModel Person person) {
return person;
}
设计参数解析器
public class SnakeCaseHandlerMethodArgumentResolver extends ServletModelAttributeMethodProcessor {
private ApplicationContext applicationContext;
public SnakeCaseHandlerMethodArgumentResolver(ApplicationContext applicationContext) {
super(false);
this.applicationContext = applicationContext;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(SnakeCaseModel.class);
}
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
Assert.state(servletRequest != null, "No ServletRequest");
TuofenExtendServletRequestDataBinder myBinder = new TuofenExtendServletRequestDataBinder(binder.getTarget(),
binder.getObjectName());
// 初始化,参考默认的 DataBinder 创建工厂org.springframework.web.bind.support.DefaultDataBinderFactory#createBinder
RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
Optional.ofNullable(requestMappingHandlerAdapter.getWebBindingInitializer())
.ifPresent(e -> e.initBinder(myBinder));
myBinder.bind(servletRequest);
}
private static class TuofenExtendServletRequestDataBinder extends ExtendedServletRequestDataBinder {
TuofenExtendServletRequestDataBinder(@Nullable Object target, String objectName) {
super(target, objectName);
}
// 负责将下划线的 HTTP 参数,转换为驼峰的形式,放入 MutablePropertyValues 中
@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
// 将下划线参数转为POJO类的字段名形式
Optional.ofNullable(getTarget()).ifPresent(e -> {
Class<?> clazz = e.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
SnakeCaseField annotation = field.getAnnotation(SnakeCaseField.class);
if (Objects.nonNull(annotation) && !mpvs.contains(field.getName()) && mpvs.contains(annotation.value())) {
PropertyValue propertyValue = mpvs.getPropertyValue(annotation.value());
if (Objects.nonNull(propertyValue)) {
mpvs.add(field.getName(), propertyValue.getValue());
}
}
}
});
}
}
}
配置到 MVC 中
WebMvcConfigurer 接口在 MVC 初始化过程中,将被回调。用于扩展默认的 MVC 功能。这里扩展的是增加一个参数的解析器
@Configuration
public class SnakeCaseHandlerMethodArgumentResolverMvcConfig implements WebMvcConfigurer, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new SnakeCaseHandlerMethodArgumentResolver(applicationContext));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
到这里就完成了一个用于 POJO 入参,接收 HTTP 下划线参数的自定义解析器开发。
可以将这些逻辑封装到一个 spring-boot starter 中,共享给多个项目使用。留给各位去实现,一定很有价值。^_^