How do you implement and register a custom HandlerMethodArgumentResolver, and where does it sit relative to the built-in resolvers?
answer
- supportsParameter on a marker annotation
- resolveArgument reads NativeWebRequest / security context
- register via WebMvcConfigurer.addArgumentResolvers
- custom = appended AFTER built-ins, before catch-alls
- can't override @RequestParam/@RequestBody this way
basics
~10 sImplement HandlerMethodArgumentResolver with supportsParameter (usually keyed on a custom annotation or type) and resolveArgument (pull the value from the request). Register it by overriding WebMvcConfigurer.addArgumentResolvers. It runs after the built-in resolvers.
solid answer
~40 sCreate a class implementing `HandlerMethodArgumentResolver`. In `supportsParameter` match on a marker annotation (`parameter.hasParameterAnnotation(CurrentUser.class)`) and/or the parameter type. In `resolveArgument` read from the `NativeWebRequest` (headers, security context, session) and return the value — throwing if it's required and missing. Register via a `WebMvcConfigurer`: ```java @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> r) { r.add(new CurrentUserResolver()); } ``` Key ordering fact: resolvers added this way are treated as **custom** resolvers and placed **after** the built-in ones (but before the two catch-alls). So you **cannot override** built-in behavior for `@RequestParam`, `@RequestBody`, etc. this way; you can only claim parameters the built-ins don't already handle. To truly override, you'd replace the full list on `RequestMappingHandlerAdapter`. A custom marker annotation avoids clashing with the catch-all model-attribute resolver.
code
java · 28 lines@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
@interface CurrentUser {}
class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService users;
CurrentUserArgumentResolver(UserService users) { this.users = users; }
@Override public boolean supportsParameter(MethodParameter p) {
return p.hasParameterAnnotation(CurrentUser.class)
&& AppUser.class.isAssignableFrom(p.getParameterType());
}
@Override public Object resolveArgument(MethodParameter p, ModelAndViewContainer m,
NativeWebRequest req, WebDataBinderFactory f) {
var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated())
throw new MissingRequestValueException("No authenticated user");
return users.load(auth.getName());
}
}
@Configuration
class WebConfig implements WebMvcConfigurer {
private final UserService users;
WebConfig(UserService users) { this.users = users; }
@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> r) {
r.add(new CurrentUserArgumentResolver(users)); // appended after built-ins
}
}go deeper
Know a custom resolver implements the two methods and is registered in config.
Implement supportsParameter on an annotation and read from NativeWebRequest in resolveArgument.
Explain the custom-after-builtins ordering and why overriding built-ins needs setArgumentResolvers.
Compare resolver vs interceptor vs filter, statelessness/caching constraints, and prefer existing SPIs like @AuthenticationPrincipal.
### Step 1 — implement the SPI ```java public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter p) { return p.hasParameterAnnotation(CurrentUser.class) && AppUser.class.isAssignableFrom(p.getParameterType()); } @Override public Object resolveArgument(MethodParameter p, ModelAndViewContainer mav, NativeWebRequest req, WebDataBinderFactory b) { Authentication a = SecurityContextHolder.getContext().getAuthentication(); if (a == null || !a.isAuthenticated()) { if (p.getParameterAnnotation(CurrentUser.class).required()) throw new MissingRequestValueException("No authenticated user"); return null; } return userService.load(a.getName()); } } ``` Define the marker annotation: ```java @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface CurrentUser { boolean required() default true; } ``` Usage: `@GetMapping("/me") String me(@CurrentUser AppUser user) { ... }`. ### Step 2 — register it Only two supported ways: 1. **`WebMvcConfigurer.addArgumentResolvers`** (the normal way): ```java @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new CurrentUserArgumentResolver()); } } ``` 2. **Directly set the full list** on `RequestMappingHandlerAdapter.setArgumentResolvers(...)` — heavy-handed; replaces defaults, so rarely done. ### The critical ordering rule Internally `RequestMappingHandlerAdapter.getDefaultArgumentResolvers()` builds the list in this order: (a) annotation-based built-ins (`@RequestParam`, `@PathVariable`, `@RequestBody`, `@ModelAttribute`, …), (b) type-based built-ins (`Model`, `Map`, `HttpServletRequest`, `Errors`, …), (c) **your custom resolvers** from `addArgumentResolvers`, then (d) two **catch-all** resolvers: `RequestParamMethodArgumentResolver(useDefaultResolution=true)` for simple types and `ServletModelAttributeMethodProcessor(annotationNotRequired=true)` for everything else. Consequences: - A custom resolver **cannot override** a built-in — if a parameter is already claimed by `@RequestParam`/`@RequestBody` handling, your resolver never sees it. - But it **does** win over the catch-all model-attribute resolver, which is exactly why an unannotated complex-type param can be captured by your resolver instead of being blindly data-bound. - To genuinely replace a built-in you must call `setArgumentResolvers` with a fully ordered list (rare, fragile across upgrades). ### supportsParameter design Match on a **dedicated annotation** (safest — no accidental capture) or a **specific, unusual type**. Avoid supporting common types like `String`/`Long` unannotated: you'll either be shadowed by built-ins or accidentally swallow other params. ### resolveArgument responsibilities - Pull data from `NativeWebRequest` (`req.getHeader`, `req.getNativeRequest(HttpServletRequest.class)`), the security context, path, session, etc. - Optionally use the `WebDataBinderFactory` to bind/convert. - Enforce required-ness by throwing a meaningful exception (surfaced through `@ExceptionHandler`/`HandlerExceptionResolver`). - May read/write the `ModelAndViewContainer` model if relevant. ### Return-value symmetry The same pattern exists for outputs: implement `HandlerMethodReturnValueHandler` and register via `addReturnValueHandlers`; custom handlers are likewise appended after defaults. ### Gotchas - Resolvers are **singletons/stateless** — never store per-request state in fields. - Result of `supportsParameter` is **cached per method parameter**, so it must be pure/deterministic. - Prefer this over an `HandlerInterceptor` that stuffs request attributes — a resolver keeps the controller signature clean and testable. - Spring Security ships `@AuthenticationPrincipal` (via `AuthenticationPrincipalArgumentResolver`) — check for an existing solution before writing your own. ### When to use Inject cross-cutting request-derived values (current user/tenant, parsed API version, a decoded custom token, pagination assembled from several params) without repeating boilerplate in every controller.
- Why can't you override @RequestParam handling by adding a resolver via addArgumentResolvers?Because resolvers registered that way are added as custom resolvers positioned after all the built-in annotation/type resolvers. The built-in RequestParamMethodArgumentResolver matches @RequestParam first, so your resolver is never consulted for that parameter. To override you must replace the whole list via RequestMappingHandlerAdapter.setArgumentResolvers.
- Is there a built-in resolver for injecting the current authenticated principal?Yes — Spring Security's AuthenticationPrincipalArgumentResolver backs @AuthenticationPrincipal, so you often don't need a custom @CurrentUser resolver unless you want extra loading/mapping logic.
saying these in an interview costs you the question
- Claiming a custom resolver can override built-in @RequestParam/@RequestBody via addArgumentResolvers
- Storing per-request state in resolver instance fields
- Registering by annotating the resolver with @Component and expecting auto-pickup (it must be added to the config)
- Matching supportsParameter on a common unannotated type and swallowing other parameters