1. 概述
本文將展示如何利用 Spring 的數據綁定機制,通過自動將原始類型轉換為對象轉換,使我們的代碼更清晰易讀。
默認情況下,Spring 僅知道如何轉換簡單類型。換句話説,一旦我們提交包含 Int、String 或 Boolean 類型的數據,它將自動將其綁定到適當的 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;
}我們將會構建一個 String 到 枚舉 轉換器,如下所示:
public class StringToEnumConverter implements Converter<String, Modes> {
@Override
public Modes convert(String from) {
return Modes.valueOf(from);
}
}然後,我們需要註冊我們的 轉換器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
}現在我們可以使用我們的 枚舉 (Enum) 作為 請求參數 (RequestParameter):
@GetMapping
public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) {
// ...
}或者,作為 PathVariable:
@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. 綁定領域對象
當我們需要將數據綁定到對象時,可能存在非直接的方式(例如,從 Session、Header 或 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