SpringBoot一個非常強大的數(shù)據(jù)綁定類
環(huán)境:SpringBoot3.2.5
1. 簡介
本篇文章將介紹Spring Boot中一個非常強大且十分重要的類Binder,該類可以將外部配置文件的屬性值綁定到Spring Boot應用程序中的Java對象上。在Spring Boot中,通常使用@ConfigurationProperties
注解來指定外部配置文件中的屬性前綴,并使用Binder的bind
方法將配置值綁定到Java對象上。這樣,Spring Boot應用程序可以方便地讀取和使用配置文件中的屬性配置。
2. 實戰(zhàn)案例
2.1 準備綁定對象
public class Person {
private Integer age ;
private String name ;
// getter, setter
}
配置文件中添加配置屬性
pack:
person:
age: 20
name: 張三
測試綁定組件
@Component
public class BinderComponent implements InitializingBean {
private final Environment env ;
// 注入該對象是為了后面我們方便注冊自定義數(shù)據(jù)類型轉(zhuǎn)換
private final ConversionService conviersionService ;
public BinderComponent(Environment env,
ConversionService conviersionService) {
this.env = env ;
this.conviersionService = conviersionService ;
}
public void afterPropertiesSet() throws Exception {
// 綁定測試都將在這里完成
}
}
后續(xù)案例都將基于上面的環(huán)境
2.2 基礎綁定
// 這里的pack.person是配置文件中的前綴
BindResult<Person> result = Binder.get(env).bind("pack.person", Person.class) ;
Person person = result.get() ;
System.out.println(person) ;
在該示例中,配置文件中的age屬性能正確的轉(zhuǎn)換為Integer。為什么能進行數(shù)據(jù)類型轉(zhuǎn)換?因為內(nèi)部(調(diào)用Binder#get(env)時)會添加TypeConverterConversionService和ApplicationConversionService兩個類型轉(zhuǎn)換器。
2.3 自定義數(shù)據(jù)類型轉(zhuǎn)換
給Person添加Date類型的字段,如下:
public class Person {
private Integer age ;
private String name ;
private Date birthday ;
// getter, setter
}
// 配置文件中添加birthday屬性
pack:
person:
birthday: 2000-01-01
在此執(zhí)行上面2.2中代碼,程序拋出了如下異常
圖片
默認的數(shù)據(jù)類型轉(zhuǎn)換器是沒有String到Date轉(zhuǎn)換功能。我們需要添加自定義的類型轉(zhuǎn)換,如下自定義類型轉(zhuǎn)換器:
@Configuration
public class DataTypeConvertConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Date>() {
@Override
public Date convert(String source) {
try {
return new SimpleDateFormat("yyyy-MM-dd").parse(source) ;
} catch (ParseException e) {
throw new RuntimeException(e) ;
}
}
});
}
}
修改數(shù)據(jù)綁定方式
Iterable<ConfigurationPropertySource> propertySources = ConfigurationPropertySources.get(env) ;
// 不使用默認的類型轉(zhuǎn)換服務,使用自定義(還是自動配置的,只是添加了我們自定義的)
Binder binder = new Binder(propertySources, null, conviersionService) ;
Person result = binder.bindOrCreate("pack.person", Person.class) ;
System.out.println(result) ;
這次成功輸出結(jié)果。
2.4 數(shù)據(jù)綁定回調(diào)
我們還可以為Binder執(zhí)行綁定時,傳入回調(diào)句柄,這樣在數(shù)據(jù)綁定的各個階段都可以進行相應的處理,如下示例:
Iterable<ConfigurationPropertySource> propertySources = ConfigurationPropertySources.get(env) ;
Binder binder = new Binder(propertySources, null, conviersionService) ;
Person result = binder.bindOrCreate("pack.person", Bindable.of(Person.class), new BindHandler() {
@Override
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
System.out.printf("準備進行數(shù)據(jù)綁定:【%s】%n", name) ;
return target ;
}
@Override
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
System.out.printf("對象綁定成功:【%s】%n", result) ;
return result ;
}
@Override
public Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
System.out.printf("準備創(chuàng)建綁定對象:【%s】%n", result) ;
return result ;
}
@Override
public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error)
throws Exception {
System.out.printf("數(shù)據(jù)綁定失敗:【%s】%n", error.getMessage()) ;
return BindHandler.super.onFailure(name, target, context, error);
}
@Override
public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result)
throws Exception {
System.out.printf("數(shù)據(jù)綁定完成:【%s】%n", result) ;
BindHandler.super.onFinish(name, target, context, result) ;
}
}) ;
System.out.println(result) ;
輸出結(jié)果
圖片
每個屬性在綁定時都會執(zhí)行相應的回調(diào)方法。
3. 都用在哪里?
在SpringBoot環(huán)境中所有的數(shù)據(jù)綁定功能都是通過Binder進行。下面列出幾個非常重要的地方
3.1 SpringBoot啟動時綁定SpringApplication
SpringBoot在啟動時初始化環(huán)境配置Environment時,會將配置文件中的spring.main.*下的配置屬性綁定到當前的SpringApplication對象上。
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
}
private ConfigurableEnvironment prepareEnvironment(...) {
// ...
bindToSpringApplication(environment);
}
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
}
}
spring.main有如下配置:
圖片
3.2 綁定使用@ConfigurationProperties類
@ConfigurationProperties注解的類是通過BeanPostProcessor處理器執(zhí)行綁定(不管是類上使用該注解,還是@Bean注解的方法都是通過該處理器進行綁定)。
public class ConfigurationPropertiesBindingPostProcessor {
// 該類是由SpringBoot自動配置
private ConfigurationPropertiesBinder binder;
// 實例化bean,執(zhí)行初始化方法之前
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 綁定;
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
}
上面的ConfigurationPropertiesBean.get方法會處理當前bean實例是獨立的一個Bean對象且類上有@ConfigurationProperties注解,或者是當前的bean實例是通過@Bean定義且方法上有@ConfigurationProperties注解。不管是哪種定義的bean只要滿足條件,都會被包裝成ConfigurationPropertiesBean對象。接下來執(zhí)行bind方法:
private void bind(ConfigurationPropertiesBean bean) {
try {
this.binder.bind(bean);
}
}
執(zhí)行綁定
class ConfigurationPropertiesBinder {
BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
Bindable<?> target = propertiesBean.asBindTarget();
ConfigurationProperties annotation = propertiesBean.getAnnotation();
BindHandler bindHandler = getBindHandler(target, annotation);
return getBinder().bind(annotation.prefix(), target, bindHandler);
}
}
以上就是@ConfigurationProperties注解的類或方法對象通過Binder綁定的原理。
3.3 SpringCloud Gateway綁定路由謂詞&過濾器
當一個路由請求過來時,會查詢相應的路由,而這個查找過程中就會通過路由的定義信息轉(zhuǎn)換為Route對象。以下是大致過程(詳細還需要自行閱讀源碼)
public class RoutePredicateHandlerMapping {
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
return lookupRoute(exchange)... ;
}
protected Mono<Route> lookupRoute(...) {
// 查找路由
return this.routeLocator.getRoutes()... ;
}
}
public class RouteDefinitionRouteLocator {
public Flux<Route> getRoutes() {
// 將在yaml配置中定義的路由轉(zhuǎn)換為Route對象
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
}
private Route convertToRoute(RouteDefinition routeDefinition) {
AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
// 獲取配置過濾器
List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);
return ... ;
}
private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
List<GatewayFilter> filters = new ArrayList<>();
if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
// loadGatewayFilters方法中進行配置的綁定
filters.addAll(loadGatewayFilters(routeDefinition.getId(),
new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
}
}
List<GatewayFilter> loadGatewayFilters(...) {
Object configuration = this.configurationService.with(factory)
...
// 該方法執(zhí)行綁定動作
.bind();
}
public T bind() {
T bound = doBind();
}
protected T doBind() {
Bindable<T> bindable = Bindable.of(this.configurable.getConfigClass());
T bound = bindOrCreate(bindable, this.normalizedProperties, this.configurable.shortcutFieldPrefix(),
/* this.name, */this.service.validator.get(), this.service.conversionService.get());
return bound;
}
}
以上源碼比較粗略,大家只要知道原理即可,沒必要任何一個點都搞的清清楚楚。