Skip to content
sheetsmith

Converters as Spring beans

In a Spring Boot application, every bean implementing CellConverter is registered as an application converter for the type it handles. This is convenient for types used everywhere, and has consequences that must be understood:

Situation What happens
A converter class annotated with @Component (or declared with @Bean) It becomes the application converter for its type: it applies to every column of that type in every sheet class, unless a column declares a field converter.
Two converter beans handling the same type Startup fails: IllegalStateException: converter beans 'a' and 'b' both handle type X; keep only one of them.
A converter bean whose handled type cannot be determined (raw type, or a lambda bean declared with a raw return type) Startup fails: IllegalStateException: cannot resolve the type handled by converter bean 'x'; declare it as a class implementing CellConverter<T>, or as a @Bean method returning CellConverter<T>, instead of a lambda or a raw type.
A converter meant for one column only Do not declare it as a bean. Declare it on the field with @ExcelColumn(converter = X.class). If it needs dependencies, give it a constructor with those dependencies: the Spring factory creates it with injection without registering it globally.
A field converter class that is also a bean (exactly one) The bean is used for the column, and it is also the application converter for its type.
A field converter class with several beans A new instance is created with injection for the field.
Field converter vs application converter on the same column The field converter always wins.

Declaring converter beans:

@Component
public class MoneyConverter implements CellConverter<Money> {
@Override
public CellValue convert(Money value, ConversionContext context) {
return CellValue.number(value.amount().doubleValue());
}
}
@Configuration
class ExportConfiguration {
@Bean
CellConverter<UUID> uuidConverter() { // the generic return type identifies the handled type
return (value, context) -> CellValue.text(value.toString());
}
}

A field converter with dependencies, not a bean:

public class CountryNameConverter implements CellConverter<String> {
private final CountryRegistry countries; // a Spring bean, injected by the factory
public CountryNameConverter(CountryRegistry countries) {
this.countries = countries;
}
@Override
public CellValue convert(String isoCode, ConversionContext context) {
return CellValue.text(countries.displayName(isoCode));
}
}
@ExcelSheet
public record ShipmentRow(
@ExcelColumn(header = "Country code", order = 10) String country,
@ExcelColumn(header = "Country", order = 20, converter = CountryNameConverter.class) String countryName) {
}

Here CountryNameConverter handles String: as a bean it would turn every string column of the application into a country name. As a field converter it applies to one column.