skip to content

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?

level: principalimportance: nice to knowfreq 22%

answer

  1. order: specific -> type -> custom -> two catch-alls
  2. catch-alls: RequestParam(true) simple, ModelAttribute(true) rest
  3. custom beats model-attribute catch-all, not explicit @RequestParam
  4. full control = setArgumentResolvers on RequestMappingHandlerAdapter
  5. replacing list can drop other autoconfig resolvers

basics

~20 s

Custom 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 s

In `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
java
// 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

for a junior

Beyond scope — just know custom resolvers are added in config.

for a middle

Know custom resolvers are appended after built-ins and can't override @RequestParam.

for a senior

Explain the specific/type/custom/catch-all ordering and why model-attribute interception works.

for a principal

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

context