yeskery

SpringMVC 对象绑定时自定义名称对应关系

这个需求来源自一个 Post 的 Controller 的请求含有太多的参数,于是想把所有的参数封装到对象中,然后 Controller 的方法接收一个对象类型的参数,这样后期扩展修改都比较方便,不需要动到方法签名。

有一句俗话说得好,需求是第一生产力,上面的这个需求就催生了这篇文章的一系列调研。

首先,这个需求 SpringMVC 本身是支持的,你把一个对象放在Controller方法的参数里,SpringMVC本身的支持就能把request中和这个对象属性同名的参数绑定到对象属性上,比如下面这样:

  1. @RequestMapping(value = "/test", method = RequestMethod.GET)
  2. public void test(Test test) {
  3. LOG.debug(test.toString());
  4. }

Test中是这样定义的

  1. public class Test {
  2. private String test1;
  3. private String test2;
  4. private String test3;
  5. }

由于我使用了 lombok,所以没有 Getter 和 Setter 方法,大家看 demo 的时候可以注意下。这个时候我们访问 /test?test1=1&test2=2&test3=3,SpringMVC 会自动把同名的属性和 Request 中的参数绑定在一起。

到这里貌似可以结题了,开玩笑,哪有这么简单。大家想这样一种情况,在 JAVA 中我们用的是驼峰命名法(比如:testName),而在前端 JS 中我们大多用的是蛇形命名法(比如:test_name)。当然,我们可以要求前端在请求接口的时候用驼峰命名法,但是问题不是这样逃避的。另外,如果我前端接口参数的名字和对象里面属性的名字不一样怎么办呢,比如前端接口里参数叫person,但是我对象里的属性叫people,当然这种情况比较少,但是也不排除在各种复杂的需求中会出现这种情况,所以我们这篇文章的目的就是做到自定义的参数名和属性名映射,想让哪个请求参数对应哪个属性都可以,想点哪里点哪里就是这个意思。

这篇文章只讲解决方案,我会在下一篇文中讲一下 SpringMVC 数据绑定的实现原理。

虽然不细说原理,但是有几个概念还是要提前说一下的,使用 SpringMVC 时,所有的请求都是最先经过 DispatcherServlet 的,然后由 DispatcherServlet 选择合适的 HandlerMapping 和 HandlerAdapter 来处理请求,HandlerMapping 的作用就是找到请求所对应的方法,而 HandlerAdapter 则来处理和请求相关的的各种事情。我们这里要讲的请求参数绑定也是 HandlerAdapter 来做的。大概就知道这些吧,我们需要写一个自定义的请求参数处理器,然后把这个处理器放到 HandlerAdapter 中,这样我们的处理器就可以被拿来处理请求了。

定义参数处理器

首先第一步,我们先来做一个参数处理器 SnakeToCamelModelAttributeMethodProcessor

  1. public class SnakeToCamelModelAttributeMethodProcessor extends ServletModelAttributeMethodProcessor implements ApplicationContextAware{
  2. ApplicationContext applicationContext;
  3. public SnakeToCamelModelAttributeMethodProcessor(boolean annotationNotRequired) {
  4. super(annotationNotRequired);
  5. }
  6. @Override
  7. protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
  8. SnakeToCamelRequestDataBinder camelBinder = new SnakeToCamelRequestDataBinder(binder.getTarget(), binder.getObjectName());
  9. RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
  10. requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(camelBinder, request);
  11. camelBinder.bind(request.getNativeRequest(ServletRequest.class));
  12. }
  13. @Override
  14. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  15. this.applicationContext = applicationContext;
  16. }
  17. }

这个处理器要继承 ServletModelAttributeMethodProcessor,来看下继承关系

ServletModelAttributeMethodProcessor

看最上面实现的是 HandlerMethodArgumentResolver 接口,这个接口代表这个类是用来处理请求参数的,有两个方法

  1. public interface HandlerMethodArgumentResolver {
  2. boolean supportsParameter(MethodParameter var1);
  3. Object resolveArgument(MethodParameter var1, ModelAndViewContainer var2, NativeWebRequest var3, WebDataBinderFactory var4) throws Exception;
  4. }

supportsParameter 返回是否支持这种参数,resolveArgument 是具体处理参数的方法。ServletModelAttributeMethodProcessor 是处理复杂对象的,也就是除了 int,char 等等简单对象之外自定义的复杂对象,比如上文中我们提到的 Test。

我们自定义的处理器也是处理复杂对象,只是扩展了可以处理名称映射,所以继承这个 ServletModelAttributeMethodProcessor 即可。好了,处理器写好了,那么接下来怎么做呢,重写父类的 bindRequestParameters 方法,这个方法就是绑定数据对象的时候调用的方法。

