A team wants a custom resolver to intercept a parameter that Spring currently data-binds as a @ModelAttribute. Why does addArgumentResolvers work here but not for overriding @RequestParam, and how would you fully control ordering?
answer
- order: specific -> type -> custom -> two catch-alls
- catch-alls: RequestParam(true) simple, ModelAttribute(true) rest
- custom beats model-attribute catch-all, not explicit @RequestParam
- full control = setArgumentResolvers on RequestMappingHandlerAdapter
- replacing list can drop other autoconfig resolvers
basics
~20 sCustom resolvers are inserted after the specific built-ins but before the two catch-all resolvers (simple-type request param and default model-attribute). So a custom resolver beats the model-attribute catch-all, but never beats the explicit @RequestParam resolver. For total control, set the full ordered list on RequestMappingHandlerAdapter.
solid answer
~40 sIn `RequestMappingHandlerAdapter.getDefaultArgumentResolvers()` the list is: explicit annotation-based resolvers (`@RequestParam`, `@PathVariable`, `@RequestBody`, `@ModelAttribute`, …), then type-based resolvers (`Model`, `Map`, servlet types), then your custom resolvers from `addArgumentResolvers`, then two catch-alls — `RequestParamMethodArgumentResolver(useDefaultResolution=true)` for simple types and `ServletModelAttributeMethodProcessor(annotationNotRequired=true)` for everything else. An **unannotated complex object** would fall to that last catch-all, so a custom resolver positioned just before it wins — that's why intercepting default model-attribute binding works. But `@RequestParam` is handled by an explicit resolver **ahead** of custom ones, so you can't override it via `addArgumentResolvers`. To fully control order you obtain the `RequestMappingHandlerAdapter` bean and call `setArgumentResolvers(List)` (or `setCustomArgumentResolvers`) with your own composed, correctly ordered list — accepting the maintenance cost across Spring upgrades.
code
java · 16 lines// Full control: put a custom resolver truly first, keeping existing defaults after it.
@Configuration
class ResolverOrdering {
private final RequestMappingHandlerAdapter adapter;
ResolverOrdering(RequestMappingHandlerAdapter adapter) { this.adapter = adapter; }
@PostConstruct
void putMineFirst() {
List<HandlerMethodArgumentResolver> ordered = new ArrayList<>();
ordered.add(new TenantArgumentResolver()); // wins over built-ins now
ordered.addAll(adapter.getArgumentResolvers()); // preserve everything else
adapter.setArgumentResolvers(ordered); // replaces the whole list
}
}
// Contrast: WebMvcConfigurer.addArgumentResolvers(...) can only APPEND (group 3),
// so it beats the model-attribute catch-all but never the explicit @RequestParam resolver.go deeper
Beyond scope — just know custom resolvers are added in config.
Know custom resolvers are appended after built-ins and can't override @RequestParam.
Explain the specific/type/custom/catch-all ordering and why model-attribute interception works.
Weigh setArgumentResolvers vs order-independent Converter/@InitBinder, and the upgrade/coupling risks of replacing the list.
### The exact default ordering (why it matters) `RequestMappingHandlerAdapter` assembles `argumentResolvers` roughly as: 1. **Annotation-based, specific**: `RequestParamMethodArgumentResolver(false)`, `RequestParamMapMethodArgumentResolver`, `PathVariableMethodArgumentResolver`, `PathVariableMapMethodArgumentResolver`, `MatrixVariable…`, `ServletModelAttributeMethodProcessor(false)`, `RequestResponseBodyMethodProcessor`, `RequestHeaderMethodArgumentResolver`, `@CookieValue`, `@Value`, `SessionAttribute`, `RequestAttribute`, … 2. **Type-based**: `ServletRequestMethodArgumentResolver` (HttpServletRequest, HttpSession, Principal, Locale, …), `ServletResponseMethodArgumentResolver`, `ModelMethodProcessor`, `MapMethodProcessor`, `ErrorsMethodArgumentResolver`, `RedirectAttributesMethodArgumentResolver`, `UriComponentsBuilderMethodArgumentResolver`, … 3. **Custom**: everything you added via `WebMvcConfigurer.addArgumentResolvers` (stored as `customArgumentResolvers`). 4. **Catch-all (last resort)**: `RequestParamMethodArgumentResolver(useDefaultResolution=true)` — claims remaining *simple* types by treating them as request params — then `ServletModelAttributeMethodProcessor(annotationNotRequired=true)` — claims *everything else* by data-binding it as a model attribute. **First-match wins**, so position decides everything. ### Why the model-attribute case works An **unannotated non-simple parameter** (e.g. `PageRequest`, a custom command object) is NOT matched by any specific resolver, nor by the simple-type catch-all. It would normally be swallowed by the final `ServletModelAttributeMethodProcessor(true)`. Because your custom resolver sits in group 3 — **before** that final catch-all — its `supportsParameter` is consulted first and can claim the parameter. Hence intercepting default model-attribute binding via `addArgumentResolvers` is fully supported. ### Why the @RequestParam case fails `@RequestParam` is handled by `RequestParamMethodArgumentResolver(false)` in group 1, **ahead** of your custom resolver. By the time the composite would reach group 3, the parameter is already claimed. So `addArgumentResolvers` **cannot override** any explicitly-annotated built-in (`@RequestParam`, `@PathVariable`, `@RequestBody`, `@RequestHeader`, explicit `@ModelAttribute`, etc.). ### How to take full control You must bypass the augment-only API and set the whole list: ```java @Configuration class AdapterTuning { @Autowired RequestMappingHandlerAdapter adapter; @PostConstruct void reorder() { List<HandlerMethodArgumentResolver> custom = new ArrayList<>(); custom.add(new MyOverridingResolver()); // now truly first custom.addAll(adapter.getArgumentResolvers()); // then the existing defaults adapter.setArgumentResolvers(custom); } } ``` Or build the adapter yourself with `@EnableWebMvc` + `WebMvcConfigurationSupport` overrides. Both approaches **fully replace** ordering and therefore let a custom resolver preempt a built-in — at the cost of being tightly coupled to the current default list (fragile across Spring upgrades; you may silently lose newly-added default resolvers). ### Design guidance (the principal-level point) - **Prefer a marker annotation** so your resolver only claims *your* parameters — you almost never need to override a built-in. - If you think you must override `@RequestParam`, reconsider: usually a `Converter`/`Formatter`, `@InitBinder`, or a `WebDataBinder` customization is the right, order-independent tool. - Overriding via `setArgumentResolvers` is a **last resort**; document it and pin/verify against Spring version bumps, because the default list changes between releases. - The **same ordering law** applies to `HandlerMethodReturnValueHandler` via `addReturnValueHandlers` vs `setReturnValueHandlers`. ### Gotchas - Multiple custom resolvers are consulted in **registration order** — order your `add(...)` calls deliberately. - `supportsParameter` results are cached per parameter, so it must be deterministic. - Replacing the list can drop resolvers added by other auto-configurations (e.g. Spring Data's `PageableHandlerMethodArgumentResolver`, Security's `AuthenticationPrincipalArgumentResolver`) — re-add them.
- Instead of overriding @RequestParam with a resolver, what order-independent tools should you reach for first?A Converter/Formatter registered in the FormattingConversionService, or an @InitBinder method customizing the WebDataBinder — these transform the value regardless of resolver ordering and don't couple you to Spring's internal default list.
- What's a risk of calling setArgumentResolvers with your own list?You can accidentally drop resolvers contributed by other auto-configurations — e.g. Spring Data's PageableHandlerMethodArgumentResolver or Security's AuthenticationPrincipalArgumentResolver — and the default list changes across Spring versions, so your fixed list can silently omit newly added defaults after an upgrade.
saying these in an interview costs you the question
- Believing addArgumentResolvers lets you override any built-in resolver
- Not knowing the two catch-all resolvers sit last
- Replacing the resolver list without re-adding other autoconfig resolvers
- Reaching for resolver overrides when a Converter/@InitBinder is the right tool