From 740cbea1050f20587c5a440b8c35db36598ced95 Mon Sep 17 00:00:00 2001 From: lucas-myx Date: Mon, 20 Nov 2023 19:12:38 +0800 Subject: [PATCH 1/5] feat: merge dynamic class record and replay --- .../agent/bootstrap/AgentInitializer.java | 1 + .../bootstrap/model/MockCategoryType.java | 28 ++- .../agent/bootstrap/util/CollectionUtil.java | 40 +++- .../inst/runtime/context/ArexContext.java | 32 ++- .../inst/runtime/context/ContextManager.java | 2 +- .../inst/runtime/model/ArexConstants.java | 11 + .../inst/runtime/model/MergeResultDTO.java | 95 +++++++++ .../MergeRecordDubboRequestHandler.java | 32 +++ .../MergeRecordServletRequestHandler.java | 32 +++ .../inst/runtime/request/RequestHandler.java | 1 + .../request/RequestHandlerManager.java | 15 ++ .../inst/runtime/util/MergeSplitUtil.java | 159 ++++++++++++++ .../io/arex/inst/runtime/util/MockUtils.java | 91 ++++++++ .../inst/runtime/util/sizeof/AgentSizeOf.java | 81 +++++++ .../util/sizeof/CombinationSizeOfFilter.java | 46 ++++ .../util/sizeof/ObjectGraphWalker.java | 177 ++++++++++++++++ .../runtime/util/sizeof/SizeOfFilter.java | 29 +++ .../runtime/util/sizeof/VisitorListener.java | 5 + .../sizeof/WeakIdentityConcurrentMap.java | 198 ++++++++++++++++++ .../apollo/ApolloDubboRequestHandler.java | 5 +- .../apollo/ApolloServletV3RequestHandler.java | 5 +- .../dubbo/alibaba/DubboProviderExtractor.java | 3 +- .../apache/v2/DubboProviderExtractor.java | 3 +- .../apache/v3/DubboProviderExtractor.java | 3 +- .../dynamic/common/DynamicClassExtractor.java | 87 ++++---- .../inst/httpservlet/ServletAdviceHelper.java | 2 +- 26 files changed, 1128 insertions(+), 55 deletions(-) create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java index 97c54bea9..30be17f6c 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java @@ -22,6 +22,7 @@ public static void initialize(Instrumentation inst, File agentFile, String agent File[] extensionFiles = getExtensionJarFiles(agentFile); classLoader = createAgentClassLoader(agentFile, extensionFiles); InstrumentationHolder.setAgentClassLoader(classLoader); + InstrumentationHolder.setInstrumentation(inst); AgentInstaller installer = createAgentInstaller(inst, agentFile, agentArgs); addJarToLoaderSearch(agentFile, extensionFiles); installer.install(); diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java index 77a252a10..03aa6907a 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java @@ -12,7 +12,7 @@ public class MockCategoryType implements Serializable { public static final MockCategoryType DATABASE = createDependency("Database"); public static final MockCategoryType HTTP_CLIENT = createDependency("HttpClient"); public static final MockCategoryType CONFIG_FILE = createSkipComparison("ConfigFile"); - public static final MockCategoryType DYNAMIC_CLASS = createSkipComparison("DynamicClass"); + public static final MockCategoryType DYNAMIC_CLASS = createMergeSkipComparison("DynamicClass"); public static final MockCategoryType REDIS = createSkipComparison("Redis"); public static final MockCategoryType MESSAGE_PRODUCER = createDependency("QMessageProducer"); public static final MockCategoryType MESSAGE_CONSUMER = createEntryPoint("QMessageConsumer"); @@ -23,7 +23,7 @@ public class MockCategoryType implements Serializable { private String name; private boolean entryPoint; private boolean skipComparison; - + private boolean mergeRecord; public static MockCategoryType createEntryPoint(String name) { return create(name, true, false); } @@ -32,6 +32,11 @@ public static MockCategoryType createSkipComparison(String name) { return create(name, false, true); } + public static MockCategoryType createMergeSkipComparison(String name) { + return CATEGORY_TYPE_MAP.computeIfAbsent(name, + key -> new MockCategoryType(name, false, true, true)); + } + public static MockCategoryType createDependency(String name) { return create(name, false, false); } @@ -45,6 +50,10 @@ public static Collection values() { return CATEGORY_TYPE_MAP.values(); } + public static MockCategoryType of(String name) { + return CATEGORY_TYPE_MAP.get(name); + } + public String getName() { return this.name; } @@ -69,22 +78,35 @@ public void setSkipComparison(boolean skipComparison) { this.skipComparison = skipComparison; } + public boolean isMergeRecord() { + return mergeRecord; + } + + public void setMergeRecord(boolean mergeRecord) { + this.mergeRecord = mergeRecord; + } + public MockCategoryType() { } private MockCategoryType(String name, boolean entryPoint, boolean skipComparison) { + this(name, entryPoint, skipComparison, false); + } + + private MockCategoryType(String name, boolean entryPoint, boolean skipComparison, boolean mergeRecord) { this.name = name; this.entryPoint = entryPoint; this.skipComparison = skipComparison; + this.mergeRecord = mergeRecord; } - @Override public String toString() { final StringBuilder builder = new StringBuilder("MockCategoryType{"); builder.append("name='").append(name).append('\''); builder.append(", entryPoint=").append(entryPoint); builder.append(", skipComparison=").append(skipComparison); + builder.append(", mergeRecord=").append(mergeRecord); builder.append('}'); return builder.toString(); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java index a206c1ba6..9104925d6 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java @@ -1,9 +1,6 @@ package io.arex.agent.bootstrap.util; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; public class CollectionUtil { private static final List EMPTY_LIST = newArrayList(); @@ -41,4 +38,39 @@ public static List newArrayList(E... elements) { Collections.addAll(list, elements); return list; } + + /** + * split to multiple list by split count + */ + public static List> split(List originalList, int splitCount) { + List> splitList = new ArrayList<>(); + if (isEmpty(originalList)) { + return splitList; + } + int originalSize = originalList.size(); + if (originalSize < splitCount || splitCount == 0) { + splitList.add(originalList); + return splitList; + } + for (int i = 0; i < splitCount; i++) { + List list = new ArrayList<>(); + splitList.add(list); + } + int index = 0; + for (V value : originalList) { + splitList.get(index).add(value); + index = (index + 1) % splitCount; + } + return splitList; + } + + public static List filterNull(List originalList) { + List filterList = new ArrayList<>(); + for (V element : originalList) { + if (element != null) { + filterList.add(element); + } + } + return filterList; + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java index e1f5293de..241f936a7 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java @@ -3,12 +3,11 @@ import io.arex.agent.bootstrap.util.ConcurrentHashSet; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeResultDTO; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class ArexContext { @@ -18,13 +17,16 @@ public class ArexContext { private final long createTime; private final AtomicInteger sequence; private Set methodSignatureHashList; - private Map cachedReplayResultMap; + private Map cachedReplayResultMap; private Map> excludeMockTemplate; private Map attachments = null; + private LinkedBlockingQueue mergeRecordQueue; + private boolean isRedirectRequest; private boolean isInvalidCase; + private boolean isMainEntryEnd; public static ArexContext of(String caseId) { return of(caseId, null); @@ -72,7 +74,7 @@ public Set getMethodSignatureHashList() { return methodSignatureHashList; } - public Map getCachedReplayResultMap() { + public Map getCachedReplayResultMap() { if (cachedReplayResultMap == null) { cachedReplayResultMap = new ConcurrentHashMap<>(); } @@ -138,6 +140,21 @@ public boolean isRedirectRequest(String referer) { return isRedirectRequest; } + public LinkedBlockingQueue getMergeRecordQueue() { + if (mergeRecordQueue == null) { + mergeRecordQueue = new LinkedBlockingQueue<>(2048); + } + return mergeRecordQueue; + } + + public boolean isMainEntryEnd() { + return isMainEntryEnd; + } + + public void setMainEntryEnd(boolean mainEntryEnd) { + isMainEntryEnd = mainEntryEnd; + } + public void clear() { if (methodSignatureHashList != null) { methodSignatureHashList.clear(); @@ -151,5 +168,8 @@ public void clear() { if (attachments != null) { attachments.clear(); } + if (mergeRecordQueue != null) { + mergeRecordQueue.clear(); + } } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java index 8b8616caa..64e1f0c49 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java @@ -80,7 +80,7 @@ public static void registerListener(ContextListener listener) { private static void publish(ArexContext context, boolean isCreate) { if (LISTENERS.size() > 0) { - LISTENERS.stream().forEach(listener -> { + LISTENERS.forEach(listener -> { if (isCreate) { listener.onCreate(context); } else { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java index 4fff6f618..80f17a496 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java @@ -31,9 +31,20 @@ private ArexConstants() {} public static final String REPLAY_ORIGINAL_MOCKER = "arex-replay-original-mocker"; public static final String AREX_EXTENSION_ATTRIBUTE = "arex-extension-attribute"; public static final String GSON_SERIALIZER = "gson"; + public static final String JACKSON_SERIALIZER = "jackson"; public static final String CONFIG_DEPENDENCY = "arex_replay_prepare_dependency"; public static final String PREFIX = "arex-"; public static final String CONFIG_VERSION = "configBatchNo"; public static final String SKIP_FLAG = "arex-skip-flag"; public static final String ORIGINAL_REQUEST = "arex-original-request"; + public static final String MERGE_RECORD_ENABLE = "arex.merge.record.enable"; + public static final String MERGE_RECORD_NAME = "arex.mergeRecord"; + public static final String MERGE_RECORD_THRESHOLD = "arex.merge.record.threshold"; + public static final String MERGE_REPLAY_THRESHOLD = "arex.merge.replay.threshold"; + public static final int MERGE_RECORD_THRESHOLD_DEFAULT = 10; + public static final int MERGE_REPLAY_THRESHOLD_DEFAULT = 1000; + public static final String MERGE_RESULT_TYPE = "java.util.ArrayList-io.arex.inst.runtime.model.MergeResultDTO"; + public static final String MERGE_MEMORY_CHECK = "arex.merge.check.memory"; + public static final String MERGE_SPLIT_COUNT = "arex.merge.split.count"; + public static final String MERGE_RECORD_KEY = "mergeRecord"; } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java new file mode 100644 index 000000000..e54410f0c --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java @@ -0,0 +1,95 @@ +package io.arex.inst.runtime.model; + +public class MergeResultDTO { + private String category; + private int methodSignatureKey; + private String clazzName; + private String methodName; + private Object[] args; + private Object result; + private String resultClazz; + private String serializeType; + + public MergeResultDTO() {} + + private MergeResultDTO(String category, String clazzName, String methodName, Object[] args, Object result, + String resultClazz, int methodSignatureKey, String serializeType) { + this.category = category; + this.clazzName = clazzName; + this.methodName = methodName; + this.args = args; + this.result = result; + this.resultClazz = resultClazz; + this.methodSignatureKey = methodSignatureKey; + this.serializeType = serializeType; + } + + public static MergeResultDTO of(String category, String clazzName, String methodName, Object[] args, + Object result, String resultClazz, int methodSignatureKey, String serializeType) { + return new MergeResultDTO(category, clazzName, methodName, args, result, resultClazz, methodSignatureKey, serializeType); + } + + public int getMethodSignatureKey() { + return methodSignatureKey; + } + + public void setMethodSignatureKey(int methodSignatureKey) { + this.methodSignatureKey = methodSignatureKey; + } + + public String getClazzName() { + return clazzName; + } + + public void setClazzName(String clazzName) { + this.clazzName = clazzName; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public Object[] getArgs() { + return args; + } + + public void setArgs(Object[] args) { + this.args = args; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public String getResultClazz() { + return resultClazz; + } + + public void setResultClazz(String resultClazz) { + this.resultClazz = resultClazz; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getSerializeType() { + return serializeType; + } + + public void setSerializeType(String serializeType) { + this.serializeType = serializeType; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java new file mode 100644 index 000000000..c9511d269 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java @@ -0,0 +1,32 @@ +package io.arex.inst.runtime.request; + +import com.google.auto.service.AutoService; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.util.MockUtils; + + +@AutoService(RequestHandler.class) +public class MergeRecordDubboRequestHandler implements RequestHandler { + @Override + public String name() { + return MockCategoryType.DUBBO_PROVIDER.getName(); + } + + @Override + public void preHandle(Object request) {} + + @Override + public void handleAfterCreateContext(Object request) { + if (!ContextManager.needReplay() || !Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { + return; + } + // init replay and cached dynamic class + MockUtils.mergeReplay(); + } + + @Override + public void postHandle(Object request, Object response) {} +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java new file mode 100644 index 000000000..81eab1980 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java @@ -0,0 +1,32 @@ +package io.arex.inst.runtime.request; + +import com.google.auto.service.AutoService; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.util.MockUtils; + + +@AutoService(RequestHandler.class) +public class MergeRecordServletRequestHandler implements RequestHandler { + @Override + public String name() { + return MockCategoryType.SERVLET.getName(); + } + + @Override + public void preHandle(Object request) {} + + @Override + public void handleAfterCreateContext(Object request) { + if (!ContextManager.needReplay() || !Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { + return; + } + // init replay and cached dynamic class + MockUtils.mergeReplay(); + } + + @Override + public void postHandle(Object request, Object response) {} +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java index b11317540..1141ab994 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java @@ -10,5 +10,6 @@ public interface RequestHandler { * add or get request information */ void preHandle(Request request); + void handleAfterCreateContext(Request request); void postHandle(Request request, Response response); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java index a6aafde15..7c3aa5497 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java @@ -33,6 +33,21 @@ public static void preHandle(Object request, String name) { } } + public static void handleAfterCreateContext(Object request, String name) { + final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); + if (CollectionUtil.isEmpty(requestHandlers)) { + return; + } + for (RequestHandler requestHandler : requestHandlers) { + try { + requestHandler.handleAfterCreateContext(request); + } catch (Throwable ex) { + // avoid affecting the remaining handlers when one handler fails + LogManager.warn("handleAfterCreateContext", ex.getMessage()); + } + } + } + public static void postHandle(Object request, Object response, String name) { try { final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java new file mode 100644 index 000000000..44f69a4c3 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java @@ -0,0 +1,159 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeResultDTO; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; + +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; + +public class MergeSplitUtil { + private static final long MERGE_MAX_SIZE_5MB = 5 * 1024L * 1024L; + + /** + * merge duplicate mocker to queue and return merged result After reach batch count + * @return if null mean no exceed merge max time limit not need to record or no merge + */ + public static List> merge(Mocker mocker) { + try { + if (!Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { + return null; + } + + ArexContext context = ContextManager.currentContext(); + if (context == null) { + return null; + } + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + MergeResultDTO mergeResultDTO = (MergeResultDTO) mocker.getTargetRequest().getAttribute(ArexConstants.MERGE_RECORD_KEY); + // offer queue to avoid block current thread + if (!mergeRecordQueue.offer(mergeResultDTO)) { + // dynamic class not replay compare, log warn temporarily + LogManager.warn("merge dynamicClass fail", "queue is full"); + return null; + } + + int recordThreshold = Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); + // if main entry record has ended, it means still async thread record(not merge and direct record) + if (mergeRecordQueue.size() < recordThreshold && !context.isMainEntryEnd()) { + return null; + } + List mergeRecordList = new ArrayList<>(); + for (int i = 0; i < recordThreshold; i++) { + MergeResultDTO mergeResult = mergeRecordQueue.poll(); + if (mergeResult != null) { + mergeRecordList.add(mergeResult); + } + } + + return checkAndSplit(mergeRecordList); + } catch (Throwable e) { + LogManager.warn("MergeUtil merge error", e); + return null; + } + } + + private static List> checkAndSplit(List mergeRecordList) { + mergeRecordList = CollectionUtil.filterNull(mergeRecordList); + if (CollectionUtil.isEmpty(mergeRecordList)) { + return null; + } + List> mergeTotalList = new ArrayList<>(); + Map> mergeRecordGroupMap = group(mergeRecordList); + for (Map.Entry> mergeRecordEntry : mergeRecordGroupMap.entrySet()) { + // check memory size and split to multiple list + if (!checkMemorySizeLimit(mergeRecordEntry.getValue())) { + mergeTotalList.addAll(split(mergeRecordEntry.getValue())); + } else { + mergeTotalList.add(mergeRecordEntry.getValue()); + } + } + return mergeTotalList; + } + + /** + * group by category(such as: dynamicClass、redis) + */ + private static Map> group(List mergeRecordList) { + Map> mergeRecordGroupMap = new HashMap<>(); + for (MergeResultDTO mergeResultDTO : mergeRecordList) { + if (mergeResultDTO == null) { + continue; + } + String category = mergeResultDTO.getCategory(); + mergeRecordGroupMap.computeIfAbsent(category, k -> new ArrayList<>()).add(mergeResultDTO); + } + return mergeRecordGroupMap; + } + + private static boolean checkMemorySizeLimit(Object obj) { + if (!Config.get().getBoolean(ArexConstants.MERGE_MEMORY_CHECK, true)) { + return true; + } + long start = System.currentTimeMillis(); + AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); + long memorySize = agentSizeOf.deepSizeOf(obj); + long cost = System.currentTimeMillis() - start; + if (cost > 10) { // longer cost mean larger memory + LogManager.warn("checkMemorySizeLimit", StringUtil.format("size: %s, cost: %s", + AgentSizeOf.humanReadableUnits(memorySize), String.valueOf(cost))); + } + return memorySize < MERGE_MAX_SIZE_5MB; + } + + /** + * split strategy: + * 1. split by config count: list[10] A -> list[5] B、list[5] C + * 2. check memory size separate list + * 3. if memory size not exceed limit return: list[[5]、[5]] R + * 4. if memory size exceed limit, split to single-size list and return: + * list[[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]] R + */ + private static List> split(List mergeRecordList) { + List> splitTotalList = new ArrayList<>(); + // default split in half + int splitCount = Config.get().getInt(ArexConstants.MERGE_SPLIT_COUNT, 2); + List> splitResultList = CollectionUtil.split(mergeRecordList, splitCount); + for (List splitList : splitResultList) { + if (checkMemorySizeLimit(splitList)) { + splitTotalList.add(splitList); + } else { + // split to single-size list + splitTotalList.addAll(CollectionUtil.split(splitList, splitList.size())); + } + } + LogManager.info("split merged record", StringUtil.format("original size: %s, split count: %s", + mergeRecordList.size() + "", splitTotalList.size() + "")); + return splitTotalList; + } + + /** + * merge all that did not exceed the threshold at the end of the request + */ + public static List> mergeRemain() { + try { + ArexContext context = ContextManager.currentContext(); + if (context == null) { + return null; + } + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + if (mergeRecordQueue.size() == 0) { + return null; + } + MergeResultDTO[] mergeRecordArray = mergeRecordQueue.toArray(new MergeResultDTO[0]); + mergeRecordQueue.clear(); + List mergeRecordList = Arrays.asList(mergeRecordArray); + return checkAndSplit(mergeRecordList); + } catch (Throwable e) { + LogManager.warn("MergeUtil mergeRemain error", e); + return null; + } + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java index 71bb247da..196d7bb00 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java @@ -5,14 +5,19 @@ import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.model.Mocker.Target; +import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeResultDTO; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataService; +import java.util.List; +import java.util.Map; public final class MockUtils { @@ -92,7 +97,24 @@ public static void recordMocker(Mocker requestMocker) { if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { return; } + if (requestMocker.getCategoryType().isMergeRecord() && requestMocker.getTargetRequest().getAttribute(ArexConstants.MERGE_RECORD_KEY) != null) { + mergeRecord(requestMocker); + return; + } + + executeRecord(requestMocker); + + if (requestMocker.getCategoryType().isEntryPoint()) { + ArexContext context = ContextManager.currentContext(); + if (context != null) { + context.setMainEntryEnd(true); + } + // after main entry record finished, record remain merge mocker that have not reached the merge threshold once(such as dynamicClass) + mergeRecordRemain(); + } + } + public static void executeRecord(Mocker requestMocker) { if (Config.get().isEnableDebug()) { LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); } @@ -165,4 +187,73 @@ public static boolean checkResponseMocker(Mocker responseMocker) { return true; } + + /** + *
+     * tip:
+     * 1. if user change result object after merge, it will also change the result in cache
+     * 2. if serialize fail, mean this list all fail, need to troubleshoot based on error log
+     * 3. if async record, main entry point has recorded end, merge record will not be executed which no reach the merge threshold
+     * 4. currently not support fuzzy match
+     * 
+ */ + private static void mergeRecord(Mocker requestMocker) { + List> splitList = MergeSplitUtil.merge(requestMocker); + if (CollectionUtil.isEmpty(splitList)) { + return; + } + batchRecord(splitList); + } + + private static void batchRecord(List> splitList) { + String serializeType = ArexConstants.JACKSON_SERIALIZER; + MockCategoryType categoryType; + for (List mergeRecords : splitList) { + if (mergeRecords.get(0).getSerializeType() != null) { + serializeType = mergeRecords.get(0).getSerializeType(); + } + categoryType = MockCategoryType.of(mergeRecords.get(0).getCategory()); + Mocker mergeMocker = MockUtils.create(categoryType, ArexConstants.MERGE_RECORD_NAME); + mergeMocker.getTargetResponse().setBody(Serializer.serialize(mergeRecords, serializeType)); + mergeMocker.getTargetResponse().setType(ArexConstants.MERGE_RESULT_TYPE); + executeRecord(mergeMocker); + LogManager.info("merge record", "size:"+mergeRecords.size()); + } + } + + private static void mergeRecordRemain() { + List> splitList = MergeSplitUtil.mergeRemain(); + if (CollectionUtil.isEmpty(splitList)) { + return; + } + batchRecord(splitList); + } + + /** + * init replay and cached dynamic class + */ + public static void mergeReplay() { + int replayThreshold = Config.get().getInt(ArexConstants.MERGE_REPLAY_THRESHOLD, ArexConstants.MERGE_REPLAY_THRESHOLD_DEFAULT); + for (MockCategoryType categoryType : MockCategoryType.values()) { + if (!categoryType.isMergeRecord()) { + continue; + } + Mocker mergeMocker = create(categoryType, ArexConstants.MERGE_RECORD_NAME); + Map cachedDynamicClassMap = ContextManager.currentContext().getCachedReplayResultMap(); + for (int i = 0; i < replayThreshold; i++) { + // loop replay until over storage size break or over max times + Object result = replayBody(mergeMocker); + if (result == null) { + break; + } + List mergeRecordList = (List) result; + for (MergeResultDTO mergeResultDTO : mergeRecordList) { + if (mergeResultDTO == null) { + continue; + } + cachedDynamicClassMap.put(mergeResultDTO.getMethodSignatureKey(), mergeResultDTO); + } + } + } + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java new file mode 100644 index 000000000..10f862bbb --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java @@ -0,0 +1,81 @@ +package io.arex.inst.runtime.util.sizeof; + +import io.arex.agent.bootstrap.InstrumentationHolder; + +import java.lang.instrument.Instrumentation; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * refer: https://github.com/ehcache/sizeof + */ +public class AgentSizeOf { + /** One kilobyte bytes. */ + public static final long ONE_KB = 1024; + /** One megabyte bytes. */ + public static final long ONE_MB = ONE_KB * ONE_KB; + /** One gigabyte bytes.*/ + public static final long ONE_GB = ONE_KB * ONE_MB; + private final Instrumentation instrumentation; + private final ObjectGraphWalker walker; + private AgentSizeOf(SizeOfFilter fieldFilter) { + this.instrumentation = InstrumentationHolder.getInstrumentation(); + ObjectGraphWalker.Visitor visitor = new SizeOfVisitor(); + this.walker = new ObjectGraphWalker(visitor, fieldFilter); + } + + /** + * Measures the size in memory (heap) of the objects passed in, walking their graph down + * Any overlap of the graphs being passed in will be recognized and only measured once + * + * @return the total size in bytes for these objects + */ + public long deepSizeOf(Object... obj) { + return walker.walk(null, obj); + } + + public static AgentSizeOf newInstance(final SizeOfFilter... filters) { + final SizeOfFilter filter = new CombinationSizeOfFilter(filters); + return new AgentSizeOf(filter); + } + + /** + * Will return the sizeOf each instance + */ + private class SizeOfVisitor implements ObjectGraphWalker.Visitor { + public long visit(Object object) { + return sizeOf(object); + } + } + + public long sizeOf(Object obj) { + if (instrumentation == null) { + return 0; + } + return instrumentation.getObjectSize(obj); + } + + /** + * Returns size in human-readable units (GB, MB, KB or bytes). + */ + public static String humanReadableUnits(long bytes) { + return humanReadableUnits(bytes, + new DecimalFormat("0.#", DecimalFormatSymbols.getInstance(Locale.ROOT))); + } + + /** + * Returns size in human-readable units (GB, MB, KB or bytes). + */ + public static String humanReadableUnits(long bytes, DecimalFormat df) { + if (bytes / ONE_GB > 0) { + return df.format((float) bytes / ONE_GB) + " GB"; + } else if (bytes / ONE_MB > 0) { + return df.format((float) bytes / ONE_MB) + " MB"; + } else if (bytes / ONE_KB > 0) { + return df.format((float) bytes / ONE_KB) + " KB"; + } else { + return bytes + " bytes"; + } + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java new file mode 100644 index 000000000..5e485d729 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java @@ -0,0 +1,46 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * Filter combining multiple filters + * + * @author Chris Dennis + */ +public class CombinationSizeOfFilter implements SizeOfFilter { + + private final SizeOfFilter[] filters; + + /** + * Constructs a filter combining multiple ones + * + * @param filters the filters to combine + */ + public CombinationSizeOfFilter(SizeOfFilter... filters) { + this.filters = filters; + } + + /** + * {@inheritDoc} + */ + public Collection filterFields(Class klazz, Collection fields) { + Collection current = fields; + for (SizeOfFilter filter : filters) { + current = filter.filterFields(klazz, current); + } + return current; + } + + /** + * {@inheritDoc} + */ + public boolean filterClass(Class klazz) { + for (SizeOfFilter filter : filters) { + if (!filter.filterClass(klazz)) { + return false; + } + } + return true; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java new file mode 100644 index 000000000..4b9618349 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java @@ -0,0 +1,177 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.ref.SoftReference; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.Set; + +import static java.util.Collections.newSetFromMap; + +public class ObjectGraphWalker { + private final WeakIdentityConcurrentMap, SoftReference>> fieldCache = + new WeakIdentityConcurrentMap<>(); + private final WeakIdentityConcurrentMap, Boolean> classCache = + new WeakIdentityConcurrentMap<>(); + + private final SizeOfFilter sizeOfFilter; + + private final Visitor visitor; + + /** + * Constructor + * + * @param visitor the visitor to use + * @param filter the filtering + * @see Visitor + * @see SizeOfFilter + */ + ObjectGraphWalker(Visitor visitor, SizeOfFilter filter) { + if(visitor == null) { + throw new NullPointerException("Visitor can't be null"); + } + if(filter == null) { + throw new NullPointerException("SizeOfFilter can't be null"); + } + this.visitor = visitor; + this.sizeOfFilter = filter; + } + + /** + * The visitor to execute the function on each node of the graph + * This is only to be used for the sizing of an object graph in memory! + */ + interface Visitor { + /** + * The visit method executed on each node + * + * @param object the reference at that node + * @return a long for you to do things with... + */ + long visit(Object object); + } + + /** + * Walk the graph and call into the "visitor" + * + * @param root the roots of the objects (a shared graph will only be visited once) + * @return the sum of all Visitor#visit returned values + */ + long walk(Object... root) { + return walk(null, root); + } + + /** + * Walk the graph and call into the "visitor" + * + * @param visitorListener A decorator for the Visitor + * @param root the roots of the objects (a shared graph will only be visited once) + * @return the sum of all Visitor#visit returned values + */ + long walk(VisitorListener visitorListener, Object... root) { + long result = 0; + Deque toVisit = new ArrayDeque<>(); + Set visited = newSetFromMap(new IdentityHashMap<>()); + + if (root != null) { + for (Object object : root) { + nullSafeAdd(toVisit, object); + } + } + + while (!toVisit.isEmpty()) { + + Object ref = toVisit.pop(); + + if (visited.add(ref)) { + Class refClass = ref.getClass(); + if (shouldWalkClass(refClass)) { + if (refClass.isArray() && !refClass.getComponentType().isPrimitive()) { + for (int i = 0; i < Array.getLength(ref); i++) { + nullSafeAdd(toVisit, Array.get(ref, i)); + } + } else { + for (Field field : getFilteredFields(refClass)) { + try { + nullSafeAdd(toVisit, field.get(ref)); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + } + + final long visitSize = visitor.visit(ref); + if (visitorListener != null) { + visitorListener.visited(ref, visitSize); + } + result += visitSize; + } + } + } + + return result; + } + + /** + * Returns the filtered fields for a particular type + * + * @param refClass the type + * @return A collection of fields to be visited + */ + private Collection getFilteredFields(Class refClass) { + SoftReference> ref = fieldCache.get(refClass); + Collection fieldList = ref != null ? ref.get() : null; + if (fieldList != null) { + return fieldList; + } else { + Collection result; + result = sizeOfFilter.filterFields(refClass, getAllFields(refClass)); + fieldCache.put(refClass, new SoftReference<>(result)); + return result; + } + } + + private boolean shouldWalkClass(Class refClass) { + Boolean cached = classCache.get(refClass); + if (cached == null) { + cached = sizeOfFilter.filterClass(refClass); + classCache.put(refClass, cached); + } + return cached; + } + + private static void nullSafeAdd(final Deque toVisit, final Object o) { + if (o != null) { + toVisit.push(o); + } + } + + /** + * Returns all non-primitive fields for the entire class hierarchy of a type + * + * @param refClass the type + * @return all fields for that type + */ + private static Collection getAllFields(Class refClass) { + Collection fields = new ArrayList<>(); + for (Class klazz = refClass; klazz != null; klazz = klazz.getSuperclass()) { + for (Field field : klazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && + !field.getType().isPrimitive()) { + try { + field.setAccessible(true); + } catch (RuntimeException e) { + continue; + } + fields.add(field); + } + } + } + return fields; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java new file mode 100644 index 000000000..62b6e0116 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java @@ -0,0 +1,29 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * Filter to filter types or fields of object graphs passed to a SizeOf engine + * + * @author Chris Dennis + */ +public interface SizeOfFilter { + + /** + * Returns the fields to walk and measure for a type + * + * @param klazz the type + * @param fields the fields already "qualified" + * @return the filtered Set + */ + Collection filterFields(Class klazz, Collection fields); + + /** + * Checks whether the type needs to be filtered + * + * @param klazz the type + * @return true, if to be filtered out + */ + boolean filterClass(Class klazz); +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java new file mode 100644 index 000000000..65a42b722 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java @@ -0,0 +1,5 @@ +package io.arex.inst.runtime.util.sizeof; + +public interface VisitorListener { + void visited(final Object object, final long size); +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java new file mode 100644 index 000000000..fa66dbd7b --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java @@ -0,0 +1,198 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class WeakIdentityConcurrentMap { + private final ConcurrentMap, V> map = new ConcurrentHashMap<>(); + private final ReferenceQueue queue = new ReferenceQueue<>(); + + private final CleanUpTask cleanUpTask; + + /** + * Constructor + */ + public WeakIdentityConcurrentMap() { + this(null); + } + + /** + * Constructor + * + * @param cleanUpTask task cleaning up references + */ + public WeakIdentityConcurrentMap(final CleanUpTask cleanUpTask) { + this.cleanUpTask = cleanUpTask; + } + + /** + * Puts into the underlying + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with key, or + * null if there was no mapping for key. + * (A null return can also indicate that the map + * previously associated null with key, + * if the implementation supports null values.) + */ + public V put(K key, V value) { + cleanUp(); + return map.put(new IdentityWeakReference<>(key, queue), value); + } + + /** + * Remove from the underlying + * + * @param key key whose mapping is to be removed from the map + * @return the previous value associated with key, or + * null if there was no mapping for key. + */ + public V remove(K key) { + cleanUp(); + return map.remove(new IdentityWeakReference<>(key, queue)); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + cleanUp(); + return map.toString(); + } + + /** + * Puts into the underlying + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or + * {@code null} if there was no mapping for the key. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with the key, + * if the implementation supports null values.) + */ + public V putIfAbsent(K key, V value) { + cleanUp(); + return map.putIfAbsent(new IdentityWeakReference<>(key, queue), value); + } + + /** + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key + */ + public V get(K key) { + cleanUp(); + return map.get(new IdentityWeakReference<>(key)); + } + + /** + * + */ + public void cleanUp() { + + Reference reference; + while ((reference = queue.poll()) != null) { + final V value = map.remove(reference); + if (cleanUpTask != null && value != null) { + cleanUpTask.cleanUp(value); + } + } + } + + /** + * @return a set view of the keys contained in this map + */ + public Set keySet() { + cleanUp(); + K k; + final HashSet ks = new HashSet<>(); + for (WeakReference weakReference : map.keySet()) { + k = weakReference.get(); + if (k != null) { + ks.add(k); + } + } + return ks; + } + + public boolean containsKey(final K key) { + cleanUp(); + return map.containsKey(new IdentityWeakReference<>(key)); + } + + /** + * @param + */ + private static final class IdentityWeakReference extends WeakReference { + + private final int hashCode; + + /** + * @param reference the referenced object + */ + IdentityWeakReference(T reference) { + this(reference, null); + } + + /** + * @param reference the references object + * @param referenceQueue the reference queue where references are kept + */ + IdentityWeakReference(T reference, ReferenceQueue referenceQueue) { + super(reference, referenceQueue); + this.hashCode = (reference == null) ? 0 : System.identityHashCode(reference); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.valueOf(get()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof IdentityWeakReference)) { + return false; + } else { + IdentityWeakReference wr = (IdentityWeakReference)o; + Object got = get(); + return (got != null && got == wr.get()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hashCode; + } + } + + /** + * @param + */ + public interface CleanUpTask { + + /** + * @param object object to cleanup + */ + void cleanUp(T object); + } +} diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java index be6666369..c4cc07dcf 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java @@ -16,7 +16,10 @@ public String name() { } @Override - public void preHandle(Map request) { + public void preHandle(Map request) {} + + @Override + public void handleAfterCreateContext(Map request) { // check business application if loaded apollo-client if (ApolloConfigChecker.unloadApollo()) { return; diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java index 1c2c3767d..8b2379a6f 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java @@ -16,7 +16,10 @@ public String name() { } @Override - public void preHandle(HttpServletRequest request) { + public void preHandle(HttpServletRequest request) {} + + @Override + public void handleAfterCreateContext(HttpServletRequest request) { // check business application if loaded apollo-client if (ApolloConfigChecker.unloadApollo()) { return; diff --git a/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java index ccc030921..ffa42c483 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java @@ -25,8 +25,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.getAttachments().put(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java index 9e2f0ad4e..d66176113 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java @@ -28,8 +28,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.setAttachment(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java index 9ff8d1760..7101bc34d 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java @@ -25,8 +25,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.getAttributes().put(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index c97270a51..fadfbfe53 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -2,6 +2,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import io.arex.agent.bootstrap.model.MockCategoryType; import io.arex.agent.bootstrap.model.MockResult; import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; @@ -12,23 +13,21 @@ import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeResultDTO; import io.arex.inst.runtime.model.DynamicClassEntity; import io.arex.inst.runtime.serializer.Serializer; -import io.arex.inst.runtime.util.IgnoreUtils; +import io.arex.inst.runtime.util.*; import io.arex.inst.runtime.log.LogManager; -import io.arex.inst.runtime.util.MockUtils; -import io.arex.inst.runtime.util.TypeUtil; + import java.lang.reflect.Array; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; public class DynamicClassExtractor { private static final int RESULT_SIZE_MAX = Integer.parseInt(System.getProperty("arex.dynamic.result.size.limit", "1000")); - private static final String SERIALIZER = "gson"; private static final String LISTENABLE_FUTURE = "com.google.common.util.concurrent.ListenableFuture"; private static final String COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture"; private static final String NEED_RECORD_TITLE = "dynamic.needRecord"; @@ -79,47 +78,69 @@ public void recordResponse(Object response) { if (needRecord()) { this.resultClazz = buildResultClazz(TypeUtil.getName(response)); Mocker mocker = makeMocker(); - this.serializedResult = serialize(this.result); - mocker.getTargetResponse().setBody(this.serializedResult); + // merge record, no parameter method not merge(currently only support accurate match) + if (Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true) && StringUtil.isNotEmpty(this.methodKey)) { + buildMergeMocker(mocker); + } else { + this.serializedResult = serialize(this.result); + mocker.getTargetResponse().setBody(this.serializedResult); + } MockUtils.recordMocker(mocker); cacheMethodSignature(); } } + private void buildMergeMocker(Mocker mocker) { + MergeResultDTO mergeResultDTO = MergeResultDTO.of(MockCategoryType.DYNAMIC_CLASS.getName(), + this.clazzName, + this.methodName, + this.args, + this.result, // do not serialize(this.result), avoid generate new json string, will increase memory + this.resultClazz, + buildMethodSignatureKey(), + ArexConstants.GSON_SERIALIZER); + mocker.getTargetRequest().setAttribute(ArexConstants.MERGE_RECORD_KEY, mergeResultDTO); + } + public MockResult replay() { if (IgnoreUtils.invalidOperation(dynamicSignature)) { LogManager.warn(NEED_REPLAY_TITLE, StringUtil.format("do not replay invalid operation: %s, can not serialize args or response", dynamicSignature)); return MockResult.IGNORE_MOCK_RESULT; } - String key = buildCacheKey(); - Map cachedReplayResultMap = ContextManager.currentContext() - .getCachedReplayResultMap(); + int signatureHashKey = buildMethodSignatureKey(); + Map cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); Object replayResult = null; // First get replay result from cache - if (key != null) { - replayResult = cachedReplayResultMap.get(key); - } - - // If not in cache, get replay result from mock server - if (replayResult == null) { + MergeResultDTO mergeResultDTO = cachedReplayResultMap.get(signatureHashKey); + String replayBody; + if (mergeResultDTO != null && MockCategoryType.DYNAMIC_CLASS.getName().equals(mergeResultDTO.getCategory())) { + replayBody = Serializer.serialize(mergeResultDTO.getResult(), ArexConstants.GSON_SERIALIZER); + replayResult = deserializeResult(replayBody, mergeResultDTO.getResultClazz()); + } else { + // compatible with old process logic: single replay + // If not in cache, get replay result from mock server Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); + String typeName = ""; if (MockUtils.checkResponseMocker(replayMocker)) { - String typeName = replayMocker.getTargetResponse().getType(); - replayResult = deserializeResult(replayMocker, typeName); + typeName = replayMocker.getTargetResponse().getType(); + replayBody = replayMocker.getTargetResponse().getBody(); + replayResult = deserializeResult(replayBody, typeName); } - replayResult = restoreResponse(replayResult); - // no key no cache, no parameter methods may return different values - if (key != null && replayResult != null) { - cachedReplayResultMap.put(key, replayResult); + // no parameter no cache, no parameter methods may return different values + if (StringUtil.isNotEmpty(this.methodKey) && replayResult != null) { + mergeResultDTO = MergeResultDTO.of(MockCategoryType.DYNAMIC_CLASS.getName(), this.clazzName, + this.methodName, this.args, replayResult, typeName, signatureHashKey, null); + cachedReplayResultMap.put(signatureHashKey, mergeResultDTO); } } + replayResult = restoreResponse(replayResult); boolean ignoreMockResult = IgnoreUtils.ignoreMockResult(clazzName, methodName); return MockResult.success(ignoreMockResult, replayResult); } - private Object deserializeResult(Mocker replayMocker, String typeName) { - return Serializer.deserialize(replayMocker.getTargetResponse().getBody(), typeName, SERIALIZER); + private Object deserializeResult(String replayResult, String typeName) { + return Serializer.deserialize(replayResult, typeName, ArexConstants.GSON_SERIALIZER); } void setFutureResponse(Future result) { @@ -200,7 +221,6 @@ private String getDynamicEntitySignature() { private Mocker makeMocker() { Mocker mocker = MockUtils.createDynamicClass(this.clazzName, this.methodName); mocker.getTargetRequest().setBody(this.methodKey); - mocker.getTargetResponse().setBody(this.serializedResult); mocker.getTargetResponse().setType(this.resultClazz); return mocker; } @@ -302,13 +322,6 @@ private void cacheMethodSignature() { } } - private String buildCacheKey() { - if (StringUtil.isNotEmpty(this.methodKey)) { - return String.format("%s_%s_%s", this.clazzName, this.methodName, this.methodKey); - } - return null; - } - public String getSerializedResult() { return serializedResult; } @@ -318,11 +331,15 @@ private String serialize(Object object) { return null; } try { - return Serializer.serializeWithException(object, SERIALIZER); + return Serializer.serializeWithException(object, ArexConstants.GSON_SERIALIZER); } catch (Throwable ex) { IgnoreUtils.addInvalidOperation(dynamicSignature); LogManager.warn("serializeWithException", StringUtil.format("can not serialize object: %s, cause: %s", TypeUtil.errorSerializeToString(object), ex.toString())); return null; } } + + private int buildMethodSignatureKey() { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", clazzName, methodName, methodKey)); + } } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java index 4a6799875..d63dba309 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java @@ -116,7 +116,7 @@ public static Pair onServiceEnter( String caseId = adapter.getRequestHeader(httpServletRequest, ArexConstants.RECORD_ID); String excludeMockTemplate = adapter.getRequestHeader(httpServletRequest, ArexConstants.HEADER_EXCLUDE_MOCK); CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); - + RequestHandlerManager.handleAfterCreateContext(httpServletRequest, MockCategoryType.SERVLET.getName()); } if (ContextManager.needRecordOrReplay()) { From 4a9350c0b78d1c5e790231bb39edfd90b2f7b711 Mon Sep 17 00:00:00 2001 From: lucas-myx Date: Mon, 15 Jan 2024 15:33:43 +0800 Subject: [PATCH 2/5] feat: merge record and replay --- .github/workflows/build.yml | 13 +- README.md | 35 ++- arex-agent-bootstrap/pom.xml | 1 - .../arex/agent/bootstrap/DecorateControl.java | 23 -- .../bootstrap/InstrumentationHolder.java | 4 +- .../arex/agent/bootstrap/cache/TimeCache.java | 6 +- .../agent/bootstrap/internal/WeakCache.java | 41 ++- .../agent/bootstrap/model/ArexMocker.java | 18 +- .../bootstrap/model/MockCategoryType.java | 33 +-- .../io/arex/agent/bootstrap/model/Mocker.java | 4 + .../model/ParameterizedTypeImpl.java | 4 +- .../arex/agent/bootstrap/util/ArrayUtils.java | 1 + .../agent/bootstrap/util/CollectionUtil.java | 3 + .../arex/agent/bootstrap/util/StringUtil.java | 3 + .../bootstrap/InstrumentationHolderTest.java | 33 +++ .../bootstrap/TraceContextManagerTest.java | 29 ++ .../agent/bootstrap/cache/TimeCacheTest.java | 44 +++ .../bootstrap/ctx/CallableWrapperTest.java | 13 +- .../bootstrap/ctx/RunnableWrapperTest.java | 11 +- .../bootstrap/internal/WeakCacheTest.java | 8 + .../bootstrap/util/CollectionUtilTest.java | 41 ++- .../agent/bootstrap/util/StringUtilTest.java | 6 +- .../InstrumentationInstaller.java | 15 +- arex-agent/pom.xml | 5 + .../java/io/arex/attacher/AgentAttacher.java | 6 +- .../inst/runtime/context/ArexContext.java | 22 +- .../inst/runtime/context/ContextManager.java | 48 ++-- .../context/LatencyContextHashMap.java | 12 +- .../inst/runtime/listener/EventProcessor.java | 1 - .../runtime/match/AbstractMatchStrategy.java | 46 +++ .../runtime/match/AccurateMatchStrategy.java | 64 +++++ .../runtime/match/EigenMatchStrategy.java | 15 + .../runtime/match/FuzzyMatchStrategy.java | 51 ++++ .../runtime/match/MatchStrategyContext.java | 61 ++++ .../runtime/match/MatchStrategyRegister.java | 28 ++ .../inst/runtime/match/ReplayMatcher.java | 45 +++ .../inst/runtime/model/ArexConstants.java | 8 +- .../runtime/model/DynamicClassEntity.java | 3 +- .../io/arex/inst/runtime/model/MergeDTO.java | 153 ++++++++++ .../inst/runtime/model/MergeReplayType.java | 19 ++ .../inst/runtime/model/MergeResultDTO.java | 95 ------- .../MergeRecordDubboRequestHandler.java | 18 +- .../MergeRecordServletRequestHandler.java | 18 +- .../inst/runtime/service/DataCollector.java | 2 + .../inst/runtime/service/DataService.java | 4 + .../arex/inst/runtime/util/CaseManager.java | 26 +- .../arex/inst/runtime/util/IgnoreUtils.java | 8 +- .../runtime/util/MergeRecordReplayUtil.java | 250 +++++++++++++++++ .../inst/runtime/util/MergeSplitUtil.java | 159 ----------- .../io/arex/inst/runtime/util/MockUtils.java | 118 +++----- .../io/arex/inst/runtime/util/TypeUtil.java | 82 +++++- .../inst/runtime/util/sizeof/AgentSizeOf.java | 17 ++ .../util/sizeof/CombinationSizeOfFilter.java | 2 - .../util/sizeof/ObjectGraphWalker.java | 10 +- .../runtime/util/sizeof/SizeOfFilter.java | 2 - .../sizeof/WeakIdentityConcurrentMap.java | 198 ------------- .../matcher/SafeExtendsClassMatcherTest.java | 30 ++ .../runtime/context/ContextManagerTest.java | 6 +- .../context/RepeatedCollectManagerTest.java | 34 +++ .../match/AbstractMatchStrategyTest.java | 42 +++ .../match/AccurateMatchStrategyTest.java | 101 +++++++ .../runtime/match/EigenMatchStrategyTest.java | 26 ++ .../runtime/match/FuzzyMatchStrategyTest.java | 65 +++++ .../match/MatchStrategyContextTest.java | 65 +++++ .../match/MatchStrategyRegisterTest.java | 14 + .../inst/runtime/match/ReplayMatcherTest.java | 60 ++++ .../MergeRecordDubboRequestHandlerTest.java | 18 ++ .../MergeRecordServletRequestHandlerTest.java | 18 ++ .../request/RequestHandlerManagerTest.java | 14 + .../inst/runtime/util/CaseManagerTest.java | 37 ++- .../inst/runtime/util/IgnoreUtilsTest.java | 2 + .../util/MergeRecordReplayUtilTest.java | 180 ++++++++++++ .../arex/inst/runtime/util/MockUtilsTest.java | 45 ++- .../arex/inst/runtime/util/TypeUtilTest.java | 40 ++- .../runtime/util/sizeof/AgentSizeOfTest.java | 62 +++++ .../sizeof/CombinationSizeOfFilterTest.java | 40 +++ .../util/sizeof/ObjectGraphWalkerTest.java | 51 ++++ .../arex/foundation/config/ConfigManager.java | 46 +-- .../serializer/ArexObjectMapper.java | 135 +++++++++ .../foundation/serializer/GsonSerializer.java | 59 ++-- .../serializer/JacksonSerializer.java | 41 ++- .../serializer/ProtoJsonSerializer.java | 40 +-- .../serializer/custom/MultiTypeElement.java | 33 +++ .../serializer/custom/NumberStrategy.java | 23 ++ .../serializer/custom/NumberTypeAdaptor.java | 100 ------- .../foundation/services/ConfigService.java | 19 +- .../services/DataCollectorService.java | 37 ++- .../foundation/config/ConfigManagerTest.java | 5 +- .../serializer/GsonSerializerTest.java | 33 ++- .../serializer/JacksonSerializerTest.java | 51 +++- .../serializer/ProtoJsonSerializerTest.java | 10 +- .../foundation/serializer/TimeTestInfo.java | 20 ++ .../serializer/custom/NumberStrategyTest.java | 58 ++++ .../services/DataCollectorServiceTest.java | 31 ++- .../foundation/services/TimerServiceTest.java | 37 +++ .../io/arex/foundation/util/NetUtilsTest.java | 45 +++ .../util/NumberTypeAdaptorTest.java | 31 --- .../common/arex-common/pom.xml | 23 ++ .../io/arex/inst/common/util/FluxUtil.java | 96 +++++++ .../arex/inst/common/util/FluxUtilTest.java | 55 ++++ .../apollo/ApolloDubboRequestHandler.java | 4 +- .../apollo/ApolloServletV3RequestHandler.java | 4 +- .../apollo/ApolloDubboRequestHandlerTest.java | 4 +- .../ApolloServletV3RequestHandlerTest.java | 4 +- .../database/common/DatabaseExtractor.java | 9 +- .../common/DatabaseExtractorTest.java | 2 +- ...bstractEntityPersisterInstrumentation.java | 12 +- .../hibernate/LoaderInstrumentation.java | 4 +- ...actEntityPersisterInstrumentationTest.java | 4 +- .../database/arex-database-mongo/pom.xml | 2 +- .../arex/inst/database/mongo/MongoHelper.java | 4 +- .../mongo/MongoModuleInstrumentation.java | 10 +- .../QueryBatchCursorInstrumentation.java | 32 +++ .../mongo/WriteOperationInstrumentation.java | 5 +- .../inst/database/mongo/MongoHelperTest.java | 4 +- .../database/arex-database-mybatis3/pom.xml | 2 +- .../mybatis3/ExecutorInstrumentation.java | 12 +- .../database/mybatis3/InternalExecutor.java | 25 +- .../mybatis3/ExecutorInstrumentationTest.java | 11 +- .../mybatis3/InternalExecutorTest.java | 12 +- .../dynamic/arex-cache/pom.xml | 6 + .../cache/arex/ArexMockInstrumentation.java | 10 +- .../cache/spring/SpringCacheAdviceHelper.java | 19 +- .../arex/ArexMockInstrumentationTest.java | 2 +- .../spring/SpringCacheAdviceHelperTest.java | 24 +- .../SpringCacheInstrumentationTest.java | 2 +- .../dynamic/arex-dynamic-common/pom.xml | 17 ++ .../dynamic/common/DynamicClassExtractor.java | 233 ++++++++++------ ...miConstants.java => DynamicConstants.java} | 2 +- .../dynamic/common/ExpressionParseUtil.java | 16 +- .../dynamic/common/listener/FluxConsumer.java | 57 ++++ .../dynamic/common/listener/MonoConsumer.java | 36 +++ .../common/DynamicClassExtractorTest.java | 262 +++++++++++++++--- .../common/ExpressionParseUtilTest.java | 33 ++- .../common/listener/FluxConsumerTest.java | 165 +++++++++++ .../dynamic/arex-dynamic/pom.xml | 6 + .../dynamic/DynamicClassInstrumentation.java | 12 +- .../DynamicClassInstrumentationTest.java | 2 +- ...nternalHttpAsyncClientInstrumentation.java | 30 +- .../common/ApacheHttpClientAdapter.java | 23 +- ...nalHttpAsyncClientInstrumentationTest.java | 23 +- .../common/ApacheHttpClientAdapterTest.java | 31 ++- .../common/HttpClientExtractor.java | 2 +- .../common/HttpResponseWrapper.java | 10 + .../common/HttpClientExtractorTest.java | 4 +- .../httpclient/arex-httpclient-feign/pom.xml | 29 ++ .../httpclient/feign/FeignClientAdapter.java | 109 ++++++++ .../feign/FeignClientInstrumentation.java | 95 +++++++ .../FeignClientModuleInstrumentation.java | 20 ++ .../feign/FeignClientAdapterTest.java | 96 +++++++ .../feign/FeignClientInstrumentationTest.java | 107 +++++++ .../FeignClientModuleInstrumentationTest.java | 14 + .../ForkJoinTaskInstrumentation.java | 2 +- arex-instrumentation/pom.xml | 4 +- .../io/arex/inst/jedis/v2/JedisWrapper.java | 56 ++-- .../arex/inst/jedis/v2/JedisWrapperTest.java | 57 ++++ .../io/arex/inst/jedis/v4/JedisWrapper.java | 5 +- .../arex/inst/jedis/v4/JedisWrapperTest.java | 33 ++- .../inst/redis/common/RedisExtractor.java | 24 +- .../inst/redis/common/RedisExtractorTest.java | 10 +- .../inst/redis/common/RedisKeyUtilTest.java | 60 ++++ .../redisson/v3/RedissonInstrumentation.java | 62 ++--- .../redisson/v3/common/RedissonHelper.java | 79 ++++++ .../v3/wrapper/RedissonBucketWrapper.java | 63 ++--- .../v3/wrapper/RedissonBucketsWrapper.java | 5 +- .../v3/wrapper/RedissonKeysWrapper.java | 3 +- .../v3/wrapper/RedissonListWrapper.java | 49 ++-- .../v3/wrapper/RedissonMapWrapper.java | 55 ++-- .../v3/wrapper/RedissonSetWrapper.java | 51 ++-- .../v3/RedissonInstrumentationTest.java | 19 +- .../v3/common/RedissonHelperTest.java | 84 ++++++ .../inst/httpservlet/ServletAdviceHelper.java | 13 +- .../io/arex/inst/httpservlet/ServletUtil.java | 56 ++++ .../adapter/impl/ServletAdapterImplV3.java | 7 +- .../adapter/impl/ServletAdapterImplV5.java | 7 +- .../wrapper/CachedBodyRequestWrapperV3.java | 18 +- .../wrapper/CachedBodyRequestWrapperV5.java | 17 +- .../httpservlet/ServletAdviceHelperTest.java | 41 ++- .../inst/httpservlet/ServletUtilTest.java | 29 ++ .../impl/ServletAdapterImplV3Test.java | 3 + .../impl/ServletAdapterImplV5Test.java | 3 + .../inst/time/DateTimeInstrumentation.java | 143 +--------- .../inst/time/TimeMachineInstrumentation.java | 6 +- .../TimeMachineModuleInstrumentation.java | 6 +- .../time/DateTimeInstrumentationTest.java | 74 +---- .../inst/time/TimeMachineInterceptorTest.java | 41 +++ .../TimeMachineModuleInstrumentationTest.java | 4 +- .../thirdparty/util/time/DateFormatUtils.java | 16 ++ pom.xml | 17 +- 189 files changed, 5192 insertions(+), 1656 deletions(-) delete mode 100644 arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/DecorateControl.java create mode 100644 arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/InstrumentationHolderTest.java create mode 100644 arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/TraceContextManagerTest.java create mode 100644 arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/cache/TimeCacheTest.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java delete mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java delete mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java delete mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/extension/matcher/SafeExtendsClassMatcherTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/RepeatedCollectManagerTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java create mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ArexObjectMapper.java create mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/MultiTypeElement.java create mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberStrategy.java delete mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberTypeAdaptor.java create mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/NumberStrategyTest.java create mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/TimerServiceTest.java create mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NetUtilsTest.java delete mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NumberTypeAdaptorTest.java create mode 100644 arex-instrumentation/common/arex-common/pom.xml create mode 100644 arex-instrumentation/common/arex-common/src/main/java/io/arex/inst/common/util/FluxUtil.java create mode 100644 arex-instrumentation/common/arex-common/src/test/java/io/arex/inst/common/util/FluxUtilTest.java create mode 100644 arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/QueryBatchCursorInstrumentation.java rename arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/{DynamiConstants.java => DynamicConstants.java} (87%) create mode 100644 arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/FluxConsumer.java create mode 100644 arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/MonoConsumer.java create mode 100644 arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/listener/FluxConsumerTest.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientAdapter.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientInstrumentation.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentation.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientAdapterTest.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientInstrumentationTest.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentationTest.java create mode 100644 arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisKeyUtilTest.java create mode 100644 arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/common/RedissonHelper.java create mode 100644 arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/common/RedissonHelperTest.java create mode 100644 arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineInterceptorTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e496278b..8c8cec5c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,14 +3,19 @@ name: Build and Test -on: [pull_request, workflow_dispatch] +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: jobs: build-source: runs-on: ubuntu-latest strategy: matrix: - java-version: [ 8, 11, 17 ] + java-version: [ 8, 11, 17, 21 ] steps: - uses: actions/checkout@v3 - name: "Set up JDK ${{ matrix.java-version }}" @@ -32,10 +37,10 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: "Set up JDK 17" + - name: "Set up JDK 21" uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: maven - name: Cache SonarCloud packages diff --git a/README.md b/README.md index 8d5867b66..babac5460 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://github.com/arextest/arex-agent-java/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/arextest/arex-agent-java/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/arextest/arex-agent-java/branch/main/graph/badge.svg)](https://app.codecov.io/gh/arextest/arex-agent-java) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=arextest_arex-agent-java&metric=coverage)](https://sonarcloud.io/summary/overall?id=arextest_arex-agent-java) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=arextest_arex-agent-java&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=arextest_arex-agent-java) # Arex Icon AREX @@ -7,7 +7,7 @@ #### An Open Source Testing Framework with Real World Data - [Introduction](#introduction) -- [Installation](#installation) +- [Building](#building) - [Getting Started](#getting-started) - [Contributing](#contributing) - [License](#license) @@ -38,6 +38,7 @@ AREX utilizes the advanced Java technique, Instrument API, and is capable of ins - OkHttp [3.0, 4.11] - Spring WebClient [5.0,) - Spring Template +- Feign [9.0,) #### Redis Client - Jedis [2.10+, 4+] - Redisson [3.0,) @@ -60,24 +61,25 @@ AREX utilizes the advanced Java technique, Instrument API, and is capable of ins - Netty server [3.x, 4.x] #### Config - Apollo Config [1.x, 2.x] -## Installation +## Building -Simply download the latest binary from [github](https://github.com/arextest/arex-agent-java/releases) -or compile it with `mvn clean package -DskipTests` by yourself. +Simply download the latest binary from [releases](https://github.com/arextest/arex-agent-java/releases) +or build the artifacts with the following commands. The build process supports JDK 8 - 21. -There are two agent files provided in the arex-agent-jar folder like below. They must be placed in the same directory. +`mvn clean install -DskipTests` + +The agent jar is in the folder `arex-agent-jar/` after the build process. +There will be two jar files in the folder. ```other arex-agent.jar arex-agent-bootstrap.jar ``` -If you need these jar with version, you can add option: `mvn clean package -DskipTests -Pjar-with-version` -```other -arex-agent-.jar -arex-agent-bootstrap-.jar -``` +If you wanna jar with version, build the artifacts with the following commands. + +`mvn clean install -DskipTests -Pjar-with-version` ## Getting Started @@ -90,17 +92,14 @@ AREX agent works along with the [AREX storage service](https://github.com/arexte You could just configure the host and port of them respectively, like below ```other -java -javaagent:/path/to/arex-agent.jar - -Darex.service.name=your-service-name - -Darex.storage.service.host=[storage.service.host:port](storage.service.host:port) - -jar your-application.jar +java -javaagent:/path/to/arex-agent.jar -Darex.service.name=my-service-name -Darex.storage.service.host= -jar my-application.jar ``` Alternatively, you can put those configuration item in `arex.agent.conf` file, like below ```other -arex.service.name=your-service-name +arex.service.name=my-service-name arex.storage.service.host= ``` @@ -108,9 +107,7 @@ arex.storage.service.host= Then simply run: ```other -java -javaagent:/path/to/arex-agent.jar - -Darex.config.path=/path/to/arex.agent.conf - -jar your-application.jar +java -javaagent:/path/to/arex-agent.jar -Darex.config.path=/path/to/arex.agent.conf -jar my-application.jar ``` diff --git a/arex-agent-bootstrap/pom.xml b/arex-agent-bootstrap/pom.xml index 74b9b9adf..28068d84d 100644 --- a/arex-agent-bootstrap/pom.xml +++ b/arex-agent-bootstrap/pom.xml @@ -16,7 +16,6 @@ net.bytebuddy byte-buddy - 1.12.8 ${project.groupId} diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/DecorateControl.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/DecorateControl.java deleted file mode 100644 index a106d8ccd..000000000 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/DecorateControl.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.arex.agent.bootstrap; - -import java.util.HashMap; - -public final class DecorateControl { - private volatile boolean hasDecorated = false; - private static HashMap, DecorateControl> callMap = new HashMap<>(10); - - private DecorateControl() { - } - - public static DecorateControl forClass(Class clazz) { - return callMap.computeIfAbsent(clazz, c -> new DecorateControl()); - } - - public boolean hasDecorated() { - return hasDecorated; - } - - public void setDecorated() { - this.hasDecorated = true; - } -} diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/InstrumentationHolder.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/InstrumentationHolder.java index 8f993cf65..7b7d9006d 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/InstrumentationHolder.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/InstrumentationHolder.java @@ -3,9 +3,9 @@ import java.lang.instrument.Instrumentation; public class InstrumentationHolder { - private static volatile Instrumentation instrumentation; + private static Instrumentation instrumentation; - private static volatile ClassLoader agentClassLoader; + private static ClassLoader agentClassLoader; public static Instrumentation getInstrumentation() { return instrumentation; diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/cache/TimeCache.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/cache/TimeCache.java index 532e89c20..7e76f3987 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/cache/TimeCache.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/cache/TimeCache.java @@ -25,8 +25,12 @@ public static void put(long value) { CACHE.put(traceId, Pair.of(value, System.nanoTime())); } } + public static void remove() { - String traceId = TraceContextManager.get(); + remove(TraceContextManager.get()); + } + + public static void remove(String traceId) { if (traceId != null) { CACHE.remove(traceId); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java index 92a3e6f14..4e9df7a12 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java @@ -5,15 +5,22 @@ import java.lang.ref.WeakReference; import java.util.concurrent.*; -class WeakCache extends ReferenceQueue implements Cache { +public class WeakCache extends ReferenceQueue implements Cache { final ConcurrentMap, V> target; + final CleanUpTask cleanUpTask; + public WeakCache() { - this(new ConcurrentHashMap<>()); + this(new ConcurrentHashMap<>(), null); + } + + public WeakCache(CleanUpTask cleanUpTask) { + this(new ConcurrentHashMap<>(), cleanUpTask); } - public WeakCache(ConcurrentMap, V> target) { + public WeakCache(ConcurrentMap, V> target, CleanUpTask cleanUpTask) { this.target = target; + this.cleanUpTask = cleanUpTask; } public V get(K key) { @@ -38,20 +45,31 @@ public void clear() { target.clear(); } + public boolean containsKey(K key) { + check(); + return target.containsKey(new WeakReferenceKey<>(key)); + } + void check() { Reference reference; while ((reference = poll()) != null) { - target.remove(reference); + final V value = target.remove(reference); + if (cleanUpTask != null && value != null) { + cleanUpTask.cleanUp(value); + } } } static final class WeakReferenceKey extends WeakReference { private final int hashCode; + WeakReferenceKey(K key) { + this(key, null); + } + WeakReferenceKey(K key, ReferenceQueue queue) { super(key, queue); - - hashCode = System.identityHashCode(key); + hashCode = (key == null) ? 0 : System.identityHashCode(key); } @Override @@ -63,10 +81,17 @@ public int hashCode() { public boolean equals(Object other) { if (other instanceof WeakCache.WeakReferenceKey) { return ((WeakReferenceKey) other).get() == get(); - } else { - return other.equals(this); } + + return other != null && other.equals(this); } } + + public interface CleanUpTask { + /** + * @param object object to cleanup + */ + void cleanUp(T object); + } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java index 450de0922..433667917 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java @@ -1,7 +1,10 @@ package io.arex.agent.bootstrap.model; -public class ArexMocker implements Mocker { +import java.util.HashMap; +import java.util.Map; +public class ArexMocker implements Mocker { + public static final Map TAGS = new HashMap<>(); private String id; private MockCategoryType categoryType; private String replayId; @@ -12,6 +15,7 @@ public class ArexMocker implements Mocker { private long creationTime; private Mocker.Target targetRequest; private Mocker.Target targetResponse; + private boolean merge; private String operationName; public ArexMocker() { @@ -21,6 +25,10 @@ public ArexMocker(MockCategoryType categoryType) { this.categoryType = categoryType; } + public Map getTags() { + return TAGS; + } + public String getId() { return this.id; } @@ -109,4 +117,12 @@ public void setTargetResponse(Mocker.Target targetResponse) { public void setOperationName(String operationName) { this.operationName = operationName; } + + public boolean isMerge() { + return merge; + } + + public void setMerge(boolean merge) { + this.merge = merge; + } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java index 03aa6907a..2e14963f4 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java @@ -12,7 +12,7 @@ public class MockCategoryType implements Serializable { public static final MockCategoryType DATABASE = createDependency("Database"); public static final MockCategoryType HTTP_CLIENT = createDependency("HttpClient"); public static final MockCategoryType CONFIG_FILE = createSkipComparison("ConfigFile"); - public static final MockCategoryType DYNAMIC_CLASS = createMergeSkipComparison("DynamicClass"); + public static final MockCategoryType DYNAMIC_CLASS = createSkipComparison("DynamicClass"); public static final MockCategoryType REDIS = createSkipComparison("Redis"); public static final MockCategoryType MESSAGE_PRODUCER = createDependency("QMessageProducer"); public static final MockCategoryType MESSAGE_CONSUMER = createEntryPoint("QMessageConsumer"); @@ -23,7 +23,7 @@ public class MockCategoryType implements Serializable { private String name; private boolean entryPoint; private boolean skipComparison; - private boolean mergeRecord; + public static MockCategoryType createEntryPoint(String name) { return create(name, true, false); } @@ -32,28 +32,19 @@ public static MockCategoryType createSkipComparison(String name) { return create(name, false, true); } - public static MockCategoryType createMergeSkipComparison(String name) { - return CATEGORY_TYPE_MAP.computeIfAbsent(name, - key -> new MockCategoryType(name, false, true, true)); - } - public static MockCategoryType createDependency(String name) { return create(name, false, false); } public static MockCategoryType create(String name, boolean entryPoint, boolean skipComparison) { return CATEGORY_TYPE_MAP.computeIfAbsent(name, - key -> new MockCategoryType(name, entryPoint, skipComparison)); + key -> new MockCategoryType(name, entryPoint, skipComparison)); } public static Collection values() { return CATEGORY_TYPE_MAP.values(); } - public static MockCategoryType of(String name) { - return CATEGORY_TYPE_MAP.get(name); - } - public String getName() { return this.name; } @@ -78,26 +69,17 @@ public void setSkipComparison(boolean skipComparison) { this.skipComparison = skipComparison; } - public boolean isMergeRecord() { - return mergeRecord; - } - - public void setMergeRecord(boolean mergeRecord) { - this.mergeRecord = mergeRecord; - } - public MockCategoryType() { } private MockCategoryType(String name, boolean entryPoint, boolean skipComparison) { - this(name, entryPoint, skipComparison, false); - } - - private MockCategoryType(String name, boolean entryPoint, boolean skipComparison, boolean mergeRecord) { this.name = name; this.entryPoint = entryPoint; this.skipComparison = skipComparison; - this.mergeRecord = mergeRecord; + } + + public static MockCategoryType of(String name) { + return CATEGORY_TYPE_MAP.get(name); } @Override @@ -106,7 +88,6 @@ public String toString() { builder.append("name='").append(name).append('\''); builder.append(", entryPoint=").append(entryPoint); builder.append(", skipComparison=").append(skipComparison); - builder.append(", mergeRecord=").append(mergeRecord); builder.append('}'); return builder.toString(); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java index 7162d5447..88fe3dc05 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java @@ -116,4 +116,8 @@ default String recordLogTitle() { default String replayLogTitle() { return "replay." + getCategoryType().getName(); } + + boolean isMerge(); + + void setMerge(boolean merge); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ParameterizedTypeImpl.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ParameterizedTypeImpl.java index c7eae422e..128653370 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ParameterizedTypeImpl.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ParameterizedTypeImpl.java @@ -207,7 +207,9 @@ public String toString() { for(Type t: actualTypeArguments) { if (!first) sb.append(", "); - sb.append(t.getTypeName()); + if (t != null) { + sb.append(t.getTypeName()); + } first = false; } sb.append(">"); diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java index d7d3038ea..f1ba40b83 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java @@ -3,6 +3,7 @@ import java.util.function.Function; public class ArrayUtils { + public static final String[] EMPTY_STRING_ARRAY = new String[0]; private ArrayUtils() {} public static byte[] addAll(final byte[] array1, final byte... array2) { diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java index 9104925d6..81df64f5c 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java @@ -66,6 +66,9 @@ public static List> split(List originalList, int splitCount) { public static List filterNull(List originalList) { List filterList = new ArrayList<>(); + if (isEmpty(originalList)) { + return filterList; + } for (V element : originalList) { if (element != null) { filterList.add(element); diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java index 5d7d60f74..5d67425a9 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/StringUtil.java @@ -352,6 +352,9 @@ public static boolean containsIgnoreCase(final CharSequence str, final CharSeque } public static boolean startWith(String source, String prefix) { + if (source == null || prefix == null) { + return source == null && prefix == null; + } return startWithFrom(source, prefix, 0); } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/InstrumentationHolderTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/InstrumentationHolderTest.java new file mode 100644 index 000000000..40a46ba8c --- /dev/null +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/InstrumentationHolderTest.java @@ -0,0 +1,33 @@ +package io.arex.agent.bootstrap; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.instrument.Instrumentation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * @since 2024/1/15 + */ +@ExtendWith(MockitoExtension.class) +class InstrumentationHolderTest { + + @Mock + Instrumentation instrumentation; + @Mock + ClassLoader agentClassLoader; + + @Test + void getInstrumentation() { + InstrumentationHolder.setInstrumentation(instrumentation); + assertEquals(instrumentation, InstrumentationHolder.getInstrumentation()); + } + + @Test + void getAgentClassLoader() { + InstrumentationHolder.setAgentClassLoader(agentClassLoader); + assertEquals(agentClassLoader, InstrumentationHolder.getAgentClassLoader()); + } +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/TraceContextManagerTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/TraceContextManagerTest.java new file mode 100644 index 000000000..c40411ec8 --- /dev/null +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/TraceContextManagerTest.java @@ -0,0 +1,29 @@ +package io.arex.agent.bootstrap; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * @since 2024/1/15 + */ +class TraceContextManagerTest { + + @Test + void test() { + TraceContextManager.init("test-ip"); + String get1 = TraceContextManager.get(true); + String get2 = TraceContextManager.get(); + assertEquals(get1, get2); + + TraceContextManager.set(get2 + "-1"); + String get3 = TraceContextManager.get(); + assertEquals(get2 + "-1", get3); + + String get4 = TraceContextManager.remove(); + assertEquals(get3, get4); + + String get5 = TraceContextManager.generateId(); + assertTrue(get5.startsWith("AREX-test-ip-")); + } +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/cache/TimeCacheTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/cache/TimeCacheTest.java new file mode 100644 index 000000000..aab9a21cc --- /dev/null +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/cache/TimeCacheTest.java @@ -0,0 +1,44 @@ +package io.arex.agent.bootstrap.cache; + +import io.arex.agent.bootstrap.TraceContextManager; +import io.arex.agent.bootstrap.internal.Pair; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +class TimeCacheTest { + @BeforeAll + static void setUp() { + Mockito.mockStatic(TraceContextManager.class); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void getAndPutAndRemove() { + // traceId is null + Mockito.when(TraceContextManager.get()).thenReturn(null); + assertEquals(0, TimeCache.get()); + TimeCache.put(1L); + assertEquals(0, TimeCache.get()); + + // traceId is not null + Mockito.when(TraceContextManager.get()).thenReturn("test"); + assertEquals(0, TimeCache.get()); + TimeCache.put(1L); + assertNotEquals(0, TimeCache.get()); + + // remove + TimeCache.remove(); + assertEquals(0, TimeCache.get()); + } +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/CallableWrapperTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/CallableWrapperTest.java index af00ce088..93c7505dd 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/CallableWrapperTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/CallableWrapperTest.java @@ -12,11 +12,16 @@ class CallableWrapperTest { @Test - void get() { + void get() throws Exception { assertNull(CallableWrapper.get(null)); TraceContextManager.set("mock"); - assertNotNull(CallableWrapper.get(new CallableTest<>())); - assertNotNull(CallableWrapper.get(() -> "mock")); + Callable objectCallable = CallableWrapper.get(new CallableTest<>()); + assertNotNull(objectCallable); + Callable stringCallable = CallableWrapper.get(() -> "mock"); + assertEquals("mock", stringCallable.call()); + assertNotNull(stringCallable.toString()); + assertTrue(stringCallable.hashCode() > 0); + assertFalse(stringCallable.equals(objectCallable)); TraceContextManager.remove(); } @@ -26,4 +31,4 @@ public final void setRawResult(T v) {} public final boolean exec() { return true; } public final T call() { return null; } } -} \ No newline at end of file +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/RunnableWrapperTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/RunnableWrapperTest.java index 3fa287eb9..6c55068ea 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/RunnableWrapperTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/ctx/RunnableWrapperTest.java @@ -14,8 +14,13 @@ class RunnableWrapperTest { void get() { assertNull(RunnableWrapper.get(null)); TraceContextManager.set("mock"); - assertNotNull(RunnableWrapper.get(new RunnableTest<>())); - assertNotNull(RunnableWrapper.get(() -> {})); + Runnable objectRunnable = RunnableWrapper.get(new RunnableTest<>()); + assertNotNull(objectRunnable); + Runnable emptyRunnable = RunnableWrapper.get(() -> {}); + assertDoesNotThrow(emptyRunnable::run); + assertNotNull(emptyRunnable.toString()); + assertTrue(emptyRunnable.hashCode() > 0); + assertFalse(emptyRunnable.equals(objectRunnable)); TraceContextManager.remove(); } @@ -25,4 +30,4 @@ public final void setRawResult(T v) {} public final boolean exec() { return true; } public final void run() {} } -} \ No newline at end of file +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/internal/WeakCacheTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/internal/WeakCacheTest.java index b94f31087..8fe070dbd 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/internal/WeakCacheTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/internal/WeakCacheTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.lang.ref.ReferenceQueue; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; @@ -56,4 +57,11 @@ void testNormalKeyValue() throws InterruptedException { // check -> remove after gc assertFalse(Cache.CAPTURED_CACHE.contains(null)); } + + @Test + void testWeakReferenceKeyEqualsReturnsFalse() { + WeakCache.WeakReferenceKey key = new WeakCache.WeakReferenceKey<>("test", new ReferenceQueue<>()); + assertFalse(key.equals(null)); + assertFalse(key.equals("test")); + } } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java index a011ab84b..e45e4ce43 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java @@ -1,11 +1,17 @@ package io.arex.agent.bootstrap.util; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.*; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class CollectionUtilTest { @@ -33,4 +39,33 @@ void newArrayList() { actualResult = CollectionUtil.newArrayList("test"); assertInstanceOf(ArrayList.class, actualResult); } + + @ParameterizedTest + @MethodSource("splitCase") + void split(List originalList, int splitCount, Predicate>> predicate) { + assertTrue(predicate.test(CollectionUtil.split(originalList, splitCount))); + } + + static Stream splitCase() { + Supplier> lessSplitCountList = () -> CollectionUtil.newArrayList("mock"); + Supplier> normalSplitCountList = () -> CollectionUtil.newArrayList("mock1", "mock2"); + + Predicate>> empty = CollectionUtil::isEmpty; + Predicate>> notEmpty = CollectionUtil::isNotEmpty; + + return Stream.of( + arguments(null, 1, empty), + arguments(lessSplitCountList.get(), 2, notEmpty), + arguments(normalSplitCountList.get(), 2, notEmpty) + ); + } + + @Test + void filterNull() { + List actualResult = CollectionUtil.filterNull(null); + assertEquals(0, actualResult.size()); + + actualResult = CollectionUtil.filterNull(CollectionUtil.newArrayList("mock")); + assertEquals(1, actualResult.size()); + } } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java index 7e9bfc071..03badeca7 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/StringUtilTest.java @@ -314,5 +314,9 @@ void isNullWord() { void startWith() { assertTrue(StringUtil.startWith("mock", "m")); assertFalse(StringUtil.startWith("mock", "o")); + assertFalse(StringUtil.startWith(null, "M")); + assertFalse(StringUtil.startWith("mock", null)); + assertTrue(StringUtil.startWith(null, null)); + assertFalse(StringUtil.startWith("", "M")); } -} \ No newline at end of file +} diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/InstrumentationInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/InstrumentationInstaller.java index 9a2587ecb..cbce599b7 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/InstrumentationInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/InstrumentationInstaller.java @@ -13,6 +13,8 @@ import io.arex.inst.runtime.model.DynamicClassStatusEnum; import io.arex.agent.bootstrap.util.ServiceLoader; import java.util.stream.Collectors; + +import io.arex.inst.runtime.util.IgnoreUtils; import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.agent.builder.ResettableClassFileTransformer; @@ -61,7 +63,7 @@ private ResettableClassFileTransformer retransform() { LOGGER.info("[AREX] No Change in dynamic class config, no need to retransform."); return resettableClassFileTransformer; } - + IgnoreUtils.clearInvalidOperation(); instrumentation.removeTransformer(resettableClassFileTransformer); resettableClassFileTransformer = install(getAgentBuilder(), true); LOGGER.info("[AREX] Agent retransform successfully."); @@ -73,7 +75,7 @@ private void resetClass() { if (CollectionUtil.isEmpty(resetClassSet)) { return; } - + IgnoreUtils.clearInvalidOperation(); instrumentation.removeTransformer(resettableClassFileTransformer); // TODO: optimize reset abstract class for (Class clazz : this.instrumentation.getAllLoadedClasses()) { @@ -197,11 +199,14 @@ private void createDumpDirectory() { try { File bytecodeDumpPath = new File(agentFile.getParent(), BYTECODE_DUMP_DIR); - if (!bytecodeDumpPath.exists()) { - bytecodeDumpPath.mkdir(); - } else { + boolean exists = bytecodeDumpPath.exists(); + boolean mkdir = false; + if (exists) { FileUtils.cleanDirectory(bytecodeDumpPath); + } else { + mkdir = bytecodeDumpPath.mkdir(); } + LOGGER.info("[arex] bytecode dump path exists: {}, mkdir: {}, path: {}", exists, mkdir, bytecodeDumpPath.getPath()); System.setProperty(TypeWriter.DUMP_PROPERTY, bytecodeDumpPath.getPath()); } catch (Exception e) { LOGGER.info("[arex] Failed to create directory to instrumented bytecode: {}", e.getMessage()); diff --git a/arex-agent/pom.xml b/arex-agent/pom.xml index d47655875..a3eb4d433 100644 --- a/arex-agent/pom.xml +++ b/arex-agent/pom.xml @@ -131,6 +131,11 @@ arex-httpclient-okhttp-v3 ${project.version} + + ${project.groupId} + arex-httpclient-feign + ${project.version} + ${project.groupId} arex-netty-v3 diff --git a/arex-attacher/src/main/java/io/arex/attacher/AgentAttacher.java b/arex-attacher/src/main/java/io/arex/attacher/AgentAttacher.java index cfc0b1232..fc0687ec3 100644 --- a/arex-attacher/src/main/java/io/arex/attacher/AgentAttacher.java +++ b/arex-attacher/src/main/java/io/arex/attacher/AgentAttacher.java @@ -4,6 +4,7 @@ import com.sun.tools.attach.VirtualMachine; import java.io.IOException; +import java.util.Arrays; /** * agent attacher @@ -52,7 +53,8 @@ public static void main(String[] args) { virtualMachine.detach(); } catch (Throwable e) { // expected behavior, it will be returned as error stream to the caller, if any - e.printStackTrace(); + System.err.printf("agent attach failed, pid: %s, args: %s, error: %s%n", + pid, Arrays.toString(args), e.getMessage()); } } -} \ No newline at end of file +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java index 241f936a7..d9ed4238a 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java @@ -3,7 +3,8 @@ import io.arex.agent.bootstrap.util.ConcurrentHashSet; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.model.MergeResultDTO; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -17,16 +18,15 @@ public class ArexContext { private final long createTime; private final AtomicInteger sequence; private Set methodSignatureHashList; - private Map cachedReplayResultMap; + private Map> cachedReplayResultMap; private Map> excludeMockTemplate; private Map attachments = null; - private LinkedBlockingQueue mergeRecordQueue; + private LinkedBlockingQueue mergeRecordQueue; private boolean isRedirectRequest; private boolean isInvalidCase; - private boolean isMainEntryEnd; public static ArexContext of(String caseId) { return of(caseId, null); @@ -74,7 +74,7 @@ public Set getMethodSignatureHashList() { return methodSignatureHashList; } - public Map getCachedReplayResultMap() { + public Map> getCachedReplayResultMap() { if (cachedReplayResultMap == null) { cachedReplayResultMap = new ConcurrentHashMap<>(); } @@ -140,21 +140,13 @@ public boolean isRedirectRequest(String referer) { return isRedirectRequest; } - public LinkedBlockingQueue getMergeRecordQueue() { + public LinkedBlockingQueue getMergeRecordQueue() { if (mergeRecordQueue == null) { mergeRecordQueue = new LinkedBlockingQueue<>(2048); } return mergeRecordQueue; } - public boolean isMainEntryEnd() { - return isMainEntryEnd; - } - - public void setMainEntryEnd(boolean mainEntryEnd) { - isMainEntryEnd = mainEntryEnd; - } - public void clear() { if (methodSignatureHashList != null) { methodSignatureHashList.clear(); @@ -169,6 +161,8 @@ public void clear() { attachments.clear(); } if (mergeRecordQueue != null) { + // async thread merge record (main entry has ended) + MergeRecordReplayUtil.recordRemain(this); mergeRecordQueue.clear(); } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java index 64e1f0c49..55a5368ed 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java @@ -1,6 +1,7 @@ package io.arex.inst.runtime.context; import io.arex.agent.bootstrap.TraceContextManager; +import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.listener.ContextListener; @@ -21,34 +22,37 @@ public static ArexContext currentContext() { /** * agent will call this method + * record scene: recordId is map key + * replay scene: replayId is map key */ - public static ArexContext currentContext(boolean createIfAbsent, String caseId) { - // replay scene - if (StringUtil.isNotEmpty(caseId)) { - TraceContextManager.set(caseId); - ArexContext context = ArexContext.of(caseId, TraceContextManager.generateId()); - publish(context, true); - // Each replay init generates the latest context(maybe exist previous recorded context) - RECORD_MAP.put(caseId, context); - return context; - } - - // record scene - caseId = TraceContextManager.get(createIfAbsent); - if (StringUtil.isEmpty(caseId)) { + public static ArexContext currentContext(boolean createIfAbsent, String recordId) { + String traceId = TraceContextManager.get(createIfAbsent); + if (StringUtil.isEmpty(traceId)) { return null; } - // first init execute if (createIfAbsent) { - ArexContext context = ArexContext.of(caseId); - publish(context, true); - return RECORD_MAP.put(caseId, context); + final ArexContext arexContext = createContext(recordId, traceId); + publish(arexContext, true); + RECORD_MAP.put(traceId, arexContext); + return arexContext; + } + return RECORD_MAP.get(traceId); + } + + /** + * ArexContext.of(recordId, replayId) + */ + private static ArexContext createContext(String recordId, String traceId) { + // replay scene: traceId is replayId + if (StringUtil.isNotEmpty(recordId)) { + return ArexContext.of(recordId, traceId); } - return RECORD_MAP.get(caseId); + // record scene: traceId is recordId + return ArexContext.of(traceId); } - public static ArexContext getRecordContext(String recordId) { - return RECORD_MAP.get(recordId); + public static ArexContext getContext(String traceId) { + return RECORD_MAP.get(traceId); } public static boolean needRecord() { @@ -79,7 +83,7 @@ public static void registerListener(ContextListener listener) { } private static void publish(ArexContext context, boolean isCreate) { - if (LISTENERS.size() > 0) { + if (CollectionUtil.isNotEmpty(LISTENERS)) { LISTENERS.forEach(listener -> { if (isCreate) { listener.onCreate(context); diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java index 683e492f6..4a6de3e53 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/LatencyContextHashMap.java @@ -1,5 +1,7 @@ package io.arex.inst.runtime.context; +import io.arex.agent.bootstrap.cache.TimeCache; + import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -28,11 +30,16 @@ public ArexContext remove(Object key) { if (key == null) { return null; } - ArexContext context = super.remove(key); - overdueCleanUp(); + ArexContext context = super.get(key); if (latencyMap != null && context != null) { latencyMap.put(String.valueOf(key), context); } + // todo: time put into ArexContext + if (latencyMap == null) { + TimeCache.remove(String.valueOf(key)); + } + super.remove(key); + overdueCleanUp(); return context; } @@ -54,6 +61,7 @@ private void overdueCleanUp() { // clear context attachments entry.getValue().clear(); latencyMap.remove(entry.getKey()); + TimeCache.remove(entry.getKey()); } } } finally { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java index 9e442128f..200401314 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/EventProcessor.java @@ -89,7 +89,6 @@ private static void initClock(){ } public static void onExit(){ - TimeCache.remove(); ContextManager.remove(); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java new file mode 100644 index 000000000..0560d1b01 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java @@ -0,0 +1,46 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; + +public abstract class AbstractMatchStrategy { + static final String MATCH_TITLE = "replay.match.fail"; + static final int ACCURATE_MATCH_ORDER = 10; + static final int FUZZY_MATCH_ORDER = 20; + static final int EIGEN_MATCH_ORDER = 30; + + public void match(MatchStrategyContext context) { + try { + if (check(context)) { + process(context); + } + } catch (Exception e) { + LogManager.warn(MATCH_TITLE, e); + } + } + + private boolean check(MatchStrategyContext context) { + if (context == null || context.getRequestMocker() == null || context.isInterrupt()) { + return false; + } + return valid(context); + } + + boolean valid(MatchStrategyContext context) { + return true; + } + abstract int order(); + abstract void process(MatchStrategyContext context) throws Exception; + + Mocker buildMatchedMocker(Mocker requestMocker, MergeDTO mergeReplayDTO) { + if (mergeReplayDTO == null) { + return null; + } + requestMocker.getTargetResponse().setBody(mergeReplayDTO.getResponse()); + requestMocker.getTargetResponse().setType(mergeReplayDTO.getResponseType()); + requestMocker.getTargetResponse().setAttributes(mergeReplayDTO.getResponseAttributes()); + mergeReplayDTO.setMatched(true); + return requestMocker; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java new file mode 100644 index 000000000..030c1d479 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java @@ -0,0 +1,64 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; + +import java.util.ArrayList; +import java.util.List; + +public class AccurateMatchStrategy extends AbstractMatchStrategy{ + private static final String ACCURATE_MATCH_TITLE = "replay.match.accurate"; + /** + * search by operationName + requestBody + */ + void process(MatchStrategyContext context) { + Mocker requestMocker = context.getRequestMocker(); + List mergeReplayList = context.getMergeReplayList(); + int methodSignatureHash = MockUtils.methodSignatureHash(requestMocker); + List matchedList = new ArrayList<>(); + for (MergeDTO mergeDTO : mergeReplayList) { + if (methodSignatureHash == mergeDTO.getMethodSignatureHash()) { + matchedList.add(mergeDTO); + } + } + int matchedCount = matchedList.size(); + /* + * 1. unmatched + * 2. matched but find last mode (like dynamicClass) + */ + if (matchedCount == 1) { + if (!matchedList.get(0).isMatched() || MockStrategyEnum.FIND_LAST == context.getMockStrategy()) { + context.setMatchMocker(buildMatchedMocker(requestMocker, matchedList.get(0))); + } else { + LogManager.info(ACCURATE_MATCH_TITLE, StringUtil.format("accurate match one result, but cannot be used, " + + "reason: matched: %s, mock strategy: %s, methodSignatureHash: %s, category: %s", + Boolean.toString(matchedList.get(0).isMatched()), context.getMockStrategy().name(), + String.valueOf(methodSignatureHash), requestMocker.getCategoryType().getName())); + } + // other modes can only be matched once, so interrupt and not continue next fuzzy match + context.setInterrupt(true); + } + // matched multiple result(like as redis: incr、decr) only retain matched item for next fuzzy match + if (matchedCount > 1) { + context.setMergeReplayList(matchedList); + } + // if strict match mode and not matched, interrupt and not continue next fuzzy match + if (matchedCount == 0 && MockStrategyEnum.STRICT_MATCH == context.getMockStrategy()) { + context.setInterrupt(true); + } + } + + @Override + boolean valid(MatchStrategyContext context) { + // if no request params, do next fuzzy match directly + return StringUtil.isNotEmpty(context.getRequestMocker().getTargetRequest().getBody()); + } + + int order() { + return ACCURATE_MATCH_ORDER; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java new file mode 100644 index 000000000..8bb846aa2 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java @@ -0,0 +1,15 @@ +package io.arex.inst.runtime.match; + +public class EigenMatchStrategy extends AbstractMatchStrategy{ + + /** + * search by eigen value of request + */ + void process(MatchStrategyContext context) { + // to be implemented after database merge replay support + } + + int order() { + return EIGEN_MATCH_ORDER; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java new file mode 100644 index 000000000..6f3b901a6 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java @@ -0,0 +1,51 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; + +import java.util.List; + +public class FuzzyMatchStrategy extends AbstractMatchStrategy { + private static final String FUZZY_MATCH_TITLE = "replay.match.fuzzy"; + + /** + * search under the same method signature + * @return unmatched or last one + */ + void process(MatchStrategyContext context) { + Mocker requestMocker = context.getRequestMocker(); + List mergeReplayList = context.getMergeReplayList(); + MergeDTO matchedDTO = null; + int size = mergeReplayList.size(); + for (int i = 0; i < size; i++) { + MergeDTO mergeReplayDTO = mergeReplayList.get(i); + if (!mergeReplayDTO.isMatched()) { + matchedDTO = mergeReplayDTO; + break; + } + } + if (matchedDTO == null && MockStrategyEnum.FIND_LAST == context.getMockStrategy()) { + matchedDTO = mergeReplayList.get(size - 1); + } + if (Config.get().isEnableDebug()) { + String response = matchedDTO != null ? matchedDTO.getResponse() : StringUtil.EMPTY; + LogManager.info(FUZZY_MATCH_TITLE, StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), requestMocker.getTargetRequest().getBody(), response)); + } + context.setMatchMocker(buildMatchedMocker(requestMocker, matchedDTO)); + } + + @Override + boolean valid(MatchStrategyContext context) { + return CollectionUtil.isNotEmpty(context.getMergeReplayList()); + } + + int order() { + return FUZZY_MATCH_ORDER; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java new file mode 100644 index 000000000..ce90477e5 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java @@ -0,0 +1,61 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; + +import java.util.List; + +public class MatchStrategyContext { + private Mocker requestMocker; + private List mergeReplayList; + private MockStrategyEnum mockStrategy; + private boolean interrupt; + private Mocker matchMocker; + + public MatchStrategyContext(Mocker requestMocker, List mergeReplayList, MockStrategyEnum mockStrategy) { + this.requestMocker = requestMocker; + this.mergeReplayList = mergeReplayList; + this.mockStrategy = mockStrategy; + } + + public Mocker getRequestMocker() { + return requestMocker; + } + + public void setRequestMocker(Mocker requestMocker) { + this.requestMocker = requestMocker; + } + + public List getMergeReplayList() { + return mergeReplayList; + } + + public void setMergeReplayList(List mergeReplayList) { + this.mergeReplayList = mergeReplayList; + } + + public MockStrategyEnum getMockStrategy() { + return mockStrategy; + } + + public void setMockStrategy(MockStrategyEnum mockStrategy) { + this.mockStrategy = mockStrategy; + } + + public boolean isInterrupt() { + return interrupt; + } + + public void setInterrupt(boolean interrupt) { + this.interrupt = interrupt; + } + + public Mocker getMatchMocker() { + return matchMocker; + } + + public void setMatchMocker(Mocker matchMocker) { + this.matchMocker = matchMocker; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java new file mode 100644 index 000000000..aa77ebd70 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java @@ -0,0 +1,28 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.util.CollectionUtil; + +import java.util.*; + +public class MatchStrategyRegister { + private static final AbstractMatchStrategy ACCURATE_STRATEGY = new AccurateMatchStrategy(); + private static final AbstractMatchStrategy FUZZY_STRATEGY = new FuzzyMatchStrategy(); + private static final AbstractMatchStrategy EIGEN_STRATEGY = new EigenMatchStrategy(); + private static final Map> MATCH_STRATEGIES = register(); + + private MatchStrategyRegister() { + } + + public static List getMatchStrategies(MockCategoryType categoryType) { + return MATCH_STRATEGIES.get(categoryType.getName()); + } + + private static Map> register() { + Map> strategyMap = new HashMap<>(); + strategyMap.put(MockCategoryType.DYNAMIC_CLASS.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, FUZZY_STRATEGY)); + strategyMap.put(MockCategoryType.REDIS.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, FUZZY_STRATEGY)); + strategyMap.put(MockCategoryType.DATABASE.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, EIGEN_STRATEGY)); + return strategyMap; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java new file mode 100644 index 000000000..0ab057d50 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java @@ -0,0 +1,45 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; + +import java.util.*; + +public class ReplayMatcher { + private static final String MATCH_TITLE = "replay.match"; + + private ReplayMatcher() { + } + + public static Mocker match(Mocker requestMocker, MockStrategyEnum mockStrategy) { + Map> cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); + // first match methodRequestTypeHash: category + operationName + requestType, ensure the same method + List mergeReplayList = cachedReplayResultMap.get(MockUtils.methodRequestTypeHash(requestMocker)); + if (CollectionUtil.isEmpty(mergeReplayList)) { + return null; + } + + List matchStrategyList = MatchStrategyRegister.getMatchStrategies(requestMocker.getCategoryType()); + MatchStrategyContext context = new MatchStrategyContext(requestMocker, mergeReplayList, mockStrategy); + for (AbstractMatchStrategy matchStrategy : matchStrategyList) { + matchStrategy.match(context); + } + + Mocker matchedMocker = context.getMatchMocker(); + if (Config.get().isEnableDebug()) { + String response = matchedMocker != null && matchedMocker.getTargetResponse() != null + ? matchedMocker.getTargetResponse().getBody() : StringUtil.EMPTY; + LogManager.info(MATCH_TITLE, StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), requestMocker.getTargetRequest().getBody(), response)); + } + return matchedMocker; + } + +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java index 80f17a496..98420e458 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java @@ -37,14 +37,14 @@ private ArexConstants() {} public static final String CONFIG_VERSION = "configBatchNo"; public static final String SKIP_FLAG = "arex-skip-flag"; public static final String ORIGINAL_REQUEST = "arex-original-request"; - public static final String MERGE_RECORD_ENABLE = "arex.merge.record.enable"; public static final String MERGE_RECORD_NAME = "arex.mergeRecord"; public static final String MERGE_RECORD_THRESHOLD = "arex.merge.record.threshold"; public static final String MERGE_REPLAY_THRESHOLD = "arex.merge.replay.threshold"; public static final int MERGE_RECORD_THRESHOLD_DEFAULT = 10; public static final int MERGE_REPLAY_THRESHOLD_DEFAULT = 1000; - public static final String MERGE_RESULT_TYPE = "java.util.ArrayList-io.arex.inst.runtime.model.MergeResultDTO"; - public static final String MERGE_MEMORY_CHECK = "arex.merge.check.memory"; + public static final String MERGE_TYPE = "java.util.ArrayList-io.arex.inst.runtime.model.MergeDTO"; public static final String MERGE_SPLIT_COUNT = "arex.merge.split.count"; - public static final String MERGE_RECORD_KEY = "mergeRecord"; + public static final long MEMORY_SIZE_1MB = 1024L * 1024L; + public static final long MEMORY_SIZE_5MB = 5 * 1024L * 1024L; + public static final String CALL_REPLAY_MAX = "callReplayMax"; } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/DynamicClassEntity.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/DynamicClassEntity.java index 70c2c7f34..377f1541e 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/DynamicClassEntity.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/DynamicClassEntity.java @@ -68,7 +68,8 @@ private boolean isReplaceMethodSignature(String additionalSignature) { ArexConstants.CURRENT_TIME_MILLIS_SIGNATURE.equals(additionalSignature) || ArexConstants.NEXT_INT_SIGNATURE.equals(additionalSignature) || // Compatible with $1.getVal() - additionalSignature.contains("$"); + additionalSignature.contains("$") || + additionalSignature.contains("#"); } public String getSignature() { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java new file mode 100644 index 000000000..5685b18f6 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java @@ -0,0 +1,153 @@ +package io.arex.inst.runtime.model; + +import java.util.Map; +import java.util.Objects; + +public class MergeDTO { + private String category; + private int methodSignatureHash; + private String operationName; + private String request; + private String recordId; + private Map requestAttributes; + private Map responseAttributes; + private int methodRequestTypeHash; + private long creationTime; + private boolean matched; + private String response; + private String responseType; + public MergeDTO() {} + private MergeDTO(String category, int methodSignatureHash, String operationName, String request, String response, String responseType, + Map requestAttributes, Map responseAttributes, String recordId) { + this.category = category; + this.methodSignatureHash = methodSignatureHash; + this.operationName = operationName; + this.request = request; + this.response = response; + this.responseType = responseType; + this.requestAttributes = requestAttributes; + this.responseAttributes = responseAttributes; + this.recordId = recordId; + } + + public static MergeDTO of(String category, int methodSignatureHash, String operationName, String request, String response, String responseType, + Map requestAttributes, Map responseAttributes, String recordId) { + return new MergeDTO(category, methodSignatureHash, operationName, request, response, responseType, requestAttributes, responseAttributes, recordId); + } + + public String getCategory() { + return category; + } + public void setCategory(String category) { + this.category = category; + } + public int getMethodSignatureHash() { + return methodSignatureHash; + } + public void setMethodSignatureHash(int methodSignatureHash) { + this.methodSignatureHash = methodSignatureHash; + } + public String getOperationName() { + return operationName; + } + public void setOperationName(String operationName) { + this.operationName = operationName; + } + public String getRequest() { + return request; + } + public void setRequest(String request) { + this.request = request; + } + public String getRecordId() { + return recordId; + } + public void setRecordId(String recordId) { + this.recordId = recordId; + } + public Map getRequestAttributes() { + return requestAttributes; + } + public void setRequestAttributes(Map requestAttributes) { + this.requestAttributes = requestAttributes; + } + public Map getResponseAttributes() { + return responseAttributes; + } + public void setResponseAttributes(Map responseAttributes) { + this.responseAttributes = responseAttributes; + } + + public int getMethodRequestTypeHash() { + return methodRequestTypeHash; + } + + public void setMethodRequestTypeHash(int methodRequestTypeHash) { + this.methodRequestTypeHash = methodRequestTypeHash; + } + + public long getCreationTime() { + return creationTime; + } + + public void setCreationTime(long creationTime) { + this.creationTime = creationTime; + } + + public boolean isMatched() { + return matched; + } + + public void setMatched(boolean matched) { + this.matched = matched; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public String getResponseType() { + return responseType; + } + + public void setResponseType(String responseType) { + this.responseType = responseType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MergeDTO mergeDTO = (MergeDTO) o; + + return methodSignatureHash == mergeDTO.methodSignatureHash; + } + + @Override + public int hashCode() { + return methodSignatureHash; + } + + @Override + public String toString() { + return "MergeDTO{" + + "category='" + category + '\'' + + ", methodSignatureHash=" + methodSignatureHash + + ", operationName='" + operationName + '\'' + + ", request='" + request + '\'' + + ", recordId='" + recordId + '\'' + + ", requestAttributes=" + requestAttributes + + ", responseAttributes=" + responseAttributes + + ", methodRequestTypeHash=" + methodRequestTypeHash + + ", creationTime=" + creationTime + + ", matched=" + matched + + ", response='" + response + '\'' + + ", responseType='" + responseType + '\'' + + '}'; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java new file mode 100644 index 000000000..ed84a013b --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java @@ -0,0 +1,19 @@ +package io.arex.inst.runtime.model; + +import io.arex.agent.bootstrap.model.MockCategoryType; + +public enum MergeReplayType { + + DYNAMIC_CLASS(MockCategoryType.DYNAMIC_CLASS), + + REDIS(MockCategoryType.REDIS); + + private MockCategoryType mockCategoryType; + MergeReplayType(MockCategoryType mockCategoryType) { + this.mockCategoryType = mockCategoryType; + } + + public MockCategoryType getMockCategoryType() { + return mockCategoryType; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java deleted file mode 100644 index e54410f0c..000000000 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeResultDTO.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.arex.inst.runtime.model; - -public class MergeResultDTO { - private String category; - private int methodSignatureKey; - private String clazzName; - private String methodName; - private Object[] args; - private Object result; - private String resultClazz; - private String serializeType; - - public MergeResultDTO() {} - - private MergeResultDTO(String category, String clazzName, String methodName, Object[] args, Object result, - String resultClazz, int methodSignatureKey, String serializeType) { - this.category = category; - this.clazzName = clazzName; - this.methodName = methodName; - this.args = args; - this.result = result; - this.resultClazz = resultClazz; - this.methodSignatureKey = methodSignatureKey; - this.serializeType = serializeType; - } - - public static MergeResultDTO of(String category, String clazzName, String methodName, Object[] args, - Object result, String resultClazz, int methodSignatureKey, String serializeType) { - return new MergeResultDTO(category, clazzName, methodName, args, result, resultClazz, methodSignatureKey, serializeType); - } - - public int getMethodSignatureKey() { - return methodSignatureKey; - } - - public void setMethodSignatureKey(int methodSignatureKey) { - this.methodSignatureKey = methodSignatureKey; - } - - public String getClazzName() { - return clazzName; - } - - public void setClazzName(String clazzName) { - this.clazzName = clazzName; - } - - public String getMethodName() { - return methodName; - } - - public void setMethodName(String methodName) { - this.methodName = methodName; - } - - public Object[] getArgs() { - return args; - } - - public void setArgs(Object[] args) { - this.args = args; - } - - public Object getResult() { - return result; - } - - public void setResult(Object result) { - this.result = result; - } - - public String getResultClazz() { - return resultClazz; - } - - public void setResultClazz(String resultClazz) { - this.resultClazz = resultClazz; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public String getSerializeType() { - return serializeType; - } - - public void setSerializeType(String serializeType) { - this.serializeType = serializeType; - } -} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java index c9511d269..e59fdee9d 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java @@ -2,10 +2,7 @@ import com.google.auto.service.AutoService; import io.arex.agent.bootstrap.model.MockCategoryType; -import io.arex.inst.runtime.config.Config; -import io.arex.inst.runtime.context.ContextManager; -import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.util.MockUtils; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; @AutoService(RequestHandler.class) @@ -16,17 +13,18 @@ public String name() { } @Override - public void preHandle(Object request) {} + public void preHandle(Object request) { + // no need implement + } @Override public void handleAfterCreateContext(Object request) { - if (!ContextManager.needReplay() || !Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { - return; - } // init replay and cached dynamic class - MockUtils.mergeReplay(); + MergeRecordReplayUtil.mergeReplay(); } @Override - public void postHandle(Object request, Object response) {} + public void postHandle(Object request, Object response) { + // no need implement + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java index 81eab1980..67ac8e607 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java @@ -2,10 +2,7 @@ import com.google.auto.service.AutoService; import io.arex.agent.bootstrap.model.MockCategoryType; -import io.arex.inst.runtime.config.Config; -import io.arex.inst.runtime.context.ContextManager; -import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.util.MockUtils; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; @AutoService(RequestHandler.class) @@ -16,17 +13,18 @@ public String name() { } @Override - public void preHandle(Object request) {} + public void preHandle(Object request) { + // no need implement + } @Override public void handleAfterCreateContext(Object request) { - if (!ContextManager.needReplay() || !Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { - return; - } // init replay and cached dynamic class - MockUtils.mergeReplay(); + MergeRecordReplayUtil.mergeReplay(); } @Override - public void postHandle(Object request, Object response) {} + public void postHandle(Object request, Object response) { + // no need implement + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java index 3926ea4f6..45a545534 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataCollector.java @@ -8,5 +8,7 @@ public interface DataCollector { void save(Mocker requestMocker); + void invalidCase(String postData); + String query(String postData, MockStrategyEnum mockStrategy); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java index 99d6e3031..6d017a0e6 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/service/DataService.java @@ -21,6 +21,10 @@ public void save(Mocker requestMocker) { saver.save(requestMocker); } + public void invalidCase(String postData) { + saver.invalidCase(postData); + } + public String query(String data, MockStrategyEnum mockStrategy) { return saver.query(data, mockStrategy); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java index 4820baabf..7e1bd94fb 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java @@ -1,33 +1,45 @@ package io.arex.inst.runtime.util; +import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.service.DataService; public class CaseManager { + private CaseManager() { } - public static void invalid(String recordId, String operationName) { + public static void invalid( + String recordId, String replayId, String operationName, String invalidReason) { try { - final ArexContext context = ContextManager.getRecordContext(recordId); + boolean isReplay = StringUtil.isNotEmpty(replayId); + final ArexContext context = isReplay ? + ContextManager.getContext(replayId) : ContextManager.getContext(recordId); if (context == null || context.isInvalidCase()) { return; } - LogManager.warn("invalidCase", - StringUtil.format("invalid case: recordId: %s operation: %s", recordId, operationName)); context.setInvalidCase(true); + String normalizeReplayId = isReplay ? replayId : StringUtil.EMPTY; + String invalidCaseJson = + StringUtil.format( + "{\"appId\":\"%s\",\"recordId\":\"%s\",\"replayId\":\"%s\",\"reason\":\"%s\"}", + System.getProperty(ConfigConstants.SERVICE_NAME), recordId, normalizeReplayId, invalidReason); + DataService.INSTANCE.invalidCase(invalidCaseJson); + LogManager.warn("invalidCase", + StringUtil.format("invalid case: recordId: %s, replayId: %s, operation: %s, reason: %s", recordId, replayId, operationName, invalidReason)); } catch (Exception ex) { LogManager.warn("invalidCase.remove", ex); } } - public static boolean isInvalidCase(String recordId) { - if (StringUtil.isEmpty(recordId)) { + public static boolean isInvalidCase(String traceId) { + if (StringUtil.isEmpty(traceId)) { return true; } - final ArexContext context = ContextManager.getRecordContext(recordId); + final ArexContext context = ContextManager.getContext(traceId); if (context == null) { return false; } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java index 1a2d2a0f2..fd37f2e8a 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java @@ -118,11 +118,15 @@ private static boolean operationMatched(String targetName, Set searchOpe } public static boolean invalidOperation(String operationSignature) { - return INVALID_OPERATION_HASH_CACHE.contains(operationSignature.hashCode()); + return INVALID_OPERATION_HASH_CACHE.contains(StringUtil.encodeAndHash(operationSignature)); } public static void addInvalidOperation(String operationSignature) { - INVALID_OPERATION_HASH_CACHE.add(operationSignature.hashCode()); + INVALID_OPERATION_HASH_CACHE.add(StringUtil.encodeAndHash(operationSignature)); + } + + public static void clearInvalidOperation() { + INVALID_OPERATION_HASH_CACHE.clear(); } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java new file mode 100644 index 000000000..e1d4b7fb0 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java @@ -0,0 +1,250 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.NumberUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.model.MergeReplayType; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; + +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * merge record and replay util + */ +public class MergeRecordReplayUtil { + private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); + + private MergeRecordReplayUtil() {} + + public static void mergeRecord(Mocker requestMocker) { + int methodSignatureHash = MockUtils.methodSignatureHash(requestMocker); + MergeDTO mergeDTO = MergeDTO.of(requestMocker.getCategoryType().getName(), + methodSignatureHash, + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getBody(), + requestMocker.getTargetResponse().getBody(), + requestMocker.getTargetResponse().getType(), + requestMocker.getTargetRequest().getAttributes(), + requestMocker.getTargetResponse().getAttributes(), + // save recordId prevent miss after clear context at async thread + ContextManager.currentContext() != null ? ContextManager.currentContext().getCaseId() : StringUtil.EMPTY); + mergeDTO.setCreationTime(requestMocker.getCreationTime()); + mergeDTO.setMethodRequestTypeHash(MockUtils.methodRequestTypeHash(requestMocker)); + List> mergeList = merge(mergeDTO); + batchRecord(mergeList); + } + + /** + * merge duplicate mocker to queue and return merged result after reach batch count + * @return if null mean no exceed merge max time limit not need to record or no merge + */ + public static List> merge(MergeDTO mergeDTO) { + try { + ArexContext context = ContextManager.currentContext(); + if (context == null) { + return Collections.emptyList(); + } + + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + // offer queue to avoid block current thread + if (!mergeRecordQueue.offer(mergeDTO)) { + // dynamic class not replay compare, log warn temporarily + LogManager.warn("merge.record.fail", "queue is full"); + return Collections.emptyList(); + } + int recordThreshold = Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); + if (mergeRecordQueue.size() < recordThreshold) { + return Collections.emptyList(); + } + + List mergeList = new ArrayList<>(); + mergeRecordQueue.drainTo(mergeList, recordThreshold); + return checkAndSplit(mergeList); + } catch (Exception e) { + LogManager.warn("merge.record.error", e); + return Collections.emptyList(); + } + } + + /** + * check memory size or split to multiple list if exceed size limit + */ + public static List> checkAndSplit(List mergeList) { + mergeList = CollectionUtil.filterNull(mergeList); + if (CollectionUtil.isEmpty(mergeList)) { + return Collections.emptyList(); + } + List> mergeTotalList = new ArrayList<>(); + Map> mergeRecordGroupMap = group(mergeList); + for (Map.Entry> mergeRecordEntry : mergeRecordGroupMap.entrySet()) { + // check memory size + if (agentSizeOf.checkMemorySizeLimit(mergeRecordEntry.getValue(), ArexConstants.MEMORY_SIZE_5MB)) { + mergeTotalList.add(mergeRecordEntry.getValue()); + } else { + // exceed size limit and split to multiple list + mergeTotalList.addAll(split(mergeRecordEntry.getValue())); + } + } + return mergeTotalList; + } + + /** + * group by category(such as: dynamicClass、redis) + */ + private static Map> group(List mergeList) { + Map> mergeGroupMap = new HashMap<>(); + for (MergeDTO mergeDTO : mergeList) { + if (mergeDTO == null) { + continue; + } + String category = mergeDTO.getCategory(); + mergeGroupMap.computeIfAbsent(category, k -> new ArrayList<>()).add(mergeDTO); + } + return mergeGroupMap; + } + + /** + * split strategy: + * 1. split by config count: list[10] A -> list[5] B、list[5] C + * 2. check memory size separate list + * 3. if memory size not exceed limit return: list[[5]、[5]] R + * 4. if memory size exceed limit, split to single-size list and return: + * list[[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]] R + */ + private static List> split(List mergeList) { + List> splitTotalList = new ArrayList<>(); + // default split in half + int splitCount = Config.get().getInt(ArexConstants.MERGE_SPLIT_COUNT, 2); + List> splitResultList = CollectionUtil.split(mergeList, splitCount); + for (List splitList : splitResultList) { + if (agentSizeOf.checkMemorySizeLimit(splitList, ArexConstants.MEMORY_SIZE_5MB)) { + splitTotalList.add(splitList); + } else { + // split to single-size list + splitTotalList.addAll(CollectionUtil.split(splitList, splitList.size())); + logBigSize(splitList); + } + } + LogManager.info("merge.record.split", StringUtil.format("original size: %s, split count: %s", + mergeList.size() + "", splitTotalList.size() + "")); + return splitTotalList; + } + + private static void logBigSize(List mergeRecordList) { + for (MergeDTO mergeDTO : mergeRecordList) { + LogManager.warn("merge.record.size.too.large", + StringUtil.format("please check following record data, if is dynamic class, recommended to modify it, " + + "category: %s, operationName: %s", + mergeDTO.getCategory(), mergeDTO.getOperationName())); + } + } + + private static void batchRecord(List> splitList) { + MockCategoryType categoryType; + for (List mergeRecords : splitList) { + categoryType = MockCategoryType.of(mergeRecords.get(0).getCategory()); + Mocker mergeMocker = MockUtils.create(categoryType, ArexConstants.MERGE_RECORD_NAME); + mergeMocker.setRecordId(mergeRecords.get(0).getRecordId()); + mergeMocker.getTargetResponse().setBody(Serializer.serialize(mergeRecords)); + mergeMocker.getTargetResponse().setType(ArexConstants.MERGE_TYPE); + MockUtils.executeRecord(mergeMocker); + } + } + + public static void recordRemain(ArexContext context) { + if (context == null) { + return; + } + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + if (mergeRecordQueue.isEmpty()) { + return; + } + try { + List mergeRecordList = new ArrayList<>(); + mergeRecordQueue.drainTo(mergeRecordList); + List> splitList = checkAndSplit(mergeRecordList); + if (CollectionUtil.isEmpty(splitList)) { + return; + } + batchRecord(splitList); + } catch (Exception e) { + LogManager.warn("merge.record.remain.error", e); + } + } + + /** + * init replay and cached replay result + */ + public static void mergeReplay() { + if (!ContextManager.needReplay()) { + return; + } + Map> cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); + // if there are other types that need to be mergeReplay in the future, please add to MergeReplayType + for (MergeReplayType mergeReplayType : MergeReplayType.values()) { + Mocker mergeMocker = MockUtils.create(mergeReplayType.getMockCategoryType(), ArexConstants.MERGE_RECORD_NAME); + int callReplayMax; + int replayCount = 0; + do { + Mocker responseMocker = MockUtils.executeReplay(mergeMocker, MockStrategyEnum.OVER_BREAK); + if (!MockUtils.checkResponseMocker(responseMocker)) { + break; + } + List mergeReplayList = Serializer.deserialize(responseMocker.getTargetResponse().getBody(), + ArexConstants.MERGE_TYPE); + if (CollectionUtil.isEmpty(mergeReplayList)) { + LogManager.warn("merge.replay.fail", "mergeReplayList is empty"); + break; + } + buildReplayResultMap(mergeReplayList, cachedReplayResultMap); + replayCount ++; + callReplayMax = getCallReplayMax(responseMocker); + } while (replayCount < callReplayMax); + } + + // ascending order + sortByCreationTime(cachedReplayResultMap); + } + + private static void buildReplayResultMap(List mergeReplayList, Map> cachedReplayResultMap) { + for (int i = 0; i < mergeReplayList.size(); i++) { + MergeDTO mergeReplayDTO = mergeReplayList.get(i); + if (mergeReplayDTO == null) { + continue; + } + cachedReplayResultMap.computeIfAbsent(mergeReplayDTO.getMethodRequestTypeHash(), k -> new ArrayList<>()).add(mergeReplayDTO); + } + } + + private static void sortByCreationTime(Map> cachedReplayResultMap) { + for (List mergeReplayList : cachedReplayResultMap.values()) { + if (mergeReplayList.size() == 1) { + continue; + } + mergeReplayList.sort((o1, o2) -> { + if (o1.getCreationTime() == o2.getCreationTime()) { + return 0; + } + return o1.getCreationTime() - o2.getCreationTime() > 0 ? 1 : -1; + }); + } + } + + private static int getCallReplayMax(Mocker replayMocker) { + Mocker.Target targetResponse = replayMocker.getTargetResponse(); + int callReplayMax = NumberUtil.toInt(String.valueOf(targetResponse.getAttribute(ArexConstants.CALL_REPLAY_MAX))); + int replayThreshold = Config.get().getInt(ArexConstants.MERGE_REPLAY_THRESHOLD, ArexConstants.MERGE_REPLAY_THRESHOLD_DEFAULT); + return callReplayMax == 0 ? replayThreshold : callReplayMax; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java deleted file mode 100644 index 44f69a4c3..000000000 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeSplitUtil.java +++ /dev/null @@ -1,159 +0,0 @@ -package io.arex.inst.runtime.util; - -import io.arex.agent.bootstrap.model.Mocker; -import io.arex.agent.bootstrap.util.CollectionUtil; -import io.arex.agent.bootstrap.util.StringUtil; -import io.arex.inst.runtime.config.Config; -import io.arex.inst.runtime.context.ArexContext; -import io.arex.inst.runtime.context.ContextManager; -import io.arex.inst.runtime.log.LogManager; -import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.model.MergeResultDTO; -import io.arex.inst.runtime.util.sizeof.AgentSizeOf; - -import java.util.*; -import java.util.concurrent.LinkedBlockingQueue; - -public class MergeSplitUtil { - private static final long MERGE_MAX_SIZE_5MB = 5 * 1024L * 1024L; - - /** - * merge duplicate mocker to queue and return merged result After reach batch count - * @return if null mean no exceed merge max time limit not need to record or no merge - */ - public static List> merge(Mocker mocker) { - try { - if (!Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true)) { - return null; - } - - ArexContext context = ContextManager.currentContext(); - if (context == null) { - return null; - } - LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); - MergeResultDTO mergeResultDTO = (MergeResultDTO) mocker.getTargetRequest().getAttribute(ArexConstants.MERGE_RECORD_KEY); - // offer queue to avoid block current thread - if (!mergeRecordQueue.offer(mergeResultDTO)) { - // dynamic class not replay compare, log warn temporarily - LogManager.warn("merge dynamicClass fail", "queue is full"); - return null; - } - - int recordThreshold = Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); - // if main entry record has ended, it means still async thread record(not merge and direct record) - if (mergeRecordQueue.size() < recordThreshold && !context.isMainEntryEnd()) { - return null; - } - List mergeRecordList = new ArrayList<>(); - for (int i = 0; i < recordThreshold; i++) { - MergeResultDTO mergeResult = mergeRecordQueue.poll(); - if (mergeResult != null) { - mergeRecordList.add(mergeResult); - } - } - - return checkAndSplit(mergeRecordList); - } catch (Throwable e) { - LogManager.warn("MergeUtil merge error", e); - return null; - } - } - - private static List> checkAndSplit(List mergeRecordList) { - mergeRecordList = CollectionUtil.filterNull(mergeRecordList); - if (CollectionUtil.isEmpty(mergeRecordList)) { - return null; - } - List> mergeTotalList = new ArrayList<>(); - Map> mergeRecordGroupMap = group(mergeRecordList); - for (Map.Entry> mergeRecordEntry : mergeRecordGroupMap.entrySet()) { - // check memory size and split to multiple list - if (!checkMemorySizeLimit(mergeRecordEntry.getValue())) { - mergeTotalList.addAll(split(mergeRecordEntry.getValue())); - } else { - mergeTotalList.add(mergeRecordEntry.getValue()); - } - } - return mergeTotalList; - } - - /** - * group by category(such as: dynamicClass、redis) - */ - private static Map> group(List mergeRecordList) { - Map> mergeRecordGroupMap = new HashMap<>(); - for (MergeResultDTO mergeResultDTO : mergeRecordList) { - if (mergeResultDTO == null) { - continue; - } - String category = mergeResultDTO.getCategory(); - mergeRecordGroupMap.computeIfAbsent(category, k -> new ArrayList<>()).add(mergeResultDTO); - } - return mergeRecordGroupMap; - } - - private static boolean checkMemorySizeLimit(Object obj) { - if (!Config.get().getBoolean(ArexConstants.MERGE_MEMORY_CHECK, true)) { - return true; - } - long start = System.currentTimeMillis(); - AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); - long memorySize = agentSizeOf.deepSizeOf(obj); - long cost = System.currentTimeMillis() - start; - if (cost > 10) { // longer cost mean larger memory - LogManager.warn("checkMemorySizeLimit", StringUtil.format("size: %s, cost: %s", - AgentSizeOf.humanReadableUnits(memorySize), String.valueOf(cost))); - } - return memorySize < MERGE_MAX_SIZE_5MB; - } - - /** - * split strategy: - * 1. split by config count: list[10] A -> list[5] B、list[5] C - * 2. check memory size separate list - * 3. if memory size not exceed limit return: list[[5]、[5]] R - * 4. if memory size exceed limit, split to single-size list and return: - * list[[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]] R - */ - private static List> split(List mergeRecordList) { - List> splitTotalList = new ArrayList<>(); - // default split in half - int splitCount = Config.get().getInt(ArexConstants.MERGE_SPLIT_COUNT, 2); - List> splitResultList = CollectionUtil.split(mergeRecordList, splitCount); - for (List splitList : splitResultList) { - if (checkMemorySizeLimit(splitList)) { - splitTotalList.add(splitList); - } else { - // split to single-size list - splitTotalList.addAll(CollectionUtil.split(splitList, splitList.size())); - } - } - LogManager.info("split merged record", StringUtil.format("original size: %s, split count: %s", - mergeRecordList.size() + "", splitTotalList.size() + "")); - return splitTotalList; - } - - /** - * merge all that did not exceed the threshold at the end of the request - */ - public static List> mergeRemain() { - try { - ArexContext context = ContextManager.currentContext(); - if (context == null) { - return null; - } - LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); - if (mergeRecordQueue.size() == 0) { - return null; - } - MergeResultDTO[] mergeRecordArray = mergeRecordQueue.toArray(new MergeResultDTO[0]); - mergeRecordQueue.clear(); - List mergeRecordList = Arrays.asList(mergeRecordArray); - return checkAndSplit(mergeRecordList); - } catch (Throwable e) { - LogManager.warn("MergeUtil mergeRemain error", e); - return null; - } - } -} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java index 196d7bb00..fb282f01a 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java @@ -5,20 +5,15 @@ import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.model.Mocker.Target; -import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; -import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.model.MergeResultDTO; +import io.arex.inst.runtime.match.ReplayMatcher; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataService; -import java.util.List; -import java.util.Map; - public final class MockUtils { private static final String EMPTY_JSON = "{}"; @@ -97,26 +92,23 @@ public static void recordMocker(Mocker requestMocker) { if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { return; } - if (requestMocker.getCategoryType().isMergeRecord() && requestMocker.getTargetRequest().getAttribute(ArexConstants.MERGE_RECORD_KEY) != null) { - mergeRecord(requestMocker); + if (requestMocker.isMerge()) { + MergeRecordReplayUtil.mergeRecord(requestMocker); return; } executeRecord(requestMocker); if (requestMocker.getCategoryType().isEntryPoint()) { - ArexContext context = ContextManager.currentContext(); - if (context != null) { - context.setMainEntryEnd(true); - } // after main entry record finished, record remain merge mocker that have not reached the merge threshold once(such as dynamicClass) - mergeRecordRemain(); + MergeRecordReplayUtil.recordRemain(ContextManager.currentContext()); } } public static void executeRecord(Mocker requestMocker) { if (Config.get().isEnableDebug()) { - LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); + LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", + requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); } DataService.INSTANCE.save(requestMocker); @@ -127,6 +119,23 @@ public static Mocker replayMocker(Mocker requestMocker) { } public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStrategy) { + if (CaseManager.isInvalidCase(requestMocker.getReplayId()) && + isNotConfigFile(requestMocker.getCategoryType())) { + return null; + } + + if (requestMocker.isMerge()) { + Mocker matchMocker = ReplayMatcher.match(requestMocker, mockStrategy); + // compatible with old version(fixed case without merge) + if (matchMocker != null) { + return matchMocker; + } + } + + return executeReplay(requestMocker, mockStrategy); + } + + public static Mocker executeReplay(Mocker requestMocker, MockStrategyEnum mockStrategy) { String postJson = Serializer.serialize(requestMocker); String data = DataService.INSTANCE.query(postJson, mockStrategy); @@ -134,7 +143,8 @@ public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStr boolean isEnableDebug = Config.get().isEnableDebug(); if (isEnableDebug) { - LogManager.info(requestMocker.replayLogTitle(), StringUtil.format("%s%nrequest: %s%nresponse: %s", requestMocker.logBuilder().toString(), postJson, data)); + LogManager.info(requestMocker.replayLogTitle(), StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), postJson, data)); } if (StringUtil.isEmpty(data) || EMPTY_JSON.equals(data)) { @@ -149,6 +159,10 @@ public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStr return Serializer.deserialize(data, ArexMocker.class); } + private static boolean isNotConfigFile(MockCategoryType mockCategoryType) { + return !MockCategoryType.CONFIG_FILE.equals(mockCategoryType); + } + public static Object replayBody(Mocker requestMocker) { return replayBody(requestMocker, MockStrategyEnum.OVER_BREAK); } @@ -188,72 +202,16 @@ public static boolean checkResponseMocker(Mocker responseMocker) { return true; } - /** - *
-     * tip:
-     * 1. if user change result object after merge, it will also change the result in cache
-     * 2. if serialize fail, mean this list all fail, need to troubleshoot based on error log
-     * 3. if async record, main entry point has recorded end, merge record will not be executed which no reach the merge threshold
-     * 4. currently not support fuzzy match
-     * 
- */ - private static void mergeRecord(Mocker requestMocker) { - List> splitList = MergeSplitUtil.merge(requestMocker); - if (CollectionUtil.isEmpty(splitList)) { - return; - } - batchRecord(splitList); - } - - private static void batchRecord(List> splitList) { - String serializeType = ArexConstants.JACKSON_SERIALIZER; - MockCategoryType categoryType; - for (List mergeRecords : splitList) { - if (mergeRecords.get(0).getSerializeType() != null) { - serializeType = mergeRecords.get(0).getSerializeType(); - } - categoryType = MockCategoryType.of(mergeRecords.get(0).getCategory()); - Mocker mergeMocker = MockUtils.create(categoryType, ArexConstants.MERGE_RECORD_NAME); - mergeMocker.getTargetResponse().setBody(Serializer.serialize(mergeRecords, serializeType)); - mergeMocker.getTargetResponse().setType(ArexConstants.MERGE_RESULT_TYPE); - executeRecord(mergeMocker); - LogManager.info("merge record", "size:"+mergeRecords.size()); - } - } - - private static void mergeRecordRemain() { - List> splitList = MergeSplitUtil.mergeRemain(); - if (CollectionUtil.isEmpty(splitList)) { - return; - } - batchRecord(splitList); + public static int methodSignatureHash(Mocker requestMocker) { + return StringUtil.encodeAndHash(String.format("%s_%s", + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getBody())); } - /** - * init replay and cached dynamic class - */ - public static void mergeReplay() { - int replayThreshold = Config.get().getInt(ArexConstants.MERGE_REPLAY_THRESHOLD, ArexConstants.MERGE_REPLAY_THRESHOLD_DEFAULT); - for (MockCategoryType categoryType : MockCategoryType.values()) { - if (!categoryType.isMergeRecord()) { - continue; - } - Mocker mergeMocker = create(categoryType, ArexConstants.MERGE_RECORD_NAME); - Map cachedDynamicClassMap = ContextManager.currentContext().getCachedReplayResultMap(); - for (int i = 0; i < replayThreshold; i++) { - // loop replay until over storage size break or over max times - Object result = replayBody(mergeMocker); - if (result == null) { - break; - } - List mergeRecordList = (List) result; - for (MergeResultDTO mergeResultDTO : mergeRecordList) { - if (mergeResultDTO == null) { - continue; - } - cachedDynamicClassMap.put(mergeResultDTO.getMethodSignatureKey(), mergeResultDTO); - } - } - } + public static int methodRequestTypeHash(Mocker requestMocker) { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", + requestMocker.getCategoryType().getName(), + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getType())); } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java index f8e1b2318..0f3ac6a23 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/TypeUtil.java @@ -16,6 +16,7 @@ import java.lang.reflect.Type; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -27,6 +28,8 @@ public class TypeUtil { public static final String DEFAULT_CLASS_NAME = "java.lang.String"; private static final ConcurrentMap GENERIC_FIELD_CACHE = new ConcurrentHashMap<>(); private static final ConcurrentMap TYPE_NAME_CACHE = new ConcurrentHashMap<>(); + private static final Class DEFAULT_LIST_CLASS = List.class; + private static final Class DEFAULT_SET_CLASS = Set.class; /** * Suppresses default constructor, ensuring non-instantiability. */ @@ -51,21 +54,26 @@ public static Type forName(String typeName) { } if (types.length > 1 && StringUtil.isNotEmpty(types[1])) { - - if (raw.getTypeParameters().length == 1) { - final Type[] args = new Type[]{forName(types[1])}; - final ParameterizedTypeImpl parameterizedType = ParameterizedTypeImpl.make(raw, args, null); - TYPE_NAME_CACHE.put(typeName, parameterizedType); - return parameterizedType; + int typeParametersLength = raw.getTypeParameters().length; + if (typeParametersLength == 1) { + return forNameWithOneGenericType(raw, types[1], typeName); } - if (raw.getTypeParameters().length == 2) { + if (typeParametersLength == 2) { final String[] split = StringUtil.splitByFirstSeparator(types[1], COMMA); Type[] args = new Type[]{forName(split[0]), forName(split[1])}; ParameterizedTypeImpl parameterizedType = ParameterizedTypeImpl.make(raw, args, null); TYPE_NAME_CACHE.put(typeName, parameterizedType); return parameterizedType; } + + if (needUseDefaultCollection(raw, types[1], typeParametersLength)) { + if (Set.class.isAssignableFrom(raw)) { + return forNameWithOneGenericType(DEFAULT_SET_CLASS, types[1], typeName); + } + return forNameWithOneGenericType(DEFAULT_LIST_CLASS, types[1], typeName); + } + TYPE_NAME_CACHE.put(typeName, raw); return raw; } @@ -77,6 +85,39 @@ public static Type forName(String typeName) { } } + /** + * If the parent class explicitly specifies generics, + * the serialization framework will process it and not need use default collection. + * eg: class WrappedList extends WrappedCollection implements List return true + * eg: class exampleCollection extends ArrayList return false + */ + private static boolean needUseDefaultCollection(Class raw, String type, int typeParametersLength) { + if (typeParametersLength != 0 || !Collection.class.isAssignableFrom(raw)) { + return false; + } + + Class tempClass = raw; + while (Object.class != tempClass) { + Type genericSuperclass = tempClass.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 0 && type.equals(actualTypeArguments[0].getTypeName())) { + return false; + } + } + tempClass = tempClass.getSuperclass(); + } + return true; + } + + private static Type forNameWithOneGenericType(Class rawClass, String genericType, String typeName) { + final Type[] args = new Type[]{forName(genericType)}; + final ParameterizedTypeImpl parameterizedType = ParameterizedTypeImpl.make(rawClass, args, null); + TYPE_NAME_CACHE.put(typeName, parameterizedType); + return parameterizedType; + } + public static String getName(Object result) { if (result == null) { return null; @@ -134,10 +175,8 @@ private static String genericTypeToString(Object result) { if (isCollection(field.getType().getName())) { genericType = filterRawGenericType(genericType); } - - if (StringUtil.isNotEmpty(genericType)) { - builder.append(genericType); - } + genericType = StringUtil.isEmpty(genericType) ? DEFAULT_CLASS_NAME : genericType; + builder.append(genericType); if (i == typeParameters.length - 1) { return builder.toString(); } @@ -275,9 +314,13 @@ private static String mapToString(Map result) { StringBuilder builder = new StringBuilder(); builder.append(resultClassName).append(HORIZONTAL_LINE); - // only get the first element + // only get the first not empty element for (Map.Entry entry : result.entrySet()) { - String valueClassName = entry.getValue() == null ? DEFAULT_CLASS_NAME : getName(entry.getValue()); + final Object value = entry.getValue(); + if (isEmpty(value)) { + continue; + } + String valueClassName = getName(value); if (typeParameters.length == 1) { builder.append(valueClassName); @@ -291,6 +334,19 @@ private static String mapToString(Map result) { return builder.toString(); } + private static boolean isEmpty(Object value) { + if (value == null) { + return true; + } + if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } + if (value instanceof Map) { + return ((Map) value).isEmpty(); + } + return false; + } + private static Class classForName(String type) { try { if (Thread.currentThread().getContextClassLoader() == null) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java index 10f862bbb..86dbafa29 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java @@ -1,6 +1,8 @@ package io.arex.inst.runtime.util.sizeof; import io.arex.agent.bootstrap.InstrumentationHolder; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; import java.lang.instrument.Instrumentation; import java.text.DecimalFormat; @@ -78,4 +80,19 @@ public static String humanReadableUnits(long bytes, DecimalFormat df) { return bytes + " bytes"; } } + + /** + * @return true: not exceed memory size limit + */ + public boolean checkMemorySizeLimit(Object obj, long sizeLimit) { + long start = System.currentTimeMillis(); + long memorySize = deepSizeOf(obj); + long cost = System.currentTimeMillis() - start; + if (cost > 50) { // longer cost mean larger memory + LogManager.info("check.memory.size", + StringUtil.format("size: %s, cost: %s", + humanReadableUnits(memorySize), String.valueOf(cost))); + } + return memorySize < sizeLimit; + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java index 5e485d729..6d57ca79f 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java @@ -5,8 +5,6 @@ /** * Filter combining multiple filters - * - * @author Chris Dennis */ public class CombinationSizeOfFilter implements SizeOfFilter { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java index 4b9618349..3a4c7b7b2 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java @@ -1,5 +1,7 @@ package io.arex.inst.runtime.util.sizeof; +import io.arex.agent.bootstrap.internal.WeakCache; + import java.lang.ref.SoftReference; import java.lang.reflect.Array; import java.lang.reflect.Field; @@ -14,13 +16,9 @@ import static java.util.Collections.newSetFromMap; public class ObjectGraphWalker { - private final WeakIdentityConcurrentMap, SoftReference>> fieldCache = - new WeakIdentityConcurrentMap<>(); - private final WeakIdentityConcurrentMap, Boolean> classCache = - new WeakIdentityConcurrentMap<>(); - + private final WeakCache, SoftReference>> fieldCache = new WeakCache<>(); + private final WeakCache, Boolean> classCache = new WeakCache<>(); private final SizeOfFilter sizeOfFilter; - private final Visitor visitor; /** diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java index 62b6e0116..348641423 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java @@ -5,8 +5,6 @@ /** * Filter to filter types or fields of object graphs passed to a SizeOf engine - * - * @author Chris Dennis */ public interface SizeOfFilter { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java deleted file mode 100644 index fa66dbd7b..000000000 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/WeakIdentityConcurrentMap.java +++ /dev/null @@ -1,198 +0,0 @@ -package io.arex.inst.runtime.util.sizeof; - -import java.lang.ref.Reference; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -public class WeakIdentityConcurrentMap { - private final ConcurrentMap, V> map = new ConcurrentHashMap<>(); - private final ReferenceQueue queue = new ReferenceQueue<>(); - - private final CleanUpTask cleanUpTask; - - /** - * Constructor - */ - public WeakIdentityConcurrentMap() { - this(null); - } - - /** - * Constructor - * - * @param cleanUpTask task cleaning up references - */ - public WeakIdentityConcurrentMap(final CleanUpTask cleanUpTask) { - this.cleanUpTask = cleanUpTask; - } - - /** - * Puts into the underlying - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - * @return the previous value associated with key, or - * null if there was no mapping for key. - * (A null return can also indicate that the map - * previously associated null with key, - * if the implementation supports null values.) - */ - public V put(K key, V value) { - cleanUp(); - return map.put(new IdentityWeakReference<>(key, queue), value); - } - - /** - * Remove from the underlying - * - * @param key key whose mapping is to be removed from the map - * @return the previous value associated with key, or - * null if there was no mapping for key. - */ - public V remove(K key) { - cleanUp(); - return map.remove(new IdentityWeakReference<>(key, queue)); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - cleanUp(); - return map.toString(); - } - - /** - * Puts into the underlying - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or - * {@code null} if there was no mapping for the key. - * (A {@code null} return can also indicate that the map - * previously associated {@code null} with the key, - * if the implementation supports null values.) - */ - public V putIfAbsent(K key, V value) { - cleanUp(); - return map.putIfAbsent(new IdentityWeakReference<>(key, queue), value); - } - - /** - * @param key the key whose associated value is to be returned - * @return the value to which the specified key is mapped, or - * {@code null} if this map contains no mapping for the key - */ - public V get(K key) { - cleanUp(); - return map.get(new IdentityWeakReference<>(key)); - } - - /** - * - */ - public void cleanUp() { - - Reference reference; - while ((reference = queue.poll()) != null) { - final V value = map.remove(reference); - if (cleanUpTask != null && value != null) { - cleanUpTask.cleanUp(value); - } - } - } - - /** - * @return a set view of the keys contained in this map - */ - public Set keySet() { - cleanUp(); - K k; - final HashSet ks = new HashSet<>(); - for (WeakReference weakReference : map.keySet()) { - k = weakReference.get(); - if (k != null) { - ks.add(k); - } - } - return ks; - } - - public boolean containsKey(final K key) { - cleanUp(); - return map.containsKey(new IdentityWeakReference<>(key)); - } - - /** - * @param - */ - private static final class IdentityWeakReference extends WeakReference { - - private final int hashCode; - - /** - * @param reference the referenced object - */ - IdentityWeakReference(T reference) { - this(reference, null); - } - - /** - * @param reference the references object - * @param referenceQueue the reference queue where references are kept - */ - IdentityWeakReference(T reference, ReferenceQueue referenceQueue) { - super(reference, referenceQueue); - this.hashCode = (reference == null) ? 0 : System.identityHashCode(reference); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return String.valueOf(get()); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof IdentityWeakReference)) { - return false; - } else { - IdentityWeakReference wr = (IdentityWeakReference)o; - Object got = get(); - return (got != null && got == wr.get()); - } - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return hashCode; - } - } - - /** - * @param - */ - public interface CleanUpTask { - - /** - * @param object object to cleanup - */ - void cleanUp(T object); - } -} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/extension/matcher/SafeExtendsClassMatcherTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/extension/matcher/SafeExtendsClassMatcherTest.java new file mode 100644 index 000000000..e4403d781 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/extension/matcher/SafeExtendsClassMatcherTest.java @@ -0,0 +1,30 @@ +package io.arex.inst.extension.matcher; + +import static org.junit.jupiter.api.Assertions.*; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.junit.jupiter.api.Test; + +/** + * @since 2024/1/12 + */ +class SafeExtendsClassMatcherTest { + + @Test + void extendsClass() { + ElementMatcher.Junction matcher = SafeExtendsClassMatcher.extendsClass( + ElementMatchers.is(SafeExtendsClassMatcher.class)); + + assertTrue(matcher.matches(TypeDescription.ForLoadedType.of(SafeExtendsClassMatcher.class))); + } + + @Test + void testExtendsClass() { + ElementMatcher.Junction matcher = SafeExtendsClassMatcher.extendsClass( + ElementMatchers.is(ElementMatcher.Junction.AbstractBase.class), true); + + assertTrue(matcher.matches(TypeDescription.ForLoadedType.of(SafeExtendsClassMatcher.class))); + } +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/ContextManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/ContextManagerTest.java index 90c732e82..b8f67e02f 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/ContextManagerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/ContextManagerTest.java @@ -43,12 +43,16 @@ static Stream currentContextCase() { Runnable mocker1 = () -> { Mockito.when(TraceContextManager.get(any(Boolean.class))).thenReturn("mock"); }; + Runnable mocker2 = () -> { + Mockito.when(TraceContextManager.get(any(Boolean.class))).thenReturn("mock2"); + }; Predicate predicate1 = Objects::isNull; Predicate predicate2 = Objects::nonNull; return Stream.of( - arguments(true, "mock", emptyMocker, predicate2), + arguments(true, "mock", emptyMocker, predicate1), arguments(true, null, emptyMocker, predicate1), arguments(true, null, mocker1, predicate2), + arguments(true, "mock", mocker2, predicate2), arguments(false, null, emptyMocker, predicate2) ); } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/RepeatedCollectManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/RepeatedCollectManagerTest.java new file mode 100644 index 000000000..66533c83f --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/context/RepeatedCollectManagerTest.java @@ -0,0 +1,34 @@ +package io.arex.inst.runtime.context; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * @since 2024/1/12 + */ +class RepeatedCollectManagerTest { + @BeforeAll + static void setUp() { + Mockito.mockStatic(ContextManager.class); + } + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void test() { + assertTrue(RepeatedCollectManager.validate()); + assertTrue(RepeatedCollectManager.exitAndValidate()); + + Mockito.when(ContextManager.needRecord()).thenReturn(true); + RepeatedCollectManager.enter(); + RepeatedCollectManager.enter(); + assertFalse(RepeatedCollectManager.exitAndValidate()); + assertTrue(RepeatedCollectManager.exitAndValidate()); + } +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java new file mode 100644 index 000000000..7a041499f --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java @@ -0,0 +1,42 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractMatchStrategyTest { + + static AbstractMatchStrategy target; + @BeforeAll + static void setUp() { + target = new FuzzyMatchStrategy(); + } + + @AfterAll + static void tearDown() { + target = null; + } + + @Test + void match() { + assertDoesNotThrow(() -> target.match(null)); + MatchStrategyContext context = new MatchStrategyContext(new ArexMocker(), null, null); + assertDoesNotThrow(() -> target.match(context)); + } + + @Test + void buildMatchedMocker() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + Mocker result = target.buildMatchedMocker(mocker, null); + assertNull(result); + result = target.buildMatchedMocker(mocker, new MergeDTO()); + assertNotNull(result); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java new file mode 100644 index 000000000..9264826af --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java @@ -0,0 +1,101 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class AccurateMatchStrategyTest { + + static AccurateMatchStrategy accurateMatchStrategy; + + @BeforeAll + static void setUp() { + accurateMatchStrategy = new AccurateMatchStrategy(); + Mockito.mockStatic(MockUtils.class); + } + + @AfterAll + static void tearDown() { + accurateMatchStrategy = null; + Mockito.clearAllCaches(); + } + + @ParameterizedTest + @MethodSource("processCase") + void process(MatchStrategyContext context, Predicate asserts) { + accurateMatchStrategy.process(context); + asserts.test(context); + } + + static Stream processCase() { + Supplier contextSupplier1 = () -> { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.setCategoryType(MockCategoryType.DYNAMIC_CLASS); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + return new MatchStrategyContext(mocker, mergeReplayList, MockStrategyEnum.FIND_LAST); + }; + Supplier contextSupplier2 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + context.getMergeReplayList().get(0).setMatched(true); + context.setMockStrategy(MockStrategyEnum.STRICT_MATCH); + return context; + }; + Supplier contextSupplier3 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + MergeDTO mergeDTO = new MergeDTO(); + context.getMergeReplayList().add(mergeDTO); + return context; + }; + Supplier contextSupplier4 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + context.getMergeReplayList().get(0).setMethodSignatureHash(1); + context.setMockStrategy(MockStrategyEnum.STRICT_MATCH); + return context; + }; + + Predicate asserts1 = context -> !context.isInterrupt(); + Predicate asserts2 = MatchStrategyContext::isInterrupt; + + return Stream.of( + arguments(contextSupplier1.get(), asserts1), + arguments(contextSupplier2.get(), asserts2), + arguments(contextSupplier3.get(), asserts1), + arguments(contextSupplier4.get(), asserts2) + ); + } + + @Test + void valid() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetRequest(new Mocker.Target()); + assertFalse(accurateMatchStrategy.valid(new MatchStrategyContext(mocker, null, null))); + } + + @Test + void order() { + assertEquals(10, accurateMatchStrategy.order()); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java new file mode 100644 index 000000000..0b15ade6e --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java @@ -0,0 +1,26 @@ +package io.arex.inst.runtime.match; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EigenMatchStrategyTest { + static EigenMatchStrategy eigenMatchStrategy; + + @BeforeAll + static void setUp() { + eigenMatchStrategy = new EigenMatchStrategy(); + } + + @AfterAll + static void tearDown() { + eigenMatchStrategy = null; + } + + @Test + void order() { + assertEquals(30, eigenMatchStrategy.order()); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java new file mode 100644 index 000000000..3d1ab3d74 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java @@ -0,0 +1,65 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FuzzyMatchStrategyTest { + static FuzzyMatchStrategy fuzzyMatchStrategy; + + @BeforeAll + static void setUp() { + fuzzyMatchStrategy = new FuzzyMatchStrategy(); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + Mockito.mockStatic(MockUtils.class); + } + + @AfterAll + static void tearDown() { + fuzzyMatchStrategy = null; + Mockito.clearAllCaches(); + } + + @Test + void process() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.setCategoryType(MockCategoryType.DYNAMIC_CLASS); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + MatchStrategyContext context =new MatchStrategyContext(mocker, mergeReplayList, MockStrategyEnum.FIND_LAST); + Mockito.when(Config.get().isEnableDebug()).thenReturn(true); + fuzzyMatchStrategy.process(context); + assertNotNull(context.getMatchMocker()); + + mergeDTO.setMatched(true); + fuzzyMatchStrategy.process(context); + assertNotNull(context.getMatchMocker()); + } + + @Test + void valid() { + assertFalse(fuzzyMatchStrategy.valid(new MatchStrategyContext(null, null, null))); + } + + @Test + void order() { + assertEquals(20, fuzzyMatchStrategy.order()); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java new file mode 100644 index 000000000..ec00f59cd --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java @@ -0,0 +1,65 @@ +package io.arex.inst.runtime.match; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MatchStrategyContextTest { + static MatchStrategyContext context; + + @BeforeAll + static void setUp() { + context = new MatchStrategyContext(null, null, null); + } + + @Test + void getRequestMocker() { + assertNull(context.getRequestMocker()); + } + + @Test + void setRequestMocker() { + assertDoesNotThrow(() -> context.setRequestMocker(null)); + } + + @Test + void getMergeReplayList() { + assertNull(context.getMergeReplayList()); + } + + @Test + void setMergeReplayList() { + assertDoesNotThrow(() -> context.setMergeReplayList(null)); + } + + @Test + void getMockStrategy() { + assertNull(context.getMockStrategy()); + } + + @Test + void setMockStrategy() { + assertDoesNotThrow(() -> context.setMockStrategy(null)); + } + + @Test + void isInterrupt() { + assertFalse(context.isInterrupt()); + } + + @Test + void setInterrupt() { + assertDoesNotThrow(() -> context.setInterrupt(false)); + } + + @Test + void getMatchMocker() { + assertNull(context.getMatchMocker()); + } + + @Test + void setMatchMocker() { + assertDoesNotThrow(() -> context.setMatchMocker(null)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java new file mode 100644 index 000000000..8357b34ea --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java @@ -0,0 +1,14 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MatchStrategyRegisterTest { + + @Test + void getMatchStrategies() { + assertNotNull(MatchStrategyRegister.getMatchStrategies(MockCategoryType.DYNAMIC_CLASS)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java new file mode 100644 index 000000000..58aa075ab --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java @@ -0,0 +1,60 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class ReplayMatcherTest { + + @BeforeAll + static void setUp() { + Mockito.mockStatic(MockUtils.class); + Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + Mockito.mockStatic(MatchStrategyRegister.class); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void match() { + ArexMocker requestMocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + requestMocker.setOperationName("mock"); + requestMocker.setTargetRequest(new Mocker.Target()); + requestMocker.setTargetResponse(new Mocker.Target()); + ArexContext context = Mockito.mock(ArexContext.class); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + Map> cachedReplayResultMap = new HashMap<>(); + Mockito.when(context.getCachedReplayResultMap()).thenReturn(cachedReplayResultMap); + assertNull(ReplayMatcher.match(requestMocker, MockStrategyEnum.FIND_LAST)); + + Mockito.when(MockUtils.methodRequestTypeHash(requestMocker)).thenReturn(1); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + cachedReplayResultMap.put(1, mergeReplayList); + Mockito.when(MatchStrategyRegister.getMatchStrategies(any())).thenReturn(Collections.singletonList(new AccurateMatchStrategy())); + Mockito.when(Config.get().isEnableDebug()).thenReturn(true); + assertNull(ReplayMatcher.match(requestMocker, MockStrategyEnum.FIND_LAST)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java new file mode 100644 index 000000000..57783b3b4 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.request; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MergeRecordDubboRequestHandlerTest { + + @Test + void name() { + assertNotNull(new MergeRecordDubboRequestHandler().name()); + } + + @Test + void handleAfterCreateContext() { + assertDoesNotThrow(() -> new MergeRecordDubboRequestHandler().handleAfterCreateContext("mock")); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java new file mode 100644 index 000000000..74cf18180 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.request; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MergeRecordServletRequestHandlerTest { + + @Test + void name() { + assertNotNull(new MergeRecordServletRequestHandler().name()); + } + + @Test + void handleAfterCreateContext() { + assertDoesNotThrow(() -> new MergeRecordServletRequestHandler().handleAfterCreateContext("mock")); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java index fab9cf0f9..2337364f8 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java @@ -21,6 +21,7 @@ static void setUp() { Mockito.when(requestHandler.name()).thenReturn("test"); Mockito.when(requestHandlerError.name()).thenReturn("testError"); Mockito.doThrow(new RuntimeException()).when(requestHandlerError).preHandle("request"); + Mockito.doThrow(new RuntimeException()).when(requestHandlerError).handleAfterCreateContext("request"); Mockito.doThrow(new RuntimeException()).when(requestHandlerError).postHandle("request", "response"); Mockito.when(ServiceLoader.load(RequestHandler.class)).thenReturn(Arrays.asList(requestHandler, requestHandlerError)); RequestHandlerManager.init(); @@ -46,6 +47,19 @@ void preHandle() { } + @Test + void handleAfterCreateContext() { + // null handler + RequestHandlerManager.handleAfterCreateContext(null, "test2"); + Mockito.verify(requestHandler, Mockito.never()).handleAfterCreateContext("test2"); + // normal + RequestHandlerManager.handleAfterCreateContext("request", "test"); + Mockito.verify(requestHandler, Mockito.times(1)).handleAfterCreateContext("request"); + // error + assertDoesNotThrow(() -> RequestHandlerManager.handleAfterCreateContext("request", "testError")); + + } + @Test void postHandle() { // null handler diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java index 643558c11..ba3b7cc3c 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java @@ -1,11 +1,11 @@ package io.arex.inst.runtime.util; -import io.arex.agent.bootstrap.model.ArexMocker; -import io.arex.agent.bootstrap.model.MockCategoryType; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.service.DataCollector; +import io.arex.inst.runtime.service.DataService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -15,41 +15,54 @@ class CaseManagerTest { static MockedStatic contextManagerMocked; + static DataCollector dataCollector; @BeforeAll static void setUp() { Serializer.builder(new TestJacksonSerializable()).build(); contextManagerMocked = Mockito.mockStatic(ContextManager.class); + dataCollector = Mockito.mock(DataCollector.class); + DataService.builder().setDataCollector(dataCollector).build(); } @AfterAll static void tearDown() { contextManagerMocked = null; + dataCollector = null; + DataService.builder().setDataCollector(null).build(); Mockito.clearAllCaches(); } @Test void invalid() { // empty recordId - Assertions.assertDoesNotThrow(() -> CaseManager.invalid(null, "testOperationName")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid(null, null, "testOperationName", "queueOverflow")); Assertions.assertTrue(CaseManager.isInvalidCase(null)); - final ArexContext context = ArexContext.of("testRecordId"); - Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + ArexContext context = ArexContext.of("testRecordId"); + Mockito.when(ContextManager.getContext("testRecordId")).thenReturn(context); Assertions.assertFalse(context.isInvalidCase()); Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); - - CaseManager.invalid("testRecordId", "testOperationName"); + System.setProperty("arex.service.name", "testServiceName"); + CaseManager.invalid("testRecordId", null, "testOperationName", "queueOverflow"); Assertions.assertTrue(context.isInvalidCase()); Assertions.assertTrue(CaseManager.isInvalidCase("testRecordId")); + Mockito.verify(dataCollector, Mockito.times(1)).invalidCase(Mockito.anyString()); + + // replayId + CaseManager.invalid("testRecordId", "testReplayId", "testOperationName", "queueOverflow"); + context = ArexContext.of("testRecordId", "testReplayId"); + Mockito.when(ContextManager.getContext("testReplayId")).thenReturn(context); + Assertions.assertFalse(context.isInvalidCase()); + Assertions.assertFalse(CaseManager.isInvalidCase("testReplayId")); // test invalid case with null context - Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(null); + Mockito.when(ContextManager.getContext("testRecordId")).thenReturn(null); Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); - Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", null, "testOperationName", "queueOverflow")); // test invalid case with exception - Mockito.when(ContextManager.getRecordContext("testRecordId")).thenThrow(new RuntimeException("test exception")); - Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + Mockito.when(ContextManager.getContext("testRecordId")).thenThrow(new RuntimeException("test exception")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", null, "testOperationName", "queueOverflow")); } -} \ No newline at end of file +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/IgnoreUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/IgnoreUtilsTest.java index 09417571a..b26183126 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/IgnoreUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/IgnoreUtilsTest.java @@ -110,5 +110,7 @@ void ignoreOperation_excludePathList() { void invalidOperation() { IgnoreUtils.addInvalidOperation("testClass.testMethod"); assertTrue(IgnoreUtils.invalidOperation("testClass.testMethod")); + IgnoreUtils.clearInvalidOperation(); + assertFalse(IgnoreUtils.invalidOperation("testClass.testMethod")); } } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java new file mode 100644 index 000000000..be2df729a --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java @@ -0,0 +1,180 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.Assert; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class MergeRecordReplayUtilTest { + static MockedStatic mockUtils; + static AgentSizeOf agentSizeOf; + static ArexMocker requestMocker; + + @BeforeAll + static void setUp() { + agentSizeOf = Mockito.mock(AgentSizeOf.class); + Mockito.mockStatic(AgentSizeOf.class); + Mockito.when(AgentSizeOf.newInstance()).thenReturn(agentSizeOf); + mockUtils = Mockito.mockStatic(MockUtils.class); + Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + requestMocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + requestMocker.setOperationName("mock"); + requestMocker.setTargetRequest(new Mocker.Target()); + requestMocker.setTargetResponse(new Mocker.Target()); + Mockito.when(MockUtils.create(any(), any())).thenReturn(requestMocker); + Mockito.mockStatic(Serializer.class); + } + + @AfterAll + static void tearDown() { + mockUtils = null; + agentSizeOf = null; + requestMocker = null; + Mockito.clearAllCaches(); + } + + @ParameterizedTest + @MethodSource("mergeRecordCase") + void mergeRecord(Runnable mocker, Mocker requestMocker, Assert asserts) { + mocker.run(); + MergeRecordReplayUtil.mergeRecord(requestMocker); + assertDoesNotThrow(asserts::verity); + } + + static Stream mergeRecordCase() { + Runnable emptyMocker = () -> {}; + ArexContext context = Mockito.mock(ArexContext.class); + LinkedBlockingQueue mergeRecordQueue = new LinkedBlockingQueue<>(1); + Runnable mocker1 = () -> { + mergeRecordQueue.offer(new MergeDTO()); + Mockito.when(context.getMergeRecordQueue()).thenReturn(mergeRecordQueue); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + }; + Runnable mocker2 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); + }; + Runnable mocker3 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(0); + }; + Runnable mocker4 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(1); + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(true); + }; + Runnable mocker5 = () -> { + mergeRecordQueue.poll(); + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(false); + }; + Assert asserts1 = () -> { + mockUtils.verify(() -> MockUtils.executeRecord(any()), times(0)); + }; + Assert asserts2 = () -> { + mockUtils.verify(() -> MockUtils.executeRecord(any()), atLeastOnce()); + }; + return Stream.of( + arguments(emptyMocker, requestMocker, asserts1), + arguments(mocker1, requestMocker, asserts1), + arguments(mocker2, requestMocker, asserts1), + arguments(mocker3, requestMocker, asserts1), + arguments(mocker4, requestMocker, asserts2), + arguments(mocker5, requestMocker, asserts2) + ); + } + + @ParameterizedTest + @MethodSource("recordRemainCase") + void recordRemain(ArexContext context, Predicate asserts) { + MergeRecordReplayUtil.recordRemain(context); + asserts.test(context); + } + + static Stream recordRemainCase() { + Supplier contextSupplier1 = () -> ArexContext.of("mock"); + Supplier contextSupplier2 = () -> { + ArexContext arexContext = contextSupplier1.get(); + arexContext.getMergeRecordQueue().offer(new MergeDTO()); + return arexContext; + }; + + Predicate asserts1 = Objects::isNull; + Predicate asserts2 = Objects::nonNull; + return Stream.of( + arguments(null, asserts1), + arguments(contextSupplier1.get(), asserts2), + arguments(contextSupplier2.get(), asserts2) + ); + } + + @ParameterizedTest + @MethodSource("mergeReplayCase") + void mergeReplay(Runnable mocker) { + mocker.run(); + assertDoesNotThrow(MergeRecordReplayUtil::mergeReplay); + } + + static Stream mergeReplayCase() { + Runnable emptyMocker = () -> {}; + Runnable mocker1 = () -> { + Mockito.when(ContextManager.needReplay()).thenReturn(true); + ArexContext context = Mockito.mock(ArexContext.class); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + Map> cachedReplayResultMap = new HashMap<>(); + Mockito.when(context.getCachedReplayResultMap()).thenReturn(cachedReplayResultMap); + }; + Runnable mocker2 = () -> { + Mockito.when(MockUtils.checkResponseMocker(any())).thenReturn(true); + requestMocker.getTargetResponse().setBody("mock"); + Mockito.when(MockUtils.executeReplay(any(), any())).thenReturn(requestMocker); + }; + Runnable mocker3 = () -> { + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeDTO.setMethodRequestTypeHash(1); + mergeReplayList.add(mergeDTO); + Mockito.when(Serializer.deserialize(anyString(), anyString())).thenReturn(mergeReplayList); + }; + return Stream.of( + arguments(emptyMocker), + arguments(mocker1), + arguments(mocker2), + arguments(mocker3) + ); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java index 8bc650f11..4bae6e0dc 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java @@ -6,11 +6,13 @@ import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.Mocker; import io.arex.inst.runtime.config.ConfigBuilder; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.listener.EventProcessorTest.TestGsonSerializer; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; +import io.arex.inst.runtime.match.ReplayMatcher; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.service.DataCollector; @@ -39,6 +41,8 @@ static void setUp() { list.add(new TestJacksonSerializable()); list.add(new TestGsonSerializer()); Serializer.builder(list).build(); + Mockito.mockStatic(MergeRecordReplayUtil.class); + Mockito.mockStatic(ReplayMatcher.class); } @AfterAll @@ -57,6 +61,16 @@ void recordMocker() { // invalid case Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(true); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); + + // merge case + Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(false); + ArexMocker servletMocker = MockUtils.createServlet("mock"); + servletMocker.setMerge(true); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); + + // remain case + servletMocker.setMerge(false); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); } @Test @@ -72,10 +86,29 @@ void replayMocker() { configBuilder.build(); String responseJson = "{\"id\":\"64ec180f7071c91a03cde866\",\"categoryType\":{\"name\":\"DynamicClass\",\"entryPoint\":false,\"skipComparison\":true},\"replayId\":null,\"recordId\":\"AREX-10-4-202-26-46993323299502\",\"appId\":\"arex-test-app\",\"recordEnvironment\":0,\"creationTime\":1693194255518,\"updateTime\":0,\"expirationTime\":1693539855663,\"targetRequest\":{\"body\":null,\"attributes\":null,\"type\":null},\"targetResponse\":{\"body\":\"1693194255518\",\"attributes\":null,\"type\":\"java.lang.Long\"},\"operationName\":\"java.lang.System.currentTimeMillis\",\"recordVersion\":\"0.3.8\"}"; Mockito.when(dataCollector.query(anyString(), any())).thenReturn(responseJson); - Mockito.when(ContextManager.currentContext()).thenReturn(ArexContext.of("mock-trace-id")); + Mockito.when(CaseManager.isInvalidCase("mock-replay-id")).thenReturn(false); + Mockito.when(ContextManager.currentContext()).thenReturn(ArexContext.of("mock-trace-id", "mock-replay-id")); dynamicClass = MockUtils.createDynamicClass("test", "test"); Object actualResult = MockUtils.replayBody(dynamicClass); assertEquals(1693194255518L, actualResult); + + // invalid case + Mockito.when(CaseManager.isInvalidCase("mock-replay-id")).thenReturn(true); + assertNull(MockUtils.replayBody(dynamicClass)); + + // null replayId but is not config file + Mockito.when(CaseManager.isInvalidCase(null)).thenReturn(true); + Mockito.when(ContextManager.currentContext()).thenReturn(ArexContext.of("mock-trace-id", null)); + assertNull(MockUtils.replayBody(dynamicClass)); + + // null replayId and is config file + ArexMocker configFile = MockUtils.createConfigFile("test"); + assertNotNull(MockUtils.replayBody(configFile)); + + // merge case + configFile.setMerge(true); + Mockito.when(ReplayMatcher.match(any(), any())).thenReturn(configFile); + assertNull(MockUtils.replayBody(configFile)); } @Test @@ -137,4 +170,12 @@ void createMocker() { actualResult = MockUtils.createDubboStreamProvider("query"); assertEquals(MockCategoryType.DUBBO_STREAM_PROVIDER, actualResult.getCategoryType()); } -} \ No newline at end of file + + @Test + void methodSignatureHash() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); + assertTrue(MockUtils.methodSignatureHash(mocker) > 0); + } +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java index 127bd6e27..50c1b99ea 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/TypeUtilTest.java @@ -11,7 +11,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Stream; @@ -83,10 +82,8 @@ public static Stream forNameArguments() { return "java.util.ArrayList".equals(parameterizedType.getRawType().getTypeName()) && "java.lang.String".equals(parameterizedType.getActualTypeArguments()[0].getTypeName()); }), - arguments("java.util.HashMap$Values-java.lang.String", (Predicate) type -> { - final Class rawClass = TypeUtil.getRawClass(type); - return "java.util.HashMap$Values".equals(rawClass.getName()); - }) + arguments("java.util.HashMap$Values-java.lang.String", (Predicate) type -> + "java.util.List".equals(type.getTypeName())) ); } @@ -104,6 +101,11 @@ void getRawClass() { void testDoubleMap() { Map> map = new HashMap<>(); Map innerMap = new HashMap<>(); + // value is empty map + map.put("emptyMap", new HashMap<>()); + String valueEmptyMapActualResult = TypeUtil.getName(map); + assertEquals("java.util.HashMap-", valueEmptyMapActualResult); + innerMap.put("key1", LocalDateTime.now()); map.put("key", innerMap); String actualResult = TypeUtil.getName(map); @@ -128,6 +130,11 @@ void testListMap() { @Test void testMapList() { Map> map = new HashMap<>(); + // value is empty list + map.put("emptyList", new ArrayList<>()); + String valueEmptyListActualResult = TypeUtil.getName(map); + assertEquals("java.util.HashMap-", valueEmptyListActualResult); + List innerList = new ArrayList<>(); innerList.add(LocalDateTime.now()); map.put("key", innerList); @@ -166,11 +173,15 @@ void testDoubleGenericType() { final Pair pairNull = Pair.of(System.currentTimeMillis(), null); final String genericNull = TypeUtil.getName(pairNull); - assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,", genericNull); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.lang.String", genericNull); final Pair pairList = Pair.of(System.currentTimeMillis(), Arrays.asList("mock")); final String genericList = TypeUtil.getName(pairList); assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.util.Arrays$ArrayList-java.lang.String", genericList); + + final Pair pairFirstNull = Pair.of(null, System.currentTimeMillis()); + final String genericFirstNull = TypeUtil.getName(pairFirstNull); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.String,java.lang.Long", genericFirstNull); } @Test @@ -359,13 +370,28 @@ void testGenericFieldInFather() { final ArrayList list = new ArrayList<>(); childClass.setValue(list); String name = TypeUtil.getName(childClass); - assertEquals("io.arex.inst.runtime.util.TypeUtilTest$ChildClass-", name); + assertEquals("io.arex.inst.runtime.util.TypeUtilTest$ChildClass-java.lang.String", name); list.add("test"); childClass.setValue(list); name = TypeUtil.getName(childClass); assertEquals("io.arex.inst.runtime.util.TypeUtilTest$ChildClass-java.lang.String", name); } + @Test + void testNeedUseDefaultCollection() { + Type type = TypeUtil.forName("io.arex.inst.runtime.util.TypeUtilTest$FlightCollection-java.time.LocalDateTime"); + assert type != null; + assertEquals("io.arex.inst.runtime.util.TypeUtilTest$FlightCollection", type.getTypeName()); + type = TypeUtil.forName("com.google.common.collect.AbstractMapBasedMultimap$RandomAccessWrappedList-java.time.LocalDateTime"); + assert type != null; + assertEquals("java.util.List", type.getTypeName()); + type = TypeUtil.forName("com.google.common.collect.AbstractMultiset$ElementSet-java.time.LocalDateTime"); + assert type != null; + assertEquals("java.util.Set", type.getTypeName()); + } + + public static class FlightCollection extends ArrayList { + } static class SingleTypeMap extends HashMap { } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java new file mode 100644 index 000000000..ade753183 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java @@ -0,0 +1,62 @@ +package io.arex.inst.runtime.util.sizeof; + +import io.arex.agent.bootstrap.InstrumentationHolder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.lang.instrument.Instrumentation; + +import static org.junit.jupiter.api.Assertions.*; + +class AgentSizeOfTest { + + static AgentSizeOf caller; + static Instrumentation instrumentation; + + @BeforeAll + static void setUp() { + caller = AgentSizeOf.newInstance(); + Mockito.mockStatic(InstrumentationHolder.class); + instrumentation = Mockito.mock(Instrumentation.class); + } + + @AfterAll + static void tearDown() { + caller = null; + instrumentation = null; + Mockito.clearAllCaches(); + } + + @Test + void deepSizeOf() { + assertEquals(caller.deepSizeOf("mock"), 0); + } + + @Test + void sizeOf() { + assertEquals(caller.sizeOf("mock"), 0); + Mockito.when(InstrumentationHolder.getInstrumentation()).thenReturn(instrumentation); + caller = AgentSizeOf.newInstance(); + assertEquals(caller.sizeOf("mock"), 0); + } + + @ParameterizedTest + @CsvSource({ + "1, 1 bytes", + "1024, 1 KB", + "1048576, 1 MB", + "1073741824, 1 GB" + }) + void humanReadableUnits(long bytes, String expected) { + assertEquals(AgentSizeOf.humanReadableUnits(bytes), expected); + } + + @Test + void checkMemorySizeLimit() { + assertTrue(caller.checkMemorySizeLimit("mock", 1)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java new file mode 100644 index 000000000..8b53e0b45 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java @@ -0,0 +1,40 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class CombinationSizeOfFilterTest { + + static CombinationSizeOfFilter caller; + static SizeOfFilter filter; + + @BeforeAll + static void setUp() { + filter = Mockito.mock(SizeOfFilter.class); + caller = new CombinationSizeOfFilter(filter); + } + + @AfterAll + static void tearDown() { + caller = null; + filter = null; + Mockito.clearAllCaches(); + } + + @Test + void filterFields() { + assertNotNull(caller.filterFields(null, null)); + } + + @Test + void filterClass() { + assertFalse(caller.filterClass(null)); + Mockito.when(filter.filterClass(any())).thenReturn(true); + assertTrue(caller.filterClass(null)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java new file mode 100644 index 000000000..006ae63f9 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java @@ -0,0 +1,51 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; + +import static org.mockito.ArgumentMatchers.any; + +class ObjectGraphWalkerTest { + static ObjectGraphWalker caller; + static SizeOfFilter sizeOfFilter; + static ObjectGraphWalker.Visitor visitor; + + @BeforeAll + static void setUp() { + sizeOfFilter = Mockito.mock(SizeOfFilter.class); + visitor = Mockito.mock(ObjectGraphWalker.Visitor.class); + caller = new ObjectGraphWalker(visitor, sizeOfFilter); + } + + @AfterAll + static void tearDown() { + caller = null; + sizeOfFilter = null; + visitor = null; + Mockito.clearAllCaches(); + } + + @Test + void walk() { + caller.walk(null); + } + + @Test + void testWalk() { + VisitorListener visitorListener = Mockito.mock(VisitorListener.class); + Mockito.when(sizeOfFilter.filterClass(any())).thenReturn(true); + Collection result = new ArrayList<>(); + result.add(TestWalker.class.getDeclaredFields()[0]); + Mockito.when(sizeOfFilter.filterFields(any(), any())).thenReturn(result); + caller.walk(visitorListener, new Object[]{new TestWalker[]{new TestWalker()}}); + } + + static class TestWalker { + String name; + } +} \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java index 887c1c309..b0b145e1d 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/config/ConfigManager.java @@ -330,31 +330,29 @@ private static Map parseConfigFile(String configPath) { public void parseAgentConfig(String args) { Map agentMap = StringUtil.asMap(args); - if (!agentMap.isEmpty()) { - for (Map.Entry entry : agentMap.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (StringUtil.isEmpty(key) || StringUtil.isEmpty(value)) { - continue; - } - - switch (key) { - case ENABLE_DEBUG: - setEnableDebug(value); - break; - case STORAGE_SERVICE_MODE: - setStorageServiceMode(value); - break; - case STORAGE_SERVICE_HOST: - case DISABLE_MODULE: - continue; - default: - System.setProperty(key, value); - break; - } + if (agentMap.isEmpty()) { + return; + } + for (Map.Entry entry : agentMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + switch (key) { + case ENABLE_DEBUG: + setEnableDebug(value); + break; + case STORAGE_SERVICE_MODE: + setStorageServiceMode(value); + break; + case STORAGE_SERVICE_HOST: + case DISABLE_MODULE: + break; + default: + System.setProperty(key, value); + break; } - updateRuntimeConfig(); } + updateRuntimeConfig(); } public void updateConfigFromService(ResponseBody serviceConfig) { @@ -385,6 +383,7 @@ private void updateRuntimeConfig() { Map extendFieldMap = getExtendField(); if (MapUtils.isNotEmpty(extendFieldMap)) { configMap.putAll(extendFieldMap); + setEnableDebug(extendFieldMap.get(ENABLE_DEBUG)); } ConfigBuilder.create(getServiceName()) @@ -557,6 +556,7 @@ public void setExcludeServiceOperations(String excludeServiceOperations) { public void setExcludeServiceOperations(Set excludeServiceOperationSet) { if (CollectionUtil.isEmpty(excludeServiceOperationSet)) { + this.excludeServiceOperations = Collections.emptySet(); return; } this.excludeServiceOperations = excludeServiceOperationSet; diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ArexObjectMapper.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ArexObjectMapper.java new file mode 100644 index 000000000..ec7d4072c --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ArexObjectMapper.java @@ -0,0 +1,135 @@ +package io.arex.foundation.serializer; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; +import com.fasterxml.jackson.databind.introspect.BasicBeanDescription; +import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector; +import com.fasterxml.jackson.databind.introspect.ClassIntrospector; +import com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector; +import io.arex.inst.runtime.log.LogManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * When the class does not have a no-argument constructor, + * create a custom-built no-argument constructor logic + */ +public class ArexObjectMapper extends ObjectMapper { + private static boolean noCollectMethodWithFiveParameter; + private static final AtomicBoolean HAS_EXCEPTION = new AtomicBoolean(false); + static { + try { + BasicClassIntrospector.class.getDeclaredMethod("collectProperties", + MapperConfig.class, JavaType.class, ClassIntrospector.MixInResolver.class, boolean.class, String.class); + } catch (Exception e) { + LogManager.warn("objectMapper.init", e); + // jackson high version delete this method, example: 2.12.x + noCollectMethodWithFiveParameter = true; + } + } + @Override + protected ClassIntrospector defaultClassIntrospector() { + return new ArexBasicClassIntrospector(); + } + + static class ArexBasicClassIntrospector extends BasicClassIntrospector { + private static final Class CLS_OBJECT = Object.class; + private static final Class CLS_STRING = String.class; + private static final Class CLS_JSON_NODE = JsonNode.class; + @Override + public BasicBeanDescription forDeserialization(DeserializationConfig config, JavaType type, MixInResolver r) { + BasicBeanDescription desc = super.forDeserialization(config, type, r); + + if (HAS_EXCEPTION.get()) { + return desc; + } + + if (isMissingDefaultConstructor(type, desc)) { + try { + if (noCollectMethodWithFiveParameter) { + return new ArexBasicBeanDescription(collectProperties(config, + type, r, false)); + } + return new ArexBasicBeanDescription(collectProperties(config, + type, r, false, null)); + } catch (Throwable e) { + LogManager.warn("forDeserialization", e); + HAS_EXCEPTION.set(true); + } + } + return desc; + } + + private boolean isMissingDefaultConstructor(JavaType type, BasicBeanDescription desc) { + if (desc.findDefaultConstructor() != null) { + return false; + } + if (isStdTypeDesc(type)) { + return false; + } + return !super._isStdJDKCollection(type); + } + + private boolean isStdTypeDesc(JavaType type) { + Class cls = type.getRawClass(); + if (cls.isPrimitive()) { + return cls == Integer.TYPE || cls == Long.TYPE || cls == Boolean.TYPE; + } + + if (cls == CLS_OBJECT || cls == CLS_STRING || cls == Integer.class || cls == Long.class || cls == Boolean.class) { + return true; + } + + return CLS_JSON_NODE.isAssignableFrom(cls); + } + } + + static class ArexBasicBeanDescription extends BasicBeanDescription { + private static Constructor objectConstructor; + private static Method constructorForSerialization; + private static Object reflectionFactory; + + static { + try { + objectConstructor = Object.class.getDeclaredConstructor(); + Class reflectionFactoryClass = Class.forName("sun.reflect.ReflectionFactory"); + Method getReflectionFactoryMethod = reflectionFactoryClass.getDeclaredMethod("getReflectionFactory"); + reflectionFactory = getReflectionFactoryMethod.invoke(null); + + Class[] parameterTypes = {Class.class, Constructor.class}; + constructorForSerialization = reflectionFactoryClass.getDeclaredMethod("newConstructorForSerialization", parameterTypes); + } catch (Exception e) { + LogManager.warn("ArexBasicBeanDescription", e); + } + } + + public ArexBasicBeanDescription(POJOPropertiesCollector coll) { + super(coll); + } + + @Override + public AnnotatedConstructor findDefaultConstructor() { + AnnotatedConstructor annotatedConstructor = super.findDefaultConstructor(); + if (annotatedConstructor == null) { + try { + // no default constructor, create one for deserialize + Constructor defaultConstructor = + (Constructor) constructorForSerialization.invoke(reflectionFactory, this.getBeanClass(), objectConstructor); + annotatedConstructor = new + AnnotatedConstructor(null, defaultConstructor, null, null); + } catch (Throwable ex) { + LogManager.warn("findDefaultConstructor", ex); + HAS_EXCEPTION.set(true); + } + } + return annotatedConstructor; + } + } + +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java index b2dee0f0c..179e109e8 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/GsonSerializer.java @@ -4,20 +4,23 @@ import com.google.common.collect.Range; import io.arex.agent.thirdparty.util.time.DateFormatUtils; +import io.arex.foundation.serializer.custom.MultiTypeElement; import io.arex.foundation.serializer.JacksonSerializer.DateFormatParser; import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; import io.arex.foundation.serializer.custom.GuavaRangeSerializer; -import io.arex.foundation.serializer.custom.NumberTypeAdaptor; import io.arex.agent.bootstrap.util.StringUtil; import com.google.gson.Gson; import com.google.gson.JsonDeserializer; +import io.arex.foundation.serializer.custom.NumberStrategy; import io.arex.foundation.serializer.custom.ProtobufAdapterFactory; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.util.TypeUtil; + import java.sql.Time; import java.time.Instant; +import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Map.Entry; import org.joda.time.DateTime; @@ -156,6 +159,17 @@ public class GsonSerializer implements StringSerializable { return (Class) TypeUtil.forName(json.getAsString()); }; + private static final JsonSerializer OFFSET_DATE_TIME_JSON_SERIALIZER = + ((src, typeOfSrc, context) -> new JsonPrimitive(DateFormatUtils.format(src, JacksonSerializer.DatePatternConstants.SIMPLE_DATE_FORMAT_WITH_TIMEZONE_DATETIME))); + + private static final JsonDeserializer OFFSET_DATE_TIME_JSON_DESERIALIZER = (json, typeOfT, context) -> + OffsetDateTime.parse(json.getAsString(), DateFormatParser.INSTANCE.getFormatter(JacksonSerializer.DatePatternConstants.SIMPLE_DATE_FORMAT_WITH_TIMEZONE_DATETIME)); + + private static final JsonSerializer TIMEZONE_JSON_SERIALIZER = (timeZone, type, context) -> new JsonPrimitive(timeZone.getID()); + + private static final JsonDeserializer TIMEZONE_JSON_DESERIALIZER = + (json, typeOfT, context) -> TimeZone.getTimeZone(json.getAsString()); + public static final GsonSerializer INSTANCE = new GsonSerializer(); private Gson serializer; private GsonBuilder gsonBuilder; @@ -172,7 +186,7 @@ public void addTypeSerializer(Class clazz, Object typeSerializer) { } public GsonSerializer() { - gsonBuilder = new GsonBuilder().registerTypeAdapterFactory(NumberTypeAdaptor.FACTORY) + gsonBuilder = new GsonBuilder() .registerTypeAdapter(DateTime.class, DATE_TIME_JSON_SERIALIZER) .registerTypeAdapter(DateTime.class, DATE_TIME_JSON_DESERIALIZER) .registerTypeAdapter(org.joda.time.LocalDateTime.class, JODA_LOCAL_DATE_TIME_JSON_SERIALIZER) @@ -205,19 +219,22 @@ public GsonSerializer() { .registerTypeAdapter(Instant.class, INSTANT_JSON_DESERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_SERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_DESERIALIZER) + .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME_JSON_SERIALIZER) + .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME_JSON_DESERIALIZER) + .registerTypeHierarchyAdapter(TimeZone.class, TIMEZONE_JSON_SERIALIZER) + .registerTypeAdapter(TimeZone.class, TIMEZONE_JSON_DESERIALIZER) .registerTypeAdapter(Range.class, new GuavaRangeSerializer.GsonRangeSerializer()) .registerTypeAdapterFactory(new FastUtilAdapterFactory()) .registerTypeAdapterFactory(new ProtobufAdapterFactory()) .enableComplexMapKeySerialization() .setExclusionStrategies(new ExcludeField()) - .disableHtmlEscaping(); + .disableHtmlEscaping() + .setObjectToNumberStrategy(new NumberStrategy()); serializer = gsonBuilder.create(); } - static class MapSerializer implements JsonSerializer>, JsonDeserializer> { - - private static final Character SEPARATOR = '-'; + public static class MapSerializer implements JsonSerializer>, JsonDeserializer> { @Override public JsonElement serialize(Map document, Type type, JsonSerializationContext context) { @@ -225,17 +242,12 @@ public JsonElement serialize(Map document, Type type, JsonSerial for (Map.Entry entry : document.entrySet()) { final Object value = entry.getValue(); - if (value == null) { - jsonObject.add(entry.getKey(), null); - continue; - } - - if (value instanceof String) { + if (value == null || value instanceof String) { jsonObject.addProperty(entry.getKey(), (String) value); continue; } - jsonObject.add(entry.getKey() + SEPARATOR + TypeUtil.getName(value), - context.serialize(value)); + + jsonObject.add(entry.getKey(), context.serialize(new MultiTypeElement(context.serialize(value), TypeUtil.getName(value)))); } return jsonObject; } @@ -249,22 +261,21 @@ public Map deserialize(JsonElement json, Type typeOfT, // only support no-arg constructor final Map map = (Map) ((Class) typeOfT).getDeclaredConstructor(null).newInstance(); for (Entry entry : jsonObject.entrySet()) { - final String[] split = StringUtil.splitByFirstSeparator(entry.getKey(), SEPARATOR); - if (split.length < 2) { - map.put(entry.getKey(), context.deserialize(entry.getValue(), String.class)); + final String key = entry.getKey(); + final JsonElement jsonElement = entry.getValue(); + if (jsonElement instanceof JsonObject) { + final MultiTypeElement mapValueElement = context.deserialize(jsonElement, MultiTypeElement.class); + final Object value = context.deserialize(mapValueElement.getValue(), + TypeUtil.forName(mapValueElement.getType())); + map.put(key, value); continue; } - String valueClazz = split[1]; - String key = split[0]; - final JsonElement valueJson = entry.getValue(); - final Type valueType = TypeUtil.forName(valueClazz); - final Object value = context.deserialize(valueJson, valueType); - map.put(key, value); + map.put(key, jsonElement.getAsString()); } return map; } catch (Exception e) { LogManager.warn("MapSerializer.deserialize", e); - return null; + return Collections.emptyMap(); } } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java index 280f6ebc0..16ca8c53a 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/JacksonSerializer.java @@ -45,6 +45,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -59,7 +60,7 @@ public final class JacksonSerializer implements StringSerializable { private static final Logger LOGGER = LoggerFactory.getLogger(JacksonSerializer.class); - private final ObjectMapper MAPPER = new ObjectMapper(); + private final ObjectMapper MAPPER = new ArexObjectMapper(); private final Map> skipInfoMap = new ConcurrentHashMap<>(); private static final SimpleModule MODULE = new JacksonSimpleModule(); @@ -80,11 +81,15 @@ public JacksonSerializer() { } private void customTypeResolver() { - TypeResolverBuilder typeResolver = new CustomTypeResolverBuilder(); - typeResolver.init(JsonTypeInfo.Id.CLASS, null); - typeResolver.inclusion(JsonTypeInfo.As.PROPERTY); - typeResolver.typeProperty("@CLASS"); - MAPPER.setDefaultTyping(typeResolver); + try { + TypeResolverBuilder typeResolver = new CustomTypeResolverBuilder(); + typeResolver.init(JsonTypeInfo.Id.CLASS, null); + typeResolver.inclusion(JsonTypeInfo.As.PROPERTY); + typeResolver.typeProperty("@CLASS"); + MAPPER.setDefaultTyping(typeResolver); + } catch (Throwable ignored) { + // jackson version is too low, ignore + } } private void buildSkipInfoMap() { @@ -195,6 +200,7 @@ private void customTimeFormatSerializer(SimpleModule module) { module.addSerializer(Date.class, new DateSerialize()); module.addSerializer(Instant.class, new InstantSerialize()); module.addSerializer(Range.class, new GuavaRangeSerializer.JacksonRangeSerializer()); + module.addSerializer(OffsetDateTime.class, new OffSetDateTimeSerialize()); } private void customTimeFormatDeserializer(SimpleModule module) { @@ -214,6 +220,7 @@ private void customTimeFormatDeserializer(SimpleModule module) { module.addDeserializer(Time.class, new SqlTimeDeserialize()); module.addDeserializer(Instant.class, new InstantDeserialize()); module.addDeserializer(Range.class, new GuavaRangeSerializer.JacksonRangeDeserializer()); + module.addDeserializer(OffsetDateTime.class, new OffSetDateTimeDeserialize()); } private static class JacksonSimpleModule extends SimpleModule { @@ -565,6 +572,23 @@ public Instant deserialize(JsonParser p, DeserializationContext ctxt) } } + static class OffSetDateTimeSerialize extends JsonSerializer { + + @Override + public void serialize(OffsetDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(DateFormatUtils.format(value, DatePatternConstants.SIMPLE_DATE_FORMAT_WITH_TIMEZONE)); + } + } + + static class OffSetDateTimeDeserialize extends JsonDeserializer { + + @Override + public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + return OffsetDateTime.parse(node.asText(), DateFormatParser.INSTANCE.getFormatter(JacksonSerializer.DatePatternConstants.SIMPLE_DATE_FORMAT_WITH_TIMEZONE_DATETIME)); + } + } + // endregion @@ -757,8 +781,9 @@ public CustomTypeResolverBuilder() { */ @Override public boolean useForType(JavaType type) { - return type.getRawClass().isInterface() && - StringUtil.startWith(type.getRawClass().getName(), FastUtilAdapterFactory.FASTUTIL_PACKAGE); + Class rawClass = type.getRawClass(); + return rawClass.isInterface() && + StringUtil.startWith(rawClass.getName(), FastUtilAdapterFactory.FASTUTIL_PACKAGE); } } } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ProtoJsonSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ProtoJsonSerializer.java index 796851c13..ad0dc8dfb 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ProtoJsonSerializer.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/ProtoJsonSerializer.java @@ -6,43 +6,42 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Type; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.log.LogManager; -import io.arex.inst.runtime.serializer.StringSerializable; -import io.arex.inst.runtime.util.TypeUtil; -public class ProtoJsonSerializer implements StringSerializable{ +public class ProtoJsonSerializer{ + private static JsonFormat.Printer jsonPrinter; + private static JsonFormat.Parser jsonParser; + + static { + try { + jsonPrinter = JsonFormat.printer().omittingInsignificantWhitespace(); + jsonParser = JsonFormat.parser().ignoringUnknownFields(); + } catch (Exception e) { + LogManager.warn("proto-serializer", e); + } + } private static final ProtoJsonSerializer INSTANCE = new ProtoJsonSerializer(); - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer().omittingInsignificantWhitespace(); - private static final JsonFormat.Parser JSON_PARSER = JsonFormat.parser().ignoringUnknownFields(); public static ProtoJsonSerializer getInstance() { return INSTANCE; } - @Override - public String name() { - return "protoJson"; - } - /** * for the type inside the Collection is the PB type, * traverse the elements inside the Collection and serialize them one by one with separators */ - @Override public String serialize(Object object) { try { - return JSON_PRINTER.print((AbstractMessage) object); + return jsonPrinter.print((AbstractMessage) object); } catch (Throwable e) { LogManager.warn("proto-serialize", e); return StringUtil.EMPTY; } } - @Override public T deserialize(String value, Class clazz) { if (StringUtil.isEmpty(value) || clazz == null) { return null; @@ -51,7 +50,7 @@ public T deserialize(String value, Class clazz) { try { Builder builder = getMessageBuilder(clazz); - JSON_PARSER.merge(value, builder); + jsonParser.merge(value, builder); return (T) builder.build(); } catch (Throwable e) { @@ -60,17 +59,6 @@ public T deserialize(String value, Class clazz) { } } - @Override - public T deserialize(String value, Type type) { - Class rawClass = (Class) TypeUtil.getRawClass(type); - return deserialize(value, rawClass); - } - - @Override - public StringSerializable reCreateSerializer() { - return INSTANCE; - } - /** * Create Class.Builder based on Class reflection for Json deserialization * @param clazz diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/MultiTypeElement.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/MultiTypeElement.java new file mode 100644 index 000000000..001ade686 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/MultiTypeElement.java @@ -0,0 +1,33 @@ +package io.arex.foundation.serializer.custom; + +import com.google.gson.JsonElement; + +public class MultiTypeElement { + private JsonElement value; + private String type; + + public MultiTypeElement() { + } + + public MultiTypeElement(final JsonElement value, final String type) { + this.value = value; + this.type = type; + } + + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonElement getValue() { + return value; + } + + public void setValue(JsonElement value) { + this.value = value; + } +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberStrategy.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberStrategy.java new file mode 100644 index 000000000..bfbe18479 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberStrategy.java @@ -0,0 +1,23 @@ +package io.arex.foundation.serializer.custom; + +import com.google.gson.ToNumberStrategy; +import com.google.gson.stream.JsonReader; + +import java.io.IOException; + +public class NumberStrategy implements ToNumberStrategy { + + @Override + public Number readNumber(JsonReader in) throws IOException { + String source = in.nextString(); + try { + if (source.contains(".")) { + return Double.valueOf(source); + } + return Integer.valueOf(source); + } catch (Exception e) { + // Integer size limit exceeded + return Long.valueOf(source); + } + } +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberTypeAdaptor.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberTypeAdaptor.java deleted file mode 100644 index 077698eee..000000000 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/NumberTypeAdaptor.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.arex.foundation.serializer.custom; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.LinkedTreeMap; -import com.google.gson.internal.bind.ObjectTypeAdapter; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class NumberTypeAdaptor extends TypeAdapter { - public static final TypeAdapterFactory FACTORY = new GsonTypeAdaptFactory(); - - private final Gson gson; - - NumberTypeAdaptor(Gson gson) { - this.gson = gson; - } - - @Override public Object read(JsonReader in) throws IOException { - JsonToken token = in.peek(); - switch (token) { - case BEGIN_ARRAY: - List list = new ArrayList(); - in.beginArray(); - while (in.hasNext()) { - list.add(read(in)); - } - in.endArray(); - return list; - - case BEGIN_OBJECT: - Map map = new LinkedTreeMap(); - in.beginObject(); - while (in.hasNext()) { - map.put(in.nextName(), read(in)); - } - in.endObject(); - return map; - - case STRING: - return in.nextString(); - case NUMBER: - String s = in.nextString(); - if (s.contains(".")) { - return Double.valueOf(s); - } else { - try { - return Integer.valueOf(s); - } catch (Exception e) { - return Long.valueOf(s); - } - } - case BOOLEAN: - return in.nextBoolean(); - - case NULL: - in.nextNull(); - return null; - - default: - throw new IllegalStateException(); - } - } - - @SuppressWarnings("unchecked") - @Override public void write(JsonWriter out, Object value) throws IOException { - if (value == null) { - out.nullValue(); - return; - } - - TypeAdapter typeAdapter = (TypeAdapter) gson.getAdapter(value.getClass()); - if (typeAdapter instanceof ObjectTypeAdapter) { - out.beginObject(); - out.endObject(); - return; - } - - typeAdapter.write(out, value); - } - - static class GsonTypeAdaptFactory implements TypeAdapterFactory { - @SuppressWarnings("unchecked") - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() == Object.class) { - return (TypeAdapter) new NumberTypeAdaptor(gson); - } - return null; - } - } -} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java index 68d90f3b0..5b42cbe78 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/ConfigService.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import io.arex.agent.bootstrap.constants.ConfigConstants; +import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.util.MapUtils; import io.arex.foundation.model.*; import io.arex.foundation.config.ConfigManager; @@ -18,6 +19,7 @@ import java.util.Objects; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +35,7 @@ public class ConfigService { private static final ObjectMapper MAPPER = new ObjectMapper(); public static final ConfigService INSTANCE = new ConfigService(); + private static final String TAGS_PREFIX = "arex.tags."; private static final String CONFIG_LOAD_URI = String.format("http://%s/api/config/agent/load", ConfigManager.INSTANCE.getConfigServiceHost()); @@ -134,13 +137,25 @@ AgentStatusEnum getAgentStatus() { public Map getSystemProperties() { Properties properties = System.getProperties(); - Map map = new HashMap<>(properties.size()); + Map map = MapUtils.newHashMapWithExpectedSize(properties.size()); for (Map.Entry entry : properties.entrySet()) { - map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); + String key = String.valueOf(entry.getKey()); + String value = String.valueOf(entry.getValue()); + map.put(key, value); + buildTags(key, value); } return map; } + /** + * ex: -Darex.tags.xxx=xxx + */ + private void buildTags(String key, String value) { + if (StringUtil.startWith(key, TAGS_PREFIX)) { + ArexMocker.TAGS.put(key.substring(TAGS_PREFIX.length()), value); + } + } + public String serialize(Object object) { if (object == null) { return null; diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java index b7dd64dbd..84b0d262c 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/services/DataCollectorService.java @@ -1,5 +1,6 @@ package io.arex.foundation.services; +import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.util.MapUtils; @@ -8,21 +9,25 @@ import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.internal.DataEntity; import io.arex.foundation.internal.MockEntityBuffer; +import io.arex.foundation.model.DecelerateReasonEnum; import io.arex.foundation.util.httpclient.AsyncHttpClientUtil; import io.arex.foundation.model.HttpClientResponse; import io.arex.foundation.util.httpclient.async.ThreadFactoryImpl; import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.util.CaseManager; import io.arex.inst.runtime.service.DataCollector; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import java.util.function.BiFunction; public class DataCollectorService implements DataCollector { public static final DataCollectorService INSTANCE = new DataCollectorService(); @@ -36,6 +41,7 @@ public class DataCollectorService implements DataCollector { private static String queryApiUrl; private static String saveApiUrl; + private static String invalidCaseApiUrl; static { initServiceHost(); @@ -49,10 +55,15 @@ public void save(Mocker requestMocker) { if (!buffer.put(new DataEntity(requestMocker))) { HealthManager.onEnqueueRejection(); - CaseManager.invalid(requestMocker.getRecordId(), requestMocker.getOperationName()); + CaseManager.invalid(requestMocker.getRecordId(), null, requestMocker.getOperationName(), DecelerateReasonEnum.QUEUE_OVERFLOW.getValue()); } } + @Override + public void invalidCase(String postData) { + AsyncHttpClientUtil.postAsyncWithJson(invalidCaseApiUrl, postData, null); + } + @Override public String query(String postData, MockStrategyEnum mockStrategy) { return queryReplayData(postData, mockStrategy); @@ -126,19 +137,36 @@ void saveData(DataEntity entity) { String queryReplayData(String postData, MockStrategyEnum mockStrategy) { Map requestHeaders = MapUtils.newHashMapWithExpectedSize(1); requestHeaders.put(MOCK_STRATEGY, mockStrategy.getCode()); - HttpClientResponse clientResponse = AsyncHttpClientUtil.postAsyncWithZstdJson(queryApiUrl, postData, - requestHeaders).join(); + + CompletableFuture responseCompletableFuture = AsyncHttpClientUtil.postAsyncWithZstdJson(queryApiUrl, postData, + requestHeaders).handle(queryMockDataFunction(postData)); + + HttpClientResponse clientResponse = responseCompletableFuture.join(); if (clientResponse == null) { return null; } return clientResponse.getBody(); } + private BiFunction queryMockDataFunction(String postData) { + return (response, throwable) -> { + if (Objects.nonNull(throwable)) { + // avoid real calls during replay and return null value when throwable occurs. + Mocker mocker = Serializer.deserialize(postData, ArexMocker.class); + if (mocker != null) { + CaseManager.invalid(mocker.getRecordId(), mocker.getReplayId(), mocker.getOperationName(), DecelerateReasonEnum.SERVICE_EXCEPTION.getValue()); + } + return null; + } + return response; + }; + } + private BiConsumer saveMockDataConsumer(DataEntity entity) { return (response, throwable) -> { long usedTime = System.nanoTime() - entity.getQueueTime(); if (Objects.nonNull(throwable)) { - CaseManager.invalid(entity.getRecordId(), entity.getOperationName()); + CaseManager.invalid(entity.getRecordId(), null, entity.getOperationName(), DecelerateReasonEnum.SERVICE_EXCEPTION.getValue()); LogManager.warn("saveMockDataConsumer", StringUtil.format("save mock data error: %s, post data: %s", throwable.toString(), entity.getPostData())); usedTime = -1; // -1:reject @@ -153,5 +181,6 @@ private static void initServiceHost() { queryApiUrl = String.format("http://%s/api/storage/record/query", storeServiceHost); saveApiUrl = String.format("http://%s/api/storage/record/save", storeServiceHost); + invalidCaseApiUrl = String.format("http://%s/api/storage/record/invalidCase", storeServiceHost); } } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java index 50747d639..e791de971 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/config/ConfigManagerTest.java @@ -103,7 +103,10 @@ void replaceConfigFromService() { @Test void parseAgentConfig() { - configManager.parseAgentConfig("arex.storage.mode=xxx;arex.enable.debug=true"); + // empty + configManager.parseAgentConfig(""); + // normal + configManager.parseAgentConfig("arex.storage.mode=xxx;arex.enable.debug=true;arex.disable.instrumentation.module=dynamic;arex.buffer.size=1024;arex.serializer.config="); assertTrue(configManager.isEnableDebug()); } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java index 695df17d9..4466d3bf7 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/GsonSerializerTest.java @@ -5,9 +5,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.gson.internal.LinkedTreeMap; +import io.arex.agent.bootstrap.internal.Pair; import io.arex.inst.runtime.util.TypeUtil; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; + +import java.lang.reflect.Type; import java.sql.Time; import java.time.LocalDateTime; import java.util.Map; @@ -72,6 +75,7 @@ public void testTimeSerializeAndDeserialize() throws Exception { assert expectedTimeTest.getJodaLocalTime().equals(deserializedTimeTest.getJodaLocalTime()); assert expectedTimeTest.getJodaLocalDateTime().equals(deserializedTimeTest.getJodaLocalDateTime()); assert expectedTimeTest.getDateTime().equals(deserializedTimeTest.getDateTime()); + assert expectedTimeTest.getTimeZone1().equals(deserializedTimeTest.getTimeZone1()); String deserializedJson = GsonSerializer.INSTANCE.serialize(deserializedTimeTest); System.out.println(deserializedJson); @@ -125,17 +129,17 @@ void testAddCustomSerializer() { assertEquals(map, deserialize); - map.put("key", "value"); + map.put("key", "AREX-101-202"); map.put("long", 2L); json = GsonSerializer.INSTANCE.serialize(map); - assertEquals("{\"key\":\"value\",\"long-java.lang.Long\":2}", json); + assertEquals("{\"key\":\"AREX-101-202\",\"long\":{\"value\":2,\"type\":\"java.lang.Long\"}}", json); final LinkedTreeMap deserialize1 = GsonSerializer.INSTANCE.deserialize(json, LinkedTreeMap.class); assertEquals(map, deserialize1); // value is null map.put("null", null); json = GsonSerializer.INSTANCE.serialize(map); - assertEquals("{\"key\":\"value\",\"long-java.lang.Long\":2}", json); + assertEquals("{\"key\":\"AREX-101-202\",\"long\":{\"value\":2,\"type\":\"java.lang.Long\"}}", json); } @Test @@ -146,4 +150,25 @@ void testFastUtil() throws Throwable { assert deserialize != null; assertEquals(hashSet, deserialize); } -} \ No newline at end of file + + @Test + void testNullField() { + final Pair pairFirstNull = Pair.of(null, System.currentTimeMillis()); + final String genericFirstNull = TypeUtil.getName(pairFirstNull); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.String,java.lang.Long", genericFirstNull); + String json = GsonSerializer.INSTANCE.serialize(pairFirstNull); + System.out.println(json); + Type type1 = TypeUtil.forName(genericFirstNull); + Pair firstNulldeserialize = GsonSerializer.INSTANCE.deserialize(json, type1); + assertNull(firstNulldeserialize.getFirst()); + + final Pair pairNull = Pair.of(System.currentTimeMillis(), null); + final String genericNull = TypeUtil.getName(pairNull); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.lang.String", genericNull); + json = GsonSerializer.INSTANCE.serialize(pairNull); + System.out.println(json); + Type type2 = TypeUtil.forName(genericNull); + firstNulldeserialize = GsonSerializer.INSTANCE.deserialize(json, type2); + assertNull(firstNulldeserialize.getSecond()); + } +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java index 24f20580b..ecc9e8c81 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/JacksonSerializerTest.java @@ -1,5 +1,8 @@ package io.arex.foundation.serializer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.arex.agent.bootstrap.model.MockCategoryType; import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest; import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest.TestType; @@ -156,6 +159,7 @@ void testCaseSensitiveProperties() throws Throwable { final CaseSensitive caseSensitive = new CaseSensitive(); caseSensitive.setAmountPaid("100"); caseSensitive.setAmountpaid("200"); + caseSensitive.setAmount(100.0f); final String jackJson = JacksonSerializer.INSTANCE.serialize(caseSensitive); final CaseSensitive deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, CaseSensitive.class); assertNotNull(deserializeJackTestType); @@ -163,9 +167,54 @@ void testCaseSensitiveProperties() throws Throwable { assertEquals("200", deserializeJackTestType.getAmountpaid()); } + @Test + void testNoDefaultCreator() throws Throwable { + final NoDefaultCreator noDefaultCreator = new NoDefaultCreator("test", 100, 100.0); + final String jackJson = JacksonSerializer.INSTANCE.serialize(noDefaultCreator); + final NoDefaultCreator deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, NoDefaultCreator.class); + assertNotNull(deserializeJackTestType); + assertEquals("test", deserializeJackTestType.getName()); + assertEquals(100, deserializeJackTestType.getAge()); + assertEquals(100.0, deserializeJackTestType.getAmount()); + } + + static class NoDefaultCreator { + private final String name; + private final int age; + + private final double amount; + + public NoDefaultCreator(String name, int age, double amount) { + this.name = name; + this.age = age; + this.amount = amount; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public double getAmount() { + return amount; + } + } + static class CaseSensitive { private String amountPaid; private String amountpaid; + private Float amount; + + public Float getAmount() { + return amount; + } + + public void setAmount(Float amount) { + this.amount = amount; + } public String getAmountPaid() { return amountPaid; @@ -184,4 +233,4 @@ public void setAmountpaid(String amountpaid) { } } -} \ No newline at end of file +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/ProtoJsonSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/ProtoJsonSerializerTest.java index abc7c94fb..e34ff847b 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/ProtoJsonSerializerTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/ProtoJsonSerializerTest.java @@ -12,9 +12,6 @@ class ProtoJsonSerializerTest { @Test void serialize() { - // name - assertEquals("protoJson", ProtoJsonSerializer.getInstance().name()); - // serialize no protobuf but not throw exception ProtoJsonSerializer.getInstance().serialize("test"); assertDoesNotThrow(() -> ProtoJsonSerializer.getInstance().serialize("test")); @@ -30,14 +27,9 @@ void serialize() { // deserialize empty final HelloProtoRequestType emptyResult = ProtoJsonSerializer.getInstance().deserialize("", HelloProtoRequestType.class); assertNull(emptyResult); - - // deserialize protobuf - String typeName = TypeUtil.getName(requestType); - final HelloProtoRequestType actualResult = ProtoJsonSerializer.getInstance().deserialize(json, TypeUtil.forName(typeName)); - assert actualResult != null; } @Test void deserialize() { } -} \ No newline at end of file +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java index feaf178cb..4fd2ccdfd 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/TimeTestInfo.java @@ -9,6 +9,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -30,6 +32,16 @@ public class TimeTestInfo { private GregorianCalendar gregorianCalendar = new GregorianCalendar(TimeZone.getTimeZone("GMT-01:00")); private Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT-01:00")); private XMLGregorianCalendar xmlGregorianCalendar; + private OffsetDateTime offsetDateTime = OffsetDateTime.now(ZoneId.of("GMT-01:00")); + private TimeZone timeZone1 = TimeZone.getDefault(); + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } { try { @@ -165,4 +177,12 @@ public DateTime getDateTime() { public void setDateTime(DateTime dateTime) { this.dateTime = dateTime; } + + public TimeZone getTimeZone1() { + return timeZone1; + } + + public void setTimeZone1(TimeZone timeZone1) { + this.timeZone1 = timeZone1; + } } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/NumberStrategyTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/NumberStrategyTest.java new file mode 100644 index 000000000..9ef097ced --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/NumberStrategyTest.java @@ -0,0 +1,58 @@ +package io.arex.foundation.serializer.custom; + +import com.google.gson.GsonBuilder; +import io.arex.foundation.serializer.GsonSerializer; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class NumberStrategyTest { + + static GsonBuilder builder = null; + @BeforeAll + static void setUp() { + builder = new GsonBuilder(); + } + + @AfterAll + static void tearDown() { + builder = null; + } + + @Test + void test() { + NumberTest numberTest = new NumberTest(); + Map map = new HashMap<>(); + map.put("Integer", 1); + map.put("Double", 1.1); + map.put("Long", 1111111111111111111L); + map.put("String", "1"); + map.put("nullValue", null); + numberTest.setMap(map); + String json = GsonSerializer.INSTANCE.serialize(numberTest); + System.out.println(json); + NumberTest deserialize = GsonSerializer.INSTANCE.deserialize(json, NumberTest.class); + assertEquals(numberTest.getMap().get("Integer"), deserialize.getMap().get("Integer")); + assertEquals(numberTest.getMap().get("Double"), deserialize.getMap().get("Double")); + assertEquals(numberTest.getMap().get("Long"), deserialize.getMap().get("Long")); + assertEquals(numberTest.getMap().get("String"), deserialize.getMap().get("String")); + assertNull(deserialize.getMap().get("nullValue")); + } + + static class NumberTest{ + private Map map; + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + } +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java index 42eb46a31..dfa9e580d 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/DataCollectorServiceTest.java @@ -8,22 +8,31 @@ import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.internal.DataEntity; +import io.arex.foundation.model.DecelerateReasonEnum; import io.arex.foundation.model.HttpClientResponse; +import io.arex.foundation.serializer.GsonSerializer; import io.arex.foundation.util.httpclient.AsyncHttpClientUtil; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import java.util.concurrent.CompletableFuture; + +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.CaseManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; class DataCollectorServiceTest { + static MockedStatic caseManagerMocked; @BeforeAll static void setUp() { Mockito.mockStatic(AsyncHttpClientUtil.class); Mockito.mockStatic(HealthManager.class); Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(Serializer.class); + caseManagerMocked = Mockito.mockStatic(CaseManager.class); } @AfterAll @@ -34,6 +43,7 @@ static void tearDown() { @Test void saveData() { final ArexMocker mocker = new ArexMocker(); + mocker.setRecordId("testRecordId"); CompletableFuture mockResponse = CompletableFuture.completedFuture(HttpClientResponse.emptyResponse()); Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockResponse); assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); @@ -42,6 +52,7 @@ void saveData() { mockException.completeExceptionally(new RuntimeException("mock exception")); Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockException); assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); + caseManagerMocked.verify(()-> CaseManager.invalid("testRecordId", null, null, DecelerateReasonEnum.SERVICE_EXCEPTION.getValue()), Mockito.times(1)); // null entity assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(null)); @@ -49,7 +60,7 @@ void saveData() { // invalid case final ArexContext context = ArexContext.of("testRecordId"); context.setInvalidCase(true); - Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + Mockito.when(ContextManager.getContext("testRecordId")).thenReturn(context); mocker.setRecordId("testRecordId"); assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); } @@ -64,10 +75,26 @@ void queryReplayData() { Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), anyString(), any())).thenReturn(mockResponse); actualResult = DataCollectorService.INSTANCE.queryReplayData("test", MockStrategyEnum.OVER_BREAK); assertEquals("test", actualResult); + // exception + ArexMocker mocker = new ArexMocker(); + mocker.setRecordId("testRecordId"); + mocker.setReplayId("testReplayId"); + String json = GsonSerializer.INSTANCE.serialize(mocker); + CompletableFuture mockException = new CompletableFuture<>(); + mockException.completeExceptionally(new RuntimeException("mock exception")); + Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockException); + Mockito.when(Serializer.deserialize(json, ArexMocker.class)).thenReturn(mocker); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.queryReplayData(json, MockStrategyEnum.OVER_BREAK)); + caseManagerMocked.verify(()-> CaseManager.invalid("testRecordId", "testReplayId", null, DecelerateReasonEnum.SERVICE_EXCEPTION.getValue()), Mockito.times(1)); + } + + @Test + void invalidCase() { + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.invalidCase("test")); } @Test void start() { assertDoesNotThrow(DataCollectorService.INSTANCE::start); } -} \ No newline at end of file +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/TimerServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/TimerServiceTest.java new file mode 100644 index 000000000..94062f8ae --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/TimerServiceTest.java @@ -0,0 +1,37 @@ +package io.arex.foundation.services; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @since 2024/1/15 + */ +class TimerServiceTest { + + @Test + void schedule() { + ScheduledFuture scheduleTest = TimerService.schedule(() -> System.out.println("schedule test"), 0, + TimeUnit.MILLISECONDS); + Assertions.assertNotNull(scheduleTest); + scheduleTest.cancel(true); + } + + @Test + void scheduleAtFixedRate() throws InterruptedException { + AtomicInteger runs = new AtomicInteger(); + ScheduledFuture scheduleTest = TimerService.scheduleAtFixedRate(() -> { + if (runs.get() >= 1) { + throw new RuntimeException("scheduleAtFixedRate test stop"); + } + System.out.println("scheduleAtFixedRate test"); + runs.getAndIncrement(); + }, 0, + 50, TimeUnit.MILLISECONDS); + Assertions.assertNotNull(scheduleTest); + scheduleTest.cancel(true); + TimeUnit.MILLISECONDS.sleep(150); + } +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NetUtilsTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NetUtilsTest.java new file mode 100644 index 000000000..f98468559 --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NetUtilsTest.java @@ -0,0 +1,45 @@ +package io.arex.foundation.util; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mockStatic; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +/** + * @since 2024/1/12 + */ +class NetUtilsTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void getIpAddress() { + try (MockedStatic netUtils = mockStatic(NetUtils.class)) { + netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); + assertEquals("127.0.0.1", NetUtils.getIpAddress()); + } + } + + @Test + void checkTcpPortAvailable() { + assertEquals(-1, NetUtils.checkTcpPortAvailable(1)); + + assertEquals(8080, NetUtils.checkTcpPortAvailable(8080)); + } + + @Test + void isTcpPortAvailable() { + assertFalse(NetUtils.isTcpPortAvailable(1)); + + assertTrue(NetUtils.isTcpPortAvailable(8080)); + } +} diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NumberTypeAdaptorTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NumberTypeAdaptorTest.java deleted file mode 100644 index 9b292b891..000000000 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/util/NumberTypeAdaptorTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.arex.foundation.util; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import io.arex.foundation.serializer.TimeTestInfo; -import io.arex.foundation.serializer.custom.NumberTypeAdaptor; -import java.util.Arrays; -import javax.xml.datatype.DatatypeConfigurationException; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class NumberTypeAdaptorTest { - - static GsonBuilder builder = null; - @BeforeAll - static void setUp() { - builder = new GsonBuilder(); - } - - @AfterAll - static void tearDown() { - builder = null; - } - - @Test - void test() throws DatatypeConfigurationException { - final Gson gson = builder.registerTypeAdapterFactory(NumberTypeAdaptor.FACTORY).create(); - final String json = gson.toJson(Arrays.asList(new TimeTestInfo())); - } -} \ No newline at end of file diff --git a/arex-instrumentation/common/arex-common/pom.xml b/arex-instrumentation/common/arex-common/pom.xml new file mode 100644 index 000000000..fc0dbdc2f --- /dev/null +++ b/arex-instrumentation/common/arex-common/pom.xml @@ -0,0 +1,23 @@ + + + + arex-instrumentation-parent + io.arex + ${revision} + ../../pom.xml + + 4.0.0 + + arex-common + + + + io.projectreactor + reactor-core + ${reactor.version} + provided + + + diff --git a/arex-instrumentation/common/arex-common/src/main/java/io/arex/inst/common/util/FluxUtil.java b/arex-instrumentation/common/arex-common/src/main/java/io/arex/inst/common/util/FluxUtil.java new file mode 100644 index 000000000..9d3ed3aa3 --- /dev/null +++ b/arex-instrumentation/common/arex-common/src/main/java/io/arex/inst/common/util/FluxUtil.java @@ -0,0 +1,96 @@ +package io.arex.inst.common.util; + +import io.arex.inst.runtime.serializer.Serializer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import reactor.core.publisher.Flux; +import io.arex.agent.bootstrap.util.CollectionUtil; + +public class FluxUtil { + + static final String FLUX_FROM_ITERATOR = "reactor.core.publisher.FluxIterable-"; + static final String FLUX_FROM_ARRAY = "reactor.core.publisher.FluxArray-"; + static final String FLUX_FROM_STREAM = "reactor.core.publisher.FluxStream-"; + + private FluxUtil() { + } + + public static Flux restore(Object fluxObj) { + if(fluxObj == null){ + return Flux.empty(); + } + FluxResult fluxResult = (FluxResult) fluxObj; + List fluxElementResults = fluxResult.getFluxElementResults(); + if (CollectionUtil.isEmpty(fluxElementResults)) { + return Flux.empty(); + } + List resultList = new ArrayList<>(fluxElementResults.size()); + sortFluxElement(fluxElementResults).forEach( + fluxElement -> resultList.add(Serializer.deserialize(fluxElement.getContent(), fluxElement.getType()))); + String responseType = fluxResult.getResponseType(); + if (responseType != null) { + if (FLUX_FROM_ITERATOR.equals(responseType)) { + return Flux.fromIterable(resultList); + } else if (FLUX_FROM_ARRAY.equals(responseType)) { + return Flux.fromArray(resultList.toArray()); + } else if (FLUX_FROM_STREAM.equals(responseType)) { + return Flux.fromStream(resultList.stream()); + } + } + return Flux.just(resultList); + } + + + private static List sortFluxElement(List list) { + Comparator comparator = Comparator.comparingInt( + FluxElementResult::getIndex); + Collections.sort(list, comparator); + return list; + } + + public static class FluxResult { + + private final String responseType; + private final List fluxElementResults; + + public FluxResult(String responseType, List fluxElementResults) { + this.responseType = responseType; + this.fluxElementResults = fluxElementResults; + } + + public String getResponseType() { + return responseType; + } + + public List getFluxElementResults() { + return fluxElementResults; + } + } + + public static class FluxElementResult { + + private final int index; + private final String content; + private final String type; + + public FluxElementResult(int index, String content, String type) { + this.index = index; + this.content = content; + this.type = type; + } + + public int getIndex() { + return index; + } + + public String getContent() { + return content; + } + + public String getType() { + return type; + } + } +} diff --git a/arex-instrumentation/common/arex-common/src/test/java/io/arex/inst/common/util/FluxUtilTest.java b/arex-instrumentation/common/arex-common/src/test/java/io/arex/inst/common/util/FluxUtilTest.java new file mode 100644 index 000000000..79f912817 --- /dev/null +++ b/arex-instrumentation/common/arex-common/src/test/java/io/arex/inst/common/util/FluxUtilTest.java @@ -0,0 +1,55 @@ +package io.arex.inst.common.util; + +import static io.arex.inst.common.util.FluxUtil.FLUX_FROM_ARRAY; +import static io.arex.inst.common.util.FluxUtil.FLUX_FROM_ITERATOR; +import static io.arex.inst.common.util.FluxUtil.FLUX_FROM_STREAM; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.arex.inst.common.util.FluxUtil.FluxElementResult; +import io.arex.inst.common.util.FluxUtil.FluxResult; +import io.arex.inst.runtime.util.TypeUtil; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class FluxUtilTest { + + @Test + void FluxRecory() { + List list = new ArrayList<>(); + FluxResult fluxResult = new FluxResult(null, list); + // flux is empty + assertNotNull(FluxUtil.restore(null)); + Flux result = FluxUtil.restore(fluxResult); + assertNotNull(result); + + // flux is not empty + FluxElementResult fluxElement1 = new FluxElementResult(1, "1", "java.lang.Integer"); + FluxElementResult fluxException1 = new FluxElementResult(2, null, "java.lang.RuntimeException"); + list.add(fluxElement1); + list.add(fluxException1); + + // Flux.just() + fluxResult = new FluxResult(null, list); + result = FluxUtil.restore(fluxResult); + assertEquals(TypeUtil.getName(result),"reactor.core.publisher.FluxJust-java.util.ArrayList-"); + + // Flux.fromIterable() + fluxResult = new FluxResult(FLUX_FROM_ITERATOR, list); + result = FluxUtil.restore(fluxResult); + assertEquals(TypeUtil.getName(result),FLUX_FROM_ITERATOR); + + // Flux.fromArray() + fluxResult = new FluxResult(FLUX_FROM_ARRAY, list); + result = FluxUtil.restore(fluxResult); + assertEquals(TypeUtil.getName(result),FLUX_FROM_ARRAY); + + // Flux.fromStream() + fluxResult = new FluxResult(FLUX_FROM_STREAM, list); + result = FluxUtil.restore(fluxResult); + assertEquals(TypeUtil.getName(result),FLUX_FROM_STREAM); + } +} diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java index c4cc07dcf..6231d8a0c 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java @@ -16,7 +16,9 @@ public String name() { } @Override - public void preHandle(Map request) {} + public void preHandle(Map request) { + // no need implement + } @Override public void handleAfterCreateContext(Map request) { diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java index 8b2379a6f..8f1e524a8 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java @@ -16,7 +16,9 @@ public String name() { } @Override - public void preHandle(HttpServletRequest request) {} + public void preHandle(HttpServletRequest request) { + // no need implement + } @Override public void handleAfterCreateContext(HttpServletRequest request) { diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java index dbc07151f..04f7d7aa3 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java @@ -47,8 +47,8 @@ void name() { } @Test - void preHandle() { - target.preHandle(new HashMap<>()); + void handleAfterCreateContext() { + target.handleAfterCreateContext(new HashMap<>()); mockStaticHelper.verify(() -> ApolloConfigHelper.initAndRecord(any(), any()), atLeastOnce()); } diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java index 2389fdf6e..27dcab3ed 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java @@ -47,9 +47,9 @@ void name() { } @Test - void preHandle() { + void handleAfterCreateContext() { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - target.preHandle(request); + target.handleAfterCreateContext(request); mockStaticHelper.verify(() -> ApolloConfigHelper.initAndRecord(any(), any()), atLeastOnce()); } diff --git a/arex-instrumentation/database/arex-database-common/src/main/java/io/arex/inst/database/common/DatabaseExtractor.java b/arex-instrumentation/database/arex-database-common/src/main/java/io/arex/inst/database/common/DatabaseExtractor.java index 6616131ad..9aa70f81e 100644 --- a/arex-instrumentation/database/arex-database-common/src/main/java/io/arex/inst/database/common/DatabaseExtractor.java +++ b/arex-instrumentation/database/arex-database-common/src/main/java/io/arex/inst/database/common/DatabaseExtractor.java @@ -56,10 +56,10 @@ public DatabaseExtractor(String dbName, String sql, String parameters, String me this.methodName = methodName; } - public void record(Object response) { - record(response, null); + public void recordDb(Object response) { + recordDb(response, null); } - public void record(Object response, String serializer) { + public void recordDb(Object response, String serializer) { MockUtils.recordMocker(makeMocker(response, serializer)); } @@ -68,7 +68,8 @@ public MockResult replay() { } public MockResult replay(String serializer) { - boolean ignoreMockResult = IgnoreUtils.ignoreMockResult(this.dbName, methodName); + // update after all dal components have obtained the real dbName(temporary solution) + boolean ignoreMockResult = IgnoreUtils.ignoreMockResult("database", methodName); Mocker replayMocker = MockUtils.replayMocker(makeMocker(null, serializer)); Object replayResult = null; if (MockUtils.checkResponseMocker(replayMocker)) { diff --git a/arex-instrumentation/database/arex-database-common/src/test/java/io/arex/inst/database/common/DatabaseExtractorTest.java b/arex-instrumentation/database/arex-database-common/src/test/java/io/arex/inst/database/common/DatabaseExtractorTest.java index 800ae61f9..418921c54 100644 --- a/arex-instrumentation/database/arex-database-common/src/test/java/io/arex/inst/database/common/DatabaseExtractorTest.java +++ b/arex-instrumentation/database/arex-database-common/src/test/java/io/arex/inst/database/common/DatabaseExtractorTest.java @@ -37,7 +37,7 @@ void record() { return null; }); - target.record(new Object()); + target.recordDb(new Object()); } } diff --git a/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentation.java b/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentation.java index 3380d6080..57fe7cd13 100644 --- a/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentation.java +++ b/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentation.java @@ -105,9 +105,9 @@ public static void onExit( if (ContextManager.needRecord()) { if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(serializable); + extractor.recordDb(serializable); } } } @@ -151,9 +151,9 @@ public static void onExit( if (ContextManager.needRecord()) { DatabaseExtractor extractor = new DatabaseExtractor(sql, object, METHOD_NAME_UPDATE); if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(0); + extractor.recordDb(0); } } } @@ -196,9 +196,9 @@ public static void onExit( if (ContextManager.needRecord()) { DatabaseExtractor extractor = new DatabaseExtractor(sql, object, METHOD_NAME_DELETE); if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(0); + extractor.recordDb(0); } } } diff --git a/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/LoaderInstrumentation.java b/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/LoaderInstrumentation.java index 45a8418ba..7018ffa51 100644 --- a/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/LoaderInstrumentation.java +++ b/arex-instrumentation/database/arex-database-hibernate/src/main/java/io/arex/inst/database/hibernate/LoaderInstrumentation.java @@ -78,9 +78,9 @@ public static void onExit(@Advice.This Loader loader, } if (ContextManager.needRecord()) { if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(list); + extractor.recordDb(list); } } } diff --git a/arex-instrumentation/database/arex-database-hibernate/src/test/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentationTest.java b/arex-instrumentation/database/arex-database-hibernate/src/test/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentationTest.java index 832364845..6b1fa6eb2 100644 --- a/arex-instrumentation/database/arex-database-hibernate/src/test/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentationTest.java +++ b/arex-instrumentation/database/arex-database-hibernate/src/test/java/io/arex/inst/database/hibernate/AbstractEntityPersisterInstrumentationTest.java @@ -132,7 +132,7 @@ void onUpdateOrInsertExit(Runnable recordType, Object mockResult) { mo.set(mock); })){ recordType.run(); - Mockito.verify(mo.get(), Mockito.times(1)).record(isA(mockResult.getClass())); + Mockito.verify(mo.get(), Mockito.times(1)).recordDb(isA(mockResult.getClass())); } } @@ -190,7 +190,7 @@ void onDeleteExit(Runnable recordType, Object mockResult) { mo.set(mock); })){ recordType.run(); - Mockito.verify(mo.get(), Mockito.times(1)).record(isA(mockResult.getClass())); + Mockito.verify(mo.get(), Mockito.times(1)).recordDb(isA(mockResult.getClass())); } } diff --git a/arex-instrumentation/database/arex-database-mongo/pom.xml b/arex-instrumentation/database/arex-database-mongo/pom.xml index 3f5dd616c..2a6f29aa9 100644 --- a/arex-instrumentation/database/arex-database-mongo/pom.xml +++ b/arex-instrumentation/database/arex-database-mongo/pom.xml @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoHelper.java b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoHelper.java index 410c88f83..372c614d2 100644 --- a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoHelper.java +++ b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoHelper.java @@ -37,10 +37,10 @@ public static void record(String methodName, MongoNamespace namespace, Object fi String parameter = Serializer.serialize(filter, GSON_SERIALIZER); final DatabaseExtractor extractor = new DatabaseExtractor(dbName, methodName, parameter, methodName); if (throwable != null) { - extractor.record(throwable, GSON_SERIALIZER); + extractor.recordDb(throwable, GSON_SERIALIZER); return; } Object actualResult = ResultWrapper.wrap(result); - extractor.record(actualResult, GSON_SERIALIZER); + extractor.recordDb(actualResult, GSON_SERIALIZER); } } diff --git a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoModuleInstrumentation.java b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoModuleInstrumentation.java index c30915c26..36784c2e3 100644 --- a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoModuleInstrumentation.java +++ b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/MongoModuleInstrumentation.java @@ -5,7 +5,6 @@ import io.arex.inst.extension.TypeInstrumentation; import java.util.Arrays; -import java.util.Collections; import java.util.List; @AutoService(ModuleInstrumentation.class) @@ -16,7 +15,12 @@ public MongoModuleInstrumentation() { @Override public List instrumentationTypes() { - return Arrays.asList(new ReadOperationInstrumentation(), new ListIndexesInstrumentation(), - new AggregateInstrumentation(), new WriteOperationInstrumentation(), new ResourceManagerInstrumentation()); + return Arrays.asList( + new ReadOperationInstrumentation(), + new ListIndexesInstrumentation(), + new AggregateInstrumentation(), + new WriteOperationInstrumentation(), + new ResourceManagerInstrumentation(), + new QueryBatchCursorInstrumentation()); } } diff --git a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/QueryBatchCursorInstrumentation.java b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/QueryBatchCursorInstrumentation.java new file mode 100644 index 000000000..de3e85cf8 --- /dev/null +++ b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/QueryBatchCursorInstrumentation.java @@ -0,0 +1,32 @@ +package io.arex.inst.database.mongo; + +import io.arex.inst.extension.MethodInstrumentation; +import io.arex.inst.extension.TypeInstrumentation; +import io.arex.inst.runtime.context.ContextManager; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.Collections; +import java.util.List; + +import static net.bytebuddy.matcher.ElementMatchers.*; + +public class QueryBatchCursorInstrumentation extends TypeInstrumentation { + @Override + protected ElementMatcher typeMatcher() { + return namedOneOf("com.mongodb.internal.operation.QueryBatchCursor", "com.mongodb.operation.QueryBatchCursor"); + } + + @Override + public List methodAdvices() { + return Collections.singletonList(new MethodInstrumentation(isMethod().and(named("close")), SkipAdvice.class.getName())); + } + + public static class SkipAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) + public static boolean onEnter() { + return ContextManager.needReplay(); + } + } +} diff --git a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/WriteOperationInstrumentation.java b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/WriteOperationInstrumentation.java index f3b11090a..75fc42390 100644 --- a/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/WriteOperationInstrumentation.java +++ b/arex-instrumentation/database/arex-database-mongo/src/main/java/io/arex/inst/database/mongo/WriteOperationInstrumentation.java @@ -84,15 +84,14 @@ public static boolean onEnter(@Advice.FieldValue("namespace") MongoNamespace nam } @Advice.OnMethodExit(onThrowable = Throwable.class ,suppress = Throwable.class) - public static void onExit(@Advice.Enter boolean needReplay, - @Advice.Origin Method method, + public static void onExit(@Advice.Origin Method method, @Advice.FieldValue("namespace") MongoNamespace namespace, @Advice.Origin("#m") String methodName, @Advice.Argument(1) Object filter, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object result, @Advice.Thrown(readOnly = false) Throwable throwable, @Advice.Local("mockResult") MockResult mockResult) { - if (needReplay) { + if (mockResult != null && mockResult.notIgnoreMockResult()) { if (throwable != null) { throwable = mockResult.getThrowable(); } else { diff --git a/arex-instrumentation/database/arex-database-mongo/src/test/java/io/arex/inst/database/mongo/MongoHelperTest.java b/arex-instrumentation/database/arex-database-mongo/src/test/java/io/arex/inst/database/mongo/MongoHelperTest.java index 443574d61..0ec74d3fb 100644 --- a/arex-instrumentation/database/arex-database-mongo/src/test/java/io/arex/inst/database/mongo/MongoHelperTest.java +++ b/arex-instrumentation/database/arex-database-mongo/src/test/java/io/arex/inst/database/mongo/MongoHelperTest.java @@ -65,10 +65,10 @@ void replay() { void record() { try(final MockedConstruction construction = Mockito.mockConstruction(DatabaseExtractor.class)) { MongoHelper.record("test", Mockito.mock(MongoNamespace.class), null, "test", null); - Mockito.verify(construction.constructed().get(0), Mockito.times(1)).record("test", "gson"); + Mockito.verify(construction.constructed().get(0), Mockito.times(1)).recordDb("test", "gson"); final RuntimeException runtimeException = new RuntimeException(); MongoHelper.record("test", Mockito.mock(MongoNamespace.class), null, "test", runtimeException); - Mockito.verify(construction.constructed().get(1), Mockito.times(1)).record(runtimeException, "gson"); + Mockito.verify(construction.constructed().get(1), Mockito.times(1)).recordDb(runtimeException, "gson"); } } diff --git a/arex-instrumentation/database/arex-database-mybatis3/pom.xml b/arex-instrumentation/database/arex-database-mybatis3/pom.xml index a6531e65c..04c77a32f 100644 --- a/arex-instrumentation/database/arex-database-mybatis3/pom.xml +++ b/arex-instrumentation/database/arex-database-mybatis3/pom.xml @@ -22,7 +22,7 @@ org.mybatis mybatis - 3.5.3 + 3.5.6 provided diff --git a/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/ExecutorInstrumentation.java b/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/ExecutorInstrumentation.java index 5423baf59..a8ab887c1 100644 --- a/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/ExecutorInstrumentation.java +++ b/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/ExecutorInstrumentation.java @@ -73,10 +73,12 @@ public static class QueryAdvice { public static boolean onMethodEnter(@Advice.Argument(0) MappedStatement var1, @Advice.Argument(1) Object var2, @Advice.Argument(5) BoundSql boundSql, - @Advice.Local("mockResult") MockResult mockResult) { + @Advice.Local("mockResult") MockResult mockResult, + @Advice.Local("originalSql") String originalSql) { + originalSql = boundSql != null ? boundSql.getSql() : null; RepeatedCollectManager.enter(); if (ContextManager.needReplay()) { - mockResult = InternalExecutor.replay(var1, var2, boundSql, METHOD_NAME_QUERY); + mockResult = InternalExecutor.replay(var1, var2, originalSql, METHOD_NAME_QUERY); } return mockResult != null && mockResult.notIgnoreMockResult(); } @@ -84,10 +86,10 @@ public static boolean onMethodEnter(@Advice.Argument(0) MappedStatement var1, @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void onExit(@Advice.Argument(0) MappedStatement var1, @Advice.Argument(1) Object var2, - @Advice.Argument(5) BoundSql boundSql, @Advice.Thrown(readOnly = false) Throwable throwable, @Advice.Return(readOnly = false) List result, - @Advice.Local("mockResult") MockResult mockResult) { + @Advice.Local("mockResult") MockResult mockResult, + @Advice.Local("originalSql") String originalSql) { if (mockResult != null && mockResult.notIgnoreMockResult()) { if (mockResult.getThrowable() != null) { throwable = mockResult.getThrowable(); @@ -102,7 +104,7 @@ public static void onExit(@Advice.Argument(0) MappedStatement var1, } if (ContextManager.needRecord()) { - InternalExecutor.record(var1, var2, boundSql, result, throwable, METHOD_NAME_QUERY); + InternalExecutor.record(var1, var2, originalSql, result, throwable, METHOD_NAME_QUERY); } } } diff --git a/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/InternalExecutor.java b/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/InternalExecutor.java index 3d2403076..0f5f5e7c8 100644 --- a/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/InternalExecutor.java +++ b/arex-instrumentation/database/arex-database-mybatis3/src/main/java/io/arex/inst/database/mybatis3/InternalExecutor.java @@ -6,7 +6,6 @@ import io.arex.inst.database.common.DatabaseExtractor; import org.apache.ibatis.binding.MapperMethod.ParamMap; -import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.reflection.Reflector; @@ -15,8 +14,8 @@ public class InternalExecutor { private static final char KEYHOLDER_SEPARATOR = ';'; private static final char KEYHOLDER_TYPE_SEPARATOR = ','; - public static MockResult replay(MappedStatement ms, Object o, BoundSql boundSql, String methodName) { - DatabaseExtractor extractor = createExtractor(ms, boundSql, o, methodName); + public static MockResult replay(MappedStatement ms, Object o, String originalSql, String methodName) { + DatabaseExtractor extractor = createExtractor(ms, originalSql, o, methodName); return extractor.replay(); } @@ -28,12 +27,12 @@ public static MockResult replay(DatabaseExtractor extractor, MappedStatement ms, return replayResult; } - public static void record(MappedStatement ms, Object o, BoundSql boundSql, U result, Throwable throwable, String methodName) { - DatabaseExtractor extractor = createExtractor(ms, boundSql, o, methodName); + public static void record(MappedStatement ms, Object o, String originalSql, U result, Throwable throwable, String methodName) { + DatabaseExtractor extractor = createExtractor(ms, originalSql, o, methodName); if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(result); + extractor.recordDb(result); } } @@ -44,9 +43,9 @@ public static void record(DatabaseExtractor extractor, } if (throwable != null) { - extractor.record(throwable); + extractor.recordDb(throwable); } else { - extractor.record(result); + extractor.recordDb(result); } } @@ -133,8 +132,10 @@ private static boolean containKeyHolder(MappedStatement ms, DatabaseExtractor ex } public static DatabaseExtractor createExtractor(MappedStatement mappedStatement, - BoundSql boundSql, Object parameters, String methodName) { - boundSql = boundSql == null ? mappedStatement.getBoundSql(parameters) : boundSql; - return new DatabaseExtractor(boundSql.getSql(), Serializer.serialize(parameters), methodName); + String originalSql, Object parameters, String methodName) { + if (StringUtil.isEmpty(originalSql) && mappedStatement != null && mappedStatement.getBoundSql(parameters) != null) { + originalSql = mappedStatement.getBoundSql(parameters).getSql(); + } + return new DatabaseExtractor(originalSql, Serializer.serialize(parameters), methodName); } } diff --git a/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/ExecutorInstrumentationTest.java b/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/ExecutorInstrumentationTest.java index 8ce2a4f9d..c04887ea0 100644 --- a/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/ExecutorInstrumentationTest.java +++ b/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/ExecutorInstrumentationTest.java @@ -1,6 +1,5 @@ package io.arex.inst.database.mybatis3; -import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.context.RepeatedCollectManager; import io.arex.agent.bootstrap.model.MockResult; @@ -12,10 +11,7 @@ import org.apache.ibatis.executor.BatchExecutor; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.executor.SimpleExecutor; -import org.apache.ibatis.mapping.BoundSql; -import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.MappedStatement.Builder; -import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -25,7 +21,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -37,7 +32,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.ArgumentMatchers.any; @ExtendWith(MockitoExtension.class) class ExecutorInstrumentationTest { @@ -70,7 +64,8 @@ void methodAdvices() { @Test void onEnter() throws SQLException { Mockito.when(ContextManager.needReplay()).thenReturn(true); - assertFalse(ExecutorInstrumentation.QueryAdvice.onMethodEnter(null, null, null, MockResult.success("mock")));Mockito.when(ContextManager.needRecordOrReplay()).thenReturn(true); + assertFalse(ExecutorInstrumentation.QueryAdvice.onMethodEnter(null, null, null, MockResult.success("mock"), null)); + Mockito.when(ContextManager.needRecordOrReplay()).thenReturn(true); assertFalse(ExecutorInstrumentation.UpdateAdvice.onMethodEnter(null, null, null, null, new SimpleExecutor(null, null))); assertFalse(ExecutorInstrumentation.UpdateAdvice.onMethodEnter(null, null, null, null, new BatchExecutor(null, null))); } @@ -79,7 +74,7 @@ void onEnter() throws SQLException { @MethodSource("onExitCase") void onExit(Runnable mocker, MockResult mockResult, Predicate predicate) { mocker.run(); - ExecutorInstrumentation.QueryAdvice.onExit(null, null, null, null, null, mockResult); + ExecutorInstrumentation.QueryAdvice.onExit(null, null, null, null, mockResult, null); assertTrue(predicate.test(mockResult)); } diff --git a/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/InternalExecutorTest.java b/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/InternalExecutorTest.java index 4966c24d3..0d35a95b5 100644 --- a/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/InternalExecutorTest.java +++ b/arex-instrumentation/database/arex-database-mybatis3/src/test/java/io/arex/inst/database/mybatis3/InternalExecutorTest.java @@ -25,14 +25,12 @@ import org.apache.ibatis.reflection.Reflector; import org.apache.ibatis.reflection.invoker.Invoker; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullSource; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -54,7 +52,6 @@ static void setUp() { mappedStatement = Mockito.mock(MappedStatement.class); Mockito.when(mappedStatement.getKeyProperties()).thenReturn(new String[]{"key"}); boundSql = Mockito.mock(BoundSql.class); - Mockito.when(boundSql.getSql()).thenReturn("insert into"); Mockito.mockStatic(ContextManager.class); } @@ -75,7 +72,7 @@ void replay() throws SQLException { mocker.setTargetResponse(new Target()); mockService.when(() -> MockUtils.createDatabase(any())).thenReturn(mocker); - assertNotNull(InternalExecutor.replay(mappedStatement, new Object(), boundSql, "insert")); + assertNotNull(InternalExecutor.replay(mappedStatement, new Object(), null, "insert")); } } @@ -98,11 +95,12 @@ void record(Throwable throwable, Object result) { try (MockedConstruction mocked = Mockito.mockConstruction(DatabaseExtractor.class, (mock, context) -> { atomicReference.set(mock); })) { - target.record(mappedStatement, new Object(), boundSql, result, throwable, "insert"); + Mockito.when(mappedStatement.getBoundSql(any())).thenReturn(boundSql); + target.record(mappedStatement, new Object(), null, result, throwable, "insert"); if (throwable != null) { - Mockito.verify(atomicReference.get(), Mockito.times(1)).record(throwable); + Mockito.verify(atomicReference.get(), Mockito.times(1)).recordDb(throwable); } else { - Mockito.verify(atomicReference.get(), Mockito.times(1)).record(result); + Mockito.verify(atomicReference.get(), Mockito.times(1)).recordDb(result); } } } diff --git a/arex-instrumentation/dynamic/arex-cache/pom.xml b/arex-instrumentation/dynamic/arex-cache/pom.xml index a4a5d1a00..b6d2dca00 100644 --- a/arex-instrumentation/dynamic/arex-cache/pom.xml +++ b/arex-instrumentation/dynamic/arex-cache/pom.xml @@ -31,5 +31,11 @@ ${arex-common.version} provided + + io.projectreactor + reactor-core + ${reactor.version} + provided + diff --git a/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/arex/ArexMockInstrumentation.java b/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/arex/ArexMockInstrumentation.java index aedb13132..9279084bb 100644 --- a/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/arex/ArexMockInstrumentation.java +++ b/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/arex/ArexMockInstrumentation.java @@ -9,7 +9,7 @@ import com.arextest.common.annotation.ArexMock; import io.arex.agent.bootstrap.model.MockResult; -import io.arex.inst.dynamic.common.DynamiConstants; +import io.arex.inst.dynamic.common.DynamicConstants; import io.arex.inst.dynamic.common.DynamicClassExtractor; import io.arex.inst.extension.MethodInstrumentation; import io.arex.inst.extension.TypeInstrumentation; @@ -29,15 +29,15 @@ public class ArexMockInstrumentation extends TypeInstrumentation { @Override protected ElementMatcher typeMatcher() { - return inheritsAnnotation(named(DynamiConstants.AREX_MOCK)); + return inheritsAnnotation(named(DynamicConstants.AREX_MOCK)); } @Override public List methodAdvices() { Junction matcher = isMethod() .and(not(returns(TypeDescription.VOID))) - .and(isAnnotatedWith(named(DynamiConstants.AREX_MOCK))) - .and(not(isAnnotatedWith(named(DynamiConstants.SPRING_CACHE)))); + .and(isAnnotatedWith(named(DynamicConstants.AREX_MOCK))) + .and(not(isAnnotatedWith(named(DynamicConstants.SPRING_CACHE)))); MethodInstrumentation method = new MethodInstrumentation(matcher, ArexMockAdvice.class.getName()); @@ -86,7 +86,7 @@ public static void onExit(@Advice.Local("extractor") DynamicClassExtractor extra return; } if (ContextManager.needRecord() && RepeatedCollectManager.exitAndValidate() && extractor != null) { - extractor.recordResponse(throwable != null ? throwable : result); + result = extractor.recordResponse(throwable != null ? throwable : result); } } } diff --git a/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/spring/SpringCacheAdviceHelper.java b/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/spring/SpringCacheAdviceHelper.java index 4abdf732f..3880e1493 100644 --- a/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/spring/SpringCacheAdviceHelper.java +++ b/arex-instrumentation/dynamic/arex-cache/src/main/java/io/arex/inst/cache/spring/SpringCacheAdviceHelper.java @@ -2,7 +2,6 @@ import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ContextManager; -import io.arex.inst.runtime.model.DynamicClassEntity; import java.lang.reflect.Method; @@ -11,16 +10,22 @@ public static boolean needRecordOrReplay(Method method) { if (!ContextManager.needRecordOrReplay() || method == null) { return false; } + String className = method.getDeclaringClass().getName(); + return onlyClassMatch(className, method) || methodSignatureMatch(className, method); + } - String methodSignature = buildMethodSignature(method); - - DynamicClassEntity dynamicEntity = Config.get().getDynamicEntity(methodSignature); + private static boolean onlyClassMatch(String className, Method method) { + return Config.get().getDynamicEntity(className) != null && + method.getParameterTypes().length > 0 && + method.getReturnType() != void.class; + } - return dynamicEntity != null; + private static boolean methodSignatureMatch(String className, Method method) { + String methodSignature = buildMethodSignature(className, method); + return Config.get().getDynamicEntity(methodSignature) != null; } - private static String buildMethodSignature(Method method) { - String className = method.getDeclaringClass().getName(); + private static String buildMethodSignature(String className, Method method) { String methodName = method.getName(); Class[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 0) { diff --git a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/arex/ArexMockInstrumentationTest.java b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/arex/ArexMockInstrumentationTest.java index 9037d86d0..e4afe2729 100644 --- a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/arex/ArexMockInstrumentationTest.java +++ b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/arex/ArexMockInstrumentationTest.java @@ -94,7 +94,7 @@ void onExit() throws NoSuchMethodException { // record try(MockedConstruction ignored = Mockito.mockConstruction(DynamicClassExtractor.class, ((extractor, context) -> { - Mockito.doNothing().when(extractor).recordResponse(throwable); + Mockito.doReturn(true).when(extractor).recordResponse(throwable); }))) { Mockito.when(ContextManager.needRecord()).thenReturn(true); Mockito.when(RepeatedCollectManager.exitAndValidate()).thenReturn(true); diff --git a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheAdviceHelperTest.java b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheAdviceHelperTest.java index 4f6636782..51b21c4ee 100644 --- a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheAdviceHelperTest.java +++ b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheAdviceHelperTest.java @@ -27,6 +27,9 @@ static void tearDown() { @Test void needRecordOrReplay() throws NoSuchMethodException { final Method method1 = SpringCacheAdviceHelperTest.class.getDeclaredMethod("method1"); + final Method method2 = SpringCacheAdviceHelperTest.class.getDeclaredMethod("method2", + String.class); + final Method method3 = SpringCacheAdviceHelperTest.class.getDeclaredMethod("method3", String.class); final ConfigBuilder configBuilder = ConfigBuilder.create("test"); configBuilder.build(); @@ -44,12 +47,19 @@ void needRecordOrReplay() throws NoSuchMethodException { final boolean noDynamicClass = SpringCacheAdviceHelper.needRecordOrReplay(method1); assertFalse(noDynamicClass); - // has dynamic class, but not contains method + // only class final List entities = new ArrayList<>(); - entities.add(new DynamicClassEntity("io.arex.inst.cache.spring.SpringCacheAdviceHelperTest", null, null, null)); + entities.add(new DynamicClassEntity("io.arex.inst.cache.spring.SpringCacheAdviceHelperTest", "", "", null)); configBuilder.dynamicClassList(entities).build(); - final boolean notContainsMethod = SpringCacheAdviceHelper.needRecordOrReplay(method1); - assertFalse(notContainsMethod); + // no args and return void return false + final boolean noArgMethod = SpringCacheAdviceHelper.needRecordOrReplay(method1); + assertFalse(noArgMethod); + // have args but return void return false + final boolean returnVoidMethod = SpringCacheAdviceHelper.needRecordOrReplay(method2); + assertFalse(returnVoidMethod); + // have args and return not void return true + final boolean returnNotVoidMethod = SpringCacheAdviceHelper.needRecordOrReplay(method3); + assertTrue(returnNotVoidMethod); // has no args dynamic class, and contains method entities.add(new DynamicClassEntity("io.arex.inst.cache.spring.SpringCacheAdviceHelperTest", "method1", null, null)); @@ -58,8 +68,6 @@ void needRecordOrReplay() throws NoSuchMethodException { assertTrue(containsMethod); // has args dynamic class, and contains method - final Method method2 = SpringCacheAdviceHelperTest.class.getDeclaredMethod("method2", - String.class); entities.add(new DynamicClassEntity("io.arex.inst.cache.spring.SpringCacheAdviceHelperTest", "method2", "java.lang.String", null)); configBuilder.dynamicClassList(entities).build(); final boolean containsMethod2 = SpringCacheAdviceHelper.needRecordOrReplay(method2); @@ -73,4 +81,8 @@ public void method1() { public void method2(String arg1) { } + + public String method3(String arg1) { + return ""; + } } \ No newline at end of file diff --git a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheInstrumentationTest.java b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheInstrumentationTest.java index a4206befd..569154e20 100644 --- a/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheInstrumentationTest.java +++ b/arex-instrumentation/dynamic/arex-cache/src/test/java/io/arex/inst/cache/spring/SpringCacheInstrumentationTest.java @@ -97,7 +97,7 @@ void onExit() throws NoSuchMethodException { // record try(MockedConstruction ignored = Mockito.mockConstruction(DynamicClassExtractor.class, ((extractor, context) -> { - Mockito.doNothing().when(extractor).recordResponse(throwable); + Mockito.doReturn(true).when(extractor).recordResponse(throwable); }))) { Mockito.when(ContextManager.needRecord()).thenReturn(true); Mockito.when(RepeatedCollectManager.exitAndValidate()).thenReturn(true); diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/pom.xml b/arex-instrumentation/dynamic/arex-dynamic-common/pom.xml index f431a82f4..3a81e6b93 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/pom.xml +++ b/arex-instrumentation/dynamic/arex-dynamic-common/pom.xml @@ -19,5 +19,22 @@ ${springframework.version} provided + + joda-time + joda-time + 2.9 + provided + + + io.arex + arex-common + ${project.version} + + + io.projectreactor + reactor-core + ${reactor.version} + provided + diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index fadfbfe53..4ea03a1a7 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -2,19 +2,21 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import io.arex.agent.bootstrap.model.MockCategoryType; import io.arex.agent.bootstrap.model.MockResult; import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.agent.thirdparty.util.time.DateFormatUtils; +import io.arex.inst.common.util.FluxUtil; +import io.arex.inst.dynamic.common.listener.FluxConsumer; import io.arex.inst.dynamic.common.listener.ListenableFutureAdapter; +import io.arex.inst.dynamic.common.listener.MonoConsumer; import io.arex.inst.dynamic.common.listener.ResponseConsumer; import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.model.ArexConstants; -import io.arex.inst.runtime.model.MergeResultDTO; import io.arex.inst.runtime.model.DynamicClassEntity; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.util.*; @@ -22,16 +24,30 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import reactor.core.publisher.Flux; + +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; +import reactor.core.publisher.Mono; public class DynamicClassExtractor { - private static final int RESULT_SIZE_MAX = Integer.parseInt(System.getProperty("arex.dynamic.result.size.limit", "1000")); private static final String LISTENABLE_FUTURE = "com.google.common.util.concurrent.ListenableFuture"; private static final String COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture"; private static final String NEED_RECORD_TITLE = "dynamic.needRecord"; private static final String NEED_REPLAY_TITLE = "dynamic.needReplay"; + public static final String MONO = "reactor.core.publisher.Mono"; + public static final String FLUX = "reactor.core.publisher.Flux"; + private static final String JODA_LOCAL_DATE_TIME = "org.joda.time.LocalDateTime"; + private static final String JODA_LOCAL_TIME = "org.joda.time.LocalTime"; + public static final String SIMPLE_DATE_FORMAT_MILLIS = "yyyy-MM-dd HH:mm:"; + private static final String SIMPLE_DATE_FORMAT_MILLIS_WITH_ZONE = "yyyy-MM-dd'T'HH:mm:"; + public static final String SHORT_TIME_FORMAT_MILLISECOND = "HH:mm:"; + private static final String TIME_ZONE = "ZZZ"; + private static final String ZERO_SECOND_TIME = "00.000"; private final String clazzName; private final String methodName; private final String methodKey; @@ -44,6 +60,8 @@ public class DynamicClassExtractor { private final Class actualType; private final Object[] args; private final String dynamicSignature; + private final String requestType; + private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); public DynamicClassExtractor(Method method, Object[] args, String keyExpression, Class actualType) { this.clazzName = method.getDeclaringClass().getName(); @@ -53,6 +71,7 @@ public DynamicClassExtractor(Method method, Object[] args, String keyExpression, this.methodKey = buildMethodKey(method, args, keyExpression); this.methodReturnType = TypeUtil.getName(method.getReturnType()); this.actualType = actualType; + this.requestType = buildRequestType(method); } public DynamicClassExtractor(Method method, Object[] args) { @@ -63,43 +82,37 @@ public DynamicClassExtractor(Method method, Object[] args) { this.methodKey = buildMethodKey(method, args); this.methodReturnType = TypeUtil.getName(method.getReturnType()); this.actualType = null; + this.requestType = buildRequestType(method); } - public void recordResponse(Object response) { + + public Object recordResponse(Object response) { if (IgnoreUtils.invalidOperation(dynamicSignature)) { LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record invalid operation: %s, can not serialize args or response", dynamicSignature)); - return; + StringUtil.format("do not record invalid operation: %s", dynamicSignature)); + return response; } if (response instanceof Future) { this.setFutureResponse((Future) response); - return; + return response; } + // Compatible with not import package reactor-core + if (MONO.equals(methodReturnType) && response instanceof Mono) { + return new MonoConsumer(this).accept((Mono) response); + } + + if (FLUX.equals(methodReturnType) && response instanceof Flux) { + return new FluxConsumer(this).accept((Flux) response); + } + this.result = response; if (needRecord()) { this.resultClazz = buildResultClazz(TypeUtil.getName(response)); Mocker mocker = makeMocker(); - // merge record, no parameter method not merge(currently only support accurate match) - if (Config.get().getBoolean(ArexConstants.MERGE_RECORD_ENABLE, true) && StringUtil.isNotEmpty(this.methodKey)) { - buildMergeMocker(mocker); - } else { - this.serializedResult = serialize(this.result); - mocker.getTargetResponse().setBody(this.serializedResult); - } + mocker.getTargetResponse().setBody(getSerializedResult()); MockUtils.recordMocker(mocker); cacheMethodSignature(); } - } - - private void buildMergeMocker(Mocker mocker) { - MergeResultDTO mergeResultDTO = MergeResultDTO.of(MockCategoryType.DYNAMIC_CLASS.getName(), - this.clazzName, - this.methodName, - this.args, - this.result, // do not serialize(this.result), avoid generate new json string, will increase memory - this.resultClazz, - buildMethodSignatureKey(), - ArexConstants.GSON_SERIALIZER); - mocker.getTargetRequest().setAttribute(ArexConstants.MERGE_RECORD_KEY, mergeResultDTO); + return response; } public MockResult replay() { @@ -108,31 +121,12 @@ public MockResult replay() { StringUtil.format("do not replay invalid operation: %s, can not serialize args or response", dynamicSignature)); return MockResult.IGNORE_MOCK_RESULT; } - int signatureHashKey = buildMethodSignatureKey(); - Map cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); Object replayResult = null; - // First get replay result from cache - MergeResultDTO mergeResultDTO = cachedReplayResultMap.get(signatureHashKey); - String replayBody; - if (mergeResultDTO != null && MockCategoryType.DYNAMIC_CLASS.getName().equals(mergeResultDTO.getCategory())) { - replayBody = Serializer.serialize(mergeResultDTO.getResult(), ArexConstants.GSON_SERIALIZER); - replayResult = deserializeResult(replayBody, mergeResultDTO.getResultClazz()); - } else { - // compatible with old process logic: single replay - // If not in cache, get replay result from mock server - Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); - String typeName = ""; - if (MockUtils.checkResponseMocker(replayMocker)) { - typeName = replayMocker.getTargetResponse().getType(); - replayBody = replayMocker.getTargetResponse().getBody(); - replayResult = deserializeResult(replayBody, typeName); - } - // no parameter no cache, no parameter methods may return different values - if (StringUtil.isNotEmpty(this.methodKey) && replayResult != null) { - mergeResultDTO = MergeResultDTO.of(MockCategoryType.DYNAMIC_CLASS.getName(), this.clazzName, - this.methodName, this.args, replayResult, typeName, signatureHashKey, null); - cachedReplayResultMap.put(signatureHashKey, mergeResultDTO); - } + Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); + if (MockUtils.checkResponseMocker(replayMocker)) { + String typeName = replayMocker.getTargetResponse().getType(); + String replayBody = replayMocker.getTargetResponse().getBody(); + replayResult = deserializeResult(replayBody, typeName); } replayResult = restoreResponse(replayResult); boolean ignoreMockResult = IgnoreUtils.ignoreMockResult(clazzName, methodName); @@ -184,13 +178,73 @@ String buildMethodKey(Method method, Object[] args, String keyExpression) { } String key = ExpressionParseUtil.generateKey(method, args, keyExpression); - if (key != null) { + if (key != null || StringUtil.isNotEmpty(keyExpression)) { return key; } - return serialize(args); + return serialize(normalizeArgs(args)); + } + + String buildRequestType(Method method) { + return ArrayUtils.toString(method.getParameterTypes(), obj -> ((Class)obj).getTypeName()); + } + + /** + * There will be a second-level difference between time type recording and playback, + * resulting in inability to accurately match data. And in order to be compatible with previously recorded data, + * the second time is cleared to zero. + * ex: 2023-01-01 12:12:01.123 -> 2023-01-01 12:12:00.000 + */ + private Object[] normalizeArgs(Object[] args) { + if (ArrayUtils.isEmpty(args)) { + return args; + } + Object[] normalizedArgs = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + normalizedArgs[i] = normalizeArg(args[i]); + } + return normalizedArgs; + } + + private Object normalizeArg(Object arg) { + if (arg == null) { + return null; + } + + if (arg instanceof LocalDateTime) { + return zeroTimeSecond(DateFormatUtils.format((LocalDateTime) arg, SIMPLE_DATE_FORMAT_MILLIS)); + } + + if (arg instanceof LocalTime) { + return zeroTimeSecond(DateFormatUtils.format((LocalTime) arg, SHORT_TIME_FORMAT_MILLISECOND)); + } + + if (arg instanceof Calendar) { + Calendar calendar = (Calendar) arg; + String timeZone = DateFormatUtils.format(calendar, TIME_ZONE, calendar.getTimeZone()); + return zeroTimeSecond(DateFormatUtils.format(calendar, SIMPLE_DATE_FORMAT_MILLIS_WITH_ZONE, calendar.getTimeZone())) + timeZone; + } + + if (arg instanceof Date) { + return zeroTimeSecond(DateFormatUtils.format((Date) arg, SIMPLE_DATE_FORMAT_MILLIS)); + } + + if (JODA_LOCAL_DATE_TIME.equals(arg.getClass().getName())) { + return zeroTimeSecond(((org.joda.time.LocalDateTime) arg).toString(SIMPLE_DATE_FORMAT_MILLIS)); + } + + if (JODA_LOCAL_TIME.equals(arg.getClass().getName())) { + return zeroTimeSecond(((org.joda.time.LocalTime) arg).toString(SHORT_TIME_FORMAT_MILLISECOND)); + } + + return arg; + } + + private String zeroTimeSecond(String text) { + return text + ZERO_SECOND_TIME; } + String buildMethodKey(Method method, Object[] args) { if (ArrayUtils.isEmpty(args)) { return null; @@ -202,7 +256,7 @@ String buildMethodKey(Method method, Object[] args) { DynamicClassEntity dynamicEntity = Config.get().getDynamicEntity(dynamicSignature); if (dynamicEntity == null || StringUtil.isEmpty(dynamicEntity.getAdditionalSignature())) { - return serialize(args); + return serialize(normalizeArgs(args)); } String keyExpression = ExpressionParseUtil.replaceToExpression(method, dynamicEntity.getAdditionalSignature()); @@ -220,8 +274,10 @@ private String getDynamicEntitySignature() { private Mocker makeMocker() { Mocker mocker = MockUtils.createDynamicClass(this.clazzName, this.methodName); + mocker.setMerge(true); mocker.getTargetRequest().setBody(this.methodKey); mocker.getTargetResponse().setType(this.resultClazz); + mocker.getTargetRequest().setType(this.requestType); return mocker; } @@ -244,50 +300,55 @@ Object restoreResponse(Object result) { return completableFuture; } + if (MONO.equals((this.methodReturnType))) { + if (result instanceof Throwable) { + return Mono.error((Throwable) result); + } + return Mono.justOrEmpty(result); + } + + if (FLUX.equals(this.methodReturnType)) { + if (result instanceof Throwable) { + return Flux.error((Throwable) result); + } + return FluxUtil.restore(result); + } return result; } private boolean needRecord() { /* * Judge whether the hash value of the method signature has been recorded to avoid repeated recording. - * The nonparametric method may return different results and needs to be recorded * */ ArexContext context = ContextManager.currentContext(); - if (context != null && methodKey != null) { + if (context != null) { this.methodSignatureKey = buildDuplicateMethodKey(); - this.methodSignatureKeyHash = StringUtil.encodeAndHash(methodSignatureKey); - if (context.getMethodSignatureHashList().contains(methodSignatureKeyHash)) { + if (methodKey != null) { + this.methodSignatureKeyHash = StringUtil.encodeAndHash(this.methodSignatureKey); + } else { + // no argument method check repeat by className + methodName + result + this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(); + } + if (context.getMethodSignatureHashList().contains(this.methodSignatureKeyHash)) { if (Config.get().isEnableDebug()) { LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record method, cuz exist same method signature: %s", methodSignatureKey)); + StringUtil.format("do not record method, cuz exist same method signature: %s", this.methodSignatureKey)); } return false; } } - if (result == null || result instanceof Throwable) { + if (this.result == null || this.result instanceof Throwable) { return true; } - try { - int size = 0; - if (result instanceof Collection) { - size = ((Collection) result).size(); - } else if (result instanceof Map) { - size = ((Map) result).size(); - } else if (result.getClass().isArray()) { - size = Array.getLength(result); - } - if (size > RESULT_SIZE_MAX) { - String methodInfo = methodSignatureKey == null ? buildDuplicateMethodKey() : methodSignatureKey; - LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record method, cuz result size:%s > max limit: %s, method info: %s", - String.valueOf(size), String.valueOf(RESULT_SIZE_MAX), methodInfo)); - return false; - } - } catch (Throwable e) { - LogManager.warn(NEED_RECORD_TITLE, e); + if (!agentSizeOf.checkMemorySizeLimit(this.result, ArexConstants.MEMORY_SIZE_1MB)) { + IgnoreUtils.addInvalidOperation(dynamicSignature); + LogManager.warn(NEED_RECORD_TITLE, StringUtil.format("dynamicClass:%s, exceed memory max limit:%s", + dynamicSignature, AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB))); + return false; } + return true; } @@ -317,13 +378,21 @@ private String getResultKey() { */ private void cacheMethodSignature() { ArexContext context = ContextManager.currentContext(); - if (context != null && this.methodKey != null && this.methodSignatureKey != null) { - context.getMethodSignatureHashList().add(this.methodSignatureKeyHash); + if (context != null) { + if (this.methodKey != null && this.methodSignatureKey != null) { + context.getMethodSignatureHashList().add(this.methodSignatureKeyHash); + } else { + // no argument method check repeat by class+method+result + context.getMethodSignatureHashList().add(buildNoArgMethodSignatureHash()); + } } } public String getSerializedResult() { - return serializedResult; + if (this.serializedResult == null) { + serializedResult = serialize(this.result); + } + return this.serializedResult; } private String serialize(Object object) { @@ -339,7 +408,7 @@ private String serialize(Object object) { } } - private int buildMethodSignatureKey() { - return StringUtil.encodeAndHash(String.format("%s_%s_%s", clazzName, methodName, methodKey)); + private int buildNoArgMethodSignatureHash() { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, getSerializedResult())); } } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamiConstants.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicConstants.java similarity index 87% rename from arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamiConstants.java rename to arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicConstants.java index 9abc26cee..012e1ecc8 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamiConstants.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicConstants.java @@ -1,6 +1,6 @@ package io.arex.inst.dynamic.common; -public class DynamiConstants { +public class DynamicConstants { public static final String AREX_MOCK = "com.arextest.common.annotation.ArexMock"; public static final String SPRING_CACHE = "org.springframework.cache.annotation.Cacheable"; diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/ExpressionParseUtil.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/ExpressionParseUtil.java index c0cb566fb..5746900e2 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/ExpressionParseUtil.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/ExpressionParseUtil.java @@ -6,6 +6,8 @@ import java.lang.reflect.Parameter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + +import io.arex.inst.runtime.serializer.Serializer; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; @@ -49,14 +51,24 @@ public static String generateKey(Method method, Object[] args, String keyExpress for (int i = 0; i < args.length; i++) { context.setVariable(parameterNames[i], args[i]); } - return expression.getValue(context, String.class); + Object expressionValue = expression.getValue(context); + if (expressionValue instanceof String) { + return (String) expressionValue; + } + return Serializer.serialize(expressionValue); } catch (Exception e) { return null; } } public static String replaceToExpression(Method method, String additionalSignature) { - if (method == null || StringUtil.isEmpty(additionalSignature) || !additionalSignature.contains("$")) { + if (method == null || StringUtil.isEmpty(additionalSignature)) { + return null; + } + if (additionalSignature.contains("#")) { + return additionalSignature; + } + if (!additionalSignature.contains("$")) { return null; } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/FluxConsumer.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/FluxConsumer.java new file mode 100644 index 000000000..82703d9de --- /dev/null +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/FluxConsumer.java @@ -0,0 +1,57 @@ +package io.arex.inst.dynamic.common.listener; + +import io.arex.agent.bootstrap.ctx.TraceTransmitter; +import io.arex.inst.common.util.FluxUtil; +import io.arex.inst.common.util.FluxUtil.FluxResult; +import io.arex.inst.dynamic.common.DynamicClassExtractor; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.TypeUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import reactor.core.publisher.Flux; + +public class FluxConsumer { + + private final TraceTransmitter traceTransmitter; + private final DynamicClassExtractor extractor; + + public FluxConsumer(DynamicClassExtractor extractor) { + this.traceTransmitter = TraceTransmitter.create(); + this.extractor = extractor; + } + + public Flux accept(Flux responseFlux) { + // use a list to record all elements + List fluxElementMockerResults = new ArrayList<>(); + AtomicInteger index = new AtomicInteger(1); + String responseType = TypeUtil.getName(responseFlux); + return responseFlux + // add element to list + .doOnNext(element -> { + try (TraceTransmitter tm = traceTransmitter.transmit()) { + fluxElementMockerResults.add( + getFluxElementMockerResult(index.getAndIncrement(), element)); + } + }) + // add error to list + .doOnError(error -> { + try (TraceTransmitter tm = traceTransmitter.transmit()) { + fluxElementMockerResults.add( + getFluxElementMockerResult(index.getAndIncrement(), error)); + } + }) + .doFinally(result -> { + try (TraceTransmitter tm = traceTransmitter.transmit()) { + FluxResult fluxResult = new FluxResult(responseType, fluxElementMockerResults); + extractor.recordResponse(fluxResult); + } + }); + } + + private FluxUtil.FluxElementResult getFluxElementMockerResult(int index, Object element) { + String content = Serializer.serialize(element, ArexConstants.GSON_SERIALIZER); + return new FluxUtil.FluxElementResult(index, content, TypeUtil.getName(element)); + } +} diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/MonoConsumer.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/MonoConsumer.java new file mode 100644 index 000000000..6ea63154d --- /dev/null +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/listener/MonoConsumer.java @@ -0,0 +1,36 @@ +package io.arex.inst.dynamic.common.listener; + +import io.arex.agent.bootstrap.ctx.TraceTransmitter; +import io.arex.inst.dynamic.common.DynamicClassExtractor; +import reactor.core.publisher.Mono; + +public class MonoConsumer { + + private final TraceTransmitter traceTransmitter; + private final DynamicClassExtractor extractor; + + public MonoConsumer(DynamicClassExtractor extractor) { + this.traceTransmitter = TraceTransmitter.create(); + this.extractor = extractor; + } + + /** + * support for Mono type recording + * @param responseMono + * @return + */ + public Mono accept(Mono responseMono) { + return responseMono + .doOnSuccess(result -> { + try (TraceTransmitter tm = traceTransmitter.transmit()) { + extractor.recordResponse(result); + } + }) + .doOnError(error-> { + try (TraceTransmitter tm = traceTransmitter.transmit()) { + extractor.recordResponse(error); + } + }); + } + +} diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java index d6848f8d3..a63662e92 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java @@ -4,6 +4,9 @@ import com.google.common.util.concurrent.ListenableFuture; import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.Mocker.Target; +import io.arex.agent.thirdparty.util.time.DateFormatUtils; +import io.arex.inst.common.util.FluxUtil; +import io.arex.inst.dynamic.common.listener.MonoConsumer; import io.arex.inst.runtime.config.ConfigBuilder; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; @@ -13,15 +16,20 @@ import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.util.IgnoreUtils; import io.arex.inst.runtime.util.MockUtils; -import io.arex.inst.runtime.util.TypeUtil; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.Set; +import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; + +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -34,12 +42,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; import org.mockito.stubbing.Answer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -50,19 +59,24 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) class DynamicClassExtractorTest { + static AgentSizeOf agentSizeOf; + @BeforeAll static void setUp() { Mockito.mockStatic(ContextManager.class); Mockito.mockStatic(Serializer.class); ConfigBuilder.create("test").enableDebug(true).build(); + agentSizeOf = Mockito.mock(AgentSizeOf.class); + Mockito.mockStatic(AgentSizeOf.class); + Mockito.when(AgentSizeOf.newInstance()).thenReturn(agentSizeOf); } @AfterAll static void tearDown() { + agentSizeOf = null; Mockito.clearAllCaches(); } @@ -81,11 +95,10 @@ void record(Runnable mocker, Object[] args, Object result, Predicate pre System.out.println("mock MockService.recordMocker"); return null; }); - - Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", String.class); + Method testWithArexMock = getMethod(result); DynamicClassExtractor extractor = new DynamicClassExtractor(testWithArexMock, args); - extractor.recordResponse(result); + assertTrue(predicate.test(result)); } } @@ -96,7 +109,7 @@ static Stream recordCase() { Runnable signatureContains = () -> { Set methodSignatureHashList = new HashSet<>(); methodSignatureHashList.add(StringUtil.encodeAndHash( - "io.arex.inst.dynamic.common.DynamicClassExtractorTest_testWithArexMock_mock Serializer.serialize_no_result" + "io.arex.inst.dynamic.common.DynamicClassExtractorTest_testWithArexMock_mock Serializer.serialize_has_result_java.lang.String" )); Mockito.when(context.getMethodSignatureHashList()).thenReturn(methodSignatureHashList); try { @@ -105,13 +118,6 @@ static Stream recordCase() { } }; - Runnable serializeThrowException = () -> { - try { - Mockito.when(Serializer.serializeWithException(any(), anyString())).thenThrow(new RuntimeException("mock Serializer.serializeThrowException")); - } catch (Throwable ignored) { - } - }; - Runnable resultIsNull = () -> { Mockito.when(context.getMethodSignatureHashList()).thenReturn(new HashSet<>()); }; @@ -119,16 +125,60 @@ static Stream recordCase() { Predicate isNull = Objects::isNull; Predicate nonNull = Objects::nonNull; return Stream.of( - arguments(signatureContains, new Object[]{"mock"}, "mock1", nonNull), - arguments(resultIsNull, new Object[]{"mock"}, null, isNull), - arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonList("mock"), nonNull), - arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonMap("key", "val"), nonNull), - arguments(resultIsNull, new Object[]{"mock"}, new int[1001], nonNull), - arguments(resultIsNull, null, null, isNull), - arguments(resultIsNull, null, Futures.immediateFuture("mock-future"), nonNull) + arguments(signatureContains, new Object[]{"mock"}, "mock1", nonNull), + arguments(resultIsNull, new Object[]{"mock"}, null, isNull), + arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonList("mock"), nonNull), + arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonMap("key", "val"), nonNull), + arguments(resultIsNull, new Object[]{"mock"}, new int[1001], nonNull), + arguments(resultIsNull, null, null, isNull), + arguments(resultIsNull, null, Mono.just("mono test"), nonNull), + arguments(resultIsNull, null, Futures.immediateFuture("mock-future"), nonNull), + arguments(resultIsNull, null, Flux.just("mock-exception"), nonNull) ); } + private Method getMethod(Object result) { + try { + + if (result instanceof Mono) { + return Mono.class.getDeclaredMethod("just", Object.class); + } else if (result instanceof Flux) { + return Flux.class.getDeclaredMethod("just", Object.class); + + } else { + return DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", String.class); + } + } catch (NoSuchMethodException e) { + return null; + } + } + + @Test + void resetMonoResponse() { + try { + Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", + String.class); + final Object[] args = {"errorSerialize"}; + final DynamicClassExtractor extractor = new DynamicClassExtractor(testWithArexMock, args); + final Predicate nonNull = Objects::nonNull; + + // exception + Mono result = monoExceptionTest(); + MonoConsumer monoConsumer = new MonoConsumer(extractor); + result = monoConsumer.accept(result); + result.subscribe(); + assertTrue(nonNull.test(result)); + + // normal + result = monoTest(); + result = monoConsumer.accept(result); + result.subscribe(); + assertTrue(nonNull.test(result)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + @ParameterizedTest @MethodSource("replayCase") void replay(Runnable mocker, Object[] args, Predicate predicate) throws Throwable { @@ -189,12 +239,12 @@ void testSetFutureResponse() throws NoSuchMethodException { // result instanceOf CompletableFuture CompletableFuture completableFuture = CompletableFuture.completedFuture("CompletableFuture-result"); extractor.setFutureResponse(completableFuture); - assertNull(extractor.getSerializedResult()); + assertDoesNotThrow(() -> extractor.getSerializedResult()); // result instanceOf ListenableFuture ListenableFuture resultFuture = Futures.immediateFuture("result"); extractor.setFutureResponse(resultFuture); - assertNull(extractor.getSerializedResult()); + assertDoesNotThrow(() -> extractor.getSerializedResult()); } @Test @@ -226,6 +276,31 @@ void restoreResponseTest() throws NoSuchMethodException, ExecutionException, Int extractor = new DynamicClassExtractor(testWithArexMock, new Object[]{"mock"}, "#val", null); actualResult = extractor.restoreResponse("test-value"); assertEquals("test-value", actualResult); + + //mono value + Method testReturnMono = DynamicClassExtractorTest.class.getDeclaredMethod("testReturnMono", String.class, + Throwable.class); + DynamicClassExtractor monoTestExtractor = new DynamicClassExtractor(testReturnMono, new Object[]{"mock"}, + "#val", null); + Object monoTestExtractorActualResult = monoTestExtractor.restoreResponse("test-value"); + assertEquals("test-value", ((Mono) monoTestExtractorActualResult).block()); + + monoTestExtractorActualResult = monoTestExtractor.restoreResponse(new RuntimeException("test-exception")); + Object monoTestFinalActualResult = monoTestExtractorActualResult; + assertThrows(RuntimeException.class, () -> ((Mono) monoTestFinalActualResult).block()); + + // flux value + Method testReturnFlux = DynamicClassExtractorTest.class.getDeclaredMethod("testReturnFlux", String.class, + Throwable.class); + DynamicClassExtractor fluxTestExtractor = new DynamicClassExtractor(testReturnFlux, new Object[]{"mock"}, + "#val", null); + List list = new ArrayList<>(); + FluxUtil.FluxResult fluxResult = new FluxUtil.FluxResult(null, list); + Object fluxNormalTest = fluxTestExtractor.restoreResponse(fluxResult); + assertNull(((Flux) fluxNormalTest).blockFirst()); + + Object fluxExceptionTest = fluxTestExtractor.restoreResponse(new RuntimeException()); + assertThrows(RuntimeException.class,()-> ((Flux) fluxExceptionTest).blockFirst()); } @Test @@ -245,7 +320,7 @@ void testBuildResultClazz() throws NoSuchMethodException { actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List-java.time.LocalDateTime", actualResult); - ConfigBuilder.create("mock-service").build(); + ConfigBuilder.create("mock-service").enableDebug(true).build(); extractor = new DynamicClassExtractor(testWithArexMock, new Object[]{"mock"}, "#val", null); // DynamicEntityMap is empty actualResult = extractor.buildResultClazz("Java.util.List"); @@ -254,14 +329,14 @@ void testBuildResultClazz() throws NoSuchMethodException { // DynamicEntityMap is not empty, actualType is empty List list = new ArrayList<>(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List", actualResult); // actualType is not empty list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "T:java.lang.String")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List-java.lang.String", actualResult); } @@ -276,7 +351,7 @@ void testBuildMethodKey() throws Throwable { assertNull(actualResult); // getDynamicClassSignatureMap is empty - ConfigBuilder.create("mock-service").build(); + ConfigBuilder.create("mock-service").enableDebug(true).build(); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenReturn("mock Serializer.serialize"); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock"}); assertEquals("mock Serializer.serialize", actualResult); @@ -284,14 +359,14 @@ void testBuildMethodKey() throws Throwable { // getDynamicClassSignatureMap is not empty, additionalSignature is empty List list = new ArrayList<>(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock"}); assertEquals("mock Serializer.serialize", actualResult); // additionalSignature is not empty list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "", "$1")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock-method-key"}); assertEquals("mock-method-key", actualResult); @@ -299,15 +374,28 @@ void testBuildMethodKey() throws Throwable { extractor = new DynamicClassExtractor(testWithArexMock, new Object[]{"mock"}, "#val", String.class); list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "$1")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock-method-key"}); assertEquals("mock-method-key", actualResult); + + // express is null + Method testWithArexMockList = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", List.class); + extractor = new DynamicClassExtractor(testWithArexMockList, new Object[]{new ArrayList<>()}, null, String.class); + list.clear(); + list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "$1.get(0)")); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); + actualResult = extractor.buildMethodKey(testWithArexMockList, new Object[]{new ArrayList<>()}); + assertNull(actualResult); } public String testWithArexMock(String val) { return val + "testWithArexMock"; } + public String testWithArexMock(List list) { + return "testWithArexMock"; + } + public ListenableFuture testReturnListenableFuture(String val, Throwable t) { if (t != null) { return Futures.immediateFailedFuture(t); @@ -328,7 +416,7 @@ public CompletableFuture testReturnCompletableFuture(String val, Throwab void invalidOperation() throws Throwable { Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", String.class); final Object[] args = {"errorSerialize"}; - ConfigBuilder.create("invalid-operation").build(); + ConfigBuilder.create("invalid-operation").enableDebug(true).build(); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenThrow(new RuntimeException("errorSerialize")); DynamicClassExtractor extractor = new DynamicClassExtractor(testWithArexMock, args); extractor.recordResponse("errorSerialize"); @@ -350,4 +438,114 @@ void emptyMethodKeyAndExceedSize() throws NoSuchMethodException { DynamicClassExtractor extractor = new DynamicClassExtractor(testEmptyArgs, new Object[0]); assertDoesNotThrow(() -> extractor.recordResponse(new int[1001])); } + + @Test + void normalizeArgsTest() throws Exception { + Method testEmptyArgs = DynamicClassExtractorTest.class.getDeclaredMethod("normalizeArgsTest"); + DynamicClassExtractor extractor = new DynamicClassExtractor(testEmptyArgs, new Object[0]); + Method normalizeArgsMethod = DynamicClassExtractor.class.getDeclaredMethod("normalizeArgs", Object[].class); + normalizeArgsMethod.setAccessible(true); + + String zeroSecond = "00.000"; + + // null + Object[] args = new Object[]{null}; + Object[] normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{args}); + assertNull(normalizedArgs[0]); + + // String + args = new Object[]{"test"}; + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{args}); + assertEquals("test", normalizedArgs[0]); + + // LocalDateTime + LocalDateTime localDateTime = LocalDateTime.now(); + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{new Object[]{localDateTime}}); + String text = DateFormatUtils.format(localDateTime, "yyyy-MM-dd HH:mm:ss.SSS"); + assertEquals(text.substring(0, text.length() - zeroSecond.length()) + zeroSecond, normalizedArgs[0]); + System.out.println("localDateTime: " + normalizedArgs[0]); + + // LocalTime + LocalTime localTime = LocalTime.now(); + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{new Object[]{localTime}}); + text = DateFormatUtils.format(localTime, "HH:mm:ss.SSS"); + assertEquals(text.substring(0, text.length() - zeroSecond.length()) + zeroSecond, normalizedArgs[0]); + System.out.println("localTime: " + normalizedArgs[0]); + + // Calendar + String zeroSecondWithZone = "00.000+08:00"; + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT-01:00")); + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{new Object[]{calendar}}); + text = DateFormatUtils.format(calendar, "yyyy-MM-dd'T'HH:mm:ss.SSSZZZ", calendar.getTimeZone()); + assertEquals(text.substring(0, text.length() - zeroSecondWithZone.length()) + "00.000-01:00", normalizedArgs[0]); + System.out.println("calendar: " + normalizedArgs[0]); + + // Date + Date date = new Date(); + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{new Object[]{date}}); + text = DateFormatUtils.format(date, "yyyy-MM-dd HH:mm:ss.SSS"); + assertEquals(text.substring(0, text.length() - zeroSecond.length()) + zeroSecond, normalizedArgs[0]); + System.out.println("date: " + normalizedArgs[0]); + + // joda LocalDateTime + org.joda.time.LocalDateTime jodaLocalDateTime = org.joda.time.LocalDateTime.now(); + String originalTimeString = jodaLocalDateTime.toString("yyyy-MM-dd HH:mm:ss.SSS"); + args = new Object[]{jodaLocalDateTime}; + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{args}); + assertEquals(originalTimeString.substring(0, originalTimeString.length() - zeroSecond.length()) + zeroSecond, normalizedArgs[0]); + System.out.println("jodaLocalDateTime: " + normalizedArgs[0]); + + // joda LocalTime + org.joda.time.LocalTime jodaLocalTime = org.joda.time.LocalTime.now(); + originalTimeString = jodaLocalTime.toString("HH:mm:ss.SSS"); + args = new Object[]{jodaLocalTime}; + normalizedArgs = (Object[]) normalizeArgsMethod.invoke(extractor, new Object[]{args}); + assertEquals(originalTimeString.substring(0, originalTimeString.length() - zeroSecond.length()) + zeroSecond, normalizedArgs[0]); + System.out.println("jodaLocalTime: " + normalizedArgs[0]); + } + + public Mono testReturnMono(String val, Throwable t) { + if (t != null) { + return Mono.error(t); + } + return Mono.justOrEmpty(val + "testReturnMono"); + } + + public Flux testReturnFlux(String val,Throwable t){ + if (t != null) { + return Flux.error(t); + } + return val == null ? Flux.empty() : Flux.just(val + "testReturnFlux"); + } + + public static Mono monoTest() { + return Mono.justOrEmpty("Mono test") + .doOnNext(value -> System.out.println("Mono context:" + value)) + .onErrorResume(t -> Mono.empty()); + } + + public static Mono monoExceptionTest() { + return Mono.error(new RuntimeException("e")) + .doOnError(throwable -> System.out.println("Mono error:" + throwable)) + .doOnSuccess(object -> System.out.println("Mono success:" + object.getClass())); + } + + public static Flux fluxTest() { + return Flux.just("flux","test") + .doOnNext(value -> System.out.println("Flux context:" + value)) + .onErrorResume(t -> Mono.empty()); + } + + public static Flux fluxOnErrorTest() { + return Flux.just("flux", "test") + .doOnNext(value -> { + throw new RuntimeException("error"); + }); + } + + public static Flux fluxExceptionTest() { + return Flux.error(new RuntimeException("e")) + .doOnError(throwable -> System.out.println("Flux error:" + throwable)) + .doOnNext(object -> System.out.println("Flux success:" + object.getClass())); + } } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/ExpressionParseUtilTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/ExpressionParseUtilTest.java index cb4ba51c0..418f52a56 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/ExpressionParseUtilTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/ExpressionParseUtilTest.java @@ -1,17 +1,31 @@ package io.arex.inst.dynamic.common; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Method; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; + +import io.arex.inst.runtime.serializer.Serializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; class ExpressionParseUtilTest { + @BeforeAll + static void setUp() { + Mockito.mockStatic(Serializer.class); + Mockito.when(Serializer.serialize(Mockito.any())).thenReturn("mock"); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } @ParameterizedTest @MethodSource("generateKeyArgs") @@ -77,6 +91,17 @@ static Stream generateKeyArgs() throws NoSuchMethodException { "#f1.getF4() + #f2.getF2().toString() + #f3.getFoo2().f1", (Predicate) Objects::isNull ), + Arguments.of( + new Object[]{ + new Foo2("p1", 1), + new Foo2("p2", 2), + new Foo1(new Foo2("p3", 3)) + }, + ExpressionParseUtilTest.class.getDeclaredMethod("testParseMethodKey3", Foo2.class, Foo2.class, + Foo1.class), + "#f3.getFoo2()", + (Predicate) "mock"::equals + ), Arguments.of( new Object[]{ new Foo2("p1", 1), @@ -114,6 +139,12 @@ static Stream replaceToExpressionArgs() throws NoSuchMethodException Foo1.class), "String.valueOf($1.getF2()) + $2.f2.toString() + $3.getFoo2().f1", (Predicate) "T(String).valueOf(#f1.getF2()) + #f2.f2.toString() + #f3.getFoo2().f1"::equals + ), + Arguments.of( + ExpressionParseUtilTest.class.getDeclaredMethod("testParseMethodKey3", Foo2.class, Foo2.class, + Foo1.class), + "#1.foo2.f1", + (Predicate) "#1.foo2.f1"::equals ) ); } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/listener/FluxConsumerTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/listener/FluxConsumerTest.java new file mode 100644 index 000000000..bef4fd909 --- /dev/null +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/listener/FluxConsumerTest.java @@ -0,0 +1,165 @@ +package io.arex.inst.dynamic.common.listener; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import io.arex.inst.dynamic.common.DynamicClassExtractor; +import io.arex.inst.runtime.config.ConfigBuilder; +import io.arex.inst.runtime.context.ContextManager; +import java.lang.reflect.Method; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Flux; + +public class FluxConsumerTest { + + static DynamicClassExtractor extractor; + static FluxConsumer fluxConsumer; + + @BeforeAll + static void setUp() { + Method testWithArexMock; + try { + testWithArexMock = FluxConsumerTest.class.getDeclaredMethod("testWithArexMock", String.class); + } catch (NoSuchMethodException e) { + testWithArexMock = null; + } + final Object[] args = {"errorSerialize"}; + extractor = new DynamicClassExtractor(testWithArexMock, args); + fluxConsumer = new FluxConsumer(extractor); + Mockito.mockStatic(ContextManager.class); + ConfigBuilder.create("test").enableDebug(true).build(); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + + @Test + void record() { + + // Record empty flux + testEmptyFlux(); + + // Record Exception + testFluxError(); + + // Normal conditions without exceptions or errors, all elements are recorded. + testNormalFlux(); + + // Elements before the error occurs and all elements in alternate Flux sequenceare when error occurs are recorded. + testFluxOnErrorResume(); + + // Except for the element when the error occurs, all other elements are recorded + testFluxOnErrorContinue(); + + // Elements before the error occurs and the exception are recorded (Flux terminates when exception is thrown). + testFluxOnError(); + } + + private static void testNormalFlux() { + Flux flux = Flux.just(1, 2, 3, 4, 5) + .doOnNext(val -> System.out.println("val" + ":" + val)) + // doFinally performs some operations that have nothing to do with the value of the element. + // If the doFinally operator is called multiple times, doFinally will be executed once at the end of each sequence. + .doFinally(System.out::println); + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + // record content: 1,2,3,4,5 + subscribe.subscribe(); + // record content: 1 + assertEquals(blockFirst.blockFirst(), 1); + } + + private static void testEmptyFlux() { + Flux flux = Flux.empty(); + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + // record content: 1,2,3,4,5 + subscribe.subscribe(); + // record content: 1 + assertNull(blockFirst.blockFirst()); + } + + + private static void testFluxOnErrorResume() { + Flux flux = Flux.just(1, 2) + .doOnNext(val -> { + if (val.equals(2)) { + throw new RuntimeException("error"); + } + }) + .doOnError(t -> System.out.println("error" + ":" + t)) + // returns an alternate Flux sequence when a Flux error occurs, + .onErrorResume(t -> Flux.just(7, 8, 9)); + + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + + // record content: 1,7,8,9 + subscribe.subscribe(); + // record content: 1 + assertEquals(blockFirst.blockFirst(), 1); + } + + private static void testFluxOnError() { + final Flux flux = Flux.just(1, 2, 3, 4, 5) + .doOnNext(val -> { + if (val.equals(3)) { + throw new RuntimeException("error"); + } + }) + .doOnError(t -> System.out.println("error" + ":" + t)); + + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + Flux blockLast = fluxConsumer.accept(flux); + + // record content: 1,2,RuntimeException + subscribe.subscribe(); + // record content: 1 + assertEquals(blockFirst.blockFirst(), 1); + // record content: RuntimeException + assertThrows(RuntimeException.class, () -> blockLast.blockLast()); + } + + private static void testFluxOnErrorContinue() { + Flux flux = Flux.just(1, 2, 3, 4, 5) + .doOnNext(val -> { + if (val.equals(3)) { + throw new RuntimeException("error"); + } + }) + .onErrorContinue((t, o) -> System.out.println("error" + ":" + t)) + .doOnNext(val -> System.out.println("val" + ":" + val)); + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + Flux blockLast = fluxConsumer.accept(flux); + + // record content: 1,2,4,5 + subscribe.subscribe(); + // record content: 1 + assertEquals(blockFirst.blockFirst(), 1); + // record content: 5 + assertEquals(blockLast.blockLast(), 5); + } + + private static void testFluxError() { + Flux flux = Flux.error(new RuntimeException("error")); + Flux subscribe = fluxConsumer.accept(flux); + Flux blockFirst = fluxConsumer.accept(flux); + // record content: RuntimeException + subscribe.subscribe(); + // record content: RuntimeException + assertThrows(RuntimeException.class, () -> blockFirst.blockFirst()); + } + + public String testWithArexMock(String val) { + return val + "testWithArexMock"; + } + +} diff --git a/arex-instrumentation/dynamic/arex-dynamic/pom.xml b/arex-instrumentation/dynamic/arex-dynamic/pom.xml index a12a3cd67..e6c3c278f 100644 --- a/arex-instrumentation/dynamic/arex-dynamic/pom.xml +++ b/arex-instrumentation/dynamic/arex-dynamic/pom.xml @@ -25,6 +25,12 @@ ${arex-common.version} provided + + io.projectreactor + reactor-core + ${reactor.version} + provided + diff --git a/arex-instrumentation/dynamic/arex-dynamic/src/main/java/io/arex/inst/dynamic/DynamicClassInstrumentation.java b/arex-instrumentation/dynamic/arex-dynamic/src/main/java/io/arex/inst/dynamic/DynamicClassInstrumentation.java index ac96cd9be..234d7f86d 100644 --- a/arex-instrumentation/dynamic/arex-dynamic/src/main/java/io/arex/inst/dynamic/DynamicClassInstrumentation.java +++ b/arex-instrumentation/dynamic/arex-dynamic/src/main/java/io/arex/inst/dynamic/DynamicClassInstrumentation.java @@ -1,7 +1,7 @@ package io.arex.inst.dynamic; import io.arex.agent.bootstrap.util.CollectionUtil; -import io.arex.inst.dynamic.common.DynamiConstants; +import io.arex.inst.dynamic.common.DynamicConstants; import io.arex.inst.dynamic.common.DynamicClassExtractor; import io.arex.inst.runtime.config.Config; @@ -78,7 +78,7 @@ public Transformer transformer() { if (replaceMethodsProvider == null) { return null; } - return (builder, typeDescription, classLoader, module) -> { + return (builder, typeDescription, classLoader, module, domain) -> { for (Map.Entry> entry : replaceMethodsProvider.getSearchMethodMap().entrySet()) { builder = builder.visit(replaceMethod(entry.getValue(), entry.getKey())); } @@ -106,8 +106,8 @@ private void removeIgnoredMethods(DynamicType.Builder builder) { public List methodAdvices() { ElementMatcher.Junction matcher = null; if (onlyClass != null) { - matcher = isMethod().and(not(takesNoArguments())) - .and(not(isAnnotatedWith(namedOneOf(DynamiConstants.SPRING_CACHE, DynamiConstants.AREX_MOCK)))); + matcher = isMethod().and(isPublic()).and(not(takesNoArguments())) + .and(not(isAnnotatedWith(namedOneOf(DynamicConstants.SPRING_CACHE, DynamicConstants.AREX_MOCK)))); if (isNotAbstractClass(onlyClass.getClazzName())) { matcher = matcher.and(not(isOverriddenFrom(namedOneOf(Config.get().getDynamicAbstractClassList())))); } @@ -129,7 +129,7 @@ public List methodAdvices() { private ElementMatcher.Junction builderMethodMatcher(DynamicClassEntity entity) { ElementMatcher.Junction matcher = parseTypeMatcher(entity.getOperation(), this::parseMethodMatcher) - .and(not(isAnnotatedWith(namedOneOf(DynamiConstants.SPRING_CACHE, DynamiConstants.AREX_MOCK)))); + .and(not(isAnnotatedWith(namedOneOf(DynamicConstants.SPRING_CACHE, DynamicConstants.AREX_MOCK)))); if (CollectionUtil.isNotEmpty(entity.getParameters())) { matcher = matcher.and(takesArguments(entity.getParameters().size())); for (int i = 0; i < entity.getParameters().size(); i++) { @@ -180,7 +180,7 @@ public static void onExit(@Advice.Local("extractor") DynamicClassExtractor extra return; } if (ContextManager.needRecord() && RepeatedCollectManager.exitAndValidate() && extractor != null) { - extractor.recordResponse(throwable != null ? throwable : result); + result = extractor.recordResponse(throwable != null ? throwable : result); } } } diff --git a/arex-instrumentation/dynamic/arex-dynamic/src/test/java/io/arex/inst/dynamic/DynamicClassInstrumentationTest.java b/arex-instrumentation/dynamic/arex-dynamic/src/test/java/io/arex/inst/dynamic/DynamicClassInstrumentationTest.java index 598160f6a..ffb4bb84d 100644 --- a/arex-instrumentation/dynamic/arex-dynamic/src/test/java/io/arex/inst/dynamic/DynamicClassInstrumentationTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic/src/test/java/io/arex/inst/dynamic/DynamicClassInstrumentationTest.java @@ -201,7 +201,7 @@ void onExit() throws NoSuchMethodException { // record try(MockedConstruction ignored = Mockito.mockConstruction(DynamicClassExtractor.class, ((extractor, context) -> { - Mockito.doNothing().when(extractor).recordResponse(throwable); + Mockito.doReturn(true).when(extractor).recordResponse(throwable); }))) { Mockito.when(ContextManager.needRecord()).thenReturn(true); Mockito.when(RepeatedCollectManager.exitAndValidate()).thenReturn(true); diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java index aef6ea403..15478ddbf 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentation.java @@ -46,29 +46,31 @@ public static class ExecuteAdvice { public static boolean onEnter(@Advice.Argument(1) HttpRequest httpRequest, @Advice.Argument(value = 3, readOnly = false) FutureCallback callback, @Advice.Local("mockResult") MockResult mockResult) { - try { - if (ApacheHttpClientHelper.ignoreRequest(httpRequest)) { - callback = FutureCallbackWrapper.wrap(callback); - return false; - } - } catch (Exception ignored) { + if (ApacheHttpClientHelper.ignoreRequest(httpRequest)) { + // for transmit trace context callback = FutureCallbackWrapper.wrap(callback); return false; } if (ContextManager.needRecordOrReplay() && RepeatedCollectManager.validate()) { - // recording works in callback wrapper FutureCallback callbackWrapper = FutureCallbackWrapper.wrap(httpRequest, callback); if (callbackWrapper != null) { - callback = callbackWrapper; - if (ContextManager.needReplay()) { - mockResult = ((FutureCallbackWrapper)callback).replay(); - return mockResult != null && mockResult.notIgnoreMockResult(); + if (ContextManager.needRecord()) { + // recording works in callback wrapper + callback = callbackWrapper; + } else if (ContextManager.needReplay()) { + mockResult = ((FutureCallbackWrapper)callbackWrapper).replay(); + boolean result = mockResult != null && mockResult.notIgnoreMockResult(); + // callback wrapper only set when mock result is not ignored + if (result) { + callback = callbackWrapper; + return true; + } } } - } else { - callback = FutureCallbackWrapper.wrap(callback); } + + callback = FutureCallbackWrapper.wrap(callback); return false; } @@ -83,4 +85,4 @@ public static void onExit(@Advice.Argument(value = 3, readOnly = false) FutureCa } } } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java index b7a28bb1e..e85d90451 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapter.java @@ -6,12 +6,14 @@ import io.arex.inst.httpclient.common.HttpResponseWrapper; import io.arex.inst.httpclient.common.HttpResponseWrapper.StringTuple; import io.arex.inst.runtime.log.LogManager; +import java.io.ByteArrayOutputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; +import org.apache.http.client.entity.GzipCompressingEntity; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.HttpEntityWrapper; @@ -21,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import org.apache.http.util.EntityUtils; public class ApacheHttpClientAdapter implements HttpClientAdapter { private final HttpUriRequest httpRequest; @@ -45,6 +48,10 @@ public byte[] getRequestBytes() { if (entity == null) { return ZERO_BYTE; } + // getContent will throw UnsupportedOperationException + if (entity instanceof GzipCompressingEntity) { + return writeTo((GzipCompressingEntity) entity); + } if (entity instanceof CachedHttpEntityWrapper) { return ((CachedHttpEntityWrapper) entity).getCachedBody(); } @@ -77,14 +84,17 @@ public URI getUri() { public HttpResponseWrapper wrap(HttpResponse response) { HttpEntity httpEntity = response.getEntity(); if (!check(httpEntity)) { + LogManager.info("AHC.wrap", "AHC response wrap failed, uri: " + getUri()); return null; } byte[] responseBody; try { responseBody = IOUtils.copyToByteArray(httpEntity.getContent()); + // For release connection, see PoolingHttpClientConnectionManager#requestConnection,releaseConnection + EntityUtils.consumeQuietly(httpEntity); } catch (Exception e) { - LogManager.warn("copyToByteArray", "getResponseBody error, uri: " + getUri(), e); + LogManager.warn("AHC.wrap", "AHC copyToByteArray error, uri: " + getUri(), e); return null; } @@ -168,4 +178,15 @@ private HttpEntityEnclosingRequest enclosingRequest(HttpRequest httpRequest) { } return null; } + + private byte[] writeTo(GzipCompressingEntity entity) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + entity.writeTo(out); + return out.toByteArray(); + } catch (Exception e) { + LogManager.warn("writeTo", "getRequestBytes error, uri: " + getUri(), e); + return ZERO_BYTE; + } + } } diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java index a1ccef0c7..b90ed7529 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/InternalHttpAsyncClientInstrumentationTest.java @@ -49,31 +49,32 @@ void methodAdvices() { @Test void onEnter() throws URISyntaxException { - HttpUriRequest request1 = Mockito.mock(HttpUriRequest.class); - Mockito.when(request1.getURI()).thenThrow(new RuntimeException("mock exception")); - boolean actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request1, null, null); - assertFalse(actualResult); - try (MockedStatic contextManager = mockStatic(ContextManager.class); MockedStatic repeatedCollectManager = mockStatic(RepeatedCollectManager.class); MockedStatic futureCallbackWrapper = mockStatic(FutureCallbackWrapper.class); MockedStatic ignoreUtils = mockStatic(IgnoreUtils.class)) { + // test ignore request ignoreUtils.when(() -> IgnoreUtils.excludeOperation(any())).thenReturn(true); HttpUriRequest request2 = Mockito.mock(HttpUriRequest.class); Mockito.when(request2.getURI()).thenReturn(new URI("http://localhost")); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); + boolean actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertFalse(actualResult); + // test need record + repeatedCollectManager.when(RepeatedCollectManager::validate).thenReturn(true); + contextManager.when(ContextManager::needRecordOrReplay).thenReturn(true); ignoreUtils.when(() -> IgnoreUtils.excludeOperation(any())).thenReturn(false); - contextManager.when(ContextManager::needRecordOrReplay).thenReturn(false); + contextManager.when(ContextManager::needRecord).thenReturn(true); + FutureCallbackWrapper wrapper = Mockito.mock(FutureCallbackWrapper.class); + Mockito.when(FutureCallbackWrapper.wrap(any())).thenReturn(wrapper); + Mockito.when(FutureCallbackWrapper.wrap(any(), any())).thenReturn(wrapper); actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertFalse(actualResult); + // test need replay repeatedCollectManager.when(RepeatedCollectManager::validate).thenReturn(true); - contextManager.when(ContextManager::needRecordOrReplay).thenReturn(true); + contextManager.when(ContextManager::needRecord).thenReturn(false); contextManager.when(ContextManager::needReplay).thenReturn(true); - - FutureCallbackWrapper wrapper = Mockito.mock(FutureCallbackWrapper.class); Mockito.when(FutureCallbackWrapper.wrap(any(), any())).thenReturn(wrapper); Mockito.when(wrapper.replay()).thenReturn(MockResult.success("mock")); @@ -90,4 +91,4 @@ void onExit() { InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onExit(callbackWrapper, null, mockResult); }); } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapterTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapterTest.java index 022da8b49..09cdc44d3 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapterTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/ApacheHttpClientAdapterTest.java @@ -1,9 +1,13 @@ package io.arex.inst.httpclient.apache.common; import io.arex.inst.httpclient.common.HttpResponseWrapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; +import org.apache.http.client.entity.GzipCompressingEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.BasicHttpEntity; @@ -53,13 +57,20 @@ void getMethod() { @ParameterizedTest @MethodSource("getRequestBytesCase") - void getRequestBytes(HttpRequestBase httpRequest, Predicate predicate) { + void getRequestBytes(HttpRequestBase httpRequest, byte[] expected) { target = new ApacheHttpClientAdapter(httpRequest); - byte[] result = target.getRequestBytes(); - assertTrue(predicate.test(result)); + byte[] actual = target.getRequestBytes(); + assertEquals(Base64.getEncoder().encodeToString(expected), Base64.getEncoder().encodeToString(actual)); } static Stream getRequestBytesCase() { + // test GzipCompressingEntity + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream("mock".getBytes())); + GzipCompressingEntity gzipCompressingEntity = new GzipCompressingEntity(httpEntity); + HttpPost httpPostWithGzipEntity = new HttpPost(); + httpPostWithGzipEntity.setEntity(gzipCompressingEntity); + // Normally read request bytes BasicHttpEntity entity = new BasicHttpEntity(); entity.setContent(new ByteArrayInputStream("mock".getBytes())); @@ -73,14 +84,12 @@ static Stream getRequestBytesCase() { // null entity HttpPost nullEntityRequest = new HttpPost(); - Predicate returnZeroByte = bytes -> Arrays.equals(new byte[0], bytes); - Predicate returnNormally = bytes -> Arrays.equals("mock".getBytes(), bytes); - return Stream.of( - arguments(request, returnZeroByte), - arguments(httpPost, returnNormally), - arguments(httpPostWithoutContent, returnZeroByte), - arguments(nullEntityRequest, returnZeroByte) + arguments(httpPostWithGzipEntity, new byte[]{31, -117, 8, 0, 0, 0, 0, 0, 0, -1, -53, -51, 79, -50, 6, 0, 107, -4, 51, 63, 4, 0, 0, 0}), + arguments(request, new byte[0]), + arguments(httpPost, "mock".getBytes()), + arguments(httpPostWithoutContent, new byte[0]), + arguments(nullEntityRequest, new byte[0]) ); } @@ -159,4 +168,4 @@ void unwrap() { void skipRemoteStorageRequest() { assertFalse(target.skipRemoteStorageRequest()); } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpClientExtractor.java b/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpClientExtractor.java index d6c19353b..976a60a30 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpClientExtractor.java +++ b/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpClientExtractor.java @@ -60,7 +60,7 @@ public MockResult replay() { TResponse response = this.adapter.unwrap((HttpResponseWrapper) object); return MockResult.success(ignoreResult, response); } - return null; + return MockResult.success(ignoreResult, null); } private Mocker makeMocker() { diff --git a/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpResponseWrapper.java b/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpResponseWrapper.java index ef773d908..9d9a84d20 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpResponseWrapper.java +++ b/arex-instrumentation/httpclient/arex-httpclient-common/src/main/java/io/arex/inst/httpclient/common/HttpResponseWrapper.java @@ -8,6 +8,16 @@ public class HttpResponseWrapper { private byte[] content; private StringTuple locale; private List headers; + private String reason; + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + public void setHeaders(List headers) { this.headers = headers; } diff --git a/arex-instrumentation/httpclient/arex-httpclient-common/src/test/java/io/arex/inst/httpclient/common/HttpClientExtractorTest.java b/arex-instrumentation/httpclient/arex-httpclient-common/src/test/java/io/arex/inst/httpclient/common/HttpClientExtractorTest.java index c80c21a79..bf50a7a09 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-common/src/test/java/io/arex/inst/httpclient/common/HttpClientExtractorTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-common/src/test/java/io/arex/inst/httpclient/common/HttpClientExtractorTest.java @@ -99,7 +99,7 @@ void replay() { // replay null mockUtils.when(() -> MockUtils.replayBody(any())).thenReturn(new Object()); mockResult = httpClientExtractor.replay(); - assertNull(mockResult); + assertNull(mockResult.getResult()); } } -} \ No newline at end of file +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml b/arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml new file mode 100644 index 000000000..25a3634fc --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml @@ -0,0 +1,29 @@ + + + + io.arex + arex-instrumentation-parent + ${revision} + ../../pom.xml + + 4.0.0 + arex-httpclient-feign + + + + ${project.groupId} + arex-httpclient-common + ${project.version} + compile + + + io.github.openfeign + feign-core + 9.4.0 + provided + + + + diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientAdapter.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientAdapter.java new file mode 100644 index 000000000..5df39ef35 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientAdapter.java @@ -0,0 +1,109 @@ +package io.arex.inst.httpclient.feign; + +import feign.Request; +import feign.Response; +import feign.Response.Body; +import feign.Util; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.inst.httpclient.common.HttpClientAdapter; +import io.arex.inst.httpclient.common.HttpResponseWrapper; +import io.arex.inst.httpclient.common.HttpResponseWrapper.StringTuple; +import io.arex.inst.runtime.log.LogManager; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FeignClientAdapter implements HttpClientAdapter { + private static final String CONTENT_TYPE = "Content-Type"; + private final Request request; + private final URI uri; + private byte[] responseBody; + + public FeignClientAdapter(Request request, URI uri) { + this.request = request; + this.uri = uri; + } + + @Override + public String getMethod() { + return request.method(); + } + + @Override + public byte[] getRequestBytes() { + return request.body(); + } + + @Override + public String getRequestContentType() { + return getRequestHeader(CONTENT_TYPE); + } + + @Override + public String getRequestHeader(String name) { + final Collection values = request.headers().get(name); + if (CollectionUtil.isEmpty(values)) { + return null; + } + return values.iterator().next(); + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public HttpResponseWrapper wrap(Response response) { + final String statusLine = String.valueOf(response.status()); + final List headers = new ArrayList<>(response.headers().size()); + response.headers().forEach((k, v) -> headers.add(new StringTuple(k, v.iterator().next()))); + HttpResponseWrapper responseWrapper = new HttpResponseWrapper(statusLine, responseBody, null, headers); + responseWrapper.setReason(response.reason()); + return responseWrapper; + } + + @Override + public Response unwrap(HttpResponseWrapper wrapped) { + final int status = parseInt(wrapped.getStatusLine()); + byte[] responseContent = wrapped.getContent(); + final List wrappedHeaders = wrapped.getHeaders(); + Map> headers = new HashMap<>(wrappedHeaders.size()); + for (StringTuple header : wrappedHeaders) { + headers.put(header.name(), Collections.singletonList(header.value())); + } + return Response.builder().body(responseContent).status(status).headers(headers).reason(wrapped.getReason()).request(request).build(); + } + + private int parseInt(String statusLine) { + try { + return Integer.parseInt(statusLine); + } catch (Exception ex) { + LogManager.warn("feign.parseInt", "statusLine: " + statusLine, ex); + return -1; + } + } + + public Response copyResponse(Response response) { + if (response == null) { + return null; + } + final Body body = response.body(); + if (body == null) { + return response; + } + try { + responseBody = Util.toByteArray(body.asInputStream()); + } catch (Exception ex) { + LogManager.warn("feign.copyResponse", "uri: " + getUri(), ex); + } + if (body.isRepeatable()) { + return response; + } + return response.toBuilder().body(responseBody).build(); + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientInstrumentation.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientInstrumentation.java new file mode 100644 index 000000000..a73aa2225 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientInstrumentation.java @@ -0,0 +1,95 @@ +package io.arex.inst.httpclient.feign; + +import feign.Request; +import feign.Response; +import io.arex.agent.bootstrap.model.MockResult; +import io.arex.inst.extension.MethodInstrumentation; +import io.arex.inst.extension.TypeInstrumentation; +import io.arex.inst.httpclient.common.HttpClientExtractor; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.context.RepeatedCollectManager; +import io.arex.inst.runtime.util.IgnoreUtils; +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Argument; +import net.bytebuddy.asm.Advice.Local; +import net.bytebuddy.asm.Advice.OnMethodEnter; +import net.bytebuddy.asm.Advice.OnMethodExit; +import net.bytebuddy.asm.Advice.OnNonDefaultValue; +import net.bytebuddy.asm.Advice.Return; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; +import net.bytebuddy.matcher.ElementMatcher; + +import static net.bytebuddy.matcher.ElementMatchers.*; + +public class FeignClientInstrumentation extends TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("feign.Client")).and(not(isInterface())); + } + + @Override + public List methodAdvices() { + return Collections.singletonList(new MethodInstrumentation( + named("execute").and(takesArguments(2)) + .and(takesArgument(0, named("feign.Request"))), + ExecuteAdvice.class.getName())); + } + + public static class ExecuteAdvice{ + @OnMethodEnter(skipOn = OnNonDefaultValue.class, suppress = Throwable.class) + public static boolean onEnter(@Argument(0)Request request, + @Local("adapter") FeignClientAdapter adapter, + @Local("extractor") HttpClientExtractor extractor, + @Local("mockResult") MockResult mockResult) { + if (ContextManager.needRecordOrReplay()) { + final URI uri = URI.create(request.url()); + if (IgnoreUtils.excludeOperation(uri.getPath())) { + return false; + } + RepeatedCollectManager.enter(); + adapter = new FeignClientAdapter(request, uri); + extractor = new HttpClientExtractor(adapter); + if (ContextManager.needReplay()) { + mockResult = extractor.replay(); + return mockResult != null && mockResult.notIgnoreMockResult(); + } + } + return false; + } + + @OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Local("adapter") FeignClientAdapter adapter, + @Local("extractor") HttpClientExtractor extractor, + @Local("mockResult") MockResult mockResult, + @Return(readOnly = false, typing = Typing.DYNAMIC) Response response, + @Advice.Thrown(readOnly = false) Throwable throwable){ + if (extractor == null) { + return; + } + + if (mockResult != null && mockResult.notIgnoreMockResult()) { + if (mockResult.getThrowable() != null) { + throwable = mockResult.getThrowable(); + } else { + response = (Response) mockResult.getResult(); + } + return; + } + + if (ContextManager.needRecord() && RepeatedCollectManager.exitAndValidate()) { + response = adapter.copyResponse(response); + if (throwable != null) { + extractor.record(throwable); + } else { + extractor.record(response); + } + } + } + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentation.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentation.java new file mode 100644 index 000000000..ad14915ea --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentation.java @@ -0,0 +1,20 @@ +package io.arex.inst.httpclient.feign; + +import com.google.auto.service.AutoService; +import io.arex.inst.extension.ModuleInstrumentation; +import io.arex.inst.extension.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(ModuleInstrumentation.class) +public class FeignClientModuleInstrumentation extends ModuleInstrumentation { + + public FeignClientModuleInstrumentation() { + super("feign-client"); + } + + @Override + public List instrumentationTypes() { + return Collections.singletonList(new FeignClientInstrumentation()); + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientAdapterTest.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientAdapterTest.java new file mode 100644 index 000000000..089aac6cf --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientAdapterTest.java @@ -0,0 +1,96 @@ +package io.arex.inst.httpclient.feign; + +import static org.junit.jupiter.api.Assertions.*; + +import feign.Request; +import feign.Response; +import feign.Util; +import io.arex.inst.httpclient.common.HttpResponseWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class FeignClientAdapterTest { + private static FeignClientAdapter feignClientAdapter; + + @BeforeAll + static void setUp() { + final HashMap> headers = new HashMap<>(); + headers.put("testKey", Collections.singletonList("testValue")); + Request request = Request.create("post", "http://localhost:8080/test", headers, null, null); + feignClientAdapter = new FeignClientAdapter(request, URI.create("http://localhost:8080/test")); + } + + @Test + void getMethod() { + assertEquals("post", feignClientAdapter.getMethod()); + } + + @Test + void getRequestBytes() { + assertNull(feignClientAdapter.getRequestBytes()); + } + + @Test + void getRequestContentType() { + assertNull(feignClientAdapter.getRequestContentType()); + } + + @Test + void getRequestHeader() { + assertNull(feignClientAdapter.getRequestHeader("test")); + assertEquals("testValue", feignClientAdapter.getRequestHeader("testKey")); + } + + @Test + void getUri() { + assertEquals("http://localhost:8080/test", feignClientAdapter.getUri().toString()); + } + + @Test + void wrapAndunwrap() throws IOException { + final HashMap> responseHeaders = new HashMap<>(); + responseHeaders.put("testKey", Collections.singletonList("testValue")); + byte[] body = "testResponse".getBytes(); + final Response response = Response.builder().body(body).reason("test").status(200).headers(responseHeaders).build(); + final HttpResponseWrapper wrap = feignClientAdapter.wrap(response); + assertEquals("testResponse", new String(wrap.getContent())); + + final Response unwrap = feignClientAdapter.unwrap(wrap); + final InputStream bodyStream = unwrap.body().asInputStream(); + final byte[] bytes = Util.toByteArray(bodyStream); + assertEquals("testResponse", new String(bytes)); + } + + @Test + @Order(1) + void copyResponse() { + // null response + assertNull(feignClientAdapter.copyResponse(null)); + + byte[] body = "testResponse".getBytes(); + // repeatable response + final Response repeatResponse = Response.builder().body(body).reason("test").status(200).headers(new HashMap<>()).build(); + final Response copyRepeatResponse = feignClientAdapter.copyResponse(repeatResponse); + assertTrue(copyRepeatResponse.body().isRepeatable()); + assertEquals(repeatResponse.hashCode(), copyRepeatResponse.hashCode()); + + // not repeatable response + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + final Response unRepeatResponse = Response.builder().body(inputStream, 1024).reason("test").status(200).headers(new HashMap<>()).build(); + assertFalse(unRepeatResponse.body().isRepeatable()); + final Response copyUnRepeatResponse = feignClientAdapter.copyResponse(unRepeatResponse); + assertTrue(copyUnRepeatResponse.body().isRepeatable()); + assertNotEquals(unRepeatResponse.hashCode(), copyUnRepeatResponse.hashCode()); + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientInstrumentationTest.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientInstrumentationTest.java new file mode 100644 index 000000000..503aa97a6 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientInstrumentationTest.java @@ -0,0 +1,107 @@ +package io.arex.inst.httpclient.feign; + +import static org.junit.jupiter.api.Assertions.*; + +import feign.Request; +import feign.Response; +import io.arex.agent.bootstrap.model.MockResult; +import io.arex.inst.httpclient.common.HttpClientAdapter; +import io.arex.inst.httpclient.common.HttpClientExtractor; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.context.RepeatedCollectManager; +import io.arex.inst.runtime.util.IgnoreUtils; +import java.util.HashMap; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class FeignClientInstrumentationTest { + private static FeignClientInstrumentation clientInstrumentation; + + @BeforeAll + static void setUp() { + clientInstrumentation = new FeignClientInstrumentation(); + Mockito.mockStatic(IgnoreUtils.class); + Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(RepeatedCollectManager.class); + } + + @AfterAll + static void tearDown() { + clientInstrumentation = null; + Mockito.clearAllCaches(); + } + + @Test + void typeMatcher() { + assertDoesNotThrow(() -> clientInstrumentation.typeMatcher()); + } + + @Test + void methodAdvices() { + assertEquals(1, clientInstrumentation.methodAdvices().size()); + assertEquals("io.arex.inst.httpclient.feign.FeignClientInstrumentation$ExecuteAdvice", + clientInstrumentation.methodAdvices().get(0).getAdviceClassName()); + } + + @Test + void onEnter() { + // not need record or replay + Mockito.when(ContextManager.needRecordOrReplay()).thenReturn(false); + assertFalse(FeignClientInstrumentation.ExecuteAdvice.onEnter(null, null, null, null)); + + // need record or replay but exclude operation + String url = "http://localhost:8080/test"; + final Request request = Request.create("post", "http://localhost:8080/test", new HashMap<>(), null, null); + Mockito.when(ContextManager.needRecordOrReplay()).thenReturn(true); + Mockito.when(IgnoreUtils.excludeOperation(url)).thenReturn(true); + assertFalse(FeignClientInstrumentation.ExecuteAdvice.onEnter(request, null, null, null)); + + // need record and not exclude operation + Mockito.when(IgnoreUtils.excludeOperation(url)).thenReturn(false); + Mockito.when(ContextManager.needReplay()).thenReturn(false); + assertFalse(FeignClientInstrumentation.ExecuteAdvice.onEnter(request, null, null, null)); + + // need replay and not exclude operation + Mockito.when(ContextManager.needReplay()).thenReturn(true); + assertThrows(NullPointerException.class, () -> FeignClientInstrumentation.ExecuteAdvice.onEnter(request, null, null, null)); + } + + @Test + void onExit() { + MockResult mockResult = Mockito.mock(MockResult.class); + final HttpClientExtractor clientExtractor = Mockito.mock(HttpClientExtractor.class); + // extractor is null + FeignClientInstrumentation.ExecuteAdvice.onExit(null,null, null, null, null); + Mockito.verify(mockResult, Mockito.never()).notIgnoreMockResult(); + + // extractor is not null but mockResult is null + FeignClientInstrumentation.ExecuteAdvice.onExit(null, clientExtractor, null, null, null); + Mockito.verify(mockResult, Mockito.never()).notIgnoreMockResult(); + + // extractor is not null and mockResult is not null and mockResult.getThrowable() is not null + Mockito.when(mockResult.getThrowable()).thenReturn(new NullPointerException()); + Mockito.when(mockResult.notIgnoreMockResult()).thenReturn(true); + FeignClientInstrumentation.ExecuteAdvice.onExit(null, clientExtractor, mockResult, null, null); + Mockito.verify(mockResult, Mockito.never()).getResult(); + + // extractor is not null and mockResult is not null and mockResult.getThrowable() is null + Mockito.when(mockResult.getThrowable()).thenReturn(null); + FeignClientInstrumentation.ExecuteAdvice.onExit(null, clientExtractor, mockResult, null, null); + Mockito.verify(mockResult, Mockito.times(1)).getResult(); + + // record exception + Mockito.when(ContextManager.needRecord()).thenReturn(true); + Mockito.when(RepeatedCollectManager.exitAndValidate()).thenReturn(true); + final FeignClientAdapter clientAdapter = Mockito.mock(FeignClientAdapter.class); + final RuntimeException exception = new RuntimeException(); + FeignClientInstrumentation.ExecuteAdvice.onExit(clientAdapter, clientExtractor, null, null, exception); + Mockito.verify(clientExtractor, Mockito.times(1)).record(exception); + + // record response + final Response response = Mockito.mock(Response.class); + FeignClientInstrumentation.ExecuteAdvice.onExit(clientAdapter, clientExtractor, null, response, null); + Mockito.verify(clientExtractor, Mockito.times(1)).record(Mockito.any()); + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentationTest.java b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentationTest.java new file mode 100644 index 000000000..025f2c294 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentationTest.java @@ -0,0 +1,14 @@ +package io.arex.inst.httpclient.feign; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class FeignClientModuleInstrumentationTest { + + @Test + void instrumentationTypes() { + assertEquals(1, new FeignClientModuleInstrumentation().instrumentationTypes().size()); + } + +} diff --git a/arex-instrumentation/internal/arex-executors/src/main/java/io/arex/inst/executors/ForkJoinTaskInstrumentation.java b/arex-instrumentation/internal/arex-executors/src/main/java/io/arex/inst/executors/ForkJoinTaskInstrumentation.java index 23ed433cc..571c499ff 100644 --- a/arex-instrumentation/internal/arex-executors/src/main/java/io/arex/inst/executors/ForkJoinTaskInstrumentation.java +++ b/arex-instrumentation/internal/arex-executors/src/main/java/io/arex/inst/executors/ForkJoinTaskInstrumentation.java @@ -23,7 +23,7 @@ public ElementMatcher typeMatcher() { @Override public List methodAdvices() { return Collections.singletonList( - new MethodInstrumentation(isMethod().and(named("exec")).and(not(isAbstract())), + new MethodInstrumentation(isMethod().and(namedOneOf("exec", "run")).and(not(isAbstract())), "io.arex.inst.executors.ForkJoinTaskInstrumentation$ExecAdvice")); } diff --git a/arex-instrumentation/pom.xml b/arex-instrumentation/pom.xml index 15123963a..3ab821708 100644 --- a/arex-instrumentation/pom.xml +++ b/arex-instrumentation/pom.xml @@ -15,6 +15,7 @@ 5.3.24 0.1.6 + 3.4.16 @@ -34,12 +35,14 @@ dynamic/arex-dynamic dynamic/arex-dynamic-common dynamic/arex-cache + common/arex-common time-machine/arex-time-machine httpclient/arex-httpclient-common httpclient/arex-httpclient-okhttp-v3 httpclient/arex-httpclient-apache-v4 httpclient/arex-httpclient-webclient-v5 httpclient/arex-httpclient-resttemplate + httpclient/arex-httpclient-feign netty/arex-netty-v3 netty/arex-netty-v4 dubbo/arex-dubbo-apache-v2 @@ -62,6 +65,5 @@ com.google.auto.service auto-service - diff --git a/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java b/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java index 2aee9bec0..fe4940bc3 100644 --- a/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java +++ b/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java @@ -1,6 +1,7 @@ package io.arex.inst.jedis.v2; import io.arex.agent.bootstrap.model.MockResult; +import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.redis.common.RedisExtractor; @@ -85,7 +86,7 @@ public Long expire(String key, int seconds) { @Override public Long expire(byte[] key, int seconds) { - return call("expire", Base64.getEncoder().encodeToString(key), () -> super.expire(key, seconds), 0L); + return call("expire", key, () -> super.expire(key, seconds), 0L); } @Override @@ -160,7 +161,7 @@ public Long append(String key, String value) { @Override public Long append(byte[] key, byte[] value) { - return call("append", Base64.getEncoder().encodeToString(key), () -> super.append(key, value), 0L); + return call("append", key, () -> super.append(key, value), 0L); } @Override @@ -171,7 +172,7 @@ public String substr(String key, int start, int end) { @Override public byte[] substr(byte[] key, int start, int end) { - return call("substr", Base64.getEncoder().encodeToString(key), + return call("substr", key, RedisKeyUtil.generate("start", String.valueOf(start), "end", String.valueOf(end)), () -> super.substr(key, start, end), null); } @@ -183,20 +184,17 @@ public Long hset(String key, String field, String value) { @Override public Long hset(byte[] key, byte[] field, byte[] value) { - return call("hset", - Base64.getEncoder().encodeToString(key), Base64.getEncoder().encodeToString(field), - () -> super.hset(key, field, value), 0L); + return call("hset", key, field, () -> super.hset(key, field, value), 0L); } @Override public Long hset(byte[] key, Map hash) { - return call("hset", Base64.getEncoder().encodeToString(key), - Serializer.serialize(hash.keySet()), () -> super.hset(key, hash), 0L); + return call("hset", key, hash.keySet(), () -> super.hset(key, hash), 0L); } @Override public Long hset(final String key, final Map hash) { - return call("hset", key, Serializer.serialize(hash.keySet()), () -> super.hset(key, hash), 0L); + return call("hset", key, hash.keySet(), () -> super.hset(key, hash), 0L); } @Override @@ -206,9 +204,7 @@ public String hget(String key, String field) { @Override public byte[] hget(byte[] key, byte[] field) { - return call("hget", - Base64.getEncoder().encodeToString(key), Base64.getEncoder().encodeToString(field), - () -> super.hget(key, field), null); + return call("hget", key, field, () -> super.hget(key, field), null); } @Override @@ -250,7 +246,7 @@ public Long hdel(String key, String... fields) { @Override public Long hdel(byte[] key, byte[]... fields) { - return call("hdel", Base64.getEncoder().encodeToString(key), RedisKeyUtil.generate(fields), () -> super.hdel(key, fields), 0L); + return call("hdel", key, RedisKeyUtil.generate(fields), () -> super.hdel(key, fields), 0L); } @Override @@ -270,7 +266,7 @@ public List hvals(String key) { @Override public List hvals(byte[] key) { - return call("hvals", Base64.getEncoder().encodeToString(key), () -> super.hvals(key), Collections.EMPTY_LIST); + return call("hvals", key, () -> super.hvals(key), Collections.EMPTY_LIST); } @Override @@ -280,7 +276,7 @@ public Map hgetAll(String key) { @Override public Map hgetAll(byte[] key) { - return call("hgetAll", Base64.getEncoder().encodeToString(key), () -> super.hgetAll(key), Collections.EMPTY_MAP); + return call("hgetAll", key, () -> super.hgetAll(key), Collections.EMPTY_MAP); } @Override @@ -414,22 +410,22 @@ public String psetex(String key, long milliseconds, String value) { @Override public String set(final byte[] key, final byte[] value) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value), null); + return call("set", key, () -> super.set(key, value), null); } @Override public String set(byte[] key, byte[] value, byte[] nxxx, byte[] expx, long time) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value, nxxx, expx, time), null); + return call("set", key, () -> super.set(key, value, nxxx, expx, time), null); } @Override public String set(byte[] key, byte[] value, byte[] expx, long time) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value, expx, time), null); + return call("set", key, () -> super.set(key, value, expx, time), null); } @Override public byte[] get(final byte[] key) { - return call("get", Base64.getEncoder().encodeToString(key), () -> super.get(key), null); + return call("get", key, () -> super.get(key), null); } @Override @@ -439,17 +435,17 @@ public Long exists(final byte[]... keys) { @Override public Boolean exists(final byte[] key) { - return call("exists", Base64.getEncoder().encodeToString(key), () -> super.exists(key), false); + return call("exists", key, () -> super.exists(key), false); } @Override public String type(final byte[] key) { - return call("type", Base64.getEncoder().encodeToString(key), () -> super.type(key), "none"); + return call("type", key, () -> super.type(key), "none"); } @Override public byte[] getSet(final byte[] key, final byte[] value) { - return call("getSet", Base64.getEncoder().encodeToString(key), () -> super.getSet(key, value), null); + return call("getSet", key, () -> super.getSet(key, value), null); } @Override @@ -459,12 +455,12 @@ public List mget(final byte[]... keys) { @Override public Long setnx(final byte[] key, final byte[] value) { - return call("setnx", Base64.getEncoder().encodeToString(key), () -> super.setnx(key, value), 0L); + return call("setnx", key, () -> super.setnx(key, value), 0L); } @Override public String setex(byte[] key, int seconds, byte[] value) { - return call("setex", Base64.getEncoder().encodeToString(key), () -> super.setex(key, seconds, value), + return call("setex", key, () -> super.setex(key, seconds, value), null); } @@ -485,7 +481,7 @@ public Long unlink(byte[]... keys) { @Override public Long unlink(byte[] key) { - return call("unlink", Base64.getEncoder().encodeToString(key), () -> super.unlink(key), 0L); + return call("unlink", key, () -> super.unlink(key), 0L); } @Override @@ -515,7 +511,7 @@ public String ping() { @Override public byte[] ping(byte[] message) { - return call("ping", Base64.getEncoder().encodeToString(message), () -> super.ping(message), null); + return call("ping", message, () -> super.ping(message), null); } @Override @@ -527,8 +523,8 @@ public String ping(String message) { * mset/msetnx */ private U call(String command, String[] keysValues, Callable callable, U defaultValue) { - if (keysValues == null || keysValues.length == 0) { - return null; + if (ArrayUtils.isEmpty(keysValues)) { + return defaultValue; } if (keysValues.length == 2) { @@ -545,11 +541,11 @@ private U call(String command, String[] keysValues, Callable callable, U return call(command, keyBuilder.toString(), null, callable, defaultValue); } - private U call(String command, String key, Callable callable, U defaultValue) { + private U call(String command, Object key, Callable callable, U defaultValue) { return call(command, key, null, callable, defaultValue); } - private U call(String command, String key, String field, Callable callable, U defaultValue) { + private U call(String command, Object key, Object field, Callable callable, U defaultValue) { if (ContextManager.needReplay()) { RedisExtractor extractor = new RedisExtractor(this.url, command, key, field); MockResult mockResult = extractor.replay(); diff --git a/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java b/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java index 3ac648147..e2c146db8 100644 --- a/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java +++ b/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java @@ -5,6 +5,7 @@ import io.arex.inst.runtime.context.ContextManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -19,6 +20,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; @@ -52,6 +55,33 @@ static void tearDown() { Mockito.clearAllCaches(); } + @Test + void callWithEmptyKeysValuesReturnsDefault() { + long result = target.msetnx( new String[]{}); + assertEquals(0, result); + } + + @Test + void callWithTwoKeysValuesReturnsCallableResult() { + Mockito.when(ContextManager.needRecord()).thenReturn(false); + Mockito.when(client.getIntegerReply()).thenReturn(1L); + try (MockedConstruction mocked = Mockito.mockConstruction(RedisExtractor.class, (mock, context) -> { + })) { + long result = target.msetnx("key", "value"); + assertEquals(1L, result); + + result = target.msetnx("key1", "value1", "key2", "value2", "key3", "value3"); + assertEquals(1L, result); + + result = target.exists("key1", "key2", "key3"); + assertEquals(1L, result); + } catch (Exception e) { + assertThrows(NullPointerException.class, () -> { + throw e; + }); + } + } + @ParameterizedTest @MethodSource("callCase") void call(Runnable mocker, Predicate predicate) { @@ -89,4 +119,31 @@ static Stream callCase() { arguments(mocker3, predicate2) ); } + + @Test + void testApi() { + assertDoesNotThrow(() -> target.expire("key".getBytes(), 1)); + assertDoesNotThrow(() -> target.append("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.substr("key".getBytes(), 1, 2)); + assertDoesNotThrow(() -> target.hset("key".getBytes(), "field".getBytes(), "value".getBytes())); + Map hash = new HashMap<>(); + assertDoesNotThrow(() -> target.hset("key".getBytes(), hash)); + Map hash1 = new HashMap<>(); + assertDoesNotThrow(() -> target.hset("key", hash1)); + assertDoesNotThrow(() -> target.hget("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.hdel("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.hvals("key".getBytes())); + assertDoesNotThrow(() -> target.hgetAll("key".getBytes())); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes(), "value".getBytes(), "value".getBytes(), 1L)); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes(), "value".getBytes(), 1L)); + assertDoesNotThrow(() -> target.get("key".getBytes())); + assertDoesNotThrow(() -> target.exists("key".getBytes())); + assertDoesNotThrow(() -> target.type("key".getBytes())); + assertDoesNotThrow(() -> target.getSet("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.setnx("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.setex("key".getBytes(), 1, "value".getBytes())); + assertDoesNotThrow(() -> target.unlink("key".getBytes())); + assertDoesNotThrow(() -> target.ping("key".getBytes())); + } } \ No newline at end of file diff --git a/arex-instrumentation/redis/arex-jedis-v4/src/main/java/io/arex/inst/jedis/v4/JedisWrapper.java b/arex-instrumentation/redis/arex-jedis-v4/src/main/java/io/arex/inst/jedis/v4/JedisWrapper.java index 12a5033ad..9c7a4afd3 100644 --- a/arex-instrumentation/redis/arex-jedis-v4/src/main/java/io/arex/inst/jedis/v4/JedisWrapper.java +++ b/arex-instrumentation/redis/arex-jedis-v4/src/main/java/io/arex/inst/jedis/v4/JedisWrapper.java @@ -1,5 +1,6 @@ package io.arex.inst.jedis.v4; +import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.serializer.Serializer; import io.arex.agent.bootstrap.model.MockResult; @@ -572,8 +573,8 @@ public String ping(String message) { * mset/msetnx */ private U call(String command, String[] keysValues, Callable callable, U defaultValue) { - if (keysValues == null || keysValues.length == 0) { - return null; + if (ArrayUtils.isEmpty(keysValues)) { + return defaultValue; } if (keysValues.length == 2) { diff --git a/arex-instrumentation/redis/arex-jedis-v4/src/test/java/io/arex/inst/jedis/v4/JedisWrapperTest.java b/arex-instrumentation/redis/arex-jedis-v4/src/test/java/io/arex/inst/jedis/v4/JedisWrapperTest.java index af8520917..e6ffb3544 100644 --- a/arex-instrumentation/redis/arex-jedis-v4/src/test/java/io/arex/inst/jedis/v4/JedisWrapperTest.java +++ b/arex-instrumentation/redis/arex-jedis-v4/src/test/java/io/arex/inst/jedis/v4/JedisWrapperTest.java @@ -5,8 +5,8 @@ import io.arex.inst.runtime.context.ContextManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -18,8 +18,8 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; @@ -48,6 +48,33 @@ static void tearDown() { Mockito.clearAllCaches(); } + @Test + void callWithEmptyKeysValuesReturnsDefault() { + long result = target.msetnx( new String[]{}); + assertEquals(0, result); + } + + @Test + void callWithTwoKeysValuesReturnsCallableResult() { + Mockito.when(ContextManager.needRecord()).thenReturn(false); + Mockito.when(connection.executeCommand(any(CommandObject.class))).thenReturn(1L); + try (MockedConstruction mocked = Mockito.mockConstruction(RedisExtractor.class, (mock, context) -> { + })) { + long result = target.msetnx("key", "value"); + assertEquals(1L, result); + + result = target.msetnx("key1", "value1", "key2", "value2", "key3", "value3"); + assertEquals(1L, result); + + result = target.exists("key1", "key2", "key3"); + assertEquals(1L, result); + } catch (Exception e) { + assertThrows(NullPointerException.class, () -> { + throw e; + }); + } + } + @ParameterizedTest @MethodSource("callCase") void call(Runnable mocker, Predicate predicate) { @@ -85,4 +112,4 @@ static Stream callCase() { arguments(mocker3, predicate2) ); } -} \ No newline at end of file +} diff --git a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java index 880e1dce2..ec88f31ff 100644 --- a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java +++ b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java @@ -3,10 +3,12 @@ import io.arex.agent.bootstrap.model.MockResult; import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.util.IgnoreUtils; import io.arex.inst.runtime.util.MockUtils; import io.arex.inst.runtime.util.TypeUtil; + import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -15,10 +17,10 @@ public class RedisExtractor { private final String clusterName; private final String command; - private final String key; - private final String field; + private final Object key; + private final Object field; - public RedisExtractor(String url, String method, String key, String field) { + public RedisExtractor(String url, String method, Object key, Object field) { this.clusterName = RedisCluster.get(url); this.command = method; this.key = key; @@ -26,29 +28,29 @@ public RedisExtractor(String url, String method, String key, String field) { } public static class RedisMultiKey { - private String key; - private String field; + private Object key; + private Object field; public RedisMultiKey() {} - public RedisMultiKey(String key, String field) { + public RedisMultiKey(Object key, Object field) { this.key = key; this.field = field; } - public String getKey() { + public Object getKey() { return key; } - public void setKey(String key) { + public void setKey(Object key) { this.key = key; } - public String getField() { + public Object getField() { return field; } - public void setField(String field) { + public void setField(Object field) { this.field = field; } } @@ -73,10 +75,12 @@ public MockResult replay() { private Mocker makeMocker(Object response) { Mocker mocker = MockUtils.createRedis(this.command); + mocker.setMerge(true); mocker.getTargetRequest().setBody(Serializer.serialize(new RedisMultiKey(key, field))); mocker.getTargetRequest().setAttribute("clusterName", this.clusterName); mocker.getTargetResponse().setBody(Serializer.serialize(response)); mocker.getTargetResponse().setType(normalizeTypeName(response)); + mocker.getTargetRequest().setType(TypeUtil.getName(CollectionUtil.newArrayList(this.key, this.field))); return mocker; } diff --git a/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java index 009f13ce3..962e44c8c 100644 --- a/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java +++ b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java @@ -16,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class RedisExtractorTest { @@ -66,4 +65,13 @@ void replay() { assertNotNull(target.replay()); } } + + @Test + void redisMultiKey() { + RedisExtractor.RedisMultiKey redisMultiKey = new RedisExtractor.RedisMultiKey(); + redisMultiKey.setKey("mock"); + redisMultiKey.getKey(); + redisMultiKey.setField("mock"); + assertNotNull(redisMultiKey.getField()); + } } \ No newline at end of file diff --git a/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisKeyUtilTest.java b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisKeyUtilTest.java new file mode 100644 index 000000000..8df510330 --- /dev/null +++ b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisKeyUtilTest.java @@ -0,0 +1,60 @@ +package io.arex.inst.redis.common; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * @since 2024/1/12 + */ +class RedisKeyUtilTest { + + @Test + void generateWithIterableKeys() { + String result = RedisKeyUtil.generate(Arrays.asList("key1", "key2", "key3")); + assertEquals("key1;key2;key3", result); + } + + @Test + void generateWithMapKeys() { + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + String result = RedisKeyUtil.generate(map); + assertTrue(result.contains("key1")); + assertTrue(result.contains("key2")); + } + + @Test + void generateWithVarargsKeys() { + String result = RedisKeyUtil.generate("key1", "key2", "key3"); + assertEquals("key1;key2;key3", result); + } + + @Test + void generateWithEmptyVarargsKeys() { + String result = RedisKeyUtil.generate(); + assertEquals("", result); + } + + @Test + void generateWithSingleVarargsKey() { + String result = RedisKeyUtil.generate("key1"); + assertEquals("key1", result); + } + + @Test + void generateWithByteValue() { + String result = RedisKeyUtil.generate(new byte[]{'k', 'e', 'y'}); + assertEquals("key", result); + } + + @Test + void generateWithCharValue() { + String result = RedisKeyUtil.generate(new char[]{'k', 'e', 'y'}); + assertEquals("key", result); + } +} diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/RedissonInstrumentation.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/RedissonInstrumentation.java index 0dfdf8208..53f7e9f59 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/RedissonInstrumentation.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/RedissonInstrumentation.java @@ -2,6 +2,7 @@ import io.arex.inst.extension.MethodInstrumentation; import io.arex.inst.extension.TypeInstrumentation; +import io.arex.inst.redisson.v3.common.RedissonHelper; import io.arex.inst.redisson.v3.wrapper.RedissonBucketWrapper; import io.arex.inst.redisson.v3.wrapper.RedissonBucketsWrapper; import io.arex.inst.redisson.v3.wrapper.RedissonKeysWrapper; @@ -22,8 +23,6 @@ import org.redisson.api.RSet; import org.redisson.api.RedissonClient; import org.redisson.client.codec.Codec; -import org.redisson.command.CommandAsyncExecutor; - import java.util.Arrays; import java.util.List; @@ -54,6 +53,7 @@ public List methodAdvices() { GetMapWithCodecAdvice.getMethodInstrumentation(), GetMapWithCodecOptionsAdvice.getMethodInstrumentation()); } + public static class GetBucketAdvice { public static MethodInstrumentation getMethodInstrumentation() { ElementMatcher.Junction matcher = @@ -71,9 +71,9 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RBucket redissonBucket) { - redissonBucket = new RedissonBucketWrapper<>(commandExecutor, name); + redissonBucket = new RedissonBucketWrapper<>(RedissonHelper.getCommandAsyncExecutor(redisson), name); } } @@ -95,9 +95,9 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) Codec codec, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RBucket redissonBucket) { - redissonBucket = new RedissonBucketWrapper<>(codec, commandExecutor, name); + redissonBucket = new RedissonBucketWrapper<>(codec, RedissonHelper.getCommandAsyncExecutor(redisson), name); } } @@ -117,9 +117,9 @@ public static boolean onEnter() { } @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit(@Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + public static void onExit(@Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RBuckets redissonBuckets) { - redissonBuckets = new RedissonBucketsWrapper(commandExecutor); + redissonBuckets = new RedissonBucketsWrapper(RedissonHelper.getCommandAsyncExecutor(redisson)); } } @@ -139,10 +139,9 @@ public static boolean onEnter() { } @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit(@Advice.Argument(0) Codec codec, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + public static void onExit(@Advice.Argument(0) Codec codec, @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RBuckets redissonBuckets) { - redissonBuckets = new RedissonBucketsWrapper(codec, commandExecutor); + redissonBuckets = new RedissonBucketsWrapper(codec, RedissonHelper.getCommandAsyncExecutor(redisson)); } } @@ -162,9 +161,9 @@ public static boolean onEnter() { } @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit(@Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, - @Advice.Return(readOnly = false) RKeys redissonKeys) { - redissonKeys = new RedissonKeysWrapper(commandExecutor); + public static void onExit(@Advice.Return(readOnly = false) RKeys redissonKeys, + @Advice.This RedissonClient redisson) { + redissonKeys = new RedissonKeysWrapper(RedissonHelper.getCommandAsyncExecutor(redisson)); } } @@ -185,9 +184,8 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.This RedissonClient redisson, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, @Advice.Return(readOnly = false) RList redissonList) { - redissonList = new RedissonListWrapper<>(commandExecutor, name, redisson); + redissonList = new RedissonListWrapper<>(RedissonHelper.getCommandAsyncExecutor(redisson), name, redisson); } } @@ -209,9 +207,10 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) Codec codec, - @Advice.This RedissonClient redisson, @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RList redissonList) { - redissonList = new RedissonListWrapper<>(codec, commandExecutor, name, redisson); + redissonList = new RedissonListWrapper<>(codec, RedissonHelper.getCommandAsyncExecutor(redisson), name, + redisson); } } @@ -232,9 +231,8 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.This RedissonClient redisson, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, @Advice.Return(readOnly = false) RSet redissonSet) { - redissonSet = new RedissonSetWrapper<>(commandExecutor, name, redisson); + redissonSet = new RedissonSetWrapper<>(RedissonHelper.getCommandAsyncExecutor(redisson), name, redisson); } } @@ -256,9 +254,10 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) Codec codec, - @Advice.This RedissonClient redisson, @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RSet redissonSet) { - redissonSet = new RedissonSetWrapper<>(codec, commandExecutor, name, redisson); + redissonSet = new RedissonSetWrapper<>(codec, RedissonHelper.getCommandAsyncExecutor(redisson), name, + redisson); } } @@ -279,9 +278,9 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.This RedissonClient redisson, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, @Advice.Return(readOnly = false) RMap redissonMap) { - redissonMap = new RedissonMapWrapper<>(commandExecutor, name, redisson, null, null); + redissonMap = new RedissonMapWrapper<>(RedissonHelper.getCommandAsyncExecutor(redisson), name, redisson, + null, null); } } @@ -303,10 +302,11 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) MapOptions options, - @Advice.This RedissonClient redisson, @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.FieldValue("writeBehindService") WriteBehindService writeBehindService, @Advice.Return(readOnly = false) RMap redissonMap) { - redissonMap = new RedissonMapWrapper<>(commandExecutor, name, redisson, options, writeBehindService); + redissonMap = new RedissonMapWrapper<>(RedissonHelper.getCommandAsyncExecutor(redisson), name, redisson, + options, writeBehindService); } } @@ -328,9 +328,10 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) Codec codec, - @Advice.This RedissonClient redisson, @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, + @Advice.This RedissonClient redisson, @Advice.Return(readOnly = false) RMap redissonMap) { - redissonMap = new RedissonMapWrapper<>(codec, commandExecutor, name, redisson, null, null); + redissonMap = new RedissonMapWrapper<>(codec, RedissonHelper.getCommandAsyncExecutor(redisson), name, + redisson, null, null); } } @@ -342,7 +343,6 @@ public static MethodInstrumentation getMethodInstrumentation() { .and(takesArgument(2, named("org.redisson.api.MapOptions"))); String advice = GetMapWithCodecOptionsAdvice.class.getName(); - return new MethodInstrumentation(matcher, advice); } @@ -354,10 +354,10 @@ public static boolean onEnter() { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) String name, @Advice.Argument(1) Codec codec, @Advice.Argument(2) MapOptions options, @Advice.This RedissonClient redisson, - @Advice.FieldValue("commandExecutor") CommandAsyncExecutor commandExecutor, @Advice.FieldValue("writeBehindService") WriteBehindService writeBehindService, @Advice.Return(readOnly = false) RMap redissonMap) { - redissonMap = new RedissonMapWrapper<>(codec, commandExecutor, name, redisson, options, writeBehindService); + redissonMap = new RedissonMapWrapper<>(codec, RedissonHelper.getCommandAsyncExecutor(redisson), name, + redisson, options, writeBehindService); } } } diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/common/RedissonHelper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/common/RedissonHelper.java new file mode 100644 index 000000000..3e57d8d55 --- /dev/null +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/common/RedissonHelper.java @@ -0,0 +1,79 @@ +package io.arex.inst.redisson.v3.common; + +import io.arex.agent.bootstrap.util.ReflectUtil; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.command.CommandAsyncExecutor; +import org.redisson.config.MasterSlaveServersConfig; +import org.redisson.connection.ConnectionManager; +import io.arex.inst.runtime.log.LogManager; + +public class RedissonHelper { + + private static Method getConfigMethod = ReflectUtil.getMethod(ConnectionManager.class, "getConfig"); + private static Set masterSlaveServersConfigSet = new HashSet<>(1); + private static Method getCommandExecutorMethod = ReflectUtil.getMethod(Redisson.class, "getCommandExecutor"); + + private RedissonHelper() { + } + + /** + * redisson vesion + * < 3.20.0 && >= 3.17.3 ConnectionManager.getConfig().getMasterAddress() + * >= 3.20.0 ConnectionManager.getServiceManager().getConfig().getMasterAddress(); + */ + public static String getRedisUri(ConnectionManager connectionManager) { + // reflect call (< 3.20.0 && >= 3.17.3) + if (getConfigMethod != null) { + return connectionManager.getConfig().getMasterAddress(); + } + // reflect call (>= 3.20.0) + if (!masterSlaveServersConfigSet.isEmpty()) { + return masterSlaveServersConfigSet.iterator().next().getMasterAddress(); + } + final MasterSlaveServersConfig masterSlaveServersConfig = getMasterSlaveServersConfig(connectionManager); + if (masterSlaveServersConfig == null) { + return null; + } + masterSlaveServersConfigSet.add(masterSlaveServersConfig); + return masterSlaveServersConfig.getMasterAddress(); + } + + public static MasterSlaveServersConfig getMasterSlaveServersConfig(ConnectionManager connectionManager) { + try { + // ServerManager + final Method getServiceMangerMethod = connectionManager.getClass().getMethod("getServiceManager"); + final Object object = getServiceMangerMethod.invoke(connectionManager); + if (object == null) { + return null; + } + // MasterSlaveServersConfig + final Method getMasterSlaveServersConfig = object.getClass().getMethod("getConfig"); + return (MasterSlaveServersConfig) getMasterSlaveServersConfig.invoke(object); + } catch (Exception e) { + LogManager.warn("redis.masterSlaveConfig", e); + return null; + } + } + + /** + * compatible with redisson versions lower than 3.16.0 (<3.16.0) + * + * @param redisson + * @return + */ + public static CommandAsyncExecutor getCommandAsyncExecutor(RedissonClient redisson) { + if (redisson == null || getCommandExecutorMethod == null) { + return null; + } + try { + return (CommandAsyncExecutor) getCommandExecutorMethod.invoke(redisson); + } catch (Exception e) { + LogManager.warn("redis.commandExecutor", e); + return null; + } + } +} diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketWrapper.java index c8382d87b..bcd8e164b 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketWrapper.java @@ -2,6 +2,7 @@ import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonBucket; import org.redisson.api.RFuture; import org.redisson.client.codec.Codec; @@ -20,115 +21,115 @@ public class RedissonBucketWrapper extends RedissonBucket { public RedissonBucketWrapper(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } public RedissonBucketWrapper(Codec codec, CommandAsyncExecutor commandExecutor, String name) { super(codec, commandExecutor, name); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } // region RedissonBucket @Override public RFuture compareAndSetAsync(V expect, V update) { - return RedissonWrapperCommon.delegateCall(redisUri, "compareAndSet", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "compareAndSet", this.name, () -> super.compareAndSetAsync(expect, update)); } @Override public RFuture getAndSetAsync(V newValue) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.GETSET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.GETSET.getName(), this.name, () -> super.getAndSetAsync(newValue)); } @Override public RFuture getAndExpireAsync(Instant time) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.GETEX.getName(), "PXAT"), getRawName(), + RedisKeyUtil.generate(RedisCommands.GETEX.getName(), "PXAT"), this.name, () -> super.getAndExpireAsync(time)); } @Override public RFuture getAndExpireAsync(Duration duration) { return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate(RedisCommands.GETEX.getName(), "PX"), - getRawName(), () -> super.getAndExpireAsync(duration)); + this.name, () -> super.getAndExpireAsync(duration)); } @Override public RFuture getAndClearExpireAsync() { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.GETEX.getName(), "PERSIST"), getRawName(), + RedisKeyUtil.generate(RedisCommands.GETEX.getName(), "PERSIST"), this.name, () -> super.getAndClearExpireAsync()); } @Override public RFuture getAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.GET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.GET.getName(), this.name, () -> super.getAsync()); } @Override public RFuture getAndDeleteAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, "getAndDelete", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "getAndDelete", this.name, () -> super.getAndDeleteAsync()); } @Override public RFuture sizeAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.STRLEN.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.STRLEN.getName(), this.name, () -> super.sizeAsync()); } @Override public RFuture setAsync(V value) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SET.getName(), this.name, () -> super.setAsync(value)); } @Override public RFuture setAsync(V value, long timeToLive, TimeUnit timeUnit) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.PSETEX.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.PSETEX.getName(), this.name, () -> super.setAsync(value, timeToLive, timeUnit)); } @Override public RFuture trySetAsync(V value) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SETNX.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SETNX.getName(), this.name, () -> super.trySetAsync(value)); } @Override public RFuture trySetAsync(V value, long timeToLive, TimeUnit timeUnit) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "PX", "NX"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "PX", "NX"), this.name, () -> super.trySetAsync(value, timeToLive, timeUnit)); } @Override public RFuture setIfExistsAsync(V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "XX"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "XX"), this.name, () -> super.setIfExistsAsync(value)); } @Override public RFuture setAndKeepTTLAsync(V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SET.getName(), "KEEPTTL"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SET.getName(), "KEEPTTL"), this.name, () -> super.setAndKeepTTLAsync(value)); } @Override public RFuture setIfExistsAsync(V value, long timeToLive, TimeUnit timeUnit) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "PX", "XX"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SET_BOOLEAN.getName(), "PX", "XX"), this.name, () -> super.setIfExistsAsync(value, timeToLive, timeUnit)); } @Override public RFuture getAndSetAsync(V value, long timeToLive, TimeUnit timeUnit) { - return RedissonWrapperCommon.delegateCall(redisUri, "getAndSet", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "getAndSet", this.name, () -> super.getAndSetAsync(value, timeToLive, timeUnit)); } @@ -138,31 +139,31 @@ public RFuture getAndSetAsync(V value, long timeToLive, TimeUnit timeUnit) { @Override public RFuture expireAsync(long timeToLive, TimeUnit timeUnit) { - return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", this.name, () -> super.expireAsync(timeToLive, timeUnit)); } @Override public RFuture expireAtAsync(long timestamp) { - return RedissonWrapperCommon.delegateCall(redisUri, "pexpireat", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "pexpireat", this.name, () -> super.expireAtAsync(timestamp)); } @Override public RFuture expireIfSetAsync(Instant time) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "XX"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "XX"), this.name, () -> super.expireIfSetAsync(time)); } @Override public RFuture expireIfNotSetAsync(Instant time) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "NX"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "NX"), this.name, () -> super.expireIfNotSetAsync(time)); } @Override public RFuture expireIfGreaterAsync(Instant time) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "GT"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "GT"), this.name, () -> super.expireIfGreaterAsync(time)); } @@ -173,47 +174,47 @@ public boolean expireIfLess(Instant time) { @Override public RFuture expireIfLessAsync(Instant time) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "LT"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpireat", "LT"), this.name, () -> super.expireIfLessAsync(time)); } @Override public RFuture expireAsync(Instant instant) { - return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", getRawName(), () -> super.expireAsync(instant)); + return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", this.name, () -> super.expireAsync(instant)); } @Override public RFuture expireAsync(Duration duration) { - return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", getRawName(), () -> super.expireAsync(duration)); + return RedissonWrapperCommon.delegateCall(redisUri, "pexpire", this.name, () -> super.expireAsync(duration)); } @Override public RFuture expireIfSetAsync(Duration duration) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "XX"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "XX"), this.name, () -> super.expireIfSetAsync(duration)); } @Override public RFuture expireIfNotSetAsync(Duration duration) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "NX"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "NX"), this.name, () -> super.expireIfNotSetAsync(duration)); } @Override public RFuture expireIfGreaterAsync(Duration duration) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "GT"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "GT"), this.name, () -> super.expireIfGreaterAsync(duration)); } @Override public RFuture expireIfLessAsync(Duration duration) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "LT"), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("pexpire", "LT"), this.name, () -> super.expireIfLessAsync(duration)); } @Override public RFuture clearExpireAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.PERSIST.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.PERSIST.getName(), this.name, () -> super.clearExpireAsync()); } diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketsWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketsWrapper.java index 006683456..7b4e6f210 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketsWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonBucketsWrapper.java @@ -2,6 +2,7 @@ import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonBuckets; import org.redisson.api.RFuture; import org.redisson.client.codec.Codec; @@ -18,12 +19,12 @@ public class RedissonBucketsWrapper extends RedissonBuckets { public RedissonBucketsWrapper(CommandAsyncExecutor commandExecutor) { super(commandExecutor); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } public RedissonBucketsWrapper(Codec codec, CommandAsyncExecutor commandExecutor) { super(codec, commandExecutor); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } @Override diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonKeysWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonKeysWrapper.java index aa1f6b2ba..8990e3328 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonKeysWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonKeysWrapper.java @@ -2,6 +2,7 @@ import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonKeys; import org.redisson.api.RFuture; import org.redisson.api.RType; @@ -20,7 +21,7 @@ public class RedissonKeysWrapper extends RedissonKeys { public RedissonKeysWrapper(CommandAsyncExecutor commandExecutor) { super(commandExecutor); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } @Override diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonListWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonListWrapper.java index f7b556fc9..175cc8523 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonListWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonListWrapper.java @@ -2,6 +2,7 @@ import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonList; import org.redisson.api.RFuture; import org.redisson.api.RedissonClient; @@ -21,80 +22,80 @@ public class RedissonListWrapper extends RedissonList { private final String redisUri; public RedissonListWrapper(CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson) { super(commandExecutor, name, redisson); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } public RedissonListWrapper(Codec codec, CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson) { super(codec, commandExecutor, name, redisson); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } @Override public RFuture sizeAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LLEN_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LLEN_INT.getName(), this.name, () -> super.sizeAsync()); } @Override public RFuture> readAllAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, "listReadAll", getRawName(), () -> super.readAllAsync()); + return RedissonWrapperCommon.delegateCall(redisUri, "listReadAll", this.name, () -> super.readAllAsync()); } @Override protected RFuture addAsync(V e, RedisCommand command) { - return RedissonWrapperCommon.delegateCall(redisUri, command.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, command.getName(), this.name, () -> super.addAsync(e, command)); } @Override public RFuture removeAsync(Object o, int count) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LREM.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LREM.getName(), this.name, RedisKeyUtil.generate("count", String.valueOf(count)), () -> super.removeAsync(o, count)); } @Override public RFuture containsAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "listContains", getRawName(), () -> super.containsAllAsync(c)); + return RedissonWrapperCommon.delegateCall(redisUri, "listContains", this.name, () -> super.containsAllAsync(c)); } @Override public RFuture addAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.RPUSH_BOOLEAN.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.RPUSH_BOOLEAN.getName(), this.name, () -> super.addAllAsync(c)); } @Override public RFuture addAllAsync(int index, Collection coll) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.RPUSH_BOOLEAN.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.RPUSH_BOOLEAN.getName(), this.name, RedisKeyUtil.generate("index", String.valueOf(index)), () -> super.addAllAsync(index, coll)); } @Override public RFuture removeAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "listRemoveAll", getRawName(), () -> super.removeAllAsync(c)); + return RedissonWrapperCommon.delegateCall(redisUri, "listRemoveAll", this.name, () -> super.removeAllAsync(c)); } @Override public RFuture retainAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "listRetainAll", getRawName(), () -> super.retainAllAsync(c)); + return RedissonWrapperCommon.delegateCall(redisUri, "listRetainAll", this.name, () -> super.retainAllAsync(c)); } @Override public RFuture getAsync(int index) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINDEX.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINDEX.getName(), this.name, String.valueOf(index), () -> super.getAsync(index)); } @Override public RFuture> getAsync(int... indexes) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINDEX.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINDEX.getName(), this.name, RedisKeyUtil.generate(indexes), () -> super.getAsync(indexes)); } @Override public RFuture setAsync(int index, V element) { - return RedissonWrapperCommon.delegateCall(redisUri, "listSet", getRawName(), String.valueOf(index), + return RedissonWrapperCommon.delegateCall(redisUri, "listSet", this.name, String.valueOf(index), () -> super.setAsync(index, element)); } @@ -105,61 +106,61 @@ public void fastSet(int index, V element) { @Override public RFuture fastSetAsync(int index, V element) { - return RedissonWrapperCommon.delegateCall(redisUri, "listFastSet", getRawName(), String.valueOf(index), + return RedissonWrapperCommon.delegateCall(redisUri, "listFastSet", this.name, String.valueOf(index), () -> super.fastSetAsync(index, element)); } @Override public RFuture removeAsync(int index) { - return RedissonWrapperCommon.delegateCall(redisUri, "listRemove", getRawName(), String.valueOf(index), + return RedissonWrapperCommon.delegateCall(redisUri, "listRemove", this.name, String.valueOf(index), () -> super.removeAsync(index)); } @Override public RFuture fastRemoveAsync(int index) { - return RedissonWrapperCommon.delegateCall(redisUri, "listFastRemove", getRawName(), String.valueOf(index), + return RedissonWrapperCommon.delegateCall(redisUri, "listFastRemove", this.name, String.valueOf(index), () -> super.fastRemoveAsync(index)); } @Override public RFuture indexOfAsync(Object o, Convertor convertor) { - return RedissonWrapperCommon.delegateCall(redisUri, "listIndexOf", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "listIndexOf", this.name, () -> super.indexOfAsync(o, convertor)); } @Override public RFuture lastIndexOfAsync(Object o) { - return RedissonWrapperCommon.delegateCall(redisUri, "listLastIndexOf", getRawName(), String.valueOf(o), + return RedissonWrapperCommon.delegateCall(redisUri, "listLastIndexOf", this.name, String.valueOf(o), () -> super.lastIndexOfAsync(o)); } @Override public RFuture lastIndexOfAsync(Object o, Convertor convertor) { - return RedissonWrapperCommon.delegateCall(redisUri, "listLastIndexOf", getRawName(), String.valueOf(o), + return RedissonWrapperCommon.delegateCall(redisUri, "listLastIndexOf", this.name, String.valueOf(o), () -> super.lastIndexOfAsync(o, convertor)); } @Override public RFuture trimAsync(int fromIndex, int toIndex) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LTRIM.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LTRIM.getName(), this.name, RedisKeyUtil.generate("from", fromIndex, "to", toIndex), () -> super.trimAsync(fromIndex, toIndex)); } @Override public RFuture addAfterAsync(V elementToFind, V element) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINSERT_INT.getName(), getRawName(), "AFTER", + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINSERT_INT.getName(), this.name, "AFTER", () -> super.addAfterAsync(elementToFind, element)); } @Override public RFuture addBeforeAsync(V elementToFind, V element) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINSERT_INT.getName(), getRawName(), "BEFORE", + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LINSERT_INT.getName(), this.name, "BEFORE", () -> super.addBeforeAsync(elementToFind, element)); } @Override public RFuture> rangeAsync(int fromIndex, int toIndex) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LRANGE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.LRANGE.getName(), this.name, RedisKeyUtil.generate("from", fromIndex, "to", toIndex), () -> super.rangeAsync(fromIndex, toIndex)); } } diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonMapWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonMapWrapper.java index 4e1bbc4d9..bd3ab2a19 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonMapWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonMapWrapper.java @@ -2,6 +2,7 @@ import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonMap; import org.redisson.WriteBehindService; import org.redisson.api.MapOptions; @@ -25,168 +26,168 @@ public class RedissonMapWrapper extends RedissonMap { public RedissonMapWrapper(CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson, MapOptions options, WriteBehindService writeBehindService) { super(commandExecutor, name, redisson, options, writeBehindService); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } public RedissonMapWrapper(Codec codec, CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson, MapOptions options, WriteBehindService writeBehindService) { super(codec, commandExecutor, name, redisson, options, writeBehindService); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } @Override public RFuture sizeAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HLEN.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HLEN.getName(), this.name, () -> super.sizeAsync()); } @Override public RFuture valueSizeAsync(K key) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSTRLEN.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSTRLEN.getName(), this.name, String.valueOf(key), () -> super.valueSizeAsync(key)); } @Override protected RFuture containsKeyOperationAsync(String name, Object key) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HEXISTS.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HEXISTS.getName(), this.name, String.valueOf(key), () -> super.containsKeyOperationAsync(name, key)); } @Override public RFuture containsValueAsync(Object value) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HVALS.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HVALS.getName(), this.name, String.valueOf(value), () -> super.containsValueAsync(value)); } @Override public RFuture> randomKeysAsync(int count) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HRANDFIELD_KEYS.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HRANDFIELD_KEYS.getName(), this.name, () -> super.randomKeysAsync(count)); } @Override public RFuture> randomEntriesAsync(int count) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HRANDFIELD.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HRANDFIELD.getName(), this.name, () -> super.randomEntriesAsync(count)); } @Override public RFuture> getAllOperationAsync(Set keys) { - return RedissonWrapperCommon.delegateCall(redisUri, "HMGET", getRawName(), RedisKeyUtil.generate(keys), + return RedissonWrapperCommon.delegateCall(redisUri, "HMGET", this.name, RedisKeyUtil.generate(keys), () -> super.getAllOperationAsync(keys)); } @Override protected RFuture putAllOperationAsync(Map map) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HMSET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HMSET.getName(), this.name, RedisKeyUtil.generate(map), () -> super.putAllOperationAsync(map)); } @Override public RFuture> readAllKeySetAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HKEYS.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HKEYS.getName(), this.name, () -> super.readAllKeySetAsync()); } @Override public RFuture> readAllValuesAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HVALS.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HVALS.getName(), this.name, () -> super.readAllValuesAsync()); } @Override public RFuture>> readAllEntrySetAsync() { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.HGETALL.getName(), "entry"), getRawName(), + RedisKeyUtil.generate(RedisCommands.HGETALL.getName(), "entry"), this.name, () -> super.readAllEntrySetAsync()); } @Override public RFuture> readAllMapAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HGETALL.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HGETALL.getName(), this.name, () -> super.readAllMapAsync()); } @Override protected RFuture putIfExistsOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate("hget", "hset", "putIfExistsOperation"), getRawName(), String.valueOf(key), + RedisKeyUtil.generate("hget", "hset", "putIfExistsOperation"), this.name, String.valueOf(key), () -> super.putIfExistsOperationAsync(key, value)); } @Override protected RFuture putIfAbsentOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate("hsetnx", "hget", "putIfAbsentOperation"), getRawName(), String.valueOf(key), + RedisKeyUtil.generate("hsetnx", "hget", "putIfAbsentOperation"), this.name, String.valueOf(key), () -> super.putIfAbsentOperationAsync(key, value)); } @Override protected RFuture fastPutIfAbsentOperationAsync(K key, V value) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSETNX.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSETNX.getName(), this.name, String.valueOf(key), () -> super.fastPutIfAbsentOperationAsync(key, value)); } @Override protected RFuture fastPutIfExistsOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate("hget", "hset", "fastPutIfExistsOperation"), getRawName(), String.valueOf(key), + RedisKeyUtil.generate("hget", "hset", "fastPutIfExistsOperation"), this.name, String.valueOf(key), () -> super.fastPutIfExistsOperationAsync(key, value)); } @Override protected RFuture removeOperationAsync(Object key, Object value) { return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("hget", "hset", "replaceOperation2"), - getRawName(), String.valueOf(key), () -> super.removeOperationAsync(key, value)); + this.name, String.valueOf(key), () -> super.removeOperationAsync(key, value)); } @Override protected RFuture replaceOperationAsync(K key, V oldValue, V newValue) { return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("hget", "hset", "replaceOperation3"), - getRawName(), String.valueOf(key), () -> super.replaceOperationAsync(key, oldValue, newValue)); + this.name, String.valueOf(key), () -> super.replaceOperationAsync(key, oldValue, newValue)); } @Override protected RFuture replaceOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("hget", "hset", "replaceOperation"), - getRawName(), String.valueOf(key), () -> super.replaceOperationAsync(key, value)); + this.name, String.valueOf(key), () -> super.replaceOperationAsync(key, value)); } @Override protected RFuture fastReplaceOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate("hexists", "hset", "fastReplaceOperation"), getRawName(), String.valueOf(key), + RedisKeyUtil.generate("hexists", "hset", "fastReplaceOperation"), this.name, String.valueOf(key), () -> super.fastReplaceOperationAsync(key, value)); } @Override public RFuture getOperationAsync(K key) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HGET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HGET.getName(), this.name, String.valueOf(key), () -> super.getOperationAsync(key)); } @Override protected RFuture putOperationAsync(K key, V value) { return RedissonWrapperCommon.delegateCall(redisUri, RedisKeyUtil.generate("hget", "hset", "putOperation"), - getRawName(), String.valueOf(key), () -> super.putOperationAsync(key, value)); + this.name, String.valueOf(key), () -> super.putOperationAsync(key, value)); } @Override protected RFuture removeOperationAsync(K key) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HDEL.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HDEL.getName(), this.name, String.valueOf(key), () -> super.removeOperationAsync(key)); } @Override protected RFuture fastPutOperationAsync(K key, V value) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSET.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.HSET.getName(), this.name, String.valueOf(key), () -> super.fastPutOperationAsync(key, value)); } @Override protected RFuture> fastRemoveOperationBatchAsync(K... keys) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.HDEL.getName(), "batch"), getRawName(), RedisKeyUtil.generate(keys), + RedisKeyUtil.generate(RedisCommands.HDEL.getName(), "batch"), this.name, RedisKeyUtil.generate(keys), () -> super.fastRemoveOperationBatchAsync(keys)); } diff --git a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonSetWrapper.java b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonSetWrapper.java index 84e1ce52c..2c284fc27 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonSetWrapper.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/main/java/io/arex/inst/redisson/v3/wrapper/RedissonSetWrapper.java @@ -3,6 +3,7 @@ import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.redis.common.RedisKeyUtil; import io.arex.inst.redisson.v3.RedissonWrapperCommon; +import io.arex.inst.redisson.v3.common.RedissonHelper; import org.redisson.RedissonSet; import org.redisson.api.RFuture; import org.redisson.api.RedissonClient; @@ -24,151 +25,151 @@ public class RedissonSetWrapper extends RedissonSet { public RedissonSetWrapper(CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson) { super(commandExecutor, name, redisson); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } public RedissonSetWrapper(Codec codec, CommandAsyncExecutor commandExecutor, String name, RedissonClient redisson) { super(codec, commandExecutor, name, redisson); - redisUri = commandExecutor.getConnectionManager().getConfig().getMasterAddress(); + redisUri = RedissonHelper.getRedisUri(commandExecutor.getConnectionManager()); } @Override public RFuture sizeAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SCARD_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SCARD_INT.getName(), this.name, () -> super.sizeAsync()); } @Override public RFuture containsAsync(Object o) { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SMEMBERS.getName(), "contains"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SMEMBERS.getName(), "contains"), this.name, () -> super.containsAsync(o)); } @Override public RFuture> readAllAsync() { return RedissonWrapperCommon.delegateCall(redisUri, - RedisKeyUtil.generate(RedisCommands.SMEMBERS.getName(), "readAll"), getRawName(), + RedisKeyUtil.generate(RedisCommands.SMEMBERS.getName(), "readAll"), this.name, () -> super.readAllAsync()); } @Override public RFuture addAsync(V e) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD_SINGLE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD_SINGLE.getName(), this.name, () -> super.addAsync(e)); } @Override public RFuture removeRandomAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SPOP_SINGLE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SPOP_SINGLE.getName(), this.name, () -> super.removeRandomAsync()); } @Override public RFuture> removeRandomAsync(int amount) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SPOP.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SPOP.getName(), this.name, () -> super.removeRandomAsync(amount)); } @Override public RFuture randomAsync() { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SRANDMEMBER_SINGLE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SRANDMEMBER_SINGLE.getName(), this.name, () -> super.randomAsync()); } @Override public RFuture> randomAsync(int count) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SRANDMEMBER.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SRANDMEMBER.getName(), this.name, () -> super.randomAsync(count)); } @Override public RFuture removeAsync(Object o) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SREM_SINGLE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SREM_SINGLE.getName(), this.name, () -> super.removeAsync(o)); } @Override public RFuture moveAsync(String destination, V member) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SMOVE.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SMOVE.getName(), this.name, () -> super.moveAsync(destination, member)); } @Override public RFuture containsAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "setContainsAll", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "setContainsAll", this.name, () -> super.containsAllAsync(c)); } @Override public RFuture addAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD_BOOL.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD_BOOL.getName(), this.name, () -> super.addAllAsync(c)); } @Override public RFuture addAllCountedAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SADD.getName(), this.name, () -> super.addAllCountedAsync(c)); } @Override public RFuture retainAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "setRetainAll", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "setRetainAll", this.name, () -> super.retainAllAsync(c)); } @Override public RFuture removeAllAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, "setRemoveAll", getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, "setRemoveAll", this.name, () -> super.removeAllAsync(c)); } @Override public RFuture removeAllCountedAsync(Collection c) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SREM.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SREM.getName(), this.name, () -> super.removeAllCountedAsync(c)); } @Override public RFuture unionAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SUNIONSTORE_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SUNIONSTORE_INT.getName(), this.name, RedisKeyUtil.generate(names), () -> super.unionAsync(names)); } @Override public RFuture> readUnionAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SUNION.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SUNION.getName(), this.name, RedisKeyUtil.generate(names), () -> super.readUnionAsync(names)); } @Override public RFuture diffAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SDIFFSTORE_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SDIFFSTORE_INT.getName(), this.name, RedisKeyUtil.generate(names), () -> super.diffAsync(names)); } @Override public RFuture> readDiffAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SDIFF.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SDIFF.getName(), this.name, RedisKeyUtil.generate(names), () -> super.readDiffAsync(names)); } @Override public RFuture intersectionAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTERSTORE_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTERSTORE_INT.getName(), this.name, RedisKeyUtil.generate(names), () -> super.intersectionAsync(names)); } @Override public RFuture> readIntersectionAsync(String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTER.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTER.getName(), this.name, RedisKeyUtil.generate(names), () -> super.readIntersectionAsync(names)); } @Override public RFuture countIntersectionAsync(int limit, String... names) { - return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTERCARD_INT.getName(), getRawName(), + return RedissonWrapperCommon.delegateCall(redisUri, RedisCommands.SINTERCARD_INT.getName(), this.name, RedisKeyUtil.generate(names), () -> super.countIntersectionAsync(names)); } diff --git a/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/RedissonInstrumentationTest.java b/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/RedissonInstrumentationTest.java index f443f4d50..f01d240c5 100644 --- a/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/RedissonInstrumentationTest.java +++ b/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/RedissonInstrumentationTest.java @@ -1,5 +1,6 @@ package io.arex.inst.redisson.v3; +import io.arex.inst.redisson.v3.common.RedissonHelper; import io.arex.inst.redisson.v3.wrapper.*; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -7,6 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.redisson.Redisson; import static org.junit.jupiter.api.Assertions.*; @@ -104,7 +106,7 @@ void onEnterGetListAdvice() { @Test void onExitGetListAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetListAdvice.onExit( - null, null, null, null)); + null, null, null)); } @Test @@ -115,7 +117,7 @@ void onEnterGetListWithCodecAdvice() { @Test void onExitGetListWithCodecAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetListWithCodecAdvice.onExit( - null, null, null, null, null)); + null, null, null, null)); } @Test @@ -126,7 +128,7 @@ void onEnterGetSetAdvice() { @Test void onExitGetSetAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetSetAdvice.onExit( - null, null, null, null)); + null, null, null)); } @Test @@ -137,7 +139,7 @@ void onEnterGetSetWithCodecAdvice() { @Test void onExitGetSetWithCodecAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetSetWithCodecAdvice.onExit( - null, null, null, null, null)); + null, null, null, null)); } @Test @@ -148,7 +150,7 @@ void onEnterGetMapAdvice() { @Test void onExitGetMapAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetMapAdvice.onExit( - null, null, null, null)); + null, null, null)); } @Test @@ -159,7 +161,7 @@ void onEnterGetMapWithOptionsAdvice() { @Test void onExitGetMapWithOptionsAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetMapWithOptionsAdvice.onExit( - null, null, null, null, null, null)); + null, null, null, null, null)); } @Test @@ -170,7 +172,7 @@ void onEnterGetMapWithCodecAdvice() { @Test void onExitGetMapWithCodecAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetMapWithCodecAdvice.onExit( - null, null, null, null, null)); + null, null, null, null)); } @Test @@ -181,6 +183,7 @@ void onEnterGetMapWithCodecOptionsAdvice() { @Test void onExitGetMapWithCodecOptionsAdvice() { assertDoesNotThrow(() -> RedissonInstrumentation.GetMapWithCodecOptionsAdvice.onExit( - null, null, null, null, null, null, null)); + null, null, null, null, null, null)); } + } \ No newline at end of file diff --git a/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/common/RedissonHelperTest.java b/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/common/RedissonHelperTest.java new file mode 100644 index 000000000..dad3e0d83 --- /dev/null +++ b/arex-instrumentation/redis/arex-redission-v3/src/test/java/io/arex/inst/redisson/v3/common/RedissonHelperTest.java @@ -0,0 +1,84 @@ +package io.arex.inst.redisson.v3.common; + +import static org.junit.jupiter.api.Assertions.assertNull; +import io.arex.agent.bootstrap.util.ReflectUtil; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.redisson.Redisson; +import org.redisson.config.MasterSlaveServersConfig; +import org.redisson.connection.ConnectionManager; + +@ExtendWith(MockitoExtension.class) +public class RedissonHelperTest { + + @BeforeAll + static void setUp() { + Mockito.mockStatic(ReflectUtil.class); + Mockito.when(ReflectUtil.getMethod(ConnectionManager.class, "getConfig")).thenReturn(null); + try { + Mockito.when(ReflectUtil.getMethod(Redisson.class, "getCommandExecutor")) + .thenReturn(Redisson.class.getMethod("getCommandExecutor", null)); + } catch (NoSuchMethodException e) { + Mockito.when(ReflectUtil.getMethod(Redisson.class, "getCommandExecutor")).thenReturn(null); + } + } + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void testRedisUri() { + ConnectionManager connectionManager = Mockito.mock(ConnectionManager.class); + MasterSlaveServersConfig masterSlaveServersConfig = new MasterSlaveServersConfig(); + masterSlaveServersConfig.setMasterAddress("104.34.56:0000"); + Mockito.lenient().when(connectionManager.getConfig()).thenReturn(masterSlaveServersConfig); + assertNull(RedissonHelper.getRedisUri(connectionManager)); + + mockRedissionHelperSet(); + assertNull(RedissonHelper.getRedisUri(connectionManager)); + } + + @Test + void testCacheRedisUri() { + ConnectionManager connectionManager = Mockito.mock(ConnectionManager.class); + assertNull(RedissonHelper.getRedisUri(connectionManager)); + + mockRedissionHelperSet(); + assertNull(RedissonHelper.getRedisUri(connectionManager)); + } + + @Test + void testGetMasterSlaveServersConfig() { + ConnectionManager connectionManager = Mockito.mock(ConnectionManager.class); + assertNull(RedissonHelper.getMasterSlaveServersConfig(connectionManager)); + } + + private void mockRedissionHelperSet() { + // mock static final field + try { + Field field = RedissonHelper.class.getDeclaredField("masterSlaveServersConfigSet"); + field.setAccessible(true); + Set set = new HashSet<>(1); + set.add(new MasterSlaveServersConfig()); + field.set(null, set); + } catch (Exception e) { + // doNothing + } + } + + @Test + void getCommandAsyncExecutor() throws NoSuchMethodException { + assertNull(RedissonHelper.getCommandAsyncExecutor(null)); + + Redisson redisson1 = Mockito.mock(Redisson.class); + assertNull(RedissonHelper.getCommandAsyncExecutor(redisson1)); + } +} diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java index d63dba309..2a1bea7d4 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java @@ -4,6 +4,7 @@ import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.internal.Pair; import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.runtime.config.Config; @@ -224,7 +225,15 @@ private static boolean shouldSkip(ServletAdapter adapter return false; } - if (IgnoreUtils.excludeEntranceOperation(requestURI)) { + // As long as one parameter is hit in includeServiceOperations, the operation will not be skipped + if (CollectionUtil.isNotEmpty(Config.get().getIncludeServiceOperations()) && + !(IgnoreUtils.includeOperation(adapter.getPattern(httpServletRequest)) || + IgnoreUtils.includeOperation(requestURI))) { + return true; + } + // As long as one parameter is hit in excludeServiceOperations, the operation will be skipped + if (IgnoreUtils.excludeOperation(adapter.getPattern(httpServletRequest)) || + IgnoreUtils.excludeOperation(requestURI)) { return true; } @@ -255,7 +264,7 @@ private static String getRedirectRecordId(ServletAdapter> requestParams, String name, String value) { + if (MapUtils.isEmpty(requestParams)) { + return false; + } + List values = requestParams.get(name); + if(CollectionUtil.isEmpty(values)){ + return false; + } + Iterator iterator = values.iterator(); + while (iterator.hasNext()) { + String next = iterator.next(); + if (StringUtil.equals(next, value)) { + iterator.remove(); + return true; + } + } + return false; + } + + /** + * obtain requset params + * + * @param queryString + * @return + */ + public static Map> getRequestParams(String queryString) { + if (StringUtil.isEmpty(queryString)) { + return Collections.emptyMap(); + } + MultiValueMap paramsKeyValueMap = UriComponentsBuilder.fromUriString( + "?" + queryString).build().getQueryParams(); + Map> requestParamsMap = new HashMap<>(); + for (Map.Entry> entry : paramsKeyValueMap.entrySet()) { + requestParamsMap.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return requestParamsMap; + } } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java index fe7f40d64..714fb5533 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3.java @@ -141,7 +141,12 @@ public String getPattern(HttpServletRequest httpServletRequest) { if (pattern != null) { return String.valueOf(pattern); } - return httpServletRequest.getRequestURI(); + final String requestURI = httpServletRequest.getRequestURI(); + if (StringUtil.isNotEmpty(httpServletRequest.getContextPath()) && requestURI.contains( + httpServletRequest.getContextPath())) { + return requestURI.replace(httpServletRequest.getContextPath(), ""); + } + return requestURI; } @Override diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java index cac4f4ee4..47e2b0d68 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5.java @@ -141,7 +141,12 @@ public String getPattern(HttpServletRequest httpServletRequest) { if (pattern != null) { return String.valueOf(pattern); } - return httpServletRequest.getRequestURI(); + final String requestURI = httpServletRequest.getRequestURI(); + if (StringUtil.isNotEmpty(httpServletRequest.getContextPath()) && requestURI.contains( + httpServletRequest.getContextPath())) { + return requestURI.replace(httpServletRequest.getContextPath(), ""); + } + return requestURI; } @Override diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV3.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV3.java index e701ba37c..dde063b59 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV3.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV3.java @@ -1,6 +1,7 @@ package io.arex.inst.httpservlet.wrapper; +import io.arex.inst.httpservlet.ServletUtil; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; @@ -23,6 +24,7 @@ */ public class CachedBodyRequestWrapperV3 extends HttpServletRequestWrapper { private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; + private static final String FORM_DATA_CONTENT_TYPE = "multipart/form-data"; private final ByteArrayOutputStream cachedContent; @@ -118,19 +120,25 @@ public String[] getParameterValues(String name) { private boolean isFormPost() { String contentType = getContentType(); - return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) && "POST".equals(getMethod())); + return (contentType != null && (contentType.contains(FORM_CONTENT_TYPE) || contentType.contains(FORM_DATA_CONTENT_TYPE) && "POST".equals(getMethod()))); } private void writeRequestParametersToCachedContent() { try { - if (this.cachedContent.size() == 0) { + // requestBody is not empty + if (this.cachedContent.size() == 0 && this.getContentLength() > 0) { + Map> requestParams = ServletUtil.getRequestParams(this.getQueryString()); String requestEncoding = getCharacterEncoding(); Map form = super.getParameterMap(); for (Iterator nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) { String name = nameIterator.next(); + boolean valueIsEmpty = true; List values = Arrays.asList(form.get(name)); for (Iterator valueIterator = values.iterator(); valueIterator.hasNext(); ) { String value = valueIterator.next(); + if (ServletUtil.matchAndRemoveRequestParams(requestParams, name, value)) { + continue; + } this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes()); if (value != null) { this.cachedContent.write('='); @@ -138,9 +146,13 @@ private void writeRequestParametersToCachedContent() { if (valueIterator.hasNext()) { this.cachedContent.write('&'); } + if (valueIsEmpty) { + valueIsEmpty = false; + } } + } - if (nameIterator.hasNext()) { + if (nameIterator.hasNext() && !valueIsEmpty) { this.cachedContent.write('&'); } } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV5.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV5.java index 490b06d72..fe7d39039 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV5.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/wrapper/CachedBodyRequestWrapperV5.java @@ -1,5 +1,6 @@ package io.arex.inst.httpservlet.wrapper; +import io.arex.inst.httpservlet.ServletUtil; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; @@ -21,6 +22,7 @@ */ public class CachedBodyRequestWrapperV5 extends HttpServletRequestWrapper { private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; + private static final String FORM_DATA_CONTENT_TYPE = "multipart/form-data"; private final ByteArrayOutputStream cachedContent; @@ -116,19 +118,25 @@ public String[] getParameterValues(String name) { private boolean isFormPost() { String contentType = getContentType(); - return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) && "POST".equals(getMethod())); + return (contentType != null && (contentType.contains(FORM_CONTENT_TYPE) || contentType.contains(FORM_DATA_CONTENT_TYPE) && "POST".equals(getMethod()))); } private void writeRequestParametersToCachedContent() { try { - if (this.cachedContent.size() == 0) { + // requestBody is not empty + if (this.cachedContent.size() == 0 && this.getContentLength() > 0) { + Map> requestParams = ServletUtil.getRequestParams(this.getQueryString()); String requestEncoding = getCharacterEncoding(); Map form = super.getParameterMap(); for (Iterator nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) { String name = nameIterator.next(); + boolean valueIsEmpty = true; List values = Arrays.asList(form.get(name)); for (Iterator valueIterator = values.iterator(); valueIterator.hasNext(); ) { String value = valueIterator.next(); + if (ServletUtil.matchAndRemoveRequestParams(requestParams, name, value)) { + continue; + } this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes()); if (value != null) { this.cachedContent.write('='); @@ -137,8 +145,11 @@ private void writeRequestParametersToCachedContent() { this.cachedContent.write('&'); } } + if (valueIsEmpty) { + valueIsEmpty = false; + } } - if (nameIterator.hasNext()) { + if (nameIterator.hasNext() && !valueIsEmpty) { this.cachedContent.write('&'); } } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletAdviceHelperTest.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletAdviceHelperTest.java index 6ac708820..cb79797f8 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletAdviceHelperTest.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletAdviceHelperTest.java @@ -10,6 +10,9 @@ import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.runtime.util.IgnoreUtils; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -104,7 +107,7 @@ static Stream onServiceEnterCase() { Mockito.when(adapter.getRequestHeader(any(), eq("referer"))).thenReturn("mock-referer"); ArexContext context = ArexContext.of("mock-record-id"); context.setAttachment(ArexConstants.REDIRECT_REFERER, "mock-referer"); - Mockito.when(ContextManager.getRecordContext(anyString())).thenReturn(context); + Mockito.when(ContextManager.getContext(anyString())).thenReturn(context); }; Runnable shouldSkip2 = () -> { Mockito.when(adapter.getRequestHeader(any(), eq(ArexConstants.RECORD_ID))).thenReturn(""); @@ -151,6 +154,36 @@ static Stream onServiceEnterCase() { Mockito.when(adapter.getAttribute(any(), eq(ArexConstants.SKIP_FLAG))).thenReturn(Boolean.TRUE); }; + Runnable shouldSkip10 = () -> { + Mockito.when(adapter.getRequestURI(any())).thenReturn("/contextPath/uri"); + Mockito.when(adapter.getRequestHeader(any(), eq(ArexConstants.RECORD_ID))).thenReturn(null); + Set includeServiceOperations = new HashSet<>(); + includeServiceOperations.add("/contextPath/uri"); + Mockito.when(Config.get().getIncludeServiceOperations()).thenReturn(includeServiceOperations); + }; + + Runnable shouldSkip11 = () -> { + Mockito.when(adapter.getRequestURI(any())).thenReturn("/uri"); + Mockito.when(adapter.getRequestHeader(any(), eq(ArexConstants.RECORD_ID))).thenReturn(null); + Mockito.when(IgnoreUtils.includeOperation(any())).thenReturn(true); + }; + + Runnable shouldSkip12 = () -> { + Mockito.when(adapter.getRequestURI(any())).thenReturn("/contextPath/uri"); + Mockito.when(adapter.getRequestHeader(any(), eq(ArexConstants.RECORD_ID))).thenReturn(null); + Mockito.when(IgnoreUtils.excludeOperation(any())).thenReturn(true); + + Mockito.when(Config.get().getIncludeServiceOperations()).thenReturn(Collections.emptySet()); + Set excludeServiceOperations = new HashSet<>(); + excludeServiceOperations.add("/contextPath/uri"); + Mockito.when(Config.get().excludeServiceOperations()).thenReturn(excludeServiceOperations); + }; + + Runnable shouldSkip13 = () -> { + Mockito.when(adapter.getRequestURI(any())).thenReturn("/uri"); + Mockito.when(IgnoreUtils.excludeOperation(any())).thenReturn(true); + }; + Predicate> predicate1 = Objects::isNull; Predicate> predicate2 = Objects::nonNull; return Stream.of( @@ -172,7 +205,11 @@ static Stream onServiceEnterCase() { arguments("shouldSkip: adapter.getRequestURI return .jsp", shouldSkip6_3, predicate2), arguments("shouldSkip: adapter.getContentType return image/", shouldSkip7, predicate1), arguments("ContextManager.needRecordOrReplay is true", shouldSkip8, predicate2), - arguments("shouldSkip: adapter.getAttribute returns true", shouldSkip9, predicate1) + arguments("shouldSkip: adapter.getAttribute returns true", shouldSkip9, predicate1), + arguments("shouldSkip: hit includeOperaions while contextPath is empty", shouldSkip10, predicate1), + arguments("shouldSkip: hit includeOperaions while contextPath is not empty", shouldSkip11, predicate1), + arguments("shouldSkip: hit excludeOperaions while contextPath is empty", shouldSkip12, predicate1), + arguments("shouldSkip: hit excludeOperaions while contextPath is not empty", shouldSkip13, predicate1) ); } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletUtilTest.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletUtilTest.java index 3f529b025..90cd0b812 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletUtilTest.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/ServletUtilTest.java @@ -2,6 +2,12 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; class ServletUtilTest { @@ -23,4 +29,27 @@ public void getFullPath() { assertEquals("/servletpath/controll/action", ServletUtil.getRequestPath("http://arextest.com/servletpath/controll/action")); assertEquals("/servletpath/controll/action?k1=v1", ServletUtil.getRequestPath("http://arextest.com/servletpath/controll/action?k1=v1")); } + + @Test + public void matchRequestParams() { + Map> requestParams = new HashMap<>(); + requestParams.put("name", new ArrayList<>(Arrays.asList("kimi", null))); + requestParams.put("age", new ArrayList<>(Arrays.asList("0"))); + assertFalse(ServletUtil.matchAndRemoveRequestParams(requestParams, "name", "lock")); + + //requestParams has null value,targetValue is not null + assertTrue(ServletUtil.matchAndRemoveRequestParams(requestParams, "age", "0")); + + //test: requestParams has null value, and targetValue is null + assertTrue(ServletUtil.matchAndRemoveRequestParams(requestParams, "name", null)); + + assertFalse(ServletUtil.matchAndRemoveRequestParams(Collections.emptyMap(), "name", "lock")); + } + + @Test + public void getRequestParams() { + String queryString = "name=kimi&age=0"; + Map> requestParams = ServletUtil.getRequestParams(queryString); + assertEquals(2, requestParams.size()); + } } diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java index 280fd35ac..80652d036 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV3Test.java @@ -155,6 +155,9 @@ void getPattern() { when(mockRequest.getAttribute(eq("org.springframework.web.servlet.HandlerMapping.bestMatchingPattern"))).thenReturn(null); when(mockRequest.getRequestURI()).thenReturn("/commutity/httpClientTest/okHttp"); assertEquals("/commutity/httpClientTest/okHttp", instance.getPattern(mockRequest)); + + when(mockRequest.getContextPath()).thenReturn("/commutity"); + assertEquals("/httpClientTest/okHttp", instance.getPattern(mockRequest)); } @Test diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java index 7ff256027..e42f7e0e8 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/test/java/io/arex/inst/httpservlet/adapter/impl/ServletAdapterImplV5Test.java @@ -157,6 +157,9 @@ void getPattern() { when(mockRequest.getAttribute(eq("org.springframework.web.servlet.HandlerMapping.bestMatchingPattern"))).thenReturn(null); when(mockRequest.getRequestURI()).thenReturn("/commutity/httpClientTest/okHttp"); assertEquals("/commutity/httpClientTest/okHttp", instance.getPattern(mockRequest)); + + when(mockRequest.getContextPath()).thenReturn("/commutity"); + assertEquals("/httpClientTest/okHttp", instance.getPattern(mockRequest)); } @Test diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/DateTimeInstrumentation.java b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/DateTimeInstrumentation.java index be3d5ce12..1fd03c310 100644 --- a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/DateTimeInstrumentation.java +++ b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/DateTimeInstrumentation.java @@ -12,12 +12,7 @@ import io.arex.agent.bootstrap.cache.TimeCache; import io.arex.inst.extension.MethodInstrumentation; import io.arex.inst.extension.TypeInstrumentation; -import java.time.Clock; import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; @@ -28,21 +23,20 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import static net.bytebuddy.matcher.ElementMatchers.*; + public class DateTimeInstrumentation extends TypeInstrumentation { private final String clazzName; static final Map INSTRUMENTATION_MAP; + static final String CLOCK_CLASS = "java.time.Clock"; static { INSTRUMENTATION_MAP = new HashMap<>(); - INSTRUMENTATION_MAP.put("java.time.Instant", InstantAdvice.getMethodInstrumentation()); - INSTRUMENTATION_MAP.put("java.time.LocalDate", LocalDateAdvice.getMethodInstrumentation()); - INSTRUMENTATION_MAP.put("java.time.LocalTime", LocalTimeAdvice.getMethodInstrumentation()); - INSTRUMENTATION_MAP.put("java.time.LocalDateTime", LocalDateTimeAdvice.getMethodInstrumentation()); + INSTRUMENTATION_MAP.put(CLOCK_CLASS, ClockAdvice.getMethodInstrumentation()); INSTRUMENTATION_MAP.put("java.util.Date", DateAdvice.getMethodInstrumentation()); INSTRUMENTATION_MAP.put("java.util.Calendar", CalendarAdvice.getMethodInstrumentation()); INSTRUMENTATION_MAP.put("org.joda.time.DateTimeUtils", DateTimeUtilsAdvice.getMethodInstrumentation()); - INSTRUMENTATION_MAP.put("java.time.ZonedDateTime", ZonedDateTimeAdvice.getMethodInstrumentation()); } public DateTimeInstrumentation(String clazzName) { @@ -51,6 +45,9 @@ public DateTimeInstrumentation(String clazzName) { @Override protected ElementMatcher typeMatcher() { + if (CLOCK_CLASS.equals(clazzName)) { + return hasSuperClass(named(CLOCK_CLASS)); + } return named(clazzName); } @@ -64,95 +61,12 @@ public List methodAdvices() { return Collections.singletonList(methodInstrumentation); } - public static class InstantAdvice { - public static MethodInstrumentation getMethodInstrumentation() { - ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(isStatic()) - .and(named("now")).and(takesNoArguments()); - - String advice = InstantAdvice.class.getName(); - - return new MethodInstrumentation(matcher, advice); - } - - @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static long onEnter() { - return TimeCache.get(); - } - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Enter long mockMills, - @Advice.Return(readOnly = false) Instant result) { - if (mockMills > 0L) { - result = Instant.ofEpochMilli(mockMills); - } - } - } - - public static class LocalDateAdvice { - - public static MethodInstrumentation getMethodInstrumentation() { - ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(isStatic()) - .and(named("now")) - .and(takesArgument(0, named("java.time.Clock"))); - - String advice = LocalDateAdvice.class.getName(); - - return new MethodInstrumentation(matcher, advice); - } - - @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static long onEnter() { - return TimeCache.get(); - } - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Enter long mockMills, - @Advice.Argument(0) Clock clock, - @Advice.Return(readOnly = false) LocalDate result) { - if (mockMills > 0L) { - result = Instant.ofEpochMilli(mockMills).atZone(clock.getZone()).toLocalDate(); - } - } - } - - public static class LocalTimeAdvice { - - public static MethodInstrumentation getMethodInstrumentation() { - ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(isStatic()) - .and(named("now")) - .and(takesArgument(0, named("java.time.Clock"))); - - String advice = LocalTimeAdvice.class.getName(); - - return new MethodInstrumentation(matcher, advice); - } - - @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static long onEnter() { - return TimeCache.get(); - } - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Enter long mockMills, - @Advice.Argument(0) Clock clock, - @Advice.Return(readOnly = false) LocalTime result) { - if (mockMills > 0L) { - result = Instant.ofEpochMilli(mockMills).atZone(clock.getZone()).toLocalTime(); - } - } - } - - public static class LocalDateTimeAdvice { + public static class ClockAdvice { public static MethodInstrumentation getMethodInstrumentation() { - ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(isStatic()) - .and(named("now")) - .and(takesArgument(0, named("java.time.Clock"))); + ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(named("instant")); - String advice = LocalDateTimeAdvice.class.getName(); + String advice = ClockAdvice.class.getName(); return new MethodInstrumentation(matcher, advice); } @@ -163,12 +77,10 @@ public static long onEnter() { } @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Enter long mockMills, - @Advice.Argument(0) Clock clock, - @Advice.Return(readOnly = false) LocalDateTime result) { + public static void onExit(@Advice.Enter long mockMills, + @Advice.Return(readOnly = false) Instant result) { if (mockMills > 0L) { - result = Instant.ofEpochMilli(mockMills).atZone(clock.getZone()).toLocalDateTime(); + result = Instant.ofEpochMilli(mockMills); } } } @@ -241,33 +153,4 @@ public static void onExit( } } } - - public static class ZonedDateTimeAdvice { - - public static MethodInstrumentation getMethodInstrumentation() { - ElementMatcher.Junction matcher = isMethod().and(isPublic()).and(isStatic()) - .and(named("now")) - .and(takesArgument(0, named("java.time.Clock"))); - - String advice = ZonedDateTimeAdvice.class.getName(); - - return new MethodInstrumentation(matcher, advice); - } - - - @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static long onEnter() { - return TimeCache.get(); - } - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Enter long mockMills, - @Advice.Argument(0) Clock clock, - @Advice.Return(readOnly = false) ZonedDateTime result) { - if (mockMills > 0L) { - result = ZonedDateTime.ofInstant(Instant.ofEpochMilli(mockMills), clock.getZone()); - } - } - } } diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineInstrumentation.java b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineInstrumentation.java index 021b8d5ac..6e2697c11 100644 --- a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineInstrumentation.java +++ b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineInstrumentation.java @@ -30,9 +30,7 @@ public List methodAdvices() { @Override public Transformer transformer() { - return (builder, typeDescription, classLoader, module) -> { - return builder.method(isNative().and(isStatic()).and(named("currentTimeMillis"))) - .intercept(Advice.to(TimeMachineInterceptor.class)); - }; + return (builder, typeDescription, classLoader, module, domain) -> builder.method(isNative().and(isStatic()).and(named("currentTimeMillis"))) + .intercept(Advice.to(TimeMachineInterceptor.class)); } } diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineModuleInstrumentation.java b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineModuleInstrumentation.java index cd5b87000..d058919e7 100644 --- a/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineModuleInstrumentation.java +++ b/arex-instrumentation/time-machine/arex-time-machine/src/main/java/io/arex/inst/time/TimeMachineModuleInstrumentation.java @@ -24,14 +24,10 @@ public List instrumentationTypes() { List typeInstList = new ArrayList<>(); if (Config.get().getBoolean("arex.time.machine", false)) { - typeInstList.add(new DateTimeInstrumentation("java.time.Instant")); - typeInstList.add(new DateTimeInstrumentation("java.time.LocalDate")); - typeInstList.add(new DateTimeInstrumentation("java.time.LocalTime")); - typeInstList.add(new DateTimeInstrumentation("java.time.LocalDateTime")); + typeInstList.add(new DateTimeInstrumentation("java.time.Clock")); typeInstList.add(new DateTimeInstrumentation("java.util.Date")); typeInstList.add(new DateTimeInstrumentation("java.util.Calendar")); typeInstList.add(new DateTimeInstrumentation("org.joda.time.DateTimeUtils")); - typeInstList.add(new DateTimeInstrumentation("java.time.ZonedDateTime")); } return typeInstList; } diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/DateTimeInstrumentationTest.java b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/DateTimeInstrumentationTest.java index 586000e75..66ab294e7 100644 --- a/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/DateTimeInstrumentationTest.java +++ b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/DateTimeInstrumentationTest.java @@ -7,11 +7,6 @@ import io.arex.inst.time.DateTimeInstrumentation.CalendarAdvice; import io.arex.inst.time.DateTimeInstrumentation.DateAdvice; import io.arex.inst.time.DateTimeInstrumentation.DateTimeUtilsAdvice; -import io.arex.inst.time.DateTimeInstrumentation.InstantAdvice; -import io.arex.inst.time.DateTimeInstrumentation.LocalDateAdvice; -import io.arex.inst.time.DateTimeInstrumentation.LocalDateTimeAdvice; -import io.arex.inst.time.DateTimeInstrumentation.LocalTimeAdvice; -import io.arex.inst.time.DateTimeInstrumentation.ZonedDateTimeAdvice; import java.lang.reflect.Method; import java.time.Clock; import java.time.Instant; @@ -46,9 +41,11 @@ static void tearDown() { @Test void typeMatcher() { - DateTimeInstrumentation inst = new DateTimeInstrumentation("java.time.LocalDate"); + DateTimeInstrumentation inst = new DateTimeInstrumentation("java.util.Calendar"); + assertTrue(inst.typeMatcher().matches(TypeDescription.ForLoadedType.of(Calendar.class))); - assertTrue(inst.typeMatcher().matches(TypeDescription.ForLoadedType.of(LocalDate.class))); + DateTimeInstrumentation inst2 = new DateTimeInstrumentation("java.time.Clock"); + assertTrue(inst2.typeMatcher().matches(TypeDescription.ForLoadedType.of(Clock.class))); } @ParameterizedTest(name = "[{index}] {1}") @@ -59,64 +56,23 @@ void methodAdvices(DateTimeInstrumentation inst, Method method) throws NoSuchMet } static Stream methodAdvicesArguments() throws NoSuchMethodException { - DateTimeInstrumentation instantInst = new DateTimeInstrumentation("java.time.Instant"); - Method instantMethod = Instant.class.getDeclaredMethod("now", null); - - DateTimeInstrumentation localDateInst = new DateTimeInstrumentation("java.time.LocalDate"); - Method localDateMethod = LocalDate.class.getDeclaredMethod("now", Clock.class); - - DateTimeInstrumentation localTimeInst = new DateTimeInstrumentation("java.time.LocalTime"); - Method localTimeMethod = LocalTime.class.getDeclaredMethod("now", Clock.class); - - DateTimeInstrumentation localDateTimeInst = new DateTimeInstrumentation("java.time.LocalDateTime"); - Method localDateTimeMethod = LocalDateTime.class.getDeclaredMethod("now", Clock.class); - DateTimeInstrumentation calendarInst = new DateTimeInstrumentation("java.util.Calendar"); Method calendarMethod = Calendar.class.getDeclaredMethod("createCalendar", java.util.TimeZone.class, java.util.Locale.class); - DateTimeInstrumentation zonedDateTimeInst = new DateTimeInstrumentation("java.time.ZonedDateTime"); - Method zonedDateTimeMethod = ZonedDateTime.class.getDeclaredMethod("now", Clock.class); + DateTimeInstrumentation clockInst = new DateTimeInstrumentation("java.time.Clock"); + Method clockMethod = Clock.class.getDeclaredMethod("instant", null); return Stream.of( - Arguments.arguments(instantInst, instantMethod), - Arguments.arguments(localDateInst, localDateMethod), - Arguments.arguments(localTimeInst, localTimeMethod), - Arguments.arguments(localDateTimeInst, localDateTimeMethod), Arguments.arguments(calendarInst, calendarMethod), - Arguments.arguments(zonedDateTimeInst, zonedDateTimeMethod) + Arguments.arguments(clockInst, clockMethod) ); } - @Test - void InstantAdvice() { - assertTrue(InstantAdvice.onEnter() > 0L); + void ClockAdvice() { + assertTrue(DateTimeInstrumentation.ClockAdvice.onEnter() > 0L); assertDoesNotThrow(() -> { - InstantAdvice.onExit(System.currentTimeMillis(), null); - }); - } - - @Test - void LocalDateAdvice() { - assertTrue(LocalDateAdvice.onEnter() > 0L); - assertDoesNotThrow(() -> { - LocalDateAdvice.onExit(System.currentTimeMillis(), Clock.systemDefaultZone(), null); - }); - } - - @Test - void LocalTimeAdvice() { - assertTrue(LocalTimeAdvice.onEnter() > 0L); - assertDoesNotThrow(() -> { - LocalTimeAdvice.onExit(System.currentTimeMillis(), Clock.systemDefaultZone(), null); - }); - } - - @Test - void LocalDateTimeAdvice() { - assertTrue(LocalDateTimeAdvice.onEnter() > 0L); - assertDoesNotThrow(() -> { - LocalDateTimeAdvice.onExit(System.currentTimeMillis(), Clock.systemDefaultZone(), null); + DateTimeInstrumentation.ClockAdvice.onExit(System.currentTimeMillis(), null); }); } @@ -141,12 +97,4 @@ void DateTimeUtilsAdvice() { DateTimeUtilsAdvice.onExit(System.currentTimeMillis(), 0L); }); } - - @Test - void ZonedDateTimeAdvice() { - assertTrue(ZonedDateTimeAdvice.onEnter() > 0L); - assertDoesNotThrow(() -> { - ZonedDateTimeAdvice.onExit(System.currentTimeMillis(), Clock.systemDefaultZone(), null); - }); - } -} \ No newline at end of file +} diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineInterceptorTest.java b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineInterceptorTest.java new file mode 100644 index 000000000..6e9687aee --- /dev/null +++ b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineInterceptorTest.java @@ -0,0 +1,41 @@ +package io.arex.inst.time; + +import static org.junit.jupiter.api.Assertions.*; + +import io.arex.agent.bootstrap.TraceContextManager; +import io.arex.agent.bootstrap.cache.TimeCache; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * @since 2024/1/12 + */ +class TimeMachineInterceptorTest { + @BeforeAll + static void setUp() { + Mockito.mockStatic(TraceContextManager.class); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void onEnter() { + // traceId is not null + Mockito.when(TraceContextManager.get()).thenReturn("test"); + assertEquals(0, TimeCache.get()); + TimeCache.put(1L); + assertNotEquals(0, TimeMachineInterceptor.onEnter()); + } + + @Test + void onExit() { + long result = 456L; + TimeMachineInterceptor.onExit(123L, result); + assertEquals(456L, result); + } +} diff --git a/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineModuleInstrumentationTest.java b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineModuleInstrumentationTest.java index bea753279..269c92951 100644 --- a/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineModuleInstrumentationTest.java +++ b/arex-instrumentation/time-machine/arex-time-machine/src/test/java/io/arex/inst/time/TimeMachineModuleInstrumentationTest.java @@ -21,6 +21,6 @@ void tearDown() { void instrumentationTypes() { ConfigBuilder.create("test").addProperty("arex.time.machine", "true").build(); TimeMachineModuleInstrumentation inst = new TimeMachineModuleInstrumentation(); - assertEquals(8, inst.instrumentationTypes().size()); + assertEquals(4, inst.instrumentationTypes().size()); } -} \ No newline at end of file +} diff --git a/arex-third-party/src/main/java/io/arex/agent/thirdparty/util/time/DateFormatUtils.java b/arex-third-party/src/main/java/io/arex/agent/thirdparty/util/time/DateFormatUtils.java index a95a99f7d..03518a796 100644 --- a/arex-third-party/src/main/java/io/arex/agent/thirdparty/util/time/DateFormatUtils.java +++ b/arex-third-party/src/main/java/io/arex/agent/thirdparty/util/time/DateFormatUtils.java @@ -1,5 +1,9 @@ package io.arex.agent.thirdparty.util.time; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -23,4 +27,16 @@ public static String format(final Calendar calendar, final String pattern, final final FastDateFormat df = FastDateFormat.getInstance(pattern, timeZone, locale); return df.format(calendar); } + + public static String format(final LocalDateTime localDateTime, final String pattern) { + return localDateTime.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static String format(final LocalTime localTime, final String pattern) { + return localTime.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static String format(final OffsetDateTime offsetDateTime, final String pattern) { + return offsetDateTime.format(DateTimeFormatter.ofPattern(pattern)); + } } diff --git a/pom.xml b/pom.xml index bc3c9ca36..eb478049f 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ - 0.3.12 + 0.3.18 UTF-8 UTF-8 @@ -62,7 +62,7 @@ 1.8 3.8.1 1.7.25 - 1.12.8 + 1.14.9 3.0.2 3.2.1 1.0 @@ -77,7 +77,9 @@ **/*Test.java, **/target/**, **/model/**, + **/constants/**, **/thirdparty/**, + **/integrationtest/**, **/*Instrumentation.java, **/JJWTGenerator.java @@ -88,12 +90,19 @@ **/test/**, **/*Test.java, + **/*Transformer.java, + **/*Constants.java, **/target/**, **/model/**, + **/constants/**, **/context/**, + **/foundation/internal/**, + **/bootstrap/internal/**, **/wrapper/**, **/thirdparty/**, - **/RedisCommandBuilderImpl.java + **/integrationtest/**, + **/RedisCommandBuilderImpl.java, + **/HttpResponseWrapper.java @@ -262,7 +271,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.11 From 6defa56c4a6323ac85d0cf39250ca0402608caf2 Mon Sep 17 00:00:00 2001 From: lucas-myx Date: Mon, 15 Jan 2024 17:39:07 +0800 Subject: [PATCH 3/5] fix: sonar --- .../util/sizeof/ObjectGraphWalker.java | 66 ++++++++++--------- .../arex/inst/runtime/util/MockUtilsTest.java | 11 ++++ .../dynamic/common/DynamicClassExtractor.java | 6 +- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java index 3a4c7b7b2..851e44560 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java @@ -76,45 +76,44 @@ long walk(VisitorListener visitorListener, Object... root) { Deque toVisit = new ArrayDeque<>(); Set visited = newSetFromMap(new IdentityHashMap<>()); - if (root != null) { - for (Object object : root) { - nullSafeAdd(toVisit, object); - } - } + nullSafeAddArray(toVisit, root); while (!toVisit.isEmpty()) { - Object ref = toVisit.pop(); + if (!visited.add(ref) || !shouldWalkClass(ref.getClass())) { + continue; + } - if (visited.add(ref)) { - Class refClass = ref.getClass(); - if (shouldWalkClass(refClass)) { - if (refClass.isArray() && !refClass.getComponentType().isPrimitive()) { - for (int i = 0; i < Array.getLength(ref); i++) { - nullSafeAdd(toVisit, Array.get(ref, i)); - } - } else { - for (Field field : getFilteredFields(refClass)) { - try { - nullSafeAdd(toVisit, field.get(ref)); - } catch (IllegalAccessException ex) { - throw new RuntimeException(ex); - } - } - } + walkField(ref, toVisit); - final long visitSize = visitor.visit(ref); - if (visitorListener != null) { - visitorListener.visited(ref, visitSize); - } - result += visitSize; - } + final long visitSize = visitor.visit(ref); + if (visitorListener != null) { + visitorListener.visited(ref, visitSize); } + result += visitSize; + } return result; } + private void walkField(Object ref, Deque toVisit) { + Class refClass = ref.getClass(); + if (refClass.isArray() && !refClass.getComponentType().isPrimitive()) { + for (int i = 0; i < Array.getLength(ref); i++) { + nullSafeAdd(toVisit, Array.get(ref, i)); + } + return; + } + for (Field field : getFilteredFields(refClass)) { + try { + nullSafeAdd(toVisit, field.get(ref)); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + } + /** * Returns the filtered fields for a particular type * @@ -143,6 +142,14 @@ private boolean shouldWalkClass(Class refClass) { return cached; } + private static void nullSafeAddArray(final Deque toVisit, final Object... root) { + if (root != null) { + for (Object object : root) { + nullSafeAdd(toVisit, object); + } + } + } + private static void nullSafeAdd(final Deque toVisit, final Object o) { if (o != null) { toVisit.push(o); @@ -159,8 +166,7 @@ private static Collection getAllFields(Class refClass) { Collection fields = new ArrayList<>(); for (Class klazz = refClass; klazz != null; klazz = klazz.getSuperclass()) { for (Field field : klazz.getDeclaredFields()) { - if (!Modifier.isStatic(field.getModifiers()) && - !field.getType().isPrimitive()) { + if (!Modifier.isStatic(field.getModifiers()) && !field.getType().isPrimitive()) { try { field.setAccessible(true); } catch (RuntimeException e) { diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java index 4bae6e0dc..9289590f0 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java @@ -169,6 +169,9 @@ void createMocker() { actualResult = MockUtils.createDubboStreamProvider("query"); assertEquals(MockCategoryType.DUBBO_STREAM_PROVIDER, actualResult.getCategoryType()); + + actualResult = MockUtils.createNettyProvider("query"); + assertEquals(MockCategoryType.NETTY_PROVIDER, actualResult.getCategoryType()); } @Test @@ -178,4 +181,12 @@ void methodSignatureHash() { mocker.getTargetRequest().setBody("mock"); assertTrue(MockUtils.methodSignatureHash(mocker) > 0); } + + @Test + void methodRequestTypeHash() { + ArexMocker mocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); + assertTrue(MockUtils.methodRequestTypeHash(mocker) > 0); + } } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index 7ff37fbe4..b01f3becb 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -33,7 +33,6 @@ import reactor.core.publisher.Mono; import io.arex.inst.runtime.util.sizeof.AgentSizeOf; -import reactor.core.publisher.Mono; public class DynamicClassExtractor { private static final String LISTENABLE_FUTURE = "com.google.common.util.concurrent.ListenableFuture"; @@ -88,8 +87,9 @@ public DynamicClassExtractor(Method method, Object[] args) { public Object recordResponse(Object response) { if (IgnoreUtils.invalidOperation(dynamicSignature)) { - LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record invalid operation: %s", dynamicSignature)); + LogManager.warn(NEED_RECORD_TITLE, StringUtil.format( + "do not record invalid operation: %s, can not serialize request or response or response too large", + dynamicSignature)); return response; } if (response instanceof Future) { From 22d45b707ff12fc8359cb8d069de025dadb2aa8f Mon Sep 17 00:00:00 2001 From: lucas-myx Date: Thu, 18 Jan 2024 15:53:53 +0800 Subject: [PATCH 4/5] fix: sonar and UT --- .../agent/bootstrap/model/ArexMocker.java | 10 +-- .../io/arex/agent/bootstrap/model/Mocker.java | 4 +- .../arex/agent/bootstrap/util/ArrayUtils.java | 1 - .../agent/bootstrap/util/CollectionUtil.java | 8 +-- .../arex/agent/bootstrap/util/MapUtils.java | 22 +++++++ .../agent/bootstrap/util/MapUtilsTest.java | 36 ++++++++++- .../inst/runtime/model/ArexConstants.java | 2 + .../runtime/util/MergeRecordReplayUtil.java | 2 +- .../io/arex/inst/runtime/util/MockUtils.java | 15 ++++- .../inst/runtime/util/sizeof/AgentSizeOf.java | 10 +-- .../util/sizeof/ObjectGraphWalker.java | 19 ++---- .../runtime/util/sizeof/ThrowableFilter.java | 18 ++++++ .../arex/inst/runtime/util/MockUtilsTest.java | 12 +++- .../runtime/util/sizeof/AgentSizeOfTest.java | 2 +- .../util/sizeof/ObjectGraphWalkerTest.java | 4 +- .../util/sizeof/ThrowableFilterTest.java | 18 ++++++ .../dynamic/common/DynamicClassExtractor.java | 63 ++++++++++--------- .../common/DynamicClassExtractorTest.java | 7 ++- .../inst/redis/common/RedisExtractor.java | 2 +- 19 files changed, 180 insertions(+), 75 deletions(-) create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java index 433667917..8fc902750 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java @@ -15,7 +15,7 @@ public class ArexMocker implements Mocker { private long creationTime; private Mocker.Target targetRequest; private Mocker.Target targetResponse; - private boolean merge; + private boolean needMerge; private String operationName; public ArexMocker() { @@ -118,11 +118,11 @@ public void setOperationName(String operationName) { this.operationName = operationName; } - public boolean isMerge() { - return merge; + public boolean isNeedMerge() { + return needMerge; } - public void setMerge(boolean merge) { - this.merge = merge; + public void setNeedMerge(boolean needMerge) { + this.needMerge = needMerge; } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java index 88fe3dc05..12f660d35 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java @@ -117,7 +117,7 @@ default String replayLogTitle() { return "replay." + getCategoryType().getName(); } - boolean isMerge(); + public boolean isNeedMerge(); - void setMerge(boolean merge); + public void setNeedMerge(boolean needMerge); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java index f1ba40b83..d7d3038ea 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/ArrayUtils.java @@ -3,7 +3,6 @@ import java.util.function.Function; public class ArrayUtils { - public static final String[] EMPTY_STRING_ARRAY = new String[0]; private ArrayUtils() {} public static byte[] addAll(final byte[] array1, final byte... array2) { diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java index 81df64f5c..153ff23ad 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java @@ -43,11 +43,11 @@ public static List newArrayList(E... elements) { * split to multiple list by split count */ public static List> split(List originalList, int splitCount) { - List> splitList = new ArrayList<>(); if (isEmpty(originalList)) { - return splitList; + return emptyList(); } int originalSize = originalList.size(); + List> splitList = new ArrayList<>(); if (originalSize < splitCount || splitCount == 0) { splitList.add(originalList); return splitList; @@ -65,10 +65,10 @@ public static List> split(List originalList, int splitCount) { } public static List filterNull(List originalList) { - List filterList = new ArrayList<>(); if (isEmpty(originalList)) { - return filterList; + return emptyList(); } + List filterList = new ArrayList<>(originalList.size()); for (V element : originalList) { if (element != null) { filterList.add(element); diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java index 8b6716239..26231ea70 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java @@ -39,4 +39,26 @@ static int capacity(int expectedSize) { // any large value return Integer.MAX_VALUE; } + + public static boolean getBoolean(final Map map, final K key) { + return getBoolean(map, key, false); + } + + public static boolean getBoolean(final Map map, final K key, boolean defaultValue) { + if (isEmpty(map) || map.get(key) == null) { + return defaultValue; + } + final Object answer = map.get(key); + if (answer instanceof Boolean) { + return (Boolean) answer; + } + if (answer instanceof String) { + return Boolean.parseBoolean((String) answer); + } + if (answer instanceof Number) { + final Number num = (Number) answer; + return num.intValue() != 0; + } + return defaultValue; + } } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java index 95938ed52..499bba7ee 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java @@ -1,12 +1,17 @@ package io.arex.agent.bootstrap.util; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class MapUtilsTest { @@ -41,4 +46,29 @@ public static int capacity(Map map) { return 0; } } + + @ParameterizedTest + @MethodSource("getBooleanCase") + void getBoolean(Map map, String key, Predicate asserts) { + asserts.test(MapUtils.getBoolean(map, key)); + } + + static Stream getBooleanCase() { + Map map = new HashMap<>(); + map.put("booleanKey", true); + map.put("stringKey", "true"); + map.put("numberKey", 1); + map.put("key", String.class); + + Predicate asserts1 = bool -> bool; + Predicate asserts2 = bool -> !bool; + + return Stream.of( + arguments(null, null, asserts1), + arguments(map, "booleanKey", asserts2), + arguments(map, "stringKey", asserts2), + arguments(map, "numberKey", asserts2), + arguments(map, "key", asserts1) + ); + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java index 98420e458..f9c222d31 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java @@ -47,4 +47,6 @@ private ArexConstants() {} public static final long MEMORY_SIZE_1MB = 1024L * 1024L; public static final long MEMORY_SIZE_5MB = 5 * 1024L * 1024L; public static final String CALL_REPLAY_MAX = "callReplayMax"; + public static final String EXCEED_MAX_SIZE_TITLE = "exceed.max.size"; + public static final String EXCEED_MAX_SIZE_FLAG = "isExceedMaxSize"; } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java index e1d4b7fb0..bcc08ce69 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java @@ -144,7 +144,7 @@ private static List> split(List mergeList) { private static void logBigSize(List mergeRecordList) { for (MergeDTO mergeDTO : mergeRecordList) { LogManager.warn("merge.record.size.too.large", - StringUtil.format("please check following record data, if is dynamic class, recommended to modify it, " + + StringUtil.format("please check following record data, if is dynamic class, suggest replace it, " + "category: %s, operationName: %s", mergeDTO.getCategory(), mergeDTO.getOperationName())); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java index fb282f01a..af7210d9f 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java @@ -5,14 +5,17 @@ import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.model.Mocker.Target; +import io.arex.agent.bootstrap.util.MapUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.match.ReplayMatcher; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataService; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; public final class MockUtils { @@ -92,7 +95,7 @@ public static void recordMocker(Mocker requestMocker) { if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { return; } - if (requestMocker.isMerge()) { + if (requestMocker.isNeedMerge()) { MergeRecordReplayUtil.mergeRecord(requestMocker); return; } @@ -124,7 +127,7 @@ public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStr return null; } - if (requestMocker.isMerge()) { + if (requestMocker.isNeedMerge()) { Mocker matchMocker = ReplayMatcher.match(requestMocker, mockStrategy); // compatible with old version(fixed case without merge) if (matchMocker != null) { @@ -190,7 +193,13 @@ public static boolean checkResponseMocker(Mocker responseMocker) { } final String body = targetResponse.getBody(); if (StringUtil.isEmpty(body)) { - LogManager.info(logTitle, "The body of targetResponse is empty"); + String exceedSizeLog = StringUtil.EMPTY; + if (MapUtils.getBoolean(targetResponse.getAttributes(), ArexConstants.EXCEED_MAX_SIZE_FLAG)) { + exceedSizeLog = StringUtil.format( + ", method:%s, because exceed memory max limit:%s, please check method return size, suggest replace it", + responseMocker.getOperationName(), AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB)); + } + LogManager.info(logTitle, "The body of targetResponse is empty" + exceedSizeLog); return false; } final String clazzType = targetResponse.getType(); diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java index 86dbafa29..2d9d089e6 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java @@ -33,8 +33,8 @@ private AgentSizeOf(SizeOfFilter fieldFilter) { * * @return the total size in bytes for these objects */ - public long deepSizeOf(Object... obj) { - return walker.walk(null, obj); + public long deepSizeOf(long sizeThreshold, Object... obj) { + return walker.walk(null, sizeThreshold, obj); } public static AgentSizeOf newInstance(final SizeOfFilter... filters) { @@ -86,11 +86,11 @@ public static String humanReadableUnits(long bytes, DecimalFormat df) { */ public boolean checkMemorySizeLimit(Object obj, long sizeLimit) { long start = System.currentTimeMillis(); - long memorySize = deepSizeOf(obj); + long memorySize = deepSizeOf(sizeLimit, obj); long cost = System.currentTimeMillis() - start; - if (cost > 50) { // longer cost mean larger memory + if (cost > 50) { // used statistical performance can be removed in the future LogManager.info("check.memory.size", - StringUtil.format("size: %s, cost: %s", + StringUtil.format("size: %s, cost: %s ms", humanReadableUnits(memorySize), String.valueOf(cost))); } return memorySize < sizeLimit; diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java index 851e44560..922a4d590 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java @@ -54,24 +54,15 @@ interface Visitor { long visit(Object object); } - /** - * Walk the graph and call into the "visitor" - * - * @param root the roots of the objects (a shared graph will only be visited once) - * @return the sum of all Visitor#visit returned values - */ - long walk(Object... root) { - return walk(null, root); - } - /** * Walk the graph and call into the "visitor" * * @param visitorListener A decorator for the Visitor - * @param root the roots of the objects (a shared graph will only be visited once) + * @param sizeThreshold if > 0 and exceed sizeThreshold will stop walk + * @param root the roots of the objects (a shared graph will only be visited once) * @return the sum of all Visitor#visit returned values */ - long walk(VisitorListener visitorListener, Object... root) { + long walk(VisitorListener visitorListener, long sizeThreshold, Object... root) { long result = 0; Deque toVisit = new ArrayDeque<>(); Set visited = newSetFromMap(new IdentityHashMap<>()); @@ -91,7 +82,9 @@ long walk(VisitorListener visitorListener, Object... root) { visitorListener.visited(ref, visitSize); } result += visitSize; - + if (sizeThreshold > 0 && result >= sizeThreshold) { + return result; + } } return result; diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java new file mode 100644 index 000000000..215da3c41 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; + +public class ThrowableFilter implements SizeOfFilter { + public static final ThrowableFilter INSTANCE = new ThrowableFilter(); + @Override + public Collection filterFields(Class klazz, Collection fields) { + return !Throwable.class.isAssignableFrom(klazz) ? fields : Collections.emptyList(); + } + + @Override + public boolean filterClass(Class klazz) { + return !Throwable.class.isAssignableFrom(klazz); + } +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java index 9289590f0..5284b0977 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java @@ -13,6 +13,7 @@ import io.arex.inst.runtime.listener.EventProcessorTest.TestGsonSerializer; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; import io.arex.inst.runtime.match.ReplayMatcher; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.service.DataCollector; @@ -65,11 +66,11 @@ void recordMocker() { // merge case Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(false); ArexMocker servletMocker = MockUtils.createServlet("mock"); - servletMocker.setMerge(true); + servletMocker.setNeedMerge(true); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); // remain case - servletMocker.setMerge(false); + servletMocker.setNeedMerge(false); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); } @@ -106,7 +107,7 @@ void replayMocker() { assertNotNull(MockUtils.replayBody(configFile)); // merge case - configFile.setMerge(true); + configFile.setNeedMerge(true); Mockito.when(ReplayMatcher.match(any(), any())).thenReturn(configFile); assertNull(MockUtils.replayBody(configFile)); } @@ -132,6 +133,11 @@ void checkResponseMocker() { // normal mocker dynamicClass.getTargetResponse().setType("java.lang.String"); assertTrue(MockUtils.checkResponseMocker(dynamicClass)); + + // test exceed size limit + dynamicClass.getTargetResponse().setBody(null); + dynamicClass.getTargetResponse().setAttribute(ArexConstants.EXCEED_MAX_SIZE_FLAG, true); + assertFalse(MockUtils.checkResponseMocker(dynamicClass)); } @Test diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java index ade753183..c90800e32 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java @@ -33,7 +33,7 @@ static void tearDown() { @Test void deepSizeOf() { - assertEquals(caller.deepSizeOf("mock"), 0); + assertEquals(caller.deepSizeOf(1, "mock"), 0); } @Test diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java index 006ae63f9..56706183b 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java @@ -32,7 +32,7 @@ static void tearDown() { @Test void walk() { - caller.walk(null); + caller.walk(null, 1); } @Test @@ -42,7 +42,7 @@ void testWalk() { Collection result = new ArrayList<>(); result.add(TestWalker.class.getDeclaredFields()[0]); Mockito.when(sizeOfFilter.filterFields(any(), any())).thenReturn(result); - caller.walk(visitorListener, new Object[]{new TestWalker[]{new TestWalker()}}); + caller.walk(visitorListener, 1, new Object[]{new TestWalker[]{new TestWalker()}}); } static class TestWalker { diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java new file mode 100644 index 000000000..6f4fdc578 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ThrowableFilterTest { + + @Test + void filterFields() { + assertNull(ThrowableFilter.INSTANCE.filterFields(String.class, null)); + } + + @Test + void filterClass() { + assertFalse(ThrowableFilter.INSTANCE.filterClass(Throwable.class)); + } +} \ No newline at end of file diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index b01f3becb..6814f9d93 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -29,6 +29,8 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; + +import io.arex.inst.runtime.util.sizeof.ThrowableFilter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -61,7 +63,8 @@ public class DynamicClassExtractor { private final Object[] args; private final String dynamicSignature; private final String requestType; - private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); + private boolean isExceedMaxSize; + private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(ThrowableFilter.INSTANCE); public DynamicClassExtractor(Method method, Object[] args, String keyExpression, Class actualType) { this.clazzName = method.getDeclaringClass().getName(); @@ -88,7 +91,7 @@ public DynamicClassExtractor(Method method, Object[] args) { public Object recordResponse(Object response) { if (IgnoreUtils.invalidOperation(dynamicSignature)) { LogManager.warn(NEED_RECORD_TITLE, StringUtil.format( - "do not record invalid operation: %s, can not serialize request or response or response too large", + "do not record invalid operation: %s, can not serialize request or response", dynamicSignature)); return response; } @@ -110,6 +113,7 @@ public Object recordResponse(Object response) { this.resultClazz = buildResultClazz(TypeUtil.getName(response)); Mocker mocker = makeMocker(); mocker.getTargetResponse().setBody(getSerializedResult()); + mocker.getTargetResponse().setAttribute(ArexConstants.EXCEED_MAX_SIZE_FLAG, this.isExceedMaxSize); MockUtils.recordMocker(mocker); cacheMethodSignature(); } @@ -275,7 +279,7 @@ private String getDynamicEntitySignature() { private Mocker makeMocker() { Mocker mocker = MockUtils.createDynamicClass(this.clazzName, this.methodName); - mocker.setMerge(true); + mocker.setNeedMerge(true); mocker.getTargetRequest().setBody(this.methodKey); mocker.getTargetResponse().setType(this.resultClazz); mocker.getTargetRequest().setType(this.requestType); @@ -318,17 +322,23 @@ Object restoreResponse(Object result) { } private boolean needRecord() { - /* - * Judge whether the hash value of the method signature has been recorded to avoid repeated recording. - * */ + // Judge whether the hash value of the method signature has been recorded to avoid repeated recording. ArexContext context = ContextManager.currentContext(); if (context != null) { this.methodSignatureKey = buildDuplicateMethodKey(); if (methodKey != null) { this.methodSignatureKeyHash = StringUtil.encodeAndHash(this.methodSignatureKey); } else { - // no argument method check repeat by className + methodName + result - this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(); + /* + * no argument method check repeat first by className + methodName + * avoid serialize big result(maybe last record result exceed size limit and already was set to empty) + * so first only check className + methodName + */ + this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(false); + if (!context.getMethodSignatureHashList().contains(this.methodSignatureKeyHash)) { + // if missed means no exceed size limit, check className + methodName + result + this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(true); + } } if (context.getMethodSignatureHashList().contains(this.methodSignatureKeyHash)) { if (Config.get().isEnableDebug()) { @@ -338,18 +348,6 @@ private boolean needRecord() { return false; } } - - if (this.result == null || this.result instanceof Throwable) { - return true; - } - - if (!agentSizeOf.checkMemorySizeLimit(this.result, ArexConstants.MEMORY_SIZE_1MB)) { - IgnoreUtils.addInvalidOperation(dynamicSignature); - LogManager.warn(NEED_RECORD_TITLE, StringUtil.format("dynamicClass:%s, exceed memory max limit:%s", - dynamicSignature, AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB))); - return false; - } - return true; } @@ -380,18 +378,20 @@ private String getResultKey() { private void cacheMethodSignature() { ArexContext context = ContextManager.currentContext(); if (context != null) { - if (this.methodKey != null && this.methodSignatureKey != null) { - context.getMethodSignatureHashList().add(this.methodSignatureKeyHash); - } else { - // no argument method check repeat by class+method+result - context.getMethodSignatureHashList().add(buildNoArgMethodSignatureHash()); - } + context.getMethodSignatureHashList().add(this.methodSignatureKeyHash); } } public String getSerializedResult() { - if (this.serializedResult == null) { - serializedResult = serialize(this.result); + if (this.serializedResult == null && !this.isExceedMaxSize) { + if (!agentSizeOf.checkMemorySizeLimit(this.result, ArexConstants.MEMORY_SIZE_1MB)) { + this.isExceedMaxSize = true; + LogManager.warn(ArexConstants.EXCEED_MAX_SIZE_TITLE, StringUtil.format("method:%s, exceed memory max limit:%s, " + + "record result will be null, please check method return size, suggest replace it", + this.dynamicSignature, AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB))); + return null; + } + this.serializedResult = serialize(this.result); } return this.serializedResult; } @@ -409,7 +409,10 @@ private String serialize(Object object) { } } - private int buildNoArgMethodSignatureHash() { - return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, getSerializedResult())); + private int buildNoArgMethodSignatureHash(boolean isNeedResult) { + if (isNeedResult) { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, getSerializedResult())); + } + return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, null)); } } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java index e262219c4..6cb445157 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java @@ -71,7 +71,7 @@ static void setUp() { ConfigBuilder.create("test").enableDebug(true).build(); agentSizeOf = Mockito.mock(AgentSizeOf.class); Mockito.mockStatic(AgentSizeOf.class); - Mockito.when(AgentSizeOf.newInstance()).thenReturn(agentSizeOf); + Mockito.when(AgentSizeOf.newInstance(any())).thenReturn(agentSizeOf); } @AfterAll @@ -431,6 +431,11 @@ void invalidOperation() throws Throwable { // invalid operation replay return ignore final MockResult replay = extractor.replay(); assertEquals(MockResult.IGNORE_MOCK_RESULT, replay); + + // test getSerializedResult serialize + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(true); + extractor = new DynamicClassExtractor(testWithArexMock, args); + assertNull(extractor.getSerializedResult()); } @Test diff --git a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java index ec88f31ff..a350439e9 100644 --- a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java +++ b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java @@ -75,7 +75,7 @@ public MockResult replay() { private Mocker makeMocker(Object response) { Mocker mocker = MockUtils.createRedis(this.command); - mocker.setMerge(true); + mocker.setNeedMerge(true); mocker.getTargetRequest().setBody(Serializer.serialize(new RedisMultiKey(key, field))); mocker.getTargetRequest().setAttribute("clusterName", this.clusterName); mocker.getTargetResponse().setBody(Serializer.serialize(response)); From d09da4678aa6bb8849621c387609a33d54773875 Mon Sep 17 00:00:00 2001 From: lucas-myx Date: Fri, 19 Jan 2024 14:03:14 +0800 Subject: [PATCH 5/5] feat: optimize name --- .../runtime/match/AbstractMatchStrategy.java | 12 ++++-------- .../runtime/match/AccurateMatchStrategy.java | 12 +++++------- .../inst/runtime/match/EigenMatchStrategy.java | 4 ---- .../inst/runtime/match/FuzzyMatchStrategy.java | 6 +----- .../runtime/match/AbstractMatchStrategyTest.java | 16 ++++++++++------ .../runtime/match/AccurateMatchStrategyTest.java | 9 ++------- .../runtime/match/EigenMatchStrategyTest.java | 5 ----- .../runtime/match/FuzzyMatchStrategyTest.java | 9 ++------- 8 files changed, 24 insertions(+), 49 deletions(-) diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java index 0560d1b01..f09038ab4 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java @@ -6,13 +6,10 @@ public abstract class AbstractMatchStrategy { static final String MATCH_TITLE = "replay.match.fail"; - static final int ACCURATE_MATCH_ORDER = 10; - static final int FUZZY_MATCH_ORDER = 20; - static final int EIGEN_MATCH_ORDER = 30; public void match(MatchStrategyContext context) { try { - if (check(context)) { + if (support(context)) { process(context); } } catch (Exception e) { @@ -20,17 +17,16 @@ public void match(MatchStrategyContext context) { } } - private boolean check(MatchStrategyContext context) { + private boolean support(MatchStrategyContext context) { if (context == null || context.getRequestMocker() == null || context.isInterrupt()) { return false; } - return valid(context); + return internalCheck(context); } - boolean valid(MatchStrategyContext context) { + boolean internalCheck(MatchStrategyContext context) { return true; } - abstract int order(); abstract void process(MatchStrategyContext context) throws Exception; Mocker buildMatchedMocker(Mocker requestMocker, MergeDTO mergeReplayDTO) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java index 030c1d479..2a86566bc 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java @@ -19,7 +19,7 @@ void process(MatchStrategyContext context) { Mocker requestMocker = context.getRequestMocker(); List mergeReplayList = context.getMergeReplayList(); int methodSignatureHash = MockUtils.methodSignatureHash(requestMocker); - List matchedList = new ArrayList<>(); + List matchedList = new ArrayList<>(mergeReplayList.size()); for (MergeDTO mergeDTO : mergeReplayList) { if (methodSignatureHash == mergeDTO.getMethodSignatureHash()) { matchedList.add(mergeDTO); @@ -41,24 +41,22 @@ void process(MatchStrategyContext context) { } // other modes can only be matched once, so interrupt and not continue next fuzzy match context.setInterrupt(true); + return; } // matched multiple result(like as redis: incr、decr) only retain matched item for next fuzzy match if (matchedCount > 1) { context.setMergeReplayList(matchedList); + return; } // if strict match mode and not matched, interrupt and not continue next fuzzy match - if (matchedCount == 0 && MockStrategyEnum.STRICT_MATCH == context.getMockStrategy()) { + if (MockStrategyEnum.STRICT_MATCH == context.getMockStrategy()) { context.setInterrupt(true); } } @Override - boolean valid(MatchStrategyContext context) { + boolean internalCheck(MatchStrategyContext context) { // if no request params, do next fuzzy match directly return StringUtil.isNotEmpty(context.getRequestMocker().getTargetRequest().getBody()); } - - int order() { - return ACCURATE_MATCH_ORDER; - } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java index 8bb846aa2..0ab3e7bf4 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java @@ -8,8 +8,4 @@ public class EigenMatchStrategy extends AbstractMatchStrategy{ void process(MatchStrategyContext context) { // to be implemented after database merge replay support } - - int order() { - return EIGEN_MATCH_ORDER; - } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java index 6f3b901a6..7395776fc 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java @@ -41,11 +41,7 @@ void process(MatchStrategyContext context) { } @Override - boolean valid(MatchStrategyContext context) { + boolean internalCheck(MatchStrategyContext context) { return CollectionUtil.isNotEmpty(context.getMergeReplayList()); } - - int order() { - return FUZZY_MATCH_ORDER; - } } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java index 7a041499f..29c758752 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java @@ -7,14 +7,21 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.ArrayList; + import static org.junit.jupiter.api.Assertions.*; class AbstractMatchStrategyTest { - static AbstractMatchStrategy target; + static ArexMocker mocker; + @BeforeAll static void setUp() { - target = new FuzzyMatchStrategy(); + target = new AccurateMatchStrategy(); + mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); } @AfterAll @@ -25,15 +32,12 @@ static void tearDown() { @Test void match() { assertDoesNotThrow(() -> target.match(null)); - MatchStrategyContext context = new MatchStrategyContext(new ArexMocker(), null, null); + MatchStrategyContext context = new MatchStrategyContext(mocker, new ArrayList<>(), null); assertDoesNotThrow(() -> target.match(context)); } @Test void buildMatchedMocker() { - ArexMocker mocker = new ArexMocker(); - mocker.setTargetResponse(new Mocker.Target()); - mocker.setTargetRequest(new Mocker.Target()); Mocker result = target.buildMatchedMocker(mocker, null); assertNull(result); result = target.buildMatchedMocker(mocker, new MergeDTO()); diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java index 9264826af..1143980a0 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java @@ -88,14 +88,9 @@ static Stream processCase() { } @Test - void valid() { + void internalCheck() { ArexMocker mocker = new ArexMocker(); mocker.setTargetRequest(new Mocker.Target()); - assertFalse(accurateMatchStrategy.valid(new MatchStrategyContext(mocker, null, null))); - } - - @Test - void order() { - assertEquals(10, accurateMatchStrategy.order()); + assertFalse(accurateMatchStrategy.internalCheck(new MatchStrategyContext(mocker, null, null))); } } \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java index 0b15ade6e..bf993f056 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java @@ -18,9 +18,4 @@ static void setUp() { static void tearDown() { eigenMatchStrategy = null; } - - @Test - void order() { - assertEquals(30, eigenMatchStrategy.order()); - } } \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java index 3d1ab3d74..8b10bb098 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java @@ -54,12 +54,7 @@ void process() { } @Test - void valid() { - assertFalse(fuzzyMatchStrategy.valid(new MatchStrategyContext(null, null, null))); - } - - @Test - void order() { - assertEquals(20, fuzzyMatchStrategy.order()); + void internalCheck() { + assertFalse(fuzzyMatchStrategy.internalCheck(new MatchStrategyContext(null, null, null))); } } \ No newline at end of file