Pass User Info from Filter to Controller via Spring MVC Argument Resolver

This article explains how to transfer the current user ID extracted from a JWT token in a filter to Spring MVC controller methods using a custom HandlerMethodArgumentResolver, highlighting drawbacks of ThreadLocal and request attributes, and demonstrating a type‑safe, extensible solution with code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Pass User Info from Filter to Controller via Spring MVC Argument Resolver

Problem: Passing Current User Across Layers

When a request carries a JWT token, the filter extracts the user ID, but controller and service layers still need to know the current user, leading to repetitive code.

Drawbacks of Traditional Solutions

ThreadLocal : thread‑safety issues, possible memory leaks, hard to trace.

HttpServletRequest.setAttribute() : type‑unsafe, boiler‑plate code, breaks controller purity.

Spring MVC Solution: HandlerMethodArgumentResolver

Spring MVC provides a custom argument resolver that injects method parameters based on request attributes, keeping the filter unchanged while handling conversion in the resolver.

Design Idea

Separation of concerns : filter handles authentication, resolver handles parameter conversion.

Extensibility : new parameter types can be added easily.

Non‑intrusive : existing code structure remains untouched.

Core Implementation

Step 1 – Create Annotation

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

Step 2 – Implement Resolver

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // Only resolve parameters annotated with @CurrentUser
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        // Get the userId set by the filter
        return webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST);
    }
}

Step 3 – Register Resolver

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private CurrentUserArgumentResolver currentUserArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(currentUserArgumentResolver);
    }
}

Usage Example

Filter Layer

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.substring(7);
            try {
                Long userId = jwtService.extractUserId(token);
                request.setAttribute("currentUserId", userId);
            } catch (Exception e) {
                // token invalid, continue
            }
        }
        filterChain.doFilter(request, response);
    }
}

Controller Layer

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/me")
    public ResponseEntity<User> getCurrentUser(@CurrentUser Long userId) {
        User user = userService.findById(userId);
        return ResponseEntity.ok(user);
    }

    @PutMapping("/me")
    public ResponseEntity<User> updateCurrentUser(@CurrentUser Long userId,
                                                   @RequestBody UserUpdateRequest request) {
        User updated = userService.updateUser(userId, request);
        return ResponseEntity.ok(updated);
    }

    @GetMapping("/permissions")
    public ResponseEntity<List<Permission>> getUserPermissions(@CurrentUser Long userId) {
        List<Permission> permissions = userService.getUserPermissions(userId);
        return ResponseEntity.ok(permissions);
    }
}

Principle Analysis

During request processing, Spring MVC iterates over all registered HandlerMethodArgumentResolver instances. For each parameter, it calls supportsParameter(); if true, resolveArgument() supplies the value, after which the controller method executes.

Advantages

Type safety : compile‑time checks prevent wrong casts.

Cleaner code : controllers no longer call request.getAttribute().

Testability : arguments can be mocked without constructing a full HttpServletRequest.

Maintainability : change user‑retrieval logic in one place.

Extensibility : resolver can return User objects, profiles, or permissions based on parameter type.

Separation of concerns : filter handles authentication, resolver handles injection.

Advanced Usage – Returning Full Objects

The resolver can inspect the target parameter type and return a User, UserProfile, or other domain objects instead of just the ID.

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        Long userId = (Long) webRequest.getAttribute("currentUserId", WebRequest.SCOPE_REQUEST);
        if (userId == null) {
            return null; // or throw exception
        }
        Class<?> type = parameter.getParameterType();
        if (type == Long.class || type == long.class) {
            return userId;
        } else if (type == User.class) {
            return userService.findById(userId);
        } else if (type == UserProfile.class) {
            return userService.getUserProfile(userId);
        }
        throw new IllegalArgumentException("Unsupported parameter type: " + type);
    }
}

Multi‑Tenant Example

Define another annotation @CurrentTenant and a corresponding resolver to inject tenant IDs alongside user IDs.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentTenant {
}

@Component
public class CurrentTenantArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentTenant.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        return webRequest.getAttribute("currentTenantId", WebRequest.SCOPE_REQUEST);
    }
}

Practical Tips

Exception handling : throw a custom UnauthorizedException when userId is missing and handle it with @ControllerAdvice.

Spring Security integration : obtain the authenticated principal from SecurityContextHolder and return its ID.

Multi‑tenant scenarios : combine @CurrentUser and @CurrentTenant to fetch data scoped to both user and tenant.

Conclusion

Using Spring MVC’s HandlerMethodArgumentResolver we can safely pass the current user from a filter to controller methods without polluting business logic, achieving type‑safe injection, better testability, and easy extensibility.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring MVCFilterHandlerMethodArgumentResolver
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.