Backend Development 11 min read

Mastering Spring Boot’s Binder: From Basics to Custom Conversions

This article explains how Spring Boot’s Binder class binds external configuration properties to Java objects, demonstrates basic and custom type conversions, shows how to use binding callbacks, and outlines key places where Binder is employed within Spring Boot and Spring Cloud Gateway.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Boot’s Binder: From Basics to Custom Conversions

1. Introduction

This article introduces the powerful Spring Boot class Binder , which binds external configuration properties to Java objects using the @ConfigurationProperties annotation and the bind method.

2. Practical Example

2.1 Prepare Binding Object

<code>public class Person {
  private Integer age;
  private String name;
  // getter, setter
}
</code>

Add the corresponding properties to the configuration file:

<code>pack:
  person:
    age: 20
    name: 张三
</code>

2.2 Basic Binding

<code>BindResult<Person> result = Binder.get(env).bind("pack.person", Person.class);
Person person = result.get();
System.out.println(person);
</code>

The default type converters (e.g., TypeConverterConversionService and ApplicationConversionService ) automatically convert the age string to an Integer .

2.3 Custom Type Conversion

Add a Date field to Person and the corresponding property:

<code>public class Person {
  private Integer age;
  private String name;
  private Date birthday;
  // getter, setter
}
</code>
<code>pack:
  person:
    birthday: 2000-01-01
</code>

Because the default converters cannot convert String to Date , a custom converter is required:

<code>@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);
        }
      }
    });
  }
}
</code>

Use the custom converter in binding:

<code>Iterable<ConfigurationPropertySource> propertySources = ConfigurationPropertySources.get(env);
Binder binder = new Binder(propertySources, null, conviersionService);
Person result = binder.bindOrCreate("pack.person", Person.class);
System.out.println(result);
</code>

The output now shows the correctly populated Person object.

Binding success
Binding success

2.4 Binding Callback

Binder allows a BindHandler to receive callbacks at different stages of the binding process:

<code>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("Preparing to bind: %s%n", name);
    return target;
  }
  @Override
  public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
    System.out.printf("Binding succeeded: %s%n", result);
    return result;
  }
  @Override
  public Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
    System.out.printf("Creating bound object: %s%n", result);
    return result;
  }
  @Override
  public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error) throws Exception {
    System.out.printf("Binding failed: %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("Binding finished: %s%n", result);
    BindHandler.super.onFinish(name, target, context, result);
  }
});
System.out.println(result);
</code>

The console prints messages for each callback stage, as shown in the following screenshot:

Binding callbacks
Binding callbacks

3. Where Is Binder Used?

3.1 SpringApplication Startup

During startup, Spring Boot binds spring.main.* properties to the SpringApplication instance:

<code>public class SpringApplication {
  public ConfigurableApplicationContext run(String... args) {
    ConfigurableEnvironment environment = prepareEnvironment(...);
  }
  private ConfigurableEnvironment prepareEnvironment(...) {
    // ...
    bindToSpringApplication(environment);
  }
  protected void bindToSpringApplication(ConfigurableEnvironment environment) {
    try {
      Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
    } catch (Exception e) { }
  }
}
</code>

3.2 @ConfigurationProperties Binding

Classes annotated with @ConfigurationProperties are processed by ConfigurationPropertiesBindingPostProcessor , which uses a ConfigurationPropertiesBinder to perform the actual binding:

<code>public class ConfigurationPropertiesBindingPostProcessor {
  private ConfigurationPropertiesBinder binder;
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    bind(ConfigurationPropertiesBean.get(applicationContext, bean, beanName));
    return bean;
  }
}
</code>
<code>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);
  }
}
</code>

3.3 Spring Cloud Gateway Route Binding

When a request arrives, the gateway converts route definitions from YAML into Route objects, using Binder to bind configuration properties to filter and predicate beans:

<code>public class RoutePredicateHandlerMapping {
  protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
    return lookupRoute(exchange);
  }
  protected Mono<Route> lookupRoute(...) {
    return routeLocator.getRoutes()...;
  }
}
</code>
<code>public class RouteDefinitionRouteLocator {
  public Flux<Route> getRoutes() {
    Flux<Route> routes = routeDefinitionLocator.getRouteDefinitions()
      .map(this::convertToRoute);
    return routes;
  }
  private Route convertToRoute(RouteDefinition routeDefinition) {
    AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
    List<GatewayFilter> filters = getFilters(routeDefinition);
    return new Route(...);
  }
  private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
    List<GatewayFilter> filters = new ArrayList<>();
    if (!gatewayProperties.getDefaultFilters().isEmpty()) {
      filters.addAll(loadGatewayFilters(routeDefinition.getId(),
        new ArrayList<>(gatewayProperties.getDefaultFilters())));
    }
    return filters;
  }
  private List<GatewayFilter> loadGatewayFilters(...) {
    Object configuration = configurationService.with(factory)
      // ...
      .bind();
    return ...;
  }
}
</code>

These examples illustrate that Binder is the central mechanism for externalized configuration throughout Spring Boot and related projects.

ConfigurationPropertiesData BindingBinderCustom Converter
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.