Optimizing Service Calls with ThreadLocal Cache and Custom Annotations in Java
The article explains how to reduce redundant service calls in Java back‑end systems by passing required data from upper layers, applying cache annotations, and implementing an elegant ThreadLocal‑based caching mechanism using custom annotations, aspects, and filters to improve performance without extensive code changes.
Background
A friend asked about interface optimization; the current design calls many internal services for a business function, leading to repeated queries across independent services.
Passing Upper‑Level Queries Down
The best practice is to fetch needed data at the upper layer and pass it down, avoiding duplicate queries. However, legacy code often contains independent queries that need refactoring.
public void xxx(int goodsId) {
Goods goods = goodsService.get(goodsId);
.....
}
public void xxx(Goods goods) {
.....
}Add Cache
If the business tolerates slight data delay, caching can prevent repeated database hits; adding a cache annotation to the query method achieves this without altering existing logic.
public void xxx(int goodsId) {
Goods goods = goodsService.get(goodsId);
.....
}
public void xxx(Goods goods) {
Goods goods = goodsService.get(goodsId);
.....
}
class GoodsService {
@Cached(expire = 10, timeUnit = TimeUnit.SECONDS)
public Goods get(int goodsId) {
return dao.findById(goodsId);
}
}If caching is not allowed, the only option is to pass required data down manually.
Custom Thread‑Local Cache
Problems identified:
Repeated identical queries within a single request cause multiple RPC calls.
High real‑time data requirements make traditional caching unsuitable.
Only the current request needs caching, without affecting other requests.
Existing code should remain unchanged.
Using ThreadLocal to store data for the duration of a request minimizes code changes and isolates the cache to the current thread.
public void xxx(int goodsId) {
Goods goods = ThreadLocal.get();
if (goods == null) {
goods = goodsService.get(goodsId);
}
.....
}This approach works but is not elegant or generic for multiple data types. A more refined solution involves a custom cache annotation combined with an aspect that stores results in a ThreadLocal‑backed Map.
Annotation Definition
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ThreadLocalCache {
/**
* Cache key, supports SPEL expression
*/
String key() default "";
}Storage Definition
/**
* Thread‑local cache manager
*/
public class ThreadLocalCacheManager {
private static ThreadLocal<Map> threadLocalCache = new ThreadLocal<>();
public static void setCache(Map value) { threadLocalCache.set(value); }
public static Map getCache() { return threadLocalCache.get(); }
public static void removeCache() { threadLocalCache.remove(); }
public static void removeCache(String key) {
Map cache = threadLocalCache.get();
if (cache != null) { cache.remove(key); }
}
}Aspect Definition
/**
* Thread‑local cache aspect
*/
@Aspect
public class ThreadLocalCacheAspect {
@Around(value = "@annotation(localCache)")
public Object aroundAdvice(ProceedingJoinPoint joinpoint, ThreadLocalCache localCache) throws Throwable {
Object[] args = joinpoint.getArgs();
Method method = ((MethodSignature) joinpoint.getSignature()).getMethod();
String className = joinpoint.getTarget().getClass().getName();
String methodName = method.getName();
String key = parseKey(localCache.key(), method, args, getDefaultKey(className, methodName, args));
Map cache = ThreadLocalCacheManager.getCache();
if (cache == null) { cache = new HashMap(); }
Map finalCache = cache;
Map<String, Object> data = new HashMap<>();
data.put("methodName", className + "." + methodName);
Object cacheResult = CatTransactionManager.newTransaction(() -> {
if (finalCache.containsKey(key)) {
return finalCache.get(key);
}
return null;
}, "ThreadLocalCache", "CacheGet", data);
if (cacheResult != null) { return cacheResult; }
return CatTransactionManager.newTransaction(() -> {
Object result = null;
try { result = joinpoint.proceed(); }
catch (Throwable throwable) { throw new RuntimeException(throwable); }
finalCache.put(key, result);
ThreadLocalCacheManager.setCache(finalCache);
return result;
}, "ThreadLocalCache", "CachePut", data);
}
private String getDefaultKey(String className, String methodName, Object[] args) {
String defaultKey = className + "." + methodName;
if (args != null) { defaultKey = defaultKey + "." + JsonUtils.toJson(args); }
return defaultKey;
}
private String parseKey(String key, Method method, Object[] args, String defaultKey){
if (!StringUtils.hasText(key)) { return defaultKey; }
LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for(int i = 0;i < paraNameArr.length; i++){
context.setVariable(paraNameArr[i], args[i]);
}
try { return parser.parseExpression(key).getValue(context, String.class); }
catch (SpelEvaluationException e) { return defaultKey; }
}
}Filter Definition
/**
* Thread‑local cache filter
*/
@Slf4j
public class ThreadLocalCacheFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
// Clear cache after request completes
ThreadLocalCacheManager.removeCache();
}
}Auto‑Configuration Class
@Configuration
public class ThreadLocalCacheAutoConfiguration {
@Bean
public FilterRegistrationBean idempotentParamtFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
ThreadLocalCacheFilter filter = new ThreadLocalCacheFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("thread-local-cache-filter");
registration.setOrder(1);
return registration;
}
@Bean
public ThreadLocalCacheAspect threadLocalCacheAspect() {
return new ThreadLocalCacheAspect();
}
}Usage Example
@Service
public class TestService {
/**
* Cached only for the current thread
*/
@ThreadLocalCache
public String getName() {
System.out.println("开始查询了");
return "yinjihaun";
}
/**
* Supports SPEL expression for key
*/
@ThreadLocalCache(key = "#id")
public String getName(String id) {
System.out.println("开始查询了");
return "yinjihaun" + id;
}
}Functional code repository: https://github.com/yinjihuan/kitty
Sample project: https://github.com/yinjihuan/kitty-samples
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.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
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.
