How do you assemble and register a custom ConversionService across the core container, Spring MVC, and Spring Boot, and what precedence/threading concerns matter?
answer
- pick base: DefaultConversionService vs DefaultFormattingConversionService vs ApplicationConversionService
- core: bean named 'conversionService' (ConversionServiceFactoryBean)
- MVC: WebMvcConfigurer#addFormatters(FormatterRegistry)
- Boot: just declare Converter/Formatter beans
- distinct instances per tier; keep converters stateless
basics
~20 sBuild a ConversionService (e.g. DefaultFormattingConversionService) and add your Converters/Formatters. For @Value, register it as the bean named 'conversionService'. In MVC, add formatters via WebMvcConfigurer#addFormatters. In Boot, just declare Converter/Formatter beans. Keep converters stateless because the service is shared across threads.
solid answer
~40 sPick the right base: DefaultConversionService for pure type conversion, or DefaultFormattingConversionService when you need locale-aware Formatters and @DateTimeFormat/@NumberFormat. Register depending on the tier. Core container: expose a bean named exactly 'conversionService' (often via ConversionServiceFactoryBean or FormattingConversionServiceFactoryBean) so ConfigurableBeanFactory uses it for @Value/SpEL coercion. Spring MVC: override WebMvcConfigurer#addFormatters(FormatterRegistry) to add converters/formatters to the MVC FormattingConversionService used for @RequestParam/@PathVariable/model binding. Spring Boot: simply declare @Component Converter/Formatter/GenericConverter/AnnotationFormatterFactory beans — Boot's autoconfiguration detects them and adds them to ApplicationConversionService, covering both tiers. Precedence: more specific ConvertiblePairs and ConditionalConverter.matches() decide selection; the MVC and bean-factory services are distinct instances. Threading: converters/formatters are singletons shared concurrently, so they must be stateless; underlying SimpleDateFormat/NumberFormat aren't thread-safe.
code
java · 24 lines// Boot: beans are auto-detected and added to the app's ConversionService(s)
@Component
class StringToInstantConverter implements Converter<String, Instant> {
@Override public Instant convert(String source) { return Instant.parse(source); }
}
// Non-Boot core container: name the bean 'conversionService' so @Value uses it
@Configuration
class ConversionConfig {
@Bean
public FormattingConversionServiceFactoryBean conversionService() {
var f = new FormattingConversionServiceFactoryBean();
f.setConverters(Set.of(new StringToInstantConverter()));
return f; // builds a DefaultFormattingConversionService with your converters + defaults
}
}
// Spring MVC: extend the MVC-specific FormattingConversionService
@Configuration
class WebConfig implements WebMvcConfigurer {
@Override public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToInstantConverter());
}
}go deeper
Know that in Boot you just declare a Converter bean and Spring uses it.
Register via WebMvcConfigurer#addFormatters and pick DefaultConversionService vs DefaultFormattingConversionService.
Explain the 'conversionService' bean-name requirement, the per-tier separate instances, and ApplicationConversionService.
Reason about precedence/matches(), multi-tier registration strategy, thread-safety of the shared service, and diagnosing works-here-not-there binding bugs.
## Step 1 — choose the base implementation - `DefaultConversionService` — `GenericConversionService` + common built-in converters (String↔Number/Boolean/Enum/Charset, collections, arrays, maps, etc.). No `Formatter` support. - `DefaultFormattingConversionService` — adds locale-aware `Formatter` support **and** registers the `@NumberFormat` / `@DateTimeFormat` annotation factories by default. Use this when you need formatting. - `ApplicationConversionService` (Spring Boot) — `DefaultFormattingConversionService` plus Boot extras: `String→Duration`, `String→Period`, `String→DataSize`, delimited `String→Collection/array`, lenient enum matching. ## Step 2 — register per tier There isn't one global `ConversionService`; **different subsystems hold their own**. The three you care about: ### a) Core container (for `@Value` / SpEL / config binding) `ConfigurableBeanFactory#setConversionService(...)` supplies the service used to coerce `@Value` results. The conventional wiring is a **bean named `conversionService`** — the context picks it up. Factory beans help build it: ```java @Bean public ConversionServiceFactoryBean conversionService() { // name matters! ConversionServiceFactoryBean f = new ConversionServiceFactoryBean(); f.setConverters(Set.of(new StringToInstantConverter())); return f; // yields a DefaultConversionService } ``` Use `FormattingConversionServiceFactoryBean` if you need Formatter/annotation support here. ### b) Spring MVC (for request/response binding) MVC maintains its **own** `FormattingConversionService` (installed by `@EnableWebMvc`). Extend it via: ```java @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToInstantConverter()); registry.addFormatter(new MoneyFormatter()); registry.addFormatterForFieldAnnotation(new MyAnnotationFormatterFactory()); } } ``` This service backs `@RequestParam`, `@PathVariable`, form/model binding, and `@DateTimeFormat` on request models. ### c) Spring Boot (both tiers, the easy path) Just declare beans: ```java @Component class StringToInstantConverter implements Converter<String, Instant> { ... } @Component class MoneyFormatter implements Formatter<Money> { ... } ``` Boot's `WebConversionService`/`ApplicationConversionService` autoconfiguration and `WebMvcAutoConfiguration` detect `Converter`, `GenericConverter`, `Formatter`, and `AnnotationFormatterFactory` beans and register them. Boot also wires an `ApplicationConversionService` for `@ConfigurationProperties` binding. ## Step 3 — precedence & selection Within one service, `GenericConversionService` searches the target's class/interface hierarchy for a matching `ConvertiblePair`, honoring `ConditionalConverter#matches()`. **More specific pairs win**; among equally applicable ones the hierarchy-search order decides — so avoid overlapping registrations, and use `matches()` to disambiguate. Across services, remember the **bean-factory service and the MVC service are different instances** — registering a converter in one does not automatically add it to the other (a classic "works for @RequestParam but not @Value" bug). ## Threading & safety - A `ConversionService` and its `Converter`/`Formatter`s are **singletons invoked concurrently** — keep them **stateless**. - `GenericConversionService` caches converter lookups; that cache is thread-safe, but your converters still must be. - Underlying `java.text.SimpleDateFormat` / `java.text.NumberFormat` are **not** thread-safe — instantiate per call or use `java.time.format.DateTimeFormatter`. - `DefaultConversionService.getSharedInstance()` returns a lazily-initialized shared, immutable-in-practice instance — fine for reads, but don't mutate it from app code. ## When to use what - Need `@Value`/config coercion for a custom type → register in the **bean factory** (`conversionService` bean) or, in Boot, a `Converter` bean (also used by `@ConfigurationProperties`). - Need request-parameter/date formatting → **MVC** `addFormatters`. - Boot app → prefer **beans**; only drop to explicit factory beans when you must control the exact service instance or exclude defaults.
- You added a Converter via WebMvcConfigurer#addFormatters and @RequestParam works, but @Value("${x}") on the same custom type fails. Why?The MVC FormattingConversionService and the bean-factory's ConversionService are separate instances. addFormatters only touches MVC's. For @Value you must register the converter with the bean factory (a bean named 'conversionService', or in Boot a Converter bean).
- Two applicable converters exist for the same source/target. How do you make selection deterministic?Prefer a more specific ConvertiblePair, or implement ConditionalGenericConverter#matches() so only the intended one participates for the given TypeDescriptors. Avoid registering overlapping unconditional converters, since hierarchy-search order otherwise decides.
saying these in an interview costs you the question
- Believing there is a single global ConversionService shared by MVC, @Value, and @ConfigurationProperties
- Registering a custom core converter under any bean name and expecting @Value to use it
- Holding mutable state (or a shared SimpleDateFormat) inside a Converter/Formatter
- Assuming addFormatters affects config-property/@Value binding