1. 概述

这篇文章将展示如何利用Spring的数据绑定机制,通过自动将对象转换为基本类型,使我们的代码更加清晰易读。

默认情况下,Spring只知道如何将简单类型(如整数、字符串或布尔值)绑定到相应的Java类型。但在实际项目中,这还不够,因为我们可能需要绑定更复杂的对象类型

2. 将单个对象绑定到请求参数

让我们从简单的开始,先绑定一个简单类型。我们需要实现Converter<S, T>接口的自定义实现,其中S是我们要转换的类型,T是我们要转换的目标类型:

@Component
public class StringToLocalDateTimeConverter
  implements Converter<String, LocalDateTime> {

    @Override
    public LocalDateTime convert(String source) {
        return LocalDateTime.parse(
          source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

然后在控制器中使用以下语法:

@GetMapping("/findbydate/{date}")
public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) {
    return ...;
}

2.1. 使用枚举作为请求参数

接下来,我们将演示如何将枚举用作请求参数

这里有一个简单的枚举Modes

public enum Modes {
    ALPHA, BETA;
}

我们将构建一个将字符串转换为枚举的Converter如下:

public class StringToEnumConverter implements Converter<String, Modes> {

    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from);
    }
}

接着,我们需要注册我们的Converter

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

现在我们可以将枚举用作请求参数:

@GetMapping
public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) {
    // ...
}

或者用作路径变量:

@GetMapping("/entity/findbymode/{mode}")
public GenericEntity findByEnum(@PathVariable("mode") Modes mode) {
    // ...
}

3. 绑定对象层次结构

有时我们需要将整个对象层次结构进行转换,拥有一个集中化的绑定方式比一组独立的转换器更有意义。

在这个例子中,我们有基类AbstractEntity

public abstract class AbstractEntity {
    long id;
    public AbstractEntity(long id){
        this.id = id;
    }
}

以及子类FooBar

public class Foo extends AbstractEntity {
    private String name;
    
    // standard constructors, getters, setters
}
public class Bar extends AbstractEntity {
    private int value;
    
    // standard constructors, getters, setters
}

在这种情况下,我们可以实现ConverterFactory<S, R>,其中S是转换的源类型,R是定义可以转换到的基类型

public class StringToAbstractEntityConverterFactory 
  implements ConverterFactory<String, AbstractEntity>{

    @Override
    public <T extends AbstractEntity> Converter<String, T> getConverter(Class<T> targetClass) {
        return new StringToAbstractEntityConverter<>(targetClass);
    }

    private static class StringToAbstractEntityConverter<T extends AbstractEntity>
      implements Converter<String, T> {

        private Class<T> targetClass;

        public StringToAbstractEntityConverter(Class<T> targetClass) {
            this.targetClass = targetClass;
        }

        @Override
        public T convert(String source) {
            long id = Long.parseLong(source);
            if(this.targetClass == Foo.class) {
                return (T) new Foo(id);
            }
            else if(this.targetClass == Bar.class) {
                return (T) new Bar(id);
            } else {
                return null;
            }
        }
    }
}

可以看到,只需要实现getConverter()方法,它返回所需的转换器。转换过程则委托给这个转换器。

然后,我们需要注册我们的ConverterFactory

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
    }
}

最后,我们可以在控制器中随意使用它:

@RestController
@RequestMapping("/string-to-abstract")
public class AbstractEntityController {

    @GetMapping("/foo/{foo}")
    public ResponseEntity<Object> getStringToFoo(@PathVariable Foo foo) {
        return ResponseEntity.ok(foo);
    }
    
    @GetMapping("/bar/{bar}")
    public ResponseEntity<Object> getStringToBar(@PathVariable Bar bar) {
        return ResponseEntity.ok(bar);
    }
}

4. 绑定领域对象

有些情况下,我们想要将数据绑定到对象上,但数据可能以非直接的方式(例如来自会话、头或cookie变量)存在,甚至存储在数据源中。在这种情况下,我们需要使用不同的解决方案。

4.1. 自定义参数解析器

首先,我们将为这些参数定义一个注解:

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

然后,我们将实现一个自定义的HandlerMethodArgumentResolver

public class HeaderVersionArgumentResolver
  implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(Version.class) != null;
    }

    @Override
    public Object resolveArgument(
      MethodParameter methodParameter, 
      ModelAndViewContainer modelAndViewContainer, 
      NativeWebRequest nativeWebRequest, 
      WebDataBinderFactory webDataBinderFactory) throws Exception {
 
        HttpServletRequest request 
          = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        return request.getHeader("Version");
    }
}

最后一步是让Spring知道在哪里查找它们:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //...

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

就这样。现在我们可以在控制器中使用它:

@GetMapping("/entity/{id}")
public ResponseEntity findByVersion(
  @PathVariable Long id, @Version String version) {
    return ...;
}

可以看到,HandlerMethodArgumentResolverresolveArgument()方法返回一个Object。换句话说,我们不仅可以返回String,还可以返回任何对象。

5. 总结

通过上述方法,我们消除了许多重复的转换工作,让Spring为我们处理大部分事情。总结如下:

  • 对于将简单类型转换为对象,应使用Converter实现。
  • 要封装一系列对象的转换逻辑,可以尝试使用ConverterFactory实现。
  • 对于间接来源的数据或需要应用额外逻辑获取关联数据的情况,最好使用HandlerMethodArgumentResolver

如往常一样,所有示例代码都可以在我们的GitHub仓库中找到。