Simplify JWT Token Handling in Spring Using Custom Annotations & ThreadLocal
This article walks through the evolution of a Spring‑based JWT token validation solution, from passing HttpServletRequest into service methods to using a custom @NeedToken annotation, reflection, a base class, and finally ThreadLocal to ensure each request’s token is safely isolated in high‑concurrency environments.
In a recent project I needed to issue and verify JWT tokens without integrating Spring Security or Shiro. The initial implementation passed HttpServletRequest into controller and service methods and extracted the token header manually, which quickly became repetitive.
Initial controller:
@RestController
@RequestMapping("api")
public class TestController {
@Autowired
MyService myService;
@GetMapping("test")
public String test(HttpServletRequest request) {
String result = myService.test(request);
return result;
}
}Initial service:
@Service
public class MyService {
public String test(HttpServletRequest request) {
String token = request.getHeader("token");
System.out.println(token);
// verify token, business logic
return "success";
}
}To remove the repeated HttpServletRequest parameter, I introduced a custom annotation @NeedToken and an aspect that injects the token automatically.
Annotation definition:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedToken {}Aspect (first version) that replaces a method argument named token:
@Aspect
@Component
public class TokenAspect {
@Pointcut("@annotation(com.cn.hydra.aspectdemo.annotation.NeedToken)")
public void tokenPointCut() {}
@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
Object[] args = point.getArgs();
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] paramName = methodSignature.getParameterNames();
List<String> paramNameList = Arrays.asList(paramName);
if (paramNameList.contains("token")) {
int pos = paramNameList.indexOf("token");
args[pos] = token;
}
Object object = point.proceed(args);
return object;
}
}Service method could now be written without the request parameter:
@Service
public class MyService {
@NeedToken
public String test(String token) {
System.out.println(token);
// verify token, business logic
return token;
}
}However, each service still needed a String token parameter, and callers had to pass null, which felt hacky. I tried moving the token field to a base class:
public class BaseService {
public String TOKEN = null;
}and let services extend it:
@Service
public class MyService extends BaseService {
@NeedToken
public String test() {
System.out.println(TOKEN);
return TOKEN;
}
}This caused a NoSuchFieldException because the aspect was trying to access the field on the concrete class. The fix was to obtain the superclass via point.getTarget().getClass().getSuperclass() before reflecting the field.
Even after fixing the reflection, a concurrency problem appeared: Spring beans are singletons, so the shared TOKEN variable could be overwritten by another thread, leading to mismatched tokens under high load.
To give each request its own token copy, I switched to ThreadLocal:
public class BaseService2 {
public static ThreadLocal<String> TOKEN = ThreadLocal.withInitial(() -> null);
}Service using the thread‑local token:
@Service
public class MyService2 extends BaseService2 {
@NeedToken
public boolean testToken(String name) {
String token = TOKEN.get();
boolean check = name.equals(token);
System.out.println(name + " " + token + " " + check);
return check;
}
}Updated aspect stores the token into the thread‑local field via reflection:
@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
Class<?> baseClazz = point.getTarget().getClass().getSuperclass();
Field tokenField = baseClazz.getDeclaredField("TOKEN");
ThreadLocal<String> local = (ThreadLocal<String>) tokenField.get(point.getTarget());
local.set(token);
tokenField.setAccessible(true);
tokenField.set(point.getTarget(), local);
Object object = point.proceed();
return object;
} catch (Throwable e) {
e.printStackTrace();
throw e;
}
}Concurrent testing with CyclicBarrier (200 threads) showed no token mismatches, confirming the thread‑local approach works under heavy load.
Overall, the progression moved from passing HttpServletRequest everywhere, to a custom annotation with AOP, to a base class with reflection, and finally to a safe ThreadLocal solution for JWT token propagation in Spring backend services.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.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.