自定义的 DataBinder

在这个方法中,我们新建了一个自定义的 DataBinder-SnakeToCamelRequestDataBinder,然后用 HandlerAdapter 初始化了这个 DataBinder,最后调了 DataBinder 的 bind 方法。DataBinder 顾名思义就是实际去把请求参数和对象绑定的类,这个自定义的 DataBinder 怎么写呢,如下:

  1. public class SnakeToCamelRequestDataBinder extends ExtendedServletRequestDataBinder {
  2. public SnakeToCamelRequestDataBinder(Object target, String objectName) {
  3. super(target, objectName);
  4. }
  5. protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
  6. super.addBindValues(mpvs, request);
  7. //处理JsonProperty注释的对象
  8. Class<?> targetClass = getTarget().getClass();
  9. Field[] fields = targetClass.getDeclaredFields();
  10. for (Field field : fields) {
  11. JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
  12. if (jsonPropertyAnnotation != null && mpvs.contains(jsonPropertyAnnotation.value())) {
  13. if (!mpvs.contains(field.getName())) {
  14. mpvs.add(field.getName(), mpvs.getPropertyValue(jsonPropertyAnnotation.value()).getValue());
  15. }
  16. }
  17. }
  18. List<PropertyValue> covertValues = new ArrayList<PropertyValue>();
  19. for (PropertyValue propertyValue : mpvs.getPropertyValueList()) {
  20. if(propertyValue.getName().contains("_")) {
  21. String camelName = SnakeToCamelRequestParameterUtil.convertSnakeToCamel(propertyValue.getName());
  22. if (!mpvs.contains(camelName)) {
  23. covertValues.add(new PropertyValue(camelName, propertyValue.getValue()));
  24. }
  25. }
  26. }
  27. mpvs.getPropertyValueList().addAll(covertValues);
  28. }
  29. }

这个自定义的 DataBinder 继承自 ExtendedServletRequestDataBinder,可扩展的 DataBinder,用来给子类复写的方法是 addBindValues,有两个参数,一个是 MutablePropertyValues 类型的,这里面存的就是请求参数的 key-value 对,还有一个参数是 request 对象本身。request 对象这里用不到,我们用的就是这个 MutablePropertyValues 类型的 mpvs。

其实处理的原理很简单,SpringMVC 在做完这一步参数绑定之后就会去通过反射调用 Controller 中的方法了,调用 Controller 方法的时候要给参数赋值,赋值的时候就是从这个mpvs里面把对应参数 name 的 value 取出来。举个例子,我们的样例 Controller 中的对象时 Test,Test 有个属性是 Test1,那么在给 Test1 赋值的时候就会从这个 mpvs 中去取 key 为 Test1 所对应的 value。可是你想想,前端请求的参数是 test_1 这样的,所以这个 mpvs 中只有一个 key 为 test_1 的值,那自然就会报错。知道了这种处理方法,就很简单了,我们在这个 mpvs 中再加一个 key 为 test1,value 和 test_1 的 value 一样的对象就可以了。

再扩展一点,说到自定义 Name,比如 test_1 对应属性为 test2 这样,我们用一个有 value 的注释,比如这里用到的 JsonProperty,在 test2 上加上注释 @JsonProperty(“test_1”),这里处理的时候会先把这个注释的值取出来,从 mpvs 里面查,如果有这个 key,那么就把 value 取出来再加进去一个 key 为 field name 的 map 就可以了。上面 SnakeToCamelRequestDataBinder 的处理方法大概就是这样了。

转换工具类

然后这里我们抽出了一个工具类,用来处理蛇形 string 到驼峰 string 的转换。

  1. public class SnakeToCamelRequestParameterUtil {
  2. public static String convertSnakeToCamel(String snake) {
  3. if (snake == null) {
  4. return null;
  5. }
  6. if (snake.indexOf("_") < 0) {
  7. return snake;
  8. }
  9. String result = "";
  10. String[] split = StringUtils.split(snake, "_");
  11. int index = 0;
  12. for (String s : split) {
  13. if (index == 0) {
  14. result += s.toLowerCase();
  15. } else {
  16. result += capitalize(s);
  17. }
  18. index++;
  19. }
  20. return result;
  21. }
  22. private static String capitalize(String s) {
  23. if (s == null) {
  24. return null;
  25. }
  26. if (s.length() == 1) {
  27. return s.toUpperCase();
  28. }
  29. return s.substring(0, 1).toUpperCase() + s.substring(1);
  30. }
  31. }

添加到 Spring HandlerAdapter

