skip to content

How do you implement and register a custom HandlerMethodArgumentResolver, and where does it sit relative to the built-in resolvers?

level: seniorimportance: should knowfreq 45%

answer

  1. supportsParameter on a marker annotation
  2. resolveArgument reads NativeWebRequest / security context
  3. register via WebMvcConfigurer.addArgumentResolvers
  4. custom = appended AFTER built-ins, before catch-alls
  5. can't override @RequestParam/@RequestBody this way

basics

~10 s

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

Create 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
java
@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

for a junior

Know a custom resolver implements the two methods and is registered in config.

for a middle

Implement supportsParameter on an annotation and read from NativeWebRequest in resolveArgument.

for a senior

Explain the custom-after-builtins ordering and why overriding built-ins needs setArgumentResolvers.

for a principal

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

context