Backend Development 8 min read

Simplify JWT Retrieval in Spring Boot Controllers with a Custom Argument Resolver

This article demonstrates how to create a custom HandlerMethodArgumentResolver in Spring Boot that automatically extracts JWT token data and injects the corresponding user object—or selected fields—directly into controller method parameters, reducing boilerplate and improving code readability.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Simplify JWT Retrieval in Spring Boot Controllers with a Custom Argument Resolver

1. Introduction

In a front‑back separated application, JWT tokens are often parsed in the backend to obtain user information. Using filters or ThreadLocal works, but you may want to inject the user directly as a controller method parameter to reduce boilerplate.

2. Practical Example

2.1 Prepare Environment

<code>public class Users {
    private String id;
    /**用户名*/
    private String username;
    /**密码*/
    private String password;
    /**身份证*/
    private String idNo;
    // getters, setters
}</code>

Define a simple service that provides login and user lookup.

<code>@Service
public class UsersService {
    // in‑memory users
    private static final List<Users> USERS = List.of(
        new Users("1", "admin", "123123", "111111"),
        new Users("2", "guest", "123456", "222222")
    );
    private static final String SECRET = "aaaabbbbccccdddd";

    public String login(String username, String password) {
        Optional<Users> optionalUser = USERS.stream()
            .filter(u -> u.getUsername().equals(username) && u.getPassword().equals(password))
            .findFirst();
        if (optionalUser.isPresent()) {
            Users user = optionalUser.get();
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", user.getId());
            return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        }
        return null;
    }

    public Users getUser() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        token = token.replace("Bearer ", "");
        Claims body = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        String id = (String) body.get("id");
        return USERS.stream().filter(u -> id.equals(u.getId())).findFirst().orElse(null);
    }
}
</code>

2.2 Define Controller

<code>@RestController
@RequestMapping("/users")
public class UsersController {
    private final UsersService usersService;
    public UsersController(UsersService usersService) {
        this.usersService = usersService;
    }

    @GetMapping("/login")
    public String login(String username, String password) {
        return usersService.login(username, password);
    }
}
</code>

The controller currently only has a login endpoint that returns a token.

2.3 Custom Annotation

<code>@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenPrincipal {
    String expression() default "";
}
</code>

2.4 Custom Argument Resolver

<code>public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
    private ExpressionParser parser = new SpelExpressionParser();
    private final UsersService usersService;

    public TokenArgumentResolver(UsersService usersService) {
        this.usersService = usersService;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return findMethodAnnotation(TokenPrincipal.class, parameter) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        Object principal = usersService.getUser();
        if (principal == null) {
            return null;
        }
        TokenPrincipal annotation = findMethodAnnotation(TokenPrincipal.class, parameter);
        String expr = annotation.expression();
        if (StringUtils.hasLength(expr)) {
            StandardEvaluationContext context = new StandardEvaluationContext();
            context.setRootObject(principal);
            Expression expression = parser.parseExpression(expr);
            principal = expression.getValue(context);
        }
        if (principal != null && !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
            return null;
        }
        return principal;
    }

    private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
        // implementation omitted for brevity
        return null;
    }
}
</code>

2.5 Register Resolver

<code>@Component
public class TokenWebMvcConfig implements WebMvcConfigurer {
    private final UsersService usersService;
    public TokenWebMvcConfig(UsersService usersService) {
        this.usersService = usersService;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new TokenArgumentResolver(this.usersService));
    }
}
</code>

2.6 Testing

After registration, the following endpoints work:

/users/get – returns the full Users object when annotated with @TokenPrincipal .

/users/username – returns only the username using @TokenPrincipal(expression = "username") .

Conclusion

Using a custom HandlerMethodArgumentResolver greatly simplifies obtaining the current user inside controller methods. For scenarios requiring the user across multiple components, combining this approach with ThreadLocal remains an effective strategy.

JavaSpring BootCustom AnnotationJWTHandlerMethodArgumentResolver
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login 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.