Skip to content

Commit e438599

Browse files
authored
feat: cache loader with external variable reference (#434)
1 parent fbd2a5e commit e438599

File tree

8 files changed

+119
-37
lines changed

8 files changed

+119
-37
lines changed

arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/caffeine/CaffeineAsyncInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static boolean onEnter(@Advice.Argument(0) Object key,
4747
if (ContextManager.needReplay() && CacheLoaderUtil.needRecordOrReplay(cacheLoader)) {
4848
String className = CacheLoaderUtil.getLocatedClass(cacheLoader);
4949
DynamicClassExtractor extractor = new DynamicClassExtractor(className, methodName, new Object[]{key}, methodReturnType);
50-
mockResult = extractor.replay();
50+
mockResult = extractor.replayOrRealCall();
5151
return mockResult != null && mockResult.notIgnoreMockResult();
5252
}
5353
return false;

arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/caffeine/CaffeineSyncInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static boolean onEnter(@Advice.Argument(0) Object key,
4646
if (ContextManager.needReplay() && CacheLoaderUtil.needRecordOrReplay(cacheLoader)) {
4747
String className = CacheLoaderUtil.getLocatedClass(cacheLoader);
4848
DynamicClassExtractor extractor = new DynamicClassExtractor(className, methodName, new Object[]{key}, methodReturnType);
49-
mockResult = extractor.replay();
49+
mockResult = extractor.replayOrRealCall();
5050
return mockResult != null && mockResult.notIgnoreMockResult();
5151
}
5252
return false;

arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/guava/GuavaCacheInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static boolean onEnter(@Advice.Origin("#m") String methodName,
4848
if (ContextManager.needReplay() && CacheLoaderUtil.needRecordOrReplay(loader)) {
4949
String className = CacheLoaderUtil.getLocatedClass(loader);
5050
DynamicClassExtractor extractor = new DynamicClassExtractor(className, methodName, new Object[]{key}, methodReturnType);
51-
mockResult = extractor.replay();
51+
mockResult = extractor.replayOrRealCall();
5252
return mockResult != null && mockResult.notIgnoreMockResult();
5353
}
5454

arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/util/CacheLoaderUtil.java

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@
77
import io.arex.inst.runtime.log.LogManager;
88

99
import java.lang.reflect.Field;
10-
import java.lang.reflect.Modifier;
1110
import java.util.Map;
1211
import java.util.concurrent.ConcurrentHashMap;
1312

1413
public class CacheLoaderUtil {
1514
private static final Map<Integer, Field> REFERENCE_FIELD_MAP = new ConcurrentHashMap<>();
1615
private static final Map<Integer, String> NO_REFERENCE_MAP = new ConcurrentHashMap<>();
1716
private static final String LAMBDA_SUFFIX = "$$";
17+
private static final String EXTERNAL_VARIABLE_REFERENCE_IDENTIFIER = "val$";
18+
private static final String EXTERNAL_INSTANCE_REFERENCE_IDENTIFIER = "this$0";
1819

1920
/**
20-
* return the reference to the outer class in an inner class, if exists.
21-
* else return the class name of the loader.
21+
* Cache loader is mainly divided into three scenarios:
22+
* 1. public static final LoadingCache<String, Object> cacheStaticData = CacheBuilder.newBuilder().build(new CacheLoader<String, Object>() {...});
23+
* In this case, we can directly obtain the cache loader class name to distinguish the cache loader.
24+
* <p>
25+
* 2. public static final LoadingCache<String, Object> fatherCache = CacheBuilder.newBuilder().build(new CacheLoader<String, Object>() {abstract load() {...}});
26+
* <p>
27+
* childCache1 extends fatherCache, childCache2 extends fatherCache. and override the load method.
28+
* In this case, we need to obtain the childCache1 or childCache2 class name to distinguish the cache loader.
29+
* the method parameter is the fatherCache, we can get the childCache1 or childCache2 class name from field this$0.
30+
* <p>
31+
* 3. public createLoadingCache(Object task) {CacheBuilder.newBuilder().build(new CacheLoader<String, Object>() {task.load()});
32+
* In this case, we need to obtain the task class name to distinguish the cache loader.
33+
* the method parameter is the cacheLoader, we can get the task class name from field val$task.
2234
*/
2335
public static String getLocatedClass(Object loader) {
2436
if (loader == null) {
@@ -31,22 +43,16 @@ public static String getLocatedClass(Object loader) {
3143
}
3244

3345
Class<?> loaderClass = loader.getClass();
34-
if (isNotAbstractOrInterface(loaderClass.getEnclosingClass())) {
35-
return generateNameWithNoReference(loaderHashCode, loaderClass);
36-
}
37-
3846
Field field = REFERENCE_FIELD_MAP.computeIfAbsent(loaderHashCode, k -> getReferenceField(loaderClass));
3947
if (field == null) {
4048
return generateNameWithNoReference(loaderHashCode, loaderClass);
4149
}
4250

43-
try {
44-
Object referenceObject = field.get(loader);
45-
return getReferenceClass(loader, referenceObject);
46-
} catch(Exception e) {
47-
LogManager.warn("CacheLoaderUtil.getLocatedClass", e);
48-
return loaderClass.getName();
51+
String className = getReferenceClass(field, loader);
52+
if (StringUtil.startWith(field.getName(), EXTERNAL_VARIABLE_REFERENCE_IDENTIFIER)) {
53+
NO_REFERENCE_MAP.put(loaderHashCode, className);
4954
}
55+
return className;
5056
}
5157

5258
/**
@@ -59,30 +65,47 @@ private static String generateNameWithNoReference(int loaderHashCode, Class<?> l
5965
return loaderClassName;
6066
}
6167

62-
private static boolean isNotAbstractOrInterface(Class<?> clazz) {
63-
if (clazz == null) {
64-
return true;
65-
}
66-
int modifiers = clazz.getModifiers();
67-
return !Modifier.isAbstract(modifiers) && !clazz.isInterface();
68-
}
69-
68+
/**
69+
* get the reference field of the loader.
70+
* the external traversal reference field has the highest priority.
71+
* If it is recognized, it can be returned. If not, all fields will be traversed.
72+
* ex:
73+
* fields [this$0, val$xx] return field[1] -> val$xx.
74+
* fields [this$0, xx] return field[0]-> this$0.
75+
* else return null.
76+
*/
7077
private static Field getReferenceField(Class<?> loaderClass) {
7178
try {
72-
Field referenceField = loaderClass.getDeclaredField("this$0");
73-
referenceField.setAccessible(true);
79+
Field referenceField = null;
80+
for (Field field : loaderClass.getDeclaredFields()) {
81+
if (StringUtil.startWith(field.getName(), EXTERNAL_VARIABLE_REFERENCE_IDENTIFIER)) {
82+
referenceField = field;
83+
referenceField.setAccessible(true);
84+
return referenceField;
85+
}
86+
if (StringUtil.equals(field.getName(), EXTERNAL_INSTANCE_REFERENCE_IDENTIFIER)) {
87+
referenceField = field;
88+
referenceField.setAccessible(true);
89+
}
90+
}
7491
return referenceField;
7592
} catch(Exception e) {
7693
LogManager.warn("CacheLoaderUtil.getReferenceField", e);
7794
return null;
7895
}
7996
}
8097

81-
private static String getReferenceClass(Object cacheLoader, Object referenceObject) {
82-
if (referenceObject == null) {
98+
private static String getReferenceClass(Field field, Object cacheLoader) {
99+
try {
100+
Object referenceObject = field.get(cacheLoader);
101+
if (referenceObject == null) {
102+
return cacheLoader.getClass().getName();
103+
}
104+
return referenceObject.getClass().getName();
105+
} catch(Exception e) {
106+
LogManager.warn("CacheLoaderUtil.getReferenceClass", e);
83107
return cacheLoader.getClass().getName();
84108
}
85-
return referenceObject.getClass().getName();
86109
}
87110

88111
public static boolean needRecordOrReplay(Object cacheLoader) {

arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/guava/GuavaCacheInstrumentationTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import org.mockito.MockedStatic;
1818
import org.mockito.Mockito;
1919

20-
import java.lang.reflect.Method;
2120
import java.util.concurrent.atomic.AtomicReference;
2221

2322
import static org.junit.jupiter.api.Assertions.*;
@@ -67,7 +66,7 @@ public String load(String key) throws Exception {
6766

6867
// replay
6968
try(MockedConstruction ignored = Mockito.mockConstruction(DynamicClassExtractor.class, ((extractor, context) -> {
70-
Mockito.when(extractor.replay()).thenReturn(MockResult.success("test"));
69+
Mockito.when(extractor.replayOrRealCall()).thenReturn(MockResult.success("test"));
7170
}))) {
7271
Mockito.when(ContextManager.needRecordOrReplay()).thenReturn(true);
7372
Mockito.when(ContextManager.needReplay()).thenReturn(true);

arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/util/CacheLoaderUtilTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.arex.inst.cache.util;
22

3+
import com.google.common.cache.CacheBuilder;
34
import com.google.common.cache.CacheLoader;
5+
import com.google.common.cache.LoadingCache;
46
import io.arex.inst.runtime.config.Config;
57
import org.junit.jupiter.api.Test;
68
import org.mockito.MockedStatic;
@@ -59,6 +61,10 @@ void getLocatedClass() throws Exception {
5961
assertEquals("io.arex.inst.cache.util.CacheLoaderUtilTest$AbstractCache2$1", locatedClass);
6062
assertEquals(2, NO_REFERENCE_MAP.size());
6163
assertEquals(1, REFERENCE_FIELD_MAP.size());
64+
65+
// external variable reference
66+
locatedClass = CacheLoaderUtil.getLocatedClass(ExternalVariableCache.cacheLoader);
67+
assertEquals(SubCache1.class.getName(), locatedClass);
6268
}
6369

6470
@Test
@@ -106,4 +112,20 @@ public Object load(String key) throws Exception {
106112
};
107113
}
108114

115+
static class ExternalVariableCache {
116+
public static CacheLoader cacheLoader = null;
117+
static {
118+
createCache(new SubCache1());
119+
}
120+
public static LoadingCache createCache(AbstractCache cache) {
121+
cacheLoader = new CacheLoader<Object, Object>() {
122+
@Override
123+
public Object load(Object key) throws Exception {
124+
return cache.abstractClasscacheLoader.load(key);
125+
}
126+
};
127+
return CacheBuilder.newBuilder().build(cacheLoader);
128+
}
129+
}
130+
109131
}

arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ public MockResult replay() {
153153
return MockResult.success(ignoreMockResult, replayResult);
154154
}
155155

156+
public MockResult replayOrRealCall() {
157+
MockResult mockResult = replay();
158+
if (mockResult != null && mockResult.getResult() == null) {
159+
return MockResult.IGNORE_MOCK_RESULT;
160+
}
161+
return mockResult;
162+
}
163+
156164
private Object deserializeResult(String replayResult, String typeName) {
157165
return Serializer.deserialize(replayResult, typeName, ArexConstants.GSON_SERIALIZER);
158166
}

arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.google.common.util.concurrent.ListenableFuture;
66
import io.arex.agent.bootstrap.model.ArexMocker;
77
import io.arex.agent.bootstrap.model.Mocker.Target;
8-
import io.arex.agent.thirdparty.util.time.DateFormatUtils;
98
import io.arex.inst.common.util.FluxReplayUtil;
109
import io.arex.inst.common.util.MonoRecordFunction;
1110
import io.arex.inst.runtime.config.ConfigBuilder;
@@ -20,22 +19,17 @@
2019
import java.lang.reflect.Field;
2120
import java.lang.reflect.Method;
2221
import java.time.LocalDateTime;
23-
import java.time.LocalTime;
24-
import java.util.Calendar;
2522
import java.util.Collections;
26-
import java.util.Date;
2723
import java.util.HashMap;
2824
import java.util.HashSet;
2925
import java.util.Map;
3026
import java.util.Set;
31-
import java.util.TimeZone;
3227
import java.util.concurrent.CompletableFuture;
3328
import java.util.concurrent.ExecutionException;
3429
import java.util.function.Function;
3530

3631
import io.arex.inst.runtime.util.sizeof.AgentSizeOf;
3732
import org.junit.jupiter.api.AfterAll;
38-
import org.junit.jupiter.api.Assertions;
3933
import org.junit.jupiter.api.BeforeAll;
4034
import org.junit.jupiter.api.Test;
4135
import org.junit.jupiter.api.extension.ExtendWith;
@@ -517,4 +511,40 @@ void cacheExtractorTest() throws Exception {
517511
clazzName.setAccessible(true);
518512
assertEquals("cacheClass", clazzName.get(extractor));
519513
}
514+
515+
@Test
516+
void replayWithRealCall() {
517+
try (MockedStatic<MockUtils> mockService = mockStatic(MockUtils.class);
518+
MockedStatic<IgnoreUtils> ignoreService = mockStatic(IgnoreUtils.class)) {
519+
ignoreService.when(() -> IgnoreUtils.ignoreMockResult(any(), any())).thenReturn(false);
520+
521+
ArexMocker arexMocker = new ArexMocker();
522+
arexMocker.setTargetRequest(new Target());
523+
arexMocker.setTargetResponse(new Target());
524+
mockService.when(() -> MockUtils.createDynamicClass(any(), any())).thenReturn(arexMocker);
525+
mockService.when(() -> MockUtils.checkResponseMocker(any())).thenReturn(true);
526+
527+
ArexMocker arexMocker2 = new ArexMocker();
528+
arexMocker2.setTargetRequest(new Target());
529+
arexMocker2.setTargetResponse(new Target());
530+
arexMocker2.getTargetResponse().setBody("mock Body");
531+
arexMocker2.getTargetResponse().setType("mock Type");
532+
mockService.when(() -> MockUtils.replayMocker(any(), any())).thenReturn(arexMocker2);
533+
534+
Mockito.when(Serializer.serializeWithException(any(), anyString())).thenReturn("mock Serializer.serialize");
535+
Mockito.when(Serializer.serializeWithException(anyString(), anyString())).thenReturn("");
536+
Mockito.when(Serializer.deserialize(anyString(), anyString(), anyString())).thenReturn("mock result");
537+
Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", String.class);
538+
539+
DynamicClassExtractor extractor = new DynamicClassExtractor("className", "methoName", null, null);
540+
MockResult mockResult = extractor.replayOrRealCall();
541+
assertTrue(mockResult.notIgnoreMockResult());
542+
// response is null
543+
arexMocker2.getTargetResponse().setBody(null);
544+
mockResult = extractor.replayOrRealCall();
545+
assertTrue(mockResult.isIgnoreMockResult());
546+
} catch (Throwable e) {
547+
throw new RuntimeException(e);
548+
}
549+
}
520550
}

0 commit comments

Comments
 (0)