SpringMVC 入参解析原理和实战

1,254 阅读2分钟

参数解析的原理

参数的解析工作由接口 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 中,共享给多个项目使用。留给各位去实现,一定很有价值。^_^