做完了上面这些,应该说处理过程就搞定了,还差最后一步,需要把我们自定义的处理器 SnakeToCamelModelAttributeMethodProcessor 加到系统的 HandlerAdapter 中去。方法有很多,如果你不知道 HandlerAdapter 是什么东西,那八成你用的是系统默认的 HandlerAdapter。加起来也很简单。如果你用了 <mvc:annotation-driven> 元素,可以用下面这个方法。

  1. <mvc:annotation-driven>
  2. <mvc:argument-resolvers>
  3. <bean class="package.name.SnakeToCamelModelAttributeMethodProcessor">
  4. <constructor-arg name="annotationNotRequired" value="true"/>
  5. </bean>
  6. </mvc:argument-resolvers>
  7. </mvc:annotation-driven>

如果你用的是 JAVA 代码配置,可以用

  1. @Configuration
  2. public class WebContextConfiguration extends WebMvcConfigurationSupport {
  3. @Override
  4. protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  5. argumentResolvers.add(processor());
  6. }
  7. @Bean
  8. protected SnakeToCamelModelAttributeMethodProcessor processor() {
  9. return new SnakeToCamelModelAttributeMethodProcessor(true);
  10. }
  11. }

像我这边,因为项目需要自定义了 HandlerAdapter。

  1. <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
  2. ...
  3. </bean>

所以我写了一个注册器,用来把处理器注册进 HandlerAdapter,代码如下。

  1. public class SnakeToCamelProcessorRegistry implements ApplicationContextAware, BeanFactoryPostProcessor {
  2. private ApplicationContext applicationContext;
  3. @Override
  4. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  5. this.applicationContext = applicationContext;
  6. }
  7. @Override
  8. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
  9. RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
  10. List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers().getResolvers();
  11. List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<HandlerMethodArgumentResolver>();
  12. for (HandlerMethodArgumentResolver resolver : resolvers) {
  13. newResolvers.add(resolver);
  14. }
  15. SnakeToCamelModelAttributeMethodProcessor processor = new SnakeToCamelModelAttributeMethodProcessor(true);
  16. processor.setApplicationContext(applicationContext);
  17. newResolvers.add(0, processor);
  18. requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(newResolvers));
  19. }
  20. }

看起来可能逻辑比较复杂,为什么要做这一堆事情呢,话要从 HandlerAdapter 里系统自带的处理器说起。我这边系统默认带了24个处理器,其中有两个 ServletModelAttributeMethodProcessor,也就是我们自定义处理器继承的系统处理器。SpringMVC 处理请求参数是轮询每一个处理器,看是否支持,也就是 supportsParameter 方法, 如果返回 true,就交给你出来,并不会问下面的处理器。这就导致了如果我们简单的把我们的自定义处理器加到 HandlerAdapter 的 Resolver 列中是不行的,需要加到第一个去。

然后 ServletModelAttributeMethodProcessor 的构造器有一个参数是 true,代表什么意思呢,看这句代码

  1. public boolean supportsParameter(MethodParameter parameter) {
  2. return parameter.hasParameterAnnotation(ModelAttribute.class)?true:(this.annotationNotRequired?!BeanUtils.isSimpleProperty(parameter.getParameterType()):false);
  3. }

ServletModelAttributeMethodProcessor 是否支持某种类型的参数,是这样判断的。首先,对象是否有 ModelAttribute 注解,如果有,则处理,如果没有,则判断 annotationNotRequired,是否不需要注释,如果 true,再判断对象是否简单对象。我们的 Test 对象是没有注释的,所以我们就需要传参为 true,表示不一定需要注解。

以上就是所有的配置过程,通过这一系列配置,我们就可以自定义前端请求参数和对象属性名称的映射关系了,通过 JsonProperty 注解,如果没有注解,会自动转换蛇形命名到驼峰命名。下面是效果,Demo 的 Controller 依然是这个:

  1. @RequestMapping(value = "/test", method = RequestMethod.GET)
  2. @ResponseBody
  3. public void test(Test test) {
  4. LOG.debug(test.toString());
  5. }

Test 对象这样写

  1. public class Test {
  2. @JsonProperty("test_1")
  3. private String test1;
  4. private String test2;
  5. @JsonProperty("test_3")
  6. private String test99;
  7. }

这样前端请求的参数中的 test_1 和 test1 都会绑定到 test1,test_2 和 test2 都会绑定到 test2,test_3,test99 和 test_99 都会绑定到 test99 上。我们试一下这个请求

/test?test_1=1&test_2=2&test_3=3

Log 输出如下

- Test(test1=1, test2=2, test3=3)

本文转载自:https://blog.csdn.net/zgzczzw/article/details/53912966

评论

发表评论 点击刷新验证码

提示

该功能暂未开放