知識庫 / Spring / Spring MVC RSS 訂閱

自定義數據綁定器在 Spring MVC 中

Spring MVC
HongKong
5
02:42 PM · Dec 06 ,2025

 

1. 概述

本文將展示如何利用 Spring 的數據綁定機制,通過自動將原始類型轉換為對象轉換,使我們的代碼更清晰易讀。

默認情況下,Spring 僅知道如何轉換簡單類型。換句話説,一旦我們提交包含 IntStringBoolean 類型的數據,它將自動將其綁定到適當的 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;
    }
}

以及子類 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. 綁定領域對象

當我們需要將數據綁定到對象時,可能存在非直接的方式(例如,從 SessionHeaderCookie 變量中)或存儲在數據源中。 在這些情況下,我們需要使用不同的解決方案。

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
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.