1 | package org.cardanofoundation.explorer.api.config.aop.singletoncache; | |
2 | ||
3 | import java.time.LocalDate; | |
4 | import java.time.LocalDateTime; | |
5 | import java.time.format.DateTimeFormatter; | |
6 | import java.util.concurrent.TimeUnit; | |
7 | ||
8 | import lombok.RequiredArgsConstructor; | |
9 | import lombok.extern.log4j.Log4j2; | |
10 | ||
11 | import org.springframework.beans.factory.annotation.Value; | |
12 | import org.springframework.data.redis.core.RedisTemplate; | |
13 | import org.springframework.stereotype.Component; | |
14 | ||
15 | import com.google.gson.Gson; | |
16 | import com.google.gson.GsonBuilder; | |
17 | import com.google.gson.JsonDeserializer; | |
18 | import com.google.gson.JsonPrimitive; | |
19 | import com.google.gson.JsonSerializer; | |
20 | import org.aspectj.lang.ProceedingJoinPoint; | |
21 | import org.aspectj.lang.annotation.Around; | |
22 | import org.aspectj.lang.annotation.Aspect; | |
23 | import org.aspectj.lang.reflect.CodeSignature; | |
24 | ||
25 | @Aspect | |
26 | @Component | |
27 | @Log4j2 | |
28 | @RequiredArgsConstructor | |
29 | public class SingletonCallAspect { | |
30 | ||
31 | @Value("${application.network}") | |
32 | private String network; | |
33 | ||
34 | private static final String LOCKED = "LOCKED"; | |
35 | private static final String PREFIX_KEY = "METHOD_CACHE:"; | |
36 | ||
37 | private final RedisTemplate<String, Object> redisTemplate; | |
38 | ||
39 | /* Config Gson for working with LocalDate/LocalDateTime */ | |
40 | private static final Gson GSON = | |
41 | new GsonBuilder() | |
42 | .registerTypeAdapter( | |
43 | LocalDate.class, | |
44 | (JsonSerializer<LocalDate>) | |
45 | (value, type, context) -> | |
46 |
1
1. lambda$static$0 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$0 → NO_COVERAGE |
new JsonPrimitive(value.format(DateTimeFormatter.ISO_LOCAL_DATE))) |
47 | .registerTypeAdapter( | |
48 | LocalDateTime.class, | |
49 | (JsonSerializer<LocalDateTime>) | |
50 | (value, type, context) -> | |
51 |
1
1. lambda$static$1 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$1 → NO_COVERAGE |
new JsonPrimitive(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))) |
52 | .registerTypeAdapter( | |
53 | LocalDate.class, | |
54 | (JsonDeserializer<LocalDate>) | |
55 | (jsonElement, type, context) -> | |
56 |
1
1. lambda$static$2 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$2 → NO_COVERAGE |
LocalDate.parse( |
57 | jsonElement.getAsJsonPrimitive().getAsString(), | |
58 | DateTimeFormatter.ISO_LOCAL_DATE)) | |
59 | .registerTypeAdapter( | |
60 | LocalDateTime.class, | |
61 | (JsonDeserializer<LocalDateTime>) | |
62 | (jsonElement, type, context) -> | |
63 |
1
1. lambda$static$3 : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::lambda$static$3 → NO_COVERAGE |
LocalDateTime.parse( |
64 | jsonElement.getAsJsonPrimitive().getAsString(), | |
65 | DateTimeFormatter.ISO_LOCAL_DATE_TIME)) | |
66 | .create(); | |
67 | ||
68 | /** | |
69 | * this method will be called with the method has annotation @SingletonCall if there are multiple | |
70 | * request calls at the same time, only first request can be processed other request must wait for | |
71 | * data from first request. Firstly, those request will check Redis cache, If having data (value | |
72 | * != LOCKED) return intermediately Or else, first request will call database and process, then | |
73 | * save data to redis cache Other requests will recall redis after each `callAfterMilis`, until | |
74 | * having data in redis This appoach will save CPU of database server in case expensive queries | |
75 | */ | |
76 | @Around("@annotation(singletonCall)") | |
77 | public Object aroundAdvice(ProceedingJoinPoint joinPoint, SingletonCall singletonCall) | |
78 | throws Throwable { | |
79 | ||
80 | CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature(); | |
81 | methodSignature.getName(); | |
82 | String[] sigParamNames = methodSignature.getParameterNames(); | |
83 | Object[] sigParamValues = joinPoint.getArgs(); | |
84 | String cacheKey = generateCacheKey(methodSignature.getName(), sigParamNames, sigParamValues); | |
85 | ||
86 | var opValuesRedis = redisTemplate.opsForValue(); | |
87 | try { | |
88 | Object cacheResult = redisTemplate.opsForValue().get(cacheKey); | |
89 |
1
1. aroundAdvice : negated conditional → NO_COVERAGE |
if (cacheResult == null) { |
90 |
1
1. aroundAdvice : removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE |
opValuesRedis.set(cacheKey, LOCKED); |
91 | Object data = joinPoint.proceed(); | |
92 |
1
1. aroundAdvice : removed call to org/springframework/data/redis/core/ValueOperations::set → NO_COVERAGE |
opValuesRedis.set( |
93 | cacheKey, GSON.toJson(data), singletonCall.expireAfterSeconds(), TimeUnit.SECONDS); | |
94 |
1
1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE |
return data; |
95 | } else { | |
96 |
1
1. aroundAdvice : negated conditional → NO_COVERAGE |
if (LOCKED.equals(cacheResult.toString())) { |
97 | do { | |
98 |
1
1. aroundAdvice : removed call to java/lang/Thread::sleep → NO_COVERAGE |
Thread.sleep(singletonCall.callAfterMilis()); |
99 | cacheResult = redisTemplate.opsForValue().get(cacheKey); | |
100 |
1
1. aroundAdvice : negated conditional → NO_COVERAGE |
if (cacheResult == null) { |
101 | return null; | |
102 | } | |
103 |
1
1. aroundAdvice : negated conditional → NO_COVERAGE |
} while (LOCKED.equals(cacheResult.toString())); |
104 | } else { | |
105 |
1
1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE |
return GSON.fromJson(cacheResult.toString(), singletonCall.typeToken().getType().get()); |
106 | } | |
107 |
1
1. aroundAdvice : replaced return value with null for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::aroundAdvice → NO_COVERAGE |
return GSON.fromJson(cacheResult.toString(), singletonCall.typeToken().getType().get()); |
108 | } | |
109 | } catch (Exception e) { | |
110 | redisTemplate.delete(cacheKey); | |
111 | throw e; | |
112 | } finally { | |
113 | // do nothing | |
114 | } | |
115 | } | |
116 | ||
117 | private String generateCacheKey( | |
118 | String methodName, String[] sigParamNames, Object[] sigParamValues) { | |
119 | StringBuilder str = new StringBuilder(); | |
120 |
2
1. generateCacheKey : negated conditional → NO_COVERAGE 2. generateCacheKey : changed conditional boundary → NO_COVERAGE |
for (int i = 0; i < sigParamNames.length; i++) { |
121 | str.append(sigParamNames[i]).append(":").append(sigParamValues[i]); | |
122 |
3
1. generateCacheKey : changed conditional boundary → NO_COVERAGE 2. generateCacheKey : Replaced integer subtraction with addition → NO_COVERAGE 3. generateCacheKey : negated conditional → NO_COVERAGE |
if (i < sigParamNames.length - 1) str.append("_"); |
123 | } | |
124 |
1
1. generateCacheKey : replaced return value with "" for org/cardanofoundation/explorer/api/config/aop/singletoncache/SingletonCallAspect::generateCacheKey → NO_COVERAGE |
return network + "-" + PREFIX_KEY + methodName + ":" + str.toString().hashCode(); |
125 | } | |
126 | } | |
Mutations | ||
46 |
1.1 |
|
51 |
1.1 |
|
56 |
1.1 |
|
63 |
1.1 |
|
89 |
1.1 |
|
90 |
1.1 |
|
92 |
1.1 |
|
94 |
1.1 |
|
96 |
1.1 |
|
98 |
1.1 |
|
100 |
1.1 |
|
103 |
1.1 |
|
105 |
1.1 |
|
107 |
1.1 |
|
120 |
1.1 2.2 |
|
122 |
1.1 2.2 3.3 |
|
124 |
1.1 |