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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
