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;
}
}
以及子类Foo
和Bar
:
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 ...;
}
可以看到,HandlerMethodArgumentResolver
的resolveArgument()
方法返回一个Object
。换句话说,我们不仅可以返回String
,还可以返回任何对象。
5. 总结
通过上述方法,我们消除了许多重复的转换工作,让Spring为我们处理大部分事情。总结如下:
- 对于将简单类型转换为对象,应使用
Converter
实现。 - 要封装一系列对象的转换逻辑,可以尝试使用
ConverterFactory
实现。 - 对于间接来源的数据或需要应用额外逻辑获取关联数据的情况,最好使用
HandlerMethodArgumentResolver
。
如往常一样,所有示例代码都可以在我们的GitHub仓库中找到。