From 0ddf443ad2bcaacd5d8e1692a590ed1f71e1814e Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:37:26 +0800 Subject: [PATCH 01/17] fix: change dynamic mock strategy (#280) --- .../io/arex/inst/dynamic/common/DynamicClassExtractor.java | 3 ++- .../arex/inst/dynamic/common/DynamicClassExtractorTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 76d92be97..2904588ec 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 @@ -3,6 +3,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; 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; @@ -128,7 +129,7 @@ public MockResult replay() { // If not in cache, get replay result from mock server if (replayResult == null) { - Mocker replayMocker = MockUtils.replayMocker(makeMocker()); + Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); if (MockUtils.checkResponseMocker(replayMocker)) { String typeName = replayMocker.getTargetResponse().getType(); replayResult = deserializeResult(replayMocker, typeName); 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 8dc828507..bb6019324 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 @@ -152,7 +152,7 @@ void replay(Runnable mocker, Object[] args, Predicate predicate) thr arexMocker2.setTargetResponse(new Target()); arexMocker2.getTargetResponse().setBody("mock Body"); arexMocker2.getTargetResponse().setType("mock Type"); - mockService.when(() -> MockUtils.replayMocker(any())).thenReturn(arexMocker2); + mockService.when(() -> MockUtils.replayMocker(any(), any())).thenReturn(arexMocker2); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenReturn("mock Serializer.serialize"); Mockito.when(Serializer.serializeWithException(anyString(), anyString())).thenReturn(""); @@ -384,7 +384,7 @@ public void testProtoBufResultReplay() { mockService.when(() -> MockUtils.createDynamicClass(any(), any())).thenReturn(arexMocker); mockService.when(() -> MockUtils.checkResponseMocker(any())).thenReturn(true); Mockito.when(ContextManager.currentContext()).thenReturn(ArexContext.of("")); - Mockito.when(MockUtils.replayMocker(any())).thenReturn(arexMocker2); + Mockito.when(MockUtils.replayMocker(any(), any())).thenReturn(arexMocker2); Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod( "testWithArexMock", String.class); From 6103955de44e98ac701db1dd7bd1c182c9807ef2 Mon Sep 17 00:00:00 2001 From: Mark Zhang Date: Wed, 6 Sep 2023 15:15:53 +0800 Subject: [PATCH 02/17] feat: add shutdown hook (#279) --- .../instrumentation/BaseAgentInstaller.java | 1 + .../foundation/model/AgentStatusEnum.java | 6 ++++- .../foundation/services/ConfigService.java | 26 +++++++++++++++++++ .../services/ConfigServiceTest.java | 19 ++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java index 53ccddc3b..642ed7ba9 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java @@ -49,6 +49,7 @@ public void install() { ClassLoader savedContextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(getClassLoader()); + Runtime.getRuntime().addShutdownHook(new Thread(ConfigService.INSTANCE::shutdown, "arex-agent-shutdown-hook")); // Timed load config for agent delay start and dynamic retransform long delayMinutes = ConfigService.INSTANCE.loadAgentConfig(agentArgs); if (delayMinutes > 0) { diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java index de1c1b4e0..0e15484a9 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/model/AgentStatusEnum.java @@ -20,7 +20,11 @@ public enum AgentStatusEnum { /** * AREX is up, but not recording maybe rate=0 or allowDayOfWeeks is not match */ - SLEEPING(4, "sleeping"); + SLEEPING(4, "sleeping"), + /** + * AREX is shutdown, need to restart + */ + SHUTDOWN(5, "shutdown"); private final int code; private final String value; 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 b810b1e0f..ae2fb508c 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 @@ -113,6 +113,10 @@ public ConfigQueryRequest buildConfigQueryRequest() { } AgentStatusEnum getAgentStatus() { + if (AgentStatusService.INSTANCE.isShutdown()) { + return AgentStatusEnum.SHUTDOWN; + } + if (firstLoad.compareAndSet(false, true)) { return AgentStatusEnum.START; } @@ -165,6 +169,18 @@ public void reportStatus() { AgentStatusService.INSTANCE.report(); } + public void shutdown() { + try { + if (AgentStatusService.INSTANCE.shutdown()) { + LOGGER.info("[AREX] Agent shutdown, stop working now."); + ConfigManager.INSTANCE.setConfigInvalid(); + reportStatus(); + } + } catch (Exception e) { + LOGGER.error("[AREX] Agent shutdown error, {}", e.getMessage()); + } + } + public boolean reloadConfig() { return reloadConfig.get(); } @@ -177,6 +193,16 @@ private static class AgentStatusService { private static final String AGENT_STATUS_URI = String.format("http://%s/api/config/agent/agentStatus", ConfigManager.INSTANCE.getStorageServiceHost()); + private final AtomicBoolean shutdown = new AtomicBoolean(false); + + private boolean shutdown() { + return shutdown.compareAndSet(false, true); + } + + public boolean isShutdown() { + return shutdown.get(); + } + public void report() { AgentStatusEnum agentStatus = ConfigService.INSTANCE.getAgentStatus(); System.setProperty("arex.agent.status", agentStatus.name()); diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java index e360ab8d2..29dec302c 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java @@ -189,4 +189,23 @@ void reportStatus() { assertFalse(ConfigService.INSTANCE.reloadConfig()); } } + + @Test + void shutdown() { + try (MockedStatic ahc = mockStatic(AsyncHttpClientUtil.class); + MockedStatic netUtils = mockStatic(NetUtils.class)){ + netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); + Map responseHeaders = new HashMap<>(); + responseHeaders.put("Last-Modified2", "Thu, 01 Jan 1970 00:00:00 GMT"); + ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), anyMap())).thenReturn( + CompletableFuture.completedFuture(new HttpClientResponse(200, responseHeaders, null))); + + ConfigService.INSTANCE.shutdown(); + + AgentStatusEnum actualResult = ConfigService.INSTANCE.getAgentStatus(); + assertEquals(AgentStatusEnum.SHUTDOWN, actualResult); + + ConfigService.INSTANCE.shutdown(); + } + } } From 89f96a7d4e834c91eae1333678e24e24b2ce9513 Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Wed, 6 Sep 2023 19:16:14 +0800 Subject: [PATCH 03/17] fix: sequence not incr (#282) --- .../src/main/java/io/arex/inst/runtime/context/ArexContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6903184f8..c0c2d4fa5 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 @@ -61,7 +61,7 @@ public boolean isReplay() { } public int calculateSequence() { - return sequence.get(); + return sequence.getAndIncrement(); } public Set getMethodSignatureHashList() { From d33428629b4599819d58a913a0fe551bbbfb79f2 Mon Sep 17 00:00:00 2001 From: Mark Zhang Date: Fri, 8 Sep 2023 11:14:14 +0800 Subject: [PATCH 04/17] feature: disable delayed start agent (#283) --- .../bootstrap/constants/ConfigConstants.java | 2 - .../instrumentation/BaseAgentInstaller.java | 32 ++++--- .../BaseAgentInstallerTest.java | 78 ++++++++++++++++-- .../arex/foundation/config/ConfigManager.java | 41 +-------- .../foundation/services/ConfigService.java | 10 +-- .../foundation/config/ConfigManagerTest.java | 58 +------------ .../services/ConfigServiceTest.java | 11 ++- docs/design/AREX-Agent-Stratup-Flowchart.png | Bin 0 -> 110806 bytes 8 files changed, 105 insertions(+), 127 deletions(-) create mode 100644 docs/design/AREX-Agent-Stratup-Flowchart.png diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java index bdab2c6d4..aa3f95eb7 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java @@ -25,8 +25,6 @@ public class ConfigConstants { public static final String DURING_WORK = "arex.during.work"; public static final String AGENT_VERSION = "arex.agent.version"; public static final String IP_VALIDATE = "arex.ip.validate"; - - public static final String ENABLE_REPORT_STATUS = "arex.enable.report.status"; public static final String CURRENT_RATE = "arex.current.rate"; public static final String DECELERATE_CODE = "arex.decelerate.code"; } diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java index 642ed7ba9..d3f50079c 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java @@ -50,19 +50,19 @@ public void install() { try { Thread.currentThread().setContextClassLoader(getClassLoader()); Runtime.getRuntime().addShutdownHook(new Thread(ConfigService.INSTANCE::shutdown, "arex-agent-shutdown-hook")); - // Timed load config for agent delay start and dynamic retransform + // Timed load config for dynamic retransform long delayMinutes = ConfigService.INSTANCE.loadAgentConfig(agentArgs); - if (delayMinutes > 0) { - TimerService.schedule(this::install, delayMinutes, TimeUnit.MINUTES); - timedReportStatus(); - } - if (!ConfigManager.INSTANCE.valid()) { + if (!allowStartAgent()) { ConfigService.INSTANCE.reportStatus(); if (!ConfigManager.FIRST_TRANSFORM.get()) { - LOGGER.warn("[AREX] Agent would not install due to {}.", ConfigManager.INSTANCE.getInvalidReason()); + LOGGER.warn("[AREX] Agent would not install due to {}.", getInvalidReason()); } return; } + if (delayMinutes > 0) { + TimerService.schedule(this::install, delayMinutes, TimeUnit.MINUTES); + timedReportStatus(); + } initDependentComponents(); transform(); ConfigService.INSTANCE.reportStatus(); @@ -71,13 +71,25 @@ public void install() { } } + boolean allowStartAgent() { + if (ConfigManager.INSTANCE.isLocalStorage()) { + return true; + } + return ConfigManager.INSTANCE.checkTargetAddress(); + } + + String getInvalidReason() { + if (!ConfigManager.INSTANCE.checkTargetAddress()) { + return "response [targetAddress] is not match"; + } + + return "invalid config"; + } + private void timedReportStatus() { if (reportStatusTask != null) { return; } - if (!ConfigManager.INSTANCE.isEnableReportStatus()) { - return; - } reportStatusTask = TimerService.scheduleAtFixedRate(() -> { try { ConfigService.INSTANCE.reportStatus(); diff --git a/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java b/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java index 8d21f2519..c3d79bfc2 100644 --- a/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java +++ b/arex-agent-core/src/test/java/io/arex/agent/instrumentation/BaseAgentInstallerTest.java @@ -1,17 +1,26 @@ package io.arex.agent.instrumentation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; import io.arex.agent.bootstrap.cache.AdviceInjectorCache; +import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.util.AdviceClassesCollector; +import io.arex.foundation.config.ConfigManager; +import io.arex.foundation.model.ConfigQueryResponse; +import io.arex.foundation.model.ConfigQueryResponse.ResponseBody; +import io.arex.foundation.model.ConfigQueryResponse.ServiceCollectConfig; import io.arex.foundation.model.HttpClientResponse; import io.arex.foundation.serializer.JacksonSerializer; import io.arex.foundation.util.NetUtils; import io.arex.foundation.util.httpclient.AsyncHttpClientUtil; +import java.lang.instrument.Instrumentation; import java.util.concurrent.CompletableFuture; -import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.ResettableClassFileTransformer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -21,19 +30,28 @@ import org.mockito.Mockito; class BaseAgentInstallerTest { - + static BaseAgentInstaller installer = null; @BeforeAll static void beforeAll() { mockStatic(AdviceInjectorCache.class); + + Instrumentation inst = Mockito.mock(Instrumentation.class); + installer = new BaseAgentInstaller(inst, null, null) { + @Override + protected ResettableClassFileTransformer transform() { + return null; + } + }; } @AfterAll static void afterAll() { + installer = null; Mockito.clearAllCaches(); } @Test - void install() { + void install() throws Throwable { Mockito.when(AdviceInjectorCache.contains(any())).thenReturn(true); try (MockedStatic ahc = mockStatic(AsyncHttpClientUtil.class); MockedStatic netUtils = mockStatic(NetUtils.class); @@ -42,18 +60,60 @@ void install() { Mockito.verify(mock, Mockito.times(1)).addClassToLoaderSearch(JacksonSerializer.class); })) { + // allow start agent = false netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), any())).thenReturn( CompletableFuture.completedFuture(HttpClientResponse.emptyResponse())); - BaseAgentInstaller installer = new BaseAgentInstaller(ByteBuddyAgent.install(), null, null) { - @Override - protected ResettableClassFileTransformer transform() { - return null; - } - }; installer.install(); + + // allow start agent = true + netUtils.when(NetUtils::getIpAddress).thenReturn("127.0.0.1"); + ConfigQueryResponse configQueryResponse = new ConfigQueryResponse(); + ServiceCollectConfig serviceCollectConfig = new ServiceCollectConfig(); + serviceCollectConfig.setAllowDayOfWeeks(127); + serviceCollectConfig.setAllowTimeOfDayFrom("00:00"); + serviceCollectConfig.setAllowTimeOfDayTo("23:59"); + serviceCollectConfig.setSampleRate(1); + + ResponseBody responseBody = new ResponseBody(); + responseBody.setTargetAddress("127.0.0.1"); + responseBody.setServiceCollectConfiguration(serviceCollectConfig); + configQueryResponse.setBody(responseBody); + CompletableFuture response = CompletableFuture.completedFuture(new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); + ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); + installer.install(); + } + } + + @Test + void allowStartAgent() { + ConfigManager.INSTANCE.setStorageServiceMode(ConfigConstants.STORAGE_MODE); + assertTrue(installer.allowStartAgent()); + + try (MockedStatic netUtils = mockStatic(NetUtils.class)) { + netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); + + ConfigManager.INSTANCE.setStorageServiceMode("not " + ConfigConstants.STORAGE_MODE); + ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); + + assertFalse(installer.allowStartAgent()); + } + } + + @Test + void getInvalidReason() { + try (MockedStatic netUtils = mockStatic(NetUtils.class)) { + netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); + + ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); + + assertEquals("response [targetAddress] is not match", installer.getInvalidReason()); + + // checkTargetAddress = true + ConfigManager.INSTANCE.setTargetAddress("172.0.0.3"); + assertEquals("invalid config", installer.getInvalidReason()); } } } 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 885142ff0..180412df0 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 @@ -40,7 +40,6 @@ public class ConfigManager { public static final AtomicBoolean FIRST_TRANSFORM = new AtomicBoolean(false); private static final int DEFAULT_RECORDING_RATE = 1; private boolean enableDebug; - private boolean enableReportStatus; private String agentVersion; private String serviceName; private String storageServiceHost; @@ -89,19 +88,6 @@ public void setEnableDebug(String enableDebug) { System.setProperty(ENABLE_DEBUG, enableDebug); } - public boolean isEnableReportStatus() { - return enableReportStatus; - } - - public void setEnableReportStatus(String enableReportStatus) { - if (StringUtil.isEmpty(enableReportStatus)) { - return; - } - - this.enableReportStatus = Boolean.parseBoolean(enableReportStatus); - System.setProperty(ENABLE_REPORT_STATUS, enableReportStatus); - } - public String getServiceName() { return serviceName; } @@ -230,7 +216,6 @@ private DynamicClassEntity createDynamicClass(DynamicClassConfiguration config, void init() { agentVersion = System.getProperty(AGENT_VERSION); setEnableDebug(System.getProperty(ENABLE_DEBUG)); - setEnableReportStatus(System.getProperty(ENABLE_REPORT_STATUS, Boolean.TRUE.toString())); setServiceName(StringUtil.strip(System.getProperty(SERVICE_NAME))); setStorageServiceHost(StringUtil.strip(System.getProperty(STORAGE_SERVICE_HOST))); configPath = StringUtil.strip(System.getProperty(CONFIG_PATH)); @@ -255,12 +240,11 @@ void readConfigFromFile(String configPath) { } Map configMap = parseConfigFile(configPath); - if (configMap.size() == 0) { + if (configMap.isEmpty()) { return; } setEnableDebug(configMap.get(ENABLE_DEBUG)); - setEnableReportStatus(System.getProperty(ENABLE_REPORT_STATUS)); setServiceName(configMap.get(SERVICE_NAME)); setStorageServiceHost(configMap.get(STORAGE_SERVICE_HOST)); setDynamicResultSizeLimit(configMap.get(DYNAMIC_RESULT_SIZE_LIMIT)); @@ -322,7 +306,7 @@ private void updateRuntimeConfig() { configMap.put(TIME_MACHINE, String.valueOf(startTimeMachine())); configMap.put(DISABLE_REPLAY, System.getProperty(DISABLE_REPLAY)); configMap.put(DISABLE_RECORD, System.getProperty(DISABLE_RECORD)); - configMap.put(DURING_WORK, Boolean.toString(nextWorkTime() <= 0)); + configMap.put(DURING_WORK, Boolean.toString(inWorkingTime())); configMap.put(AGENT_VERSION, agentVersion); configMap.put(IP_VALIDATE, Boolean.toString(checkTargetAddress())); configMap.put(STORAGE_SERVICE_MODE, storageServiceMode); @@ -351,13 +335,6 @@ private void publish(Config config) { } } - public boolean valid() { - if (isLocalStorage()) { - return true; - } - return checkTargetAddress() && inWorkingTime(); - } - public void setConfigInvalid() { setRecordRate(0); setAllowDayOfWeeks(0); @@ -522,7 +499,7 @@ public void setTargetAddress(String targetAddress) { this.targetAddress = targetAddress; } - private boolean checkTargetAddress() { + public boolean checkTargetAddress() { String localHost = NetUtils.getIpAddress(); // Compatible containers can't get IPAddress if (StringUtil.isEmpty(localHost)) { @@ -565,16 +542,4 @@ public String toString() { ", dynamicClassList='" + dynamicClassList + '\'' + '}'; } - - public String getInvalidReason() { - if (!checkTargetAddress()) { - return "response [targetAddress] is not match"; - } - - if (!inWorkingTime()) { - return "not in working time, allow day of weeks is " + allowDayOfWeeks + ", time is " + allowTimeOfDayFrom + "-" + allowTimeOfDayTo; - } - - return "invalid config"; - } } 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 ae2fb508c..c42c1d150 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 @@ -121,12 +121,12 @@ AgentStatusEnum getAgentStatus() { return AgentStatusEnum.START; } - if (ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.getRecordRate() > 0) { - return AgentStatusEnum.WORKING; - } - if (ConfigManager.FIRST_TRANSFORM.get()) { - return AgentStatusEnum.SLEEPING; + if (ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0) { + return AgentStatusEnum.WORKING; + } else { + return AgentStatusEnum.SLEEPING; + } } return AgentStatusEnum.UN_START; 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 22e22eaa9..81a5dd453 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 @@ -3,7 +3,6 @@ import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.foundation.model.ConfigQueryResponse; import io.arex.foundation.model.ConfigQueryResponse.DynamicClassConfiguration; -import io.arex.foundation.util.NetUtils; import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.model.DynamicClassEntity; import io.arex.inst.runtime.model.DynamicClassStatusEnum; @@ -17,9 +16,6 @@ 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 java.io.File; import java.net.URISyntaxException; @@ -28,20 +24,14 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Stream; -import org.mockito.MockedStatic; -import static io.arex.agent.bootstrap.constants.ConfigConstants.ENABLE_REPORT_STATUS; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.mockStatic; class ConfigManagerTest { static ConfigManager configManager = null; @BeforeAll - static void setUp() throws URISyntaxException { + static void setUp() { configManager = ConfigManager.INSTANCE; } @@ -59,7 +49,6 @@ void initFromSystemPropertyTest() { assertEquals("test-your-service", configManager.getServiceName()); assertEquals("test-storage-service.host", configManager.getStorageServiceHost()); - assertTrue(configManager.isEnableReportStatus()); } @Test @@ -94,33 +83,6 @@ void setExcludeServiceOperations() { assertFalse(configManager.getExcludeServiceOperations().isEmpty()); } - @ParameterizedTest - @MethodSource("validCase") - void valid(Runnable mocker, Predicate predicate) { - mocker.run(); - assertTrue(predicate.test(configManager.valid())); - } - - static Stream validCase() { - Runnable mocker1 = () -> { - configManager.setStorageServiceMode(ConfigConstants.STORAGE_MODE); - }; - Runnable mocker2 = () -> { - configManager.setStorageServiceMode("xxx"); - }; - Runnable mocker3 = () -> { - configManager.setTargetAddress("mock"); - }; - - Predicate predicate1 = result -> result; - Predicate predicate2 = result -> !result; - return Stream.of( - arguments(mocker1, predicate1), - arguments(mocker2, predicate2), - arguments(mocker3, predicate2) - ); - } - @Test void replaceConfigFromService() { ConfigQueryResponse.ResponseBody serviceConfig = new ConfigQueryResponse.ResponseBody(); @@ -264,24 +226,6 @@ void setDynamicClassList() { assertNotNull(ConfigManager.INSTANCE.toString()); } - @Test - void getInvalidReason() { - try (MockedStatic netUtils = mockStatic(NetUtils.class)) { - netUtils.when(NetUtils::getIpAddress).thenReturn("172.0.0.3"); - - // check target address is not match - ConfigManager.INSTANCE.setTargetAddress("172.0.0.1"); - String reason = ConfigManager.INSTANCE.getInvalidReason(); - assertEquals("response [targetAddress] is not match", reason); - - // check inWorkingTime is false - ConfigManager.INSTANCE.setTargetAddress("172.0.0.3"); - reason = ConfigManager.INSTANCE.getInvalidReason(); - assertTrue(reason.startsWith("not in working time")); - assertFalse(reason.contains(LocalDate.now().getDayOfWeek().name())); - } - } - @Test void setDynamicClassListWithKeyFormula() throws Exception { DynamicClassConfiguration dynamicClassConfiguration1 = new DynamicClassConfiguration(); diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java index 29dec302c..3a20c3722 100644 --- a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/services/ConfigServiceTest.java @@ -40,7 +40,7 @@ void tearDown() { } @Test - void loadAgentConfig() { + void loadAgentConfig() throws Throwable { long DELAY_MINUTES = 15L; // local long actualResult = ConfigService.INSTANCE.loadAgentConfig("arex.service.name=unit-test-service;arex.enable.debug=true;arex.storage.mode=local"); @@ -104,10 +104,11 @@ void loadAgentConfig() { CompletableFuture response = CompletableFuture.completedFuture(new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); assertEquals(DELAY_MINUTES, ConfigService.INSTANCE.loadAgentConfig(null)); - assertTrue(ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + assertTrue(ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + ConfigManager.FIRST_TRANSFORM.compareAndSet(false, true); assertEquals(AgentStatusEnum.WORKING, ConfigService.INSTANCE.getAgentStatus()); - ConfigManager.FIRST_TRANSFORM.compareAndSet(false, true); + // valid response, agentStatus=SLEEPING serviceCollectConfig.setAllowDayOfWeeks(0); response = CompletableFuture.completedFuture( @@ -123,10 +124,8 @@ void loadAgentConfig() { new HttpClientResponse(200, null, JacksonSerializer.INSTANCE.serialize(configQueryResponse))); ahc.when(() -> AsyncHttpClientUtil.postAsyncWithJson(anyString(), anyString(), eq(null))).thenReturn(response); assertEquals(DELAY_MINUTES, ConfigService.INSTANCE.loadAgentConfig(null)); - assertTrue(ConfigManager.INSTANCE.valid() && ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); + assertTrue(ConfigManager.INSTANCE.inWorkingTime() && ConfigManager.INSTANCE.getRecordRate() > 0); assertEquals(AgentStatusEnum.WORKING, ConfigService.INSTANCE.getAgentStatus()); - } catch (Throwable e) { - throw new RuntimeException(e); } } diff --git a/docs/design/AREX-Agent-Stratup-Flowchart.png b/docs/design/AREX-Agent-Stratup-Flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..19ef0099feab0e14e7ef90502230a09b8893f419 GIT binary patch literal 110806 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�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 Date: Mon, 11 Sep 2023 19:17:11 +0800 Subject: [PATCH 05/17] fix: get generic type name error (#286) --- .../main/java/io/arex/inst/runtime/util/TypeUtil.java | 11 ++++++++++- .../java/io/arex/inst/runtime/util/TypeUtilTest.java | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) 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 b918e4f5e..f8e1b2318 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 @@ -128,7 +128,16 @@ private static String genericTypeToString(Object result) { field.setAccessible(true); GENERIC_FIELD_CACHE.put(cacheKey, field); } - builder.append(filterRawGenericType(invokeGetFieldType(field, result))); + + String genericType = invokeGetFieldType(field, result); + // only collection field need to filter raw generic type + if (isCollection(field.getType().getName())) { + genericType = filterRawGenericType(genericType); + } + + if (StringUtil.isNotEmpty(genericType)) { + builder.append(genericType); + } if (i == typeParameters.length - 1) { return builder.toString(); } 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 d94f46199..127bd6e27 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 @@ -170,7 +170,7 @@ void testDoubleGenericType() { 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.lang.String", genericList); + assertEquals("io.arex.agent.bootstrap.internal.Pair-java.lang.Long,java.util.Arrays$ArrayList-java.lang.String", genericList); } @Test From 129c7174eed71de0b6de0d0b8a156ed4df4d901f Mon Sep 17 00:00:00 2001 From: Mark Zhang Date: Wed, 13 Sep 2023 20:39:46 +0800 Subject: [PATCH 06/17] feat: support apache httpclient request repeatable reading (#288) --- .../io/arex/agent/bootstrap/util/IOUtils.java | 30 ++++++++ .../agent/bootstrap/util/IOUtilsTest.java | 21 ++++++ arex-instrumentation-foundation/pom.xml | 2 +- .../java/io/arex/foundation/util/IOUtils.java | 35 --------- .../arex-httpclient-apache-v4/pom.xml | 2 +- .../apache/async/FutureCallbackWrapper.java | 31 +++----- ...nternalHttpAsyncClientInstrumentation.java | 16 ++-- .../common/ApacheHttpClientAdapter.java | 73 ++++++++++++------- .../common/CachedHttpEntityWrapper.java | 54 ++++++++++++++ .../async/FutureCallbackWrapperTest.java | 35 +++------ ...nalHttpAsyncClientInstrumentationTest.java | 35 ++++----- .../common/CachedHttpEntityWrapperTest.java | 64 ++++++++++++++++ 12 files changed, 265 insertions(+), 133 deletions(-) create mode 100644 arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java create mode 100644 arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java delete mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java create mode 100644 arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java new file mode 100644 index 000000000..657e16675 --- /dev/null +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/IOUtils.java @@ -0,0 +1,30 @@ +package io.arex.agent.bootstrap.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class IOUtils { + private IOUtils() {} + public static final int EOF = -1; + private static final int DEFAULT_BUFFER_SIZE = 4096; + + public static long copy(InputStream inputStream, OutputStream outputStream) throws IOException { + final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long total = 0; + int read; + while (EOF != (read = inputStream.read(buffer))) { + outputStream.write(buffer, 0, read); + total += read; + } + outputStream.flush(); + return total; + } + + public static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } +} diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java new file mode 100644 index 000000000..06310a10f --- /dev/null +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/IOUtilsTest.java @@ -0,0 +1,21 @@ +package io.arex.agent.bootstrap.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class IOUtilsTest { + @Test + void copyToByteArray() throws IOException { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 2000; i++) { + builder.append(i); + } + InputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes()); + byte[] bytes = IOUtils.copyToByteArray(inputStream); + assertEquals(builder.toString(), new String(bytes)); + } +} diff --git a/arex-instrumentation-foundation/pom.xml b/arex-instrumentation-foundation/pom.xml index 5ec3895d9..da4c02599 100644 --- a/arex-instrumentation-foundation/pom.xml +++ b/arex-instrumentation-foundation/pom.xml @@ -54,7 +54,7 @@ org.apache.httpcomponents httpasyncclient - 4.1.4 + 4.1.5 com.github.luben diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java deleted file mode 100644 index b1c0d3e52..000000000 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/util/IOUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.arex.foundation.util; - -import java.io.*; - -public class IOUtils { - - public static String toString(InputStream input) throws IOException { - BufferedReader br = null; - try { - StringBuilder sb = new StringBuilder(); - br = new BufferedReader(new InputStreamReader(input)); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - return sb.toString(); - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException e) { - // ignore - } - } - } - } - - public static void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } -} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml index 188f5e6bf..a325a8ef4 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/pom.xml @@ -20,7 +20,7 @@ org.apache.httpcomponents httpasyncclient - 4.1.4 + 4.1.5 provided diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java index 9d0fefc5f..ca6231d4b 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapper.java @@ -9,12 +9,8 @@ import org.apache.http.HttpResponse; import org.apache.http.concurrent.BasicFuture; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class FutureCallbackWrapper implements FutureCallback { - private static final Logger LOGGER = LoggerFactory.getLogger(FutureCallbackWrapper.class); private final FutureCallback delegate; private final TraceTransmitter traceTransmitter; @@ -79,27 +75,24 @@ public Future replay(MockResult mockResult) { return basicFuture; } - public static FutureCallbackWrapper get(HttpAsyncRequestProducer requestProducer, FutureCallback delegate) { + public static FutureCallback wrap(HttpRequest httpRequest, FutureCallback delegate) { if (delegate instanceof FutureCallbackWrapper) { - return ((FutureCallbackWrapper) delegate); + return delegate; } - ApacheHttpClientAdapter adapter; - HttpClientExtractor extractor; - - try { - adapter = new ApacheHttpClientAdapter(requestProducer.generateRequest()); - if (adapter.skipRemoteStorageRequest()) { - return null; - } - extractor = new HttpClientExtractor<>(adapter); - } catch (Throwable ex) { - LOGGER.warn("create async wrapper error:{}, record or replay was skipped", ex.getMessage(), ex); + ApacheHttpClientAdapter adapter = new ApacheHttpClientAdapter(httpRequest); + if (adapter.skipRemoteStorageRequest()) { return null; } - return new FutureCallbackWrapper<>(extractor, delegate); + return new FutureCallbackWrapper<>(new HttpClientExtractor<>(adapter), delegate); } + /** + * Wrap the delegate with FutureCallbackWrapper for arex trace propagation + */ public static FutureCallback wrap(FutureCallback delegate) { + if (delegate instanceof FutureCallbackWrapper) { + return delegate; + } return new FutureCallbackWrapper<>(delegate); } -} \ No newline at end of file +} 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 f58b8c143..aef6ea403 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 @@ -9,8 +9,8 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import org.apache.http.HttpRequest; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; import java.util.List; import java.util.concurrent.Future; @@ -25,7 +25,7 @@ public class InternalHttpAsyncClientInstrumentation extends TypeInstrumentation @Override public ElementMatcher typeMatcher() { - return named("org.apache.http.impl.nio.client.InternalHttpAsyncClient"); + return named("org.apache.http.impl.nio.client.CloseableHttpAsyncClient"); } @Override @@ -33,8 +33,8 @@ public List methodAdvices() { return singletonList(new MethodInstrumentation( isMethod().and(named("execute")) .and(takesArguments(4)) - .and(takesArgument(0, named("org.apache.http.nio.protocol.HttpAsyncRequestProducer"))) - .and(takesArgument(1, named("org.apache.http.nio.protocol.HttpAsyncResponseConsumer"))) + .and(takesArgument(0, named("org.apache.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.http.HttpRequest"))) .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))) .and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))), this.getClass().getName() + "$ExecuteAdvice")); @@ -43,22 +43,22 @@ public List methodAdvices() { @SuppressWarnings("unused") public static class ExecuteAdvice { @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class) - public static boolean onEnter(@Advice.Argument(0) HttpAsyncRequestProducer producer, + 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(producer.generateRequest())) { + if (ApacheHttpClientHelper.ignoreRequest(httpRequest)) { callback = FutureCallbackWrapper.wrap(callback); return false; } - } catch (Throwable ignored) { + } catch (Exception ignored) { callback = FutureCallbackWrapper.wrap(callback); return false; } if (ContextManager.needRecordOrReplay() && RepeatedCollectManager.validate()) { // recording works in callback wrapper - FutureCallbackWrapper callbackWrapper = FutureCallbackWrapper.get(producer, callback); + FutureCallback callbackWrapper = FutureCallbackWrapper.wrap(httpRequest, callback); if (callbackWrapper != null) { callback = callbackWrapper; if (ContextManager.needReplay()) { 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 c87897e0c..b7a28bb1e 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 @@ -1,11 +1,11 @@ package io.arex.inst.httpclient.apache.common; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; 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.io.ByteArrayOutputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -15,8 +15,6 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.HttpEntityWrapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.net.URI; @@ -25,11 +23,11 @@ import java.util.Locale; public class ApacheHttpClientAdapter implements HttpClientAdapter { - private static final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientAdapter.class); private final HttpUriRequest httpRequest; public ApacheHttpClientAdapter(HttpRequest httpRequest) { this.httpRequest = (HttpUriRequest) httpRequest; + wrapHttpEntity(httpRequest); } @Override @@ -39,23 +37,23 @@ public String getMethod() { @Override public byte[] getRequestBytes() { - if (!(this.httpRequest instanceof HttpEntityEnclosingRequest)) { + HttpEntityEnclosingRequest enclosingRequest = enclosingRequest(httpRequest); + if (enclosingRequest == null) { return ZERO_BYTE; } - HttpEntityEnclosingRequest enclosingRequestBase = (HttpEntityEnclosingRequest) this.httpRequest; - HttpEntity entity = enclosingRequestBase.getEntity(); + HttpEntity entity = enclosingRequest.getEntity(); if (entity == null) { return ZERO_BYTE; } - byte[] content; - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ - entity.writeTo(byteArrayOutputStream); - content = byteArrayOutputStream.toByteArray(); - } catch (Throwable e) { - LogManager.warn("getRequestBytes", e); - content = ZERO_BYTE; + if (entity instanceof CachedHttpEntityWrapper) { + return ((CachedHttpEntityWrapper) entity).getCachedBody(); + } + try { + return IOUtils.copyToByteArray(entity.getContent()); + } catch (Exception e) { + LogManager.warn("copyToByteArray", "getRequestBytes error, uri: " + getUri(), e); + return ZERO_BYTE; } - return content; } @Override @@ -82,22 +80,21 @@ public HttpResponseWrapper wrap(HttpResponse response) { return null; } - byte[] content; - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ - httpEntity.writeTo(byteArrayOutputStream); - content = byteArrayOutputStream.toByteArray(); - } catch (Throwable e) { - LogManager.warn("readContent", e); + byte[] responseBody; + try { + responseBody = IOUtils.copyToByteArray(httpEntity.getContent()); + } catch (Exception e) { + LogManager.warn("copyToByteArray", "getResponseBody error, uri: " + getUri(), e); return null; } if (httpEntity instanceof BasicHttpEntity) { - ((BasicHttpEntity) httpEntity).setContent(new ByteArrayInputStream(content)); + ((BasicHttpEntity) httpEntity).setContent(new ByteArrayInputStream(responseBody)); response.setEntity(httpEntity); } else if (httpEntity instanceof HttpEntityWrapper) { // Output response normally now, later need to check revert DecompressingEntity BasicHttpEntity entity = ApacheHttpClientHelper.createHttpEntity(response); - entity.setContent(new ByteArrayInputStream(content)); + entity.setContent(new ByteArrayInputStream(responseBody)); response.setEntity(entity); } @@ -110,9 +107,9 @@ public HttpResponseWrapper wrap(HttpResponse response) { headers.add(new HttpResponseWrapper.StringTuple(header.getName(), header.getValue())); } - return new HttpResponseWrapper(response.getStatusLine().toString(), content, - new HttpResponseWrapper.StringTuple(locale.getLanguage(), locale.getCountry()), - headers); + return new HttpResponseWrapper(response.getStatusLine().toString(), responseBody, + new HttpResponseWrapper.StringTuple(locale.getLanguage(), locale.getCountry()), + headers); } @Override @@ -149,4 +146,26 @@ private static boolean ignoreUserAgent(String userAgent) { return userAgent != null && userAgent.contains("arex"); } -} \ No newline at end of file + private void wrapHttpEntity(HttpRequest httpRequest) { + HttpEntityEnclosingRequest enclosingRequest = enclosingRequest(httpRequest); + if (enclosingRequest == null) { + return; + } + HttpEntity entity = enclosingRequest.getEntity(); + if (entity == null || entity.isRepeatable()) { + return; + } + try { + enclosingRequest.setEntity(new CachedHttpEntityWrapper(entity)); + } catch (Exception ignore) { + // ignore exception + } + } + + private HttpEntityEnclosingRequest enclosingRequest(HttpRequest httpRequest) { + if (httpRequest instanceof HttpEntityEnclosingRequest) { + return (HttpEntityEnclosingRequest) httpRequest; + } + return null; + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java new file mode 100644 index 000000000..00be09451 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/main/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapper.java @@ -0,0 +1,54 @@ +package io.arex.inst.httpclient.apache.common; + +import io.arex.agent.bootstrap.util.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.http.HttpEntity; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.util.Args; + +public class CachedHttpEntityWrapper extends AbstractHttpEntity { + + private final byte[] cachedBody; + private final InputStream content; + private final HttpEntity entity; + + public CachedHttpEntityWrapper(HttpEntity entity) throws IOException { + this.entity = entity; + this.cachedBody = IOUtils.copyToByteArray(entity.getContent()); + this.content = new ByteArrayInputStream(cachedBody); + + } + + @Override + public boolean isRepeatable() { + return this.entity.isRepeatable(); + } + + @Override + public long getContentLength() { + return this.entity.getContentLength(); + } + + @Override + public InputStream getContent() throws UnsupportedOperationException { + return new ByteArrayInputStream(this.cachedBody); + } + + @Override + public void writeTo(OutputStream outStream) throws IOException { + Args.notNull(outStream, "Output stream"); + IOUtils.copy(this.content, outStream); + } + + @Override + public boolean isStreaming() { + return this.entity.isStreaming(); + } + + public byte[] getCachedBody() { + return this.cachedBody; + } +} diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java index b40f10ea0..e3af9de8c 100644 --- a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/async/FutureCallbackWrapperTest.java @@ -14,18 +14,14 @@ import io.arex.inst.httpclient.common.HttpClientExtractor; import io.arex.inst.runtime.context.ContextManager; -import java.io.IOException; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Stream; -import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.concurrent.FutureCallback; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -95,32 +91,25 @@ void testReplayWithMockResult() { } @ParameterizedTest - @MethodSource("getCase") - void get(FutureCallback delegate, boolean skip, Predicate predicate, HttpAsyncRequestProducer requestProducer) { + @MethodSource("wrapTestCase") + void wrap(FutureCallback delegate, boolean skip, Predicate> predicate, HttpRequest httpRequest) { try (MockedConstruction mocked = Mockito.mockConstruction(ApacheHttpClientAdapter.class, (mock, context) -> { Mockito.when(mock.skipRemoteStorageRequest()).thenReturn(skip); })) { - FutureCallbackWrapper result = target.get(requestProducer, delegate); + FutureCallback result = target.wrap(httpRequest, delegate); assertTrue(predicate.test(result)); - } catch (Exception e) { - e.printStackTrace(); + } catch (Exception ignore) { + // ignore exception } } - static Stream getCase() throws Exception { - FutureCallback delegate1 = Mockito.mock(FutureCallbackWrapper.class); - HttpAsyncRequestProducer requestProducer1 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(requestProducer1.generateRequest()).thenThrow(new IOException()); - - HttpAsyncRequestProducer requestProducer2 = Mockito.mock(HttpAsyncRequestProducer.class); - - Predicate predicate1 = Objects::nonNull; - Predicate predicate2 = Objects::isNull; + static Stream wrapTestCase() throws Exception { + Predicate> nonNull = Objects::nonNull; + Predicate> isNull = Objects::isNull; return Stream.of( - arguments(delegate1, true, predicate1, requestProducer2), - arguments(delegate, true, predicate2, requestProducer2), - arguments(delegate, false, predicate1, requestProducer2), - arguments(delegate, false, predicate2, requestProducer1) + arguments(new FutureCallbackWrapper<>(null), true, nonNull, null), + arguments(delegate, true, isNull, null), + arguments(delegate, false, nonNull, null) ); } @@ -129,4 +118,4 @@ void wrap() { FutureCallback delegateCallback = Mockito.mock(FutureCallback.class); assertInstanceOf(FutureCallbackWrapper.class, FutureCallbackWrapper.wrap(delegateCallback)); } -} \ No newline at end of file +} 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 43683c5f8..a1ccef0c7 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 @@ -1,7 +1,6 @@ package io.arex.inst.httpclient.apache.async; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,11 +11,9 @@ import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.context.RepeatedCollectManager; import io.arex.inst.runtime.util.IgnoreUtils; -import java.io.IOException; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.nio.protocol.HttpAsyncRequestProducer; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.client.methods.HttpUriRequest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -51,25 +48,25 @@ void methodAdvices() { } @Test - void onEnter() throws HttpException, IOException { - HttpAsyncRequestProducer producer1 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(producer1.generateRequest()).thenThrow(new RuntimeException("mock exception")); - boolean actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer1, null, null); - assertFalse(actualResult); - - HttpAsyncRequestProducer producer2 = Mockito.mock(HttpAsyncRequestProducer.class); - Mockito.when(producer2.generateRequest()).thenReturn(Mockito.mock(HttpRequest.class)); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + 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)) { - Mockito.when(producer2.generateRequest()).thenReturn(new HttpPost("localhost")); + 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); + assertFalse(actualResult); + ignoreUtils.when(() -> IgnoreUtils.excludeOperation(any())).thenReturn(false); contextManager.when(ContextManager::needRecordOrReplay).thenReturn(false); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertFalse(actualResult); repeatedCollectManager.when(RepeatedCollectManager::validate).thenReturn(true); @@ -77,10 +74,10 @@ void onEnter() throws HttpException, IOException { contextManager.when(ContextManager::needReplay).thenReturn(true); FutureCallbackWrapper wrapper = Mockito.mock(FutureCallbackWrapper.class); - Mockito.when(FutureCallbackWrapper.get(any(), any())).thenReturn(wrapper); + Mockito.when(FutureCallbackWrapper.wrap(any(), any())).thenReturn(wrapper); Mockito.when(wrapper.replay()).thenReturn(MockResult.success("mock")); - actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(producer2, null, null); + actualResult = InternalHttpAsyncClientInstrumentation.ExecuteAdvice.onEnter(request2, null, null); assertTrue(actualResult); } } diff --git a/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java new file mode 100644 index 000000000..755776676 --- /dev/null +++ b/arex-instrumentation/httpclient/arex-httpclient-apache-v4/src/test/java/io/arex/inst/httpclient/apache/common/CachedHttpEntityWrapperTest.java @@ -0,0 +1,64 @@ +package io.arex.inst.httpclient.apache.common; + +import static org.junit.jupiter.api.Assertions.*; + +import io.arex.agent.bootstrap.util.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.http.HttpEntity; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.InputStreamEntity; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CachedHttpEntityWrapperTest { + static CachedHttpEntityWrapper wrapper; + + @BeforeAll + static void setUp() throws IOException { + HttpEntity httpEntity = new InputStreamEntity(new ByteArrayInputStream("mock".getBytes())); + wrapper = new CachedHttpEntityWrapper(httpEntity); + } + + @AfterAll + static void tearDown() { + wrapper = null; + } + + @Test + void isRepeatable() { + assertFalse(wrapper.isRepeatable()); + } + + @Test + void getContentLength() { + assertEquals(-1, wrapper.getContentLength()); + } + + @Test + void getContent() throws IOException { + byte[] content = IOUtils.copyToByteArray(wrapper.getContent()); + assertEquals("mock", new String(content)); + } + + @Test + void writeTo() { + ByteArrayOutputStream baous = new ByteArrayOutputStream(); + assertDoesNotThrow(() -> wrapper.writeTo(baous)); + assertEquals("mock", baous.toString()); + } + + @Test + void isStreaming() { + assertTrue(wrapper.isStreaming()); + } + + @Test + void getCachedBody() { + assertEquals("mock", new String(wrapper.getCachedBody())); + } +} From 02d461003d5bd780f53a50e510fdddcb5752242b Mon Sep 17 00:00:00 2001 From: Mark Zhang Date: Thu, 14 Sep 2023 17:15:30 +0800 Subject: [PATCH 07/17] fix: get servlet request bytes (#292) --- .../adapter/impl/ServletAdapterImplV3.java | 16 ++++++- .../adapter/impl/ServletAdapterImplV5.java | 16 ++++++- .../impl/ServletAdapterImplV3Test.java | 47 +++++++++++++++++- .../impl/ServletAdapterImplV5Test.java | 48 ++++++++++++++++++- 4 files changed, 121 insertions(+), 6 deletions(-) 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 6ee07ffbf..fe7f40d64 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 @@ -1,5 +1,6 @@ package io.arex.inst.httpservlet.adapter.impl; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV3; @@ -165,7 +166,20 @@ public Collection getResponseHeaderNames(HttpServletResponse httpServlet @Override public byte[] getRequestBytes(HttpServletRequest httpServletRequest) { - return ((CachedBodyRequestWrapperV3) httpServletRequest).getContentAsByteArray(); + CachedBodyRequestWrapperV3 requestWrapper = (CachedBodyRequestWrapperV3) httpServletRequest; + byte[] content = requestWrapper.getContentAsByteArray(); + if (content.length > 0) { + return content; + } + // read request body to cache + if (httpServletRequest.getContentLength() > 0) { + try { + return IOUtils.copyToByteArray(requestWrapper.getInputStream()); + } catch (Exception ignore) { + // ignore exception + } + } + return content; } @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 64147b8ba..cac4f4ee4 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 @@ -1,5 +1,6 @@ package io.arex.inst.httpservlet.adapter.impl; +import io.arex.agent.bootstrap.util.IOUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.httpservlet.adapter.ServletAdapter; import io.arex.inst.httpservlet.listener.ServletAsyncListenerV5; @@ -165,7 +166,20 @@ public Collection getResponseHeaderNames(HttpServletResponse httpServlet @Override public byte[] getRequestBytes(HttpServletRequest httpServletRequest) { - return ((CachedBodyRequestWrapperV5) httpServletRequest).getContentAsByteArray(); + CachedBodyRequestWrapperV5 requestWrapper = (CachedBodyRequestWrapperV5) httpServletRequest; + byte[] content = requestWrapper.getContentAsByteArray(); + if (content.length > 0) { + return content; + } + // read request body to cache + if (httpServletRequest.getContentLength() > 0) { + try { + return IOUtils.copyToByteArray(requestWrapper.getInputStream()); + } catch (Exception ignore) { + // ignore exception + } + } + return content; } @Override 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 237cfa9ed..280fd35ac 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 @@ -7,10 +7,14 @@ import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV3; import io.arex.inst.httpservlet.wrapper.CachedBodyResponseWrapperV3; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; import javax.servlet.AsyncContext; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; @@ -189,9 +193,48 @@ void getResponseHeaderNames() { assertEquals("mock-header-name", instance.getResponseHeaderNames(mockResponse).stream().findFirst().get()); } + static class MockServletInputStream extends ServletInputStream { + private final InputStream delegate; + public MockServletInputStream(byte[] body) { + this.delegate = new ByteArrayInputStream(body); + } + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return this.delegate.read(); + } + } + @Test - void getRequestBytes() { - assertEquals(0, instance.getRequestBytes(instance.wrapRequest(mockRequest)).length); + void getRequestBytes() throws IOException { + HttpServletRequest requestWrapper = instance.wrapRequest(mockRequest); + // content empty + assertArrayEquals(new byte[0], instance.getRequestBytes(requestWrapper)); + + + byte[] body = "mock".getBytes(); + when(mockRequest.getInputStream()).thenReturn(new MockServletInputStream(body)); + when(mockRequest.getContentLength()).thenReturn(body.length); + + // read request body to cache + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); + + // getContentAsByteArray.length > 0 + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); } @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 874c42e1e..7ff256027 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 @@ -7,6 +7,11 @@ import io.arex.inst.httpservlet.wrapper.CachedBodyRequestWrapperV5; import io.arex.inst.httpservlet.wrapper.CachedBodyResponseWrapperV5; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; import jakarta.servlet.AsyncContext; @@ -190,9 +195,48 @@ void getResponseHeaderNames() { assertEquals("mock-header-name", instance.getResponseHeaderNames(mockResponse).stream().findFirst().get()); } + static class MockServletInputStream extends ServletInputStream { + private final InputStream delegate; + public MockServletInputStream(byte[] body) { + this.delegate = new ByteArrayInputStream(body); + } + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return this.delegate.read(); + } + } + @Test - void getRequestBytes() { - assertEquals(0, instance.getRequestBytes(instance.wrapRequest(mockRequest)).length); + void getRequestBytes() throws IOException { + HttpServletRequest requestWrapper = instance.wrapRequest(mockRequest); + // content empty + assertArrayEquals(new byte[0], instance.getRequestBytes(requestWrapper)); + + + byte[] body = "mock".getBytes(); + when(mockRequest.getInputStream()).thenReturn(new MockServletInputStream(body)); + when(mockRequest.getContentLength()).thenReturn(body.length); + + // read request body to cache + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); + + // getContentAsByteArray.length > 0 + assertArrayEquals(body, instance.getRequestBytes(requestWrapper)); } @Test From 8017b4416e159d19d22240cc910982d1a24f600a Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:57:19 +0800 Subject: [PATCH 08/17] feat: support serializer config (#289) --- .../bootstrap/constants/ConfigConstants.java | 4 ++- .../inst/runtime/listener/EventProcessor.java | 1 + .../inst/runtime/serializer/Serializer.java | 31 ++++++++++++++++++- .../runtime/listener/EventProcessorTest.java | 2 +- .../runtime/serializer/SerializerTest.java | 30 +++++++++++++++++- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java index aa3f95eb7..944dabdf8 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java @@ -1,7 +1,8 @@ package io.arex.agent.bootstrap.constants; public class ConfigConstants { - + private ConfigConstants() { + } public static final String ENABLE_DEBUG = "arex.enable.debug"; public static final String SERVICE_NAME = "arex.service.name"; public static final String STORAGE_SERVICE_HOST = "arex.storage.service.host"; @@ -27,4 +28,5 @@ public class ConfigConstants { public static final String IP_VALIDATE = "arex.ip.validate"; public static final String CURRENT_RATE = "arex.current.rate"; public static final String DECELERATE_CODE = "arex.decelerate.code"; + public static final String SERIALIZER_CONFIG = "arex.serializer.config"; } 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 12504f3a1..9e442128f 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 @@ -57,6 +57,7 @@ private static void addEnterLog() { * user loader to load serializer, ex: ParallelWebappClassLoader */ private static void initSerializer() { + Serializer.initSerializerConfigMap(); final List serializableList = ServiceLoader.load(StringSerializable.class, Thread.currentThread().getContextClassLoader()); Serializer.builder(serializableList).build(); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java index 9101d4f59..2217c69fb 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java @@ -1,9 +1,11 @@ package io.arex.inst.runtime.serializer; +import io.arex.agent.bootstrap.constants.ConfigConstants; import io.arex.agent.bootstrap.util.ArrayUtils; import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.agent.bootstrap.util.ReflectUtil; 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.util.TypeUtil; import org.slf4j.Logger; @@ -11,9 +13,11 @@ import java.lang.reflect.Type; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class Serializer { private static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class); + private static final Map SERIALIZER_CONFIG_MAP = new ConcurrentHashMap<>(); private static Serializer INSTANCE; @@ -33,6 +37,28 @@ public static Builder builder(List serializableList) { private final StringSerializable defaultSerializer; private final Map serializers; + /** + * ex: DubboProvider:jackson,DubboConsumer:gson + */ + public static void initSerializerConfigMap() { + try { + String serializerConfig = Config.get().getString(ConfigConstants.SERIALIZER_CONFIG); + if (StringUtil.isEmpty(serializerConfig)) { + return; + } + final String[] configArray = StringUtil.split(serializerConfig, ','); + for (String config : configArray) { + final String[] configElement = StringUtil.split(config, ':'); + if (configElement.length != 2) { + continue; + } + SERIALIZER_CONFIG_MAP.put(configElement[0], configElement[1]); + } + } catch (Exception ex) { + LogManager.warn("serializer.config", StringUtil.format("can not init serializer config, cause: %s", ex.toString())); + } + } + /** * serialize throw throwable */ @@ -68,6 +94,10 @@ private static String serializeNestedCollection(String serializer, Collection serializableList) { for (StringSerializable serializable : serializableList) { if (serializable.isDefault()) { this.defaultSerializer = serializable; - continue; } this.serializers.put(serializable.name(), serializable); } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java index 11282a441..46de4e0df 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/listener/EventProcessorTest.java @@ -88,7 +88,7 @@ void testInit() { // serializer Assertions.assertNotNull(Serializer.getINSTANCE()); Assertions.assertEquals("gson", Serializer.getINSTANCE().getSerializer().name()); - Assertions.assertEquals(1, Serializer.getINSTANCE().getSerializers().size()); + Assertions.assertEquals(2, Serializer.getINSTANCE().getSerializers().size()); // atomic load, only load once Mockito.when(ServiceLoader.load(StringSerializable.class, Thread.currentThread() diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java index db2a4d91f..a6c10d539 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java @@ -2,9 +2,13 @@ import static org.junit.jupiter.api.Assertions.*; +import io.arex.agent.bootstrap.constants.ConfigConstants; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.config.ConfigBuilder; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; import io.arex.inst.runtime.listener.EventProcessorTest.TestGsonSerializer; import io.arex.inst.runtime.util.TypeUtil; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; @@ -33,7 +37,7 @@ static void tearDown() { @Test void builder() { assertNotNull(Serializer.getINSTANCE()); - assertEquals(1, Serializer.getINSTANCE().getSerializers().size()); + assertEquals(2, Serializer.getINSTANCE().getSerializers().size()); } @Test @@ -121,4 +125,28 @@ void nullObjectOrType() { // serialize Throwable Assertions.assertDoesNotThrow(() -> Serializer.serialize(new Throwable())); } + + @Test + void testInitSerializerConfigMap() throws Exception { + // null config + final Field instance = Config.class.getDeclaredField("INSTANCE"); + instance.setAccessible(true); + instance.set(null, null); + Assertions.assertDoesNotThrow(Serializer::initSerializerConfigMap); + + // empty serializer config + ConfigBuilder builder = new ConfigBuilder("testSerializer"); + builder.build(); + Serializer.initSerializerConfigMap(); + assertNull(Serializer.getSerializerFromType("dubboRequest")); + + // serializer config + builder = new ConfigBuilder("testSerializer"); + builder.addProperty(ConfigConstants.SERIALIZER_CONFIG, "soa:gson,dubboRequest:jackson,httpRequest"); + builder.build(); + Serializer.initSerializerConfigMap(); + assertEquals("jackson", Serializer.getSerializerFromType("dubboRequest")); + assertEquals("gson", Serializer.getSerializerFromType("soa")); + assertNull(Serializer.getSerializerFromType("httpRequest")); + } } \ No newline at end of file From 6d50137dbb61c542f33ff901d856a9579eb8c0f1 Mon Sep 17 00:00:00 2001 From: Mo <63041625+mengqcc@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:04:05 +0800 Subject: [PATCH 09/17] feat: support for adding extended transformers (#291) --- .../instrumentation/BaseAgentInstaller.java | 12 ++++++++ .../inst/extension/ExtensionTransformer.java | 17 +++++++++++ .../inst/runtime/context/ContextManager.java | 28 +++++++++++++++++-- .../context/LatencyContextHashMap.java | 4 +-- .../runtime/listener/ContextListener.java | 8 ++++++ .../arex/foundation/config/ConfigManager.java | 24 ++++++++++++++-- 6 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java index d3f50079c..6e2abe6d4 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java @@ -13,6 +13,7 @@ import io.arex.foundation.services.TimerService; import io.arex.foundation.util.NetUtils; import io.arex.foundation.util.NumberTypeAdaptor; +import io.arex.inst.extension.ExtensionTransformer; import io.arex.inst.runtime.context.RecordLimiter; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataCollector; @@ -65,12 +66,23 @@ public void install() { } initDependentComponents(); transform(); + + for (ExtensionTransformer transformer : loadTransformers()) { + if (transformer.validate()) { + instrumentation.addTransformer(transformer, true); + } + } + ConfigService.INSTANCE.reportStatus(); } finally { Thread.currentThread().setContextClassLoader(savedContextClassLoader); } } + private List loadTransformers() { + return ServiceLoader.load(ExtensionTransformer.class, getClassLoader()); + } + boolean allowStartAgent() { if (ConfigManager.INSTANCE.isLocalStorage()) { return true; diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java b/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java new file mode 100644 index 000000000..bdb0d5152 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/extension/ExtensionTransformer.java @@ -0,0 +1,17 @@ +package io.arex.inst.extension; + +import java.lang.instrument.ClassFileTransformer; + +public abstract class ExtensionTransformer implements ClassFileTransformer { + private String name; + + public ExtensionTransformer(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public abstract boolean validate(); +} 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 c1c62fe6c..8b8616caa 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 @@ -2,11 +2,15 @@ import io.arex.agent.bootstrap.TraceContextManager; import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.listener.ContextListener; +import java.util.ArrayList; +import java.util.List; import java.util.Map; public class ContextManager { private static final Map RECORD_MAP = new LatencyContextHashMap(); + private static final List LISTENERS = new ArrayList<>(); /** * agent call this method @@ -23,6 +27,7 @@ public static ArexContext currentContext(boolean createIfAbsent, String caseId) 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; @@ -35,7 +40,9 @@ public static ArexContext currentContext(boolean createIfAbsent, String caseId) } // first init execute if (createIfAbsent) { - return RECORD_MAP.computeIfAbsent(caseId, ArexContext::of); + ArexContext context = ArexContext.of(caseId); + publish(context, true); + return RECORD_MAP.put(caseId, context); } return RECORD_MAP.get(caseId); } @@ -63,6 +70,23 @@ public static void remove() { if (StringUtil.isEmpty(caseId)) { return; } - RECORD_MAP.remove(caseId); + ArexContext context = RECORD_MAP.remove(caseId); + publish(context, false); + } + + public static void registerListener(ContextListener listener) { + LISTENERS.add(listener); + } + + private static void publish(ArexContext context, boolean isCreate) { + if (LISTENERS.size() > 0) { + LISTENERS.stream().forEach(listener -> { + if (isCreate) { + listener.onCreate(context); + } else { + listener.onComplete(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 6bd685c87..f392cb790 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 @@ -40,7 +40,7 @@ private ArexContext initOrGet(Object key) { } private void overdueCleanUp() { - if (latencyMap != null && CLEANUP_LOCK.tryLock() && latencyMap.mappingCount() > CLEANUP_THRESHOLD) { + if (latencyMap != null && CLEANUP_LOCK.tryLock()) { try { long now = System.currentTimeMillis(); for (Map.Entry entry: latencyMap.entrySet()) { @@ -56,7 +56,7 @@ private void overdueCleanUp() { } // Compatible where map.remove() not called - if (CLEANUP_LOCK.tryLock() && this.mappingCount() > CLEANUP_THRESHOLD) { + if (this.mappingCount() > CLEANUP_THRESHOLD && CLEANUP_LOCK.tryLock()) { try { long now = System.currentTimeMillis(); for (Map.Entry entry: super.entrySet()) { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java new file mode 100644 index 000000000..d0ada3639 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/listener/ContextListener.java @@ -0,0 +1,8 @@ +package io.arex.inst.runtime.listener; + +import io.arex.inst.runtime.context.ArexContext; + +public interface ContextListener { + void onCreate(ArexContext arexContext); + void onComplete(ArexContext arexContext); +} 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 180412df0..54da9ac50 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 @@ -279,8 +279,28 @@ private static Map parseConfigFile(String configPath) { public void parseAgentConfig(String args) { Map agentMap = StringUtil.asMap(args); if (!agentMap.isEmpty()) { - setStorageServiceMode(agentMap.get(STORAGE_SERVICE_MODE)); - setEnableDebug(agentMap.get(ENABLE_DEBUG)); + 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; + } + } updateRuntimeConfig(); } } From c8a5cea470013c34e6317323ba0fdbb6c0040010 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 21 Sep 2023 16:15:59 +0800 Subject: [PATCH 10/17] fix: optimize apollo init check (#293) --- .../request/RequestHandlerManager.java | 17 +++++++++-------- .../config/apollo/ApolloConfigChecker.java | 19 +++++++++++++++++++ .../config/apollo/ApolloConfigHelper.java | 14 -------------- .../apollo/ApolloDubboRequestHandler.java | 4 ++-- .../apollo/ApolloServletV3RequestHandler.java | 4 ++-- .../apollo/ApolloConfigCheckerTest.java | 13 +++++++++++++ .../config/apollo/ApolloConfigHelperTest.java | 5 ----- 7 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java create mode 100644 arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java 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 831651e2a..a6aafde15 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 @@ -19,16 +19,17 @@ public static void init() { } public static void preHandle(Object request, String name) { - try { - final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); - if (CollectionUtil.isEmpty(requestHandlers)) { - return; - } - for (RequestHandler requestHandler : requestHandlers) { + final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); + if (CollectionUtil.isEmpty(requestHandlers)) { + return; + } + for (RequestHandler requestHandler : requestHandlers) { + try { requestHandler.preHandle(request); + } catch (Throwable ex) { + // avoid affecting the remaining handlers when one handler fails + LogManager.warn("preHandler", ex.getMessage()); } - } catch (Throwable ex) { - LogManager.warn("preHandler", ex.getMessage()); } } diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java new file mode 100644 index 000000000..53176c7cd --- /dev/null +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigChecker.java @@ -0,0 +1,19 @@ +package io.arex.inst.config.apollo; + +public class ApolloConfigChecker { + + private static boolean isLoadedApollo = false; + + static { + try { + Class.forName("com.ctrip.framework.apollo.ConfigService"); + isLoadedApollo = true; + } catch (ClassNotFoundException e) { + // ignore, means business application unLoad apollo-client + } + } + + public static boolean unloadApollo() { + return !isLoadedApollo; + } +} diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java index 2ee6cf16c..515a52310 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloConfigHelper.java @@ -50,16 +50,6 @@ */ public class ApolloConfigHelper { private static Field configInstancesField; - private static boolean isLoadedApollo = false; - - static { - try { - Class.forName("com.ctrip.framework.apollo.ConfigService"); - isLoadedApollo = true; - } catch (ClassNotFoundException e) { - // ignore, means business application unLoad apollo-client - } - } public static void initAndRecord(Supplier recordIdSpl, Supplier versionSpl) { String recordId = recordIdSpl.get(); @@ -223,8 +213,4 @@ because this configuration has already been replayed, during the first full repl private static String getReleaseKey() { return ArexConstants.PREFIX + ApolloConfigExtractor.currentReplayConfigBatchNo(); } - - public static boolean unloadApollo() { - return !isLoadedApollo; - } } 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 b3083a894..be6666369 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 @@ -18,7 +18,7 @@ public String name() { @Override public void preHandle(Map request) { // check business application if loaded apollo-client - if (ApolloConfigHelper.unloadApollo()) { + if (ApolloConfigChecker.unloadApollo()) { return; } ApolloConfigHelper.initAndRecord( @@ -44,6 +44,6 @@ private boolean postInvalid(Map request, Map res if (request == null) { return true; } - return !ContextManager.needRecord() || ApolloConfigHelper.unloadApollo(); + return !ContextManager.needRecord() || ApolloConfigChecker.unloadApollo(); } } 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 90ed744ea..1c2c3767d 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 @@ -18,7 +18,7 @@ public String name() { @Override public void preHandle(HttpServletRequest request) { // check business application if loaded apollo-client - if (ApolloConfigHelper.unloadApollo()) { + if (ApolloConfigChecker.unloadApollo()) { return; } ApolloConfigHelper.initAndRecord( @@ -47,6 +47,6 @@ private boolean postInvalid(HttpServletRequest request, HttpServletResponse resp if (response.getHeader(ArexConstants.RECORD_ID) != null) { return true; } - return !ContextManager.needRecord() || ApolloConfigHelper.unloadApollo(); + return !ContextManager.needRecord() || ApolloConfigChecker.unloadApollo(); } } diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java new file mode 100644 index 000000000..6ddee0279 --- /dev/null +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigCheckerTest.java @@ -0,0 +1,13 @@ +package io.arex.inst.config.apollo; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ApolloConfigCheckerTest { + + @Test + void unloadApollo() { + assertFalse(ApolloConfigChecker.unloadApollo()); + } +} \ No newline at end of file diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java index 384d9b613..42a8d09cc 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloConfigHelperTest.java @@ -176,9 +176,4 @@ static Stream getReplayConfigCase() { arguments(mocker2, previous3, predicate_nonNull) ); } - - @Test - void unloadApollo() { - assertFalse(ApolloConfigHelper.unloadApollo()); - } } \ No newline at end of file From fdb555b68c07331fa179aa5ff6b17e64a2a4e2f7 Mon Sep 17 00:00:00 2001 From: yanni Date: Thu, 21 Sep 2023 17:29:19 +0800 Subject: [PATCH 11/17] fix: get configBatchNo first from arex-extension-attribute (#294) * fix: get configBatchNo first from arex-extension-attribute * fix: get configBatchNo first from arex-extension-attribute --- .../main/java/io/arex/inst/httpservlet/ServletExtractor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java index bdcc58070..e288db42d 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletExtractor.java @@ -108,8 +108,8 @@ private void doExecute() { requestAttributes.put("RequestPath", requestPath); Map requestHeaders = getRequestHeaders(); requestAttributes.put("Headers", requestHeaders); - requestAttributes.put(ArexConstants.CONFIG_VERSION, - adapter.getAttribute(httpServletRequest, ArexConstants.CONFIG_VERSION)); + requestAttributes.computeIfAbsent(ArexConstants.CONFIG_VERSION, + key -> adapter.getAttribute(httpServletRequest, ArexConstants.CONFIG_VERSION)); String originalMocker = requestHeaders.get(ArexConstants.REPLAY_ORIGINAL_MOCKER); MockCategoryType mockCategoryType = From 86a7569ad7f52309b86a78223eb54e14d3b4dbdb Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 22 Sep 2023 12:09:31 +0800 Subject: [PATCH 12/17] fix: add redis v3 dependency (#295) --- arex-agent/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arex-agent/pom.xml b/arex-agent/pom.xml index f18676058..f3b2e8163 100644 --- a/arex-agent/pom.xml +++ b/arex-agent/pom.xml @@ -81,6 +81,11 @@ arex-redis-common ${project.version} + + ${project.groupId} + arex-jedis-v2 + ${project.version} + ${project.groupId} arex-jedis-v4 From 7ce00b471c36737ba34e6184a143d9fd0ec806ce Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:17:02 +0800 Subject: [PATCH 13/17] fix: exception type judgment error (#296) --- .../arex/inst/runtime/serializer/Serializer.java | 15 ++++++++------- .../inst/runtime/serializer/SerializerTest.java | 10 ++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java index 2217c69fb..778716a23 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/serializer/Serializer.java @@ -7,6 +7,7 @@ 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.ArexConstants; import io.arex.inst.runtime.util.TypeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,6 +68,10 @@ public static String serializeWithException(Object object, String serializer) th return null; } + if (object instanceof Throwable) { + return INSTANCE.getSerializer(ArexConstants.GSON_SERIALIZER).serialize(object); + } + Collection> nestedCollection = TypeUtil.toNestedCollection(object); if (nestedCollection != null) { return serializeNestedCollection(serializer, nestedCollection); @@ -105,9 +110,6 @@ public static String getSerializerFromType(String categoryType) { * @return result string */ public static String serialize(Object object) { - if (object instanceof Throwable) { - return serialize(object, "gson"); - } return serialize(object, null); } @@ -153,6 +155,9 @@ public static T deserialize(String value, Type type, String serializer) { } try { + if (Throwable.class.isAssignableFrom(TypeUtil.getRawClass(type))) { + serializer = ArexConstants.GSON_SERIALIZER; + } return INSTANCE.getSerializer(serializer).deserialize(value, type); } catch (Throwable ex) { LogManager.warn("serializer-deserialize-type", StringUtil.format("can not deserialize value %s to type %s, cause: %s", value, type.getTypeName(), ex.toString())); @@ -176,10 +181,6 @@ public static T deserialize(String value, String typeName, String serializer return null; } - if (typeName.endsWith("Exception")) { - serializer = "gson"; - } - if (typeName.startsWith(HASH_MAP_VALUES_CLASS)) { return (T) restoreHashMapValues(value, typeName, serializer); } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java index a6c10d539..a1453668d 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/serializer/SerializerTest.java @@ -149,4 +149,14 @@ void testInitSerializerConfigMap() throws Exception { assertEquals("gson", Serializer.getSerializerFromType("soa")); assertNull(Serializer.getSerializerFromType("httpRequest")); } + + @Test + void testTypeIsException() { + final RuntimeException runtimeException = new RuntimeException(); + final String json = Serializer.serialize(runtimeException); + String typeName = TypeUtil.getName(runtimeException); + assertNotNull(json); + final RuntimeException actualResult = Serializer.deserialize(json, TypeUtil.forName(typeName)); + assertEquals(runtimeException.getClass(), actualResult.getClass()); + } } \ No newline at end of file From c5692307c21fc62423e81fb079faa774e7510bfb Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Mon, 9 Oct 2023 19:28:37 +0800 Subject: [PATCH 14/17] feat: invalid case (#297) * feat: invalid case * feat: invalid case --- .../inst/runtime/context/ArexContext.java | 9 ++++ .../inst/runtime/service/DataCollector.java | 3 +- .../inst/runtime/service/DataService.java | 5 +- .../arex/inst/runtime/util/CaseManager.java | 33 ++++++++++++ .../io/arex/inst/runtime/util/MockUtils.java | 8 +-- .../inst/runtime/util/CaseManagerTest.java | 51 +++++++++++++++++++ .../arex/inst/runtime/util/MockUtilsTest.java | 5 ++ .../arex/foundation/internal/DataEntity.java | 20 +++++++- .../services/DataCollectorService.java | 11 +++- .../services/DataCollectorServiceTest.java | 23 +++++++-- 10 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java create mode 100644 arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java 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 c0c2d4fa5..e1f5293de 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 @@ -24,6 +24,7 @@ public class ArexContext { private Map attachments = null; private boolean isRedirectRequest; + private boolean isInvalidCase; public static ArexContext of(String caseId) { return of(caseId, null); @@ -109,6 +110,14 @@ public void setRedirectRequest(boolean redirectRequest) { isRedirectRequest = redirectRequest; } + public boolean isInvalidCase() { + return isInvalidCase; + } + + public void setInvalidCase(boolean invalidCase) { + isInvalidCase = invalidCase; + } + public boolean isRedirectRequest(String referer) { if (attachments == null) { isRedirectRequest = false; 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 344b52522..3926ea4f6 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 @@ -1,11 +1,12 @@ package io.arex.inst.runtime.service; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; public interface DataCollector { void start(); - void save(String mockData); + void save(Mocker requestMocker); 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 fc5303680..99d6e3031 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 @@ -1,6 +1,7 @@ package io.arex.inst.runtime.service; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; public class DataService { @@ -16,8 +17,8 @@ public static Builder builder() { this.saver = dataSaver; } - public void save(String data) { - saver.save(data); + public void save(Mocker requestMocker) { + saver.save(requestMocker); } public String query(String data, MockStrategyEnum 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 new file mode 100644 index 000000000..7ab772ff5 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/CaseManager.java @@ -0,0 +1,33 @@ +package io.arex.inst.runtime.util; + +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; + +public class CaseManager { + private CaseManager() { + } + + public static void invalid(String recordId, String operationName) { + try { + final ArexContext context = ContextManager.getRecordContext(recordId); + if (context == null || context.isInvalidCase()) { + return; + } + LogManager.warn("invalidCase", + StringUtil.format("invalid case: recordId: %s operation: %s", recordId, operationName)); + context.setInvalidCase(true); + } catch (Exception ex) { + LogManager.warn("invalidCase.remove", ex); + } + } + + public static boolean isInvalidCase(String recordId) { + final ArexContext context = ContextManager.getRecordContext(recordId); + if (context == null) { + return false; + } + return context.isInvalidCase(); + } +} 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 e1e4d41dc..11187623b 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 @@ -85,13 +85,15 @@ public static ArexMocker create(MockCategoryType categoryType, String operationN } public static void recordMocker(Mocker requestMocker) { - String postJson = Serializer.serialize(requestMocker); + if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { + return; + } if (Config.get().isEnableDebug()) { - LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), postJson)); + LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); } - DataService.INSTANCE.save(postJson); + DataService.INSTANCE.save(requestMocker); } public static Mocker replayMocker(Mocker requestMocker) { 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 new file mode 100644 index 000000000..ff689e786 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/CaseManagerTest.java @@ -0,0 +1,51 @@ +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 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.mockito.MockedStatic; +import org.mockito.Mockito; + +class CaseManagerTest { + static MockedStatic contextManagerMocked; + + @BeforeAll + static void setUp() { + Serializer.builder(new TestJacksonSerializable()).build(); + contextManagerMocked = Mockito.mockStatic(ContextManager.class); + } + + @AfterAll + static void tearDown() { + contextManagerMocked = null; + Mockito.clearAllCaches(); + } + + @Test + void invalid() { + final ArexContext context = ArexContext.of("testRecordId"); + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + Assertions.assertFalse(context.isInvalidCase()); + Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); + + CaseManager.invalid("testRecordId", "testOperationName"); + Assertions.assertTrue(context.isInvalidCase()); + Assertions.assertTrue(CaseManager.isInvalidCase("testRecordId")); + + // test invalid case with null context + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(null); + Assertions.assertFalse(CaseManager.isInvalidCase("testRecordId")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + + // test invalid case with exception + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenThrow(new RuntimeException("test exception")); + Assertions.assertDoesNotThrow(() -> CaseManager.invalid("testRecordId", "testOperationName")); + } +} \ 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 f664e6a31..8bc650f11 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 @@ -29,6 +29,7 @@ class MockUtilsTest { @BeforeAll static void setUp() { Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(CaseManager.class); configBuilder = ConfigBuilder.create("test"); dataCollector = Mockito.mock(DataCollector.class); @@ -52,6 +53,10 @@ void recordMocker() { configBuilder.build(); ArexMocker dynamicClass = MockUtils.createDynamicClass("test", "test"); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); + + // invalid case + Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(true); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); } @Test diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java index 8a8b0ec63..5938169e2 100644 --- a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/internal/DataEntity.java @@ -1,12 +1,19 @@ package io.arex.foundation.internal; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.serializer.Serializer; + public class DataEntity { private final long queueTime; private final String postData; + private final String recordId; + private final String operationName; - public DataEntity(String postData) { - this.postData = postData; + public DataEntity(Mocker requestMocker) { + this.postData = Serializer.serialize(requestMocker); this.queueTime = System.nanoTime(); + this.recordId = requestMocker.getRecordId(); + this.operationName = requestMocker.getOperationName(); } public long getQueueTime() { @@ -16,4 +23,13 @@ public long getQueueTime() { public String getPostData() { return postData; } + + public String getRecordId() { + return recordId; + } + + public String getOperationName() { + return operationName; + } + } 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 7a26cf8e7..da778908c 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,6 +1,7 @@ package io.arex.foundation.services; import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.util.MapUtils; import io.arex.foundation.config.ConfigManager; import io.arex.foundation.healthy.HealthManager; @@ -10,6 +11,7 @@ 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.util.CaseManager; import io.arex.inst.runtime.service.DataCollector; import java.util.Map; @@ -39,13 +41,14 @@ public class DataCollectorService implements DataCollector { } @Override - public void save(String mockData) { + public void save(Mocker requestMocker) { if (HealthManager.isFastRejection()) { return; } - if (!buffer.put(new DataEntity(mockData))) { + if (!buffer.put(new DataEntity(requestMocker))) { HealthManager.onEnqueueRejection(); + CaseManager.invalid(requestMocker.getRecordId(), requestMocker.getOperationName()); } } @@ -109,6 +112,9 @@ static void doSleep(long millis) { private static final String MOCK_STRATEGY = "X-AREX-Mock-Strategy-Code"; void saveData(DataEntity entity) { + if (entity == null || CaseManager.isInvalidCase(entity.getRecordId())) { + return; + } AsyncHttpClientUtil.postAsyncWithZstdJson(saveApiUrl, entity.getPostData(), null) .whenComplete(saveMockDataConsumer(entity)); } @@ -131,6 +137,7 @@ private BiConsumer saveMockDataConsumer(DataEntity entity) { return (response, throwable) -> { long usedTime = System.nanoTime() - entity.getQueueTime(); if (Objects.nonNull(throwable)) { + CaseManager.invalid(entity.getRecordId(), entity.getOperationName()); LogManager.warn("saveMockDataConsumer", "save mock data error"); usedTime = -1; // -1:reject HealthManager.onDataServiceRejection(); 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 fa6eb7a66..04f66ec5e 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 @@ -4,11 +4,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.internal.DataEntity; import io.arex.foundation.model.HttpClientResponse; 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 org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -20,6 +23,7 @@ class DataCollectorServiceTest { static void setUp() { Mockito.mockStatic(AsyncHttpClientUtil.class); Mockito.mockStatic(HealthManager.class); + Mockito.mockStatic(ContextManager.class); } @AfterAll @@ -29,14 +33,25 @@ static void tearDown() { @Test void saveData() { + final ArexMocker mocker = new ArexMocker(); CompletableFuture mockResponse = CompletableFuture.completedFuture(HttpClientResponse.emptyResponse()); - Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), anyString(), any())).thenReturn(mockResponse); - assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity("test"))); + Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockResponse); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); CompletableFuture mockException = new CompletableFuture<>(); mockException.completeExceptionally(new RuntimeException("mock exception")); - Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), anyString(), any())).thenReturn(mockException); - assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity("test"))); + Mockito.when(AsyncHttpClientUtil.postAsyncWithZstdJson(anyString(), any(), any())).thenReturn(mockException); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); + + // null entity + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(null)); + + // invalid case + final ArexContext context = ArexContext.of("testRecordId"); + context.setInvalidCase(true); + Mockito.when(ContextManager.getRecordContext("testRecordId")).thenReturn(context); + mocker.setRecordId("testRecordId"); + assertDoesNotThrow(()-> DataCollectorService.INSTANCE.saveData(new DataEntity(mocker))); } @Test From 3399d80f22e79cad944a352974eb63be0d8a4b53 Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:06:34 +0800 Subject: [PATCH 15/17] feat: support guava range and fastutil (#263) --- .../arex/agent/bootstrap/util/StringUtil.java | 4 + .../agent/bootstrap/util/StringUtilTest.java | 6 + .../instrumentation/BaseAgentInstaller.java | 4 + arex-instrumentation-foundation/pom.xml | 11 ++ .../foundation/serializer/GsonSerializer.java | 5 + .../serializer/JacksonSerializer.java | 36 +++- .../custom/FastUtilAdapterFactory.java | 57 +++++++ .../custom/GuavaRangeSerializer.java | 160 ++++++++++++++++++ .../serializer/GsonSerializerTest.java | 12 ++ .../serializer/JacksonSerializerTest.java | 12 +- .../foundation/serializer/TimeTestInfo.java | 11 ++ .../custom/FastUtilAdapterFactoryTest.java | 106 ++++++++++++ .../custom/GuavaRangeSerializerTest.java | 85 ++++++++++ 13 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java create mode 100644 arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java create mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java create mode 100644 arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java 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 d69630f90..5d7d60f74 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 @@ -351,6 +351,10 @@ public static boolean containsIgnoreCase(final CharSequence str, final CharSeque return false; } + public static boolean startWith(String source, String prefix) { + return startWithFrom(source, prefix, 0); + } + public static boolean startWithFrom(String source, String prefix, int checkStartIndex) { int length = prefix.length(); if (checkStartIndex < 0 || checkStartIndex + length > source.length()) { 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 a5745c98c..7e9bfc071 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 @@ -309,4 +309,10 @@ void isNullWord() { assertTrue(StringUtil.isNullWord("NULL")); assertFalse(StringUtil.isNullWord("mock")); } + + @Test + void startWith() { + assertTrue(StringUtil.startWith("mock", "m")); + assertFalse(StringUtil.startWith("mock", "o")); + } } \ No newline at end of file diff --git a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java index 6e2abe6d4..4a5b63251 100644 --- a/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java +++ b/arex-agent-core/src/main/java/io/arex/agent/instrumentation/BaseAgentInstaller.java @@ -8,6 +8,8 @@ import io.arex.foundation.healthy.HealthManager; import io.arex.foundation.serializer.GsonSerializer; import io.arex.foundation.serializer.JacksonSerializer; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; +import io.arex.foundation.serializer.custom.GuavaRangeSerializer; import io.arex.foundation.services.ConfigService; import io.arex.foundation.services.DataCollectorService; import io.arex.foundation.services.TimerService; @@ -131,6 +133,8 @@ private void initSerializer() { AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(JacksonSerializer.class); AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(GsonSerializer.class); AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(NumberTypeAdaptor.class); + AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(GuavaRangeSerializer.class); + AdviceClassesCollector.INSTANCE.addClassToLoaderSearch(FastUtilAdapterFactory.class); Serializer.builder(JacksonSerializer.INSTANCE).build(); } private void initDataCollector() { diff --git a/arex-instrumentation-foundation/pom.xml b/arex-instrumentation-foundation/pom.xml index da4c02599..188d49abb 100644 --- a/arex-instrumentation-foundation/pom.xml +++ b/arex-instrumentation-foundation/pom.xml @@ -66,6 +66,12 @@ joda-time 2.9 + + it.unimi.dsi + fastutil + 8.2.2 + test + @@ -89,6 +95,11 @@ com.google shaded.com.google + + com.google.common.collect.Range + com.google.common.collect.RangeGwtSerializationDependencies + com.google.common.collect.BoundType + 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 483bacfff..cd74ba622 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 @@ -2,8 +2,11 @@ import com.google.auto.service.AutoService; +import com.google.common.collect.Range; import io.arex.agent.thirdparty.util.time.DateFormatUtils; 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.util.NumberTypeAdaptor; import io.arex.agent.bootstrap.util.StringUtil; import com.google.gson.Gson; @@ -201,6 +204,8 @@ public GsonSerializer() { .registerTypeAdapter(Instant.class, INSTANT_JSON_DESERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_SERIALIZER) .registerTypeAdapter(Class.class, CLASS_JSON_DESERIALIZER) + .registerTypeAdapter(Range.class, new GuavaRangeSerializer.GsonRangeSerializer()) + .registerTypeAdapterFactory(new FastUtilAdapterFactory()) .enableComplexMapKeySerialization() .setExclusionStrategies(new ExcludeField()) .disableHtmlEscaping(); 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 f06d077a5..ac7324e12 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 @@ -1,6 +1,12 @@ package io.arex.foundation.serializer; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTypeResolverBuilder; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.google.auto.service.AutoService; +import com.google.common.collect.Range; import io.arex.agent.bootstrap.util.StringUtil; import com.fasterxml.jackson.annotation.JsonIgnoreType; @@ -13,6 +19,8 @@ import io.arex.agent.thirdparty.util.time.DateFormatUtils; import io.arex.agent.thirdparty.util.time.FastDateFormat; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactory; +import io.arex.foundation.serializer.custom.GuavaRangeSerializer; import io.arex.foundation.util.JdkUtils; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; @@ -20,7 +28,6 @@ import io.arex.inst.runtime.model.SerializeSkipInfo; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.util.TypeUtil; - import java.sql.Time; import java.time.Instant; import org.joda.time.DateTime; @@ -68,10 +75,18 @@ public JacksonSerializer() { configMapper(); customTimeFormatSerializer(MODULE); customTimeFormatDeserializer(MODULE); - + customTypeResolver(); MAPPER.registerModule(MODULE); } + private void customTypeResolver() { + TypeResolverBuilder typeResolver = new CustomTypeResolverBuilder(); + typeResolver.init(JsonTypeInfo.Id.CLASS, null); + typeResolver.inclusion(JsonTypeInfo.As.PROPERTY); + typeResolver.typeProperty("@CLASS"); + MAPPER.setDefaultTyping(typeResolver); + } + private void buildSkipInfoMap() { try { Config config = Config.get(); @@ -180,6 +195,7 @@ private void customTimeFormatSerializer(SimpleModule module) { // java.sql.Date/Time serialize same as java.util.Date module.addSerializer(Date.class, new DateSerialize()); module.addSerializer(Instant.class, new InstantSerialize()); + module.addSerializer(Range.class, new GuavaRangeSerializer.JacksonRangeSerializer()); } private void customTimeFormatDeserializer(SimpleModule module) { @@ -198,6 +214,7 @@ private void customTimeFormatDeserializer(SimpleModule module) { module.addDeserializer(java.sql.Date.class, new SqlDateDeserialize()); module.addDeserializer(Time.class, new SqlTimeDeserialize()); module.addDeserializer(Instant.class, new InstantDeserialize()); + module.addDeserializer(Range.class, new GuavaRangeSerializer.JacksonRangeDeserializer()); } private static class JacksonSimpleModule extends SimpleModule { @@ -730,4 +747,19 @@ public DateTimeFormatter getFormatter(final String pattern) { } } + private static class CustomTypeResolverBuilder extends DefaultTypeResolverBuilder { + + public CustomTypeResolverBuilder() { + super(DefaultTyping.NON_FINAL, LaissezFaireSubTypeValidator.instance); + } + + /** + * @return true will serialize with runtime type info + */ + @Override + public boolean useForType(JavaType type) { + return type.getRawClass().isInterface() && + StringUtil.startWith(type.getRawClass().getName(), FastUtilAdapterFactory.FASTUTIL_PACKAGE); + } + } } diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java new file mode 100644 index 000000000..142fb0914 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactory.java @@ -0,0 +1,57 @@ +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.reflect.TypeToken; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; + +public class FastUtilAdapterFactory implements TypeAdapterFactory { + public static final String FASTUTIL_PACKAGE = "it.unimi.dsi.fastutil"; + private static final String SET_NAME = "Set"; + private static final String LIST_NAME = "List"; + private static final String MAP_NAME = "Map"; + private static final Gson GSON = new Gson(); + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + final Class rawClass = type.getRawType(); + if (rawClass == null) { + return null; + } + String rawClassName = rawClass.getName(); + if (rawClass.isInterface() && StringUtil.startWith(rawClassName, FASTUTIL_PACKAGE)) { + // example: it.unimi.dsi.fastutil.ints.IntSet -> IntOpenHashSet + if (rawClassName.endsWith(SET_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 3), "OpenHashSet"); + } + if (rawClassName.endsWith(LIST_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 4), "ArrayList"); + } + if (rawClassName.endsWith(MAP_NAME)) { + return getImplType(getImplClassPrefix(rawClassName, 3), "OpenHashMap"); + } + } + + return null; + } + + private String getImplClassPrefix(String rawClassName, int endIndex) { + return rawClassName.substring(0, rawClassName.length() - endIndex); + } + + private TypeAdapter getImplType(String implClassPrefix, String implClassSuffix) { + String implName = implClassPrefix + implClassSuffix; + try { + return (TypeAdapter) GSON.getAdapter(Class.forName(implName)); + } catch (Exception ex) { + LogManager.warn("getImplClass",StringUtil.format("Failed to load class: %s", implName), ex); + return null; + } + } +} diff --git a/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java new file mode 100644 index 000000000..80670e142 --- /dev/null +++ b/arex-instrumentation-foundation/src/main/java/io/arex/foundation/serializer/custom/GuavaRangeSerializer.java @@ -0,0 +1,160 @@ +package io.arex.foundation.serializer.custom; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.arex.inst.runtime.util.TypeUtil; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; + +public class GuavaRangeSerializer { + + private static final String LOWER_BOUND_TYPE = "lowerBoundType"; + private static final String UPPER_BOUND_TYPE = "upperBoundType"; + + private static final String LOWER_BOUND = "lowerBound"; + private static final String UPPER_BOUND = "upperBound"; + + private static final String LOWER_BOUND_VALUE_TYPE = "lowerBoundValueType"; + private static final String UPPER_BOUND_VALUE_TYPE = "upperBoundValueType"; + + private static Range restoreRange(Comparable lowerBound, BoundType lowerBoundType, Comparable upperBound, + BoundType upperBoundType) { + if (lowerBound == null && upperBound != null) { + return Range.upTo(upperBound, upperBoundType); + } + + if (lowerBound != null && upperBound == null) { + return Range.downTo(lowerBound, lowerBoundType); + } + + if (lowerBound == null) { + return Range.all(); + } + + return Range.range(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + + public static class GsonRangeSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Range range, Type type, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + + if (range.hasLowerBound()) { + jsonObject.add(LOWER_BOUND_TYPE, context.serialize(range.lowerBoundType())); + jsonObject.add(LOWER_BOUND, context.serialize(range.lowerEndpoint())); + jsonObject.addProperty(LOWER_BOUND_VALUE_TYPE, TypeUtil.getName(range.lowerEndpoint())); + } else { + jsonObject.add(LOWER_BOUND_TYPE, context.serialize(BoundType.OPEN)); + } + + if (range.hasUpperBound()) { + jsonObject.add(UPPER_BOUND_TYPE, context.serialize(range.upperBoundType())); + jsonObject.add(UPPER_BOUND, context.serialize(range.upperEndpoint())); + jsonObject.addProperty(UPPER_BOUND_VALUE_TYPE, TypeUtil.getName(range.upperEndpoint())); + } else { + jsonObject.add(UPPER_BOUND_TYPE, context.serialize(BoundType.OPEN)); + } + return jsonObject; + } + + @Override + public Range deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + + final JsonObject jsonObject = json.getAsJsonObject(); + final JsonElement lowerBoundTypeJsonElement = jsonObject.get(LOWER_BOUND_TYPE); + final JsonElement upperBoundTypeJsonElement = jsonObject.get(UPPER_BOUND_TYPE); + + final BoundType lowerBoundType = context.deserialize(lowerBoundTypeJsonElement, BoundType.class); + final JsonElement lowerBoundJsonElement = jsonObject.get(LOWER_BOUND); + final Comparable lowerBound = + lowerBoundJsonElement == null ? null : context.deserialize(lowerBoundJsonElement, + TypeUtil.forName(jsonObject.get(LOWER_BOUND_VALUE_TYPE).getAsString())); + + final BoundType upperBoundType = context.deserialize(upperBoundTypeJsonElement, BoundType.class); + final JsonElement upperBoundJsonElement = jsonObject.get(UPPER_BOUND); + final Comparable upperBound = + upperBoundJsonElement == null ? null : context.deserialize(upperBoundJsonElement, + TypeUtil.forName(jsonObject.get(UPPER_BOUND_VALUE_TYPE).getAsString())); + + return restoreRange(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + } + + public static class JacksonRangeSerializer extends com.fasterxml.jackson.databind.JsonSerializer { + + @Override + public void serialize(Range range, JsonGenerator gen, SerializerProvider serializers) throws IOException { + final HashMap map = new HashMap<>(3); + if (range.hasLowerBound()) { + map.put(LOWER_BOUND_TYPE, range.lowerBoundType()); + map.put(LOWER_BOUND, range.lowerEndpoint()); + map.put(LOWER_BOUND_VALUE_TYPE, TypeUtil.getName(range.lowerEndpoint())); + } else { + map.put(LOWER_BOUND_TYPE, BoundType.OPEN); + } + + if (range.hasUpperBound()) { + map.put(UPPER_BOUND_TYPE, range.upperBoundType()); + map.put(UPPER_BOUND, range.upperEndpoint()); + map.put(UPPER_BOUND_VALUE_TYPE, TypeUtil.getName(range.upperEndpoint())); + } else { + map.put(UPPER_BOUND_TYPE, BoundType.OPEN); + } + gen.writeObject(map); + } + } + + public static class JacksonRangeDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer> { + + @Override + public Range deserialize(com.fasterxml.jackson.core.JsonParser p, + com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException { + final JsonNode treeNode = p.getCodec().readTree(p); + final JsonNode lowBoundTypeNode = treeNode.get(LOWER_BOUND_TYPE); + final JsonNode upBoundTypeNode = treeNode.get(UPPER_BOUND_TYPE); + final JsonNode lowBoundNode = treeNode.get(LOWER_BOUND); + final JsonNode upBoundNode = treeNode.get(UPPER_BOUND); + final JsonNode lowBoundValueTypeNode = treeNode.get(LOWER_BOUND_VALUE_TYPE); + final JsonNode upBoundValueTypeNode = treeNode.get(UPPER_BOUND_VALUE_TYPE); + + final BoundType lowerBoundType = lowBoundTypeNode == null ? null : ctxt.readTreeAsValue(lowBoundTypeNode, BoundType.class); + final BoundType upperBoundType = upBoundTypeNode == null ? null : ctxt.readTreeAsValue(upBoundTypeNode, BoundType.class); + final JavaType lowerBoundJavaType = lowBoundValueTypeNode == null ? null : ctxt.constructType( + TypeUtil.forName(lowBoundValueTypeNode.asText())); + final JavaType upperBoundJavaType = upBoundValueTypeNode == null ? null : ctxt.constructType( + TypeUtil.forName(upBoundValueTypeNode.asText())); + + Comparable lowerBound; + Comparable upperBound ; + + if (lowerBoundJavaType == null || lowBoundNode == null) { + lowerBound = null; + } else { + lowerBound = ctxt.readTreeAsValue(lowBoundNode, lowerBoundJavaType); + } + + if (upperBoundJavaType == null || upBoundNode == null) { + upperBound = null; + } else { + upperBound = ctxt.readTreeAsValue(upBoundNode, upperBoundJavaType); + } + + return restoreRange(lowerBound, lowerBoundType, upperBound, upperBoundType); + } + } + +} 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 c989a646e..19c664272 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 @@ -3,9 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import com.google.gson.internal.LinkedTreeMap; import io.arex.inst.runtime.util.TypeUtil; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; import java.sql.Time; import java.time.LocalDateTime; import java.util.Map; @@ -134,4 +137,13 @@ void testAddCustomSerializer() { json = GsonSerializer.INSTANCE.serialize(map); assertEquals("{\"key\":\"value\",\"long-java.lang.Long\":2}", json); } + + @Test + void testFastUtil() throws Throwable { + final IntOpenHashSet hashSet = new IntOpenHashSet(); + final String json = GsonSerializer.INSTANCE.serialize(hashSet); + final IntSet deserialize = GsonSerializer.INSTANCE.deserialize(json, IntSet.class); + assert deserialize != null; + assertEquals(hashSet, deserialize); + } } \ No newline at end of file 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 7f8b043fa..3b154184f 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,11 +1,13 @@ package io.arex.foundation.serializer; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest; +import io.arex.foundation.serializer.custom.FastUtilAdapterFactoryTest.TestType; + import static org.junit.jupiter.api.Assertions.*; import io.arex.inst.runtime.util.TypeUtil; import java.sql.Time; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -139,4 +141,12 @@ void deserializeType() throws Throwable { assertNotNull(JacksonSerializer.INSTANCE.deserialize(json, TypeUtil.forName(TypeUtil.getName(LocalDateTime.now())))); } + @Test + void testFastUtil() throws Throwable { + final TestType testType = FastUtilAdapterFactoryTest.getTestType(); + final String jackJson = JacksonSerializer.INSTANCE.serialize(testType); + final TestType deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, TestType.class); + assertNotNull(deserializeJackTestType); + } + } \ 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 102640dfd..feaf178cb 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 @@ -1,5 +1,6 @@ package io.arex.foundation.serializer; +import com.google.common.collect.Range; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -43,6 +44,16 @@ public class TimeTestInfo { private Instant instant = Instant.now(); + private Range range; + + public Range getRange() { + return range; + } + + public void setRange(Range range) { + this.range = range; + } + public Instant getInstant() { return instant; } diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java new file mode 100644 index 000000000..21d318f3d --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/FastUtilAdapterFactoryTest.java @@ -0,0 +1,106 @@ +package io.arex.foundation.serializer.custom; + +import static org.junit.jupiter.api.Assertions.*; + +import io.arex.foundation.serializer.GsonSerializer; +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.floats.FloatList; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +public class FastUtilAdapterFactoryTest { + public static TestType getTestType() { + final TestType testType = new TestType(1, "test"); + final IntOpenHashSet intOpenHashSet = new IntOpenHashSet(); + intOpenHashSet.add(2); + testType.setIntSet(intOpenHashSet); + final FloatArrayList floats = new FloatArrayList(); + floats.add(1.0f); + testType.setFloats(floats); + final LongLinkedOpenHashSet linkedOpenHashSet = new LongLinkedOpenHashSet(); + linkedOpenHashSet.add(3L); + testType.setLongSet(linkedOpenHashSet); + final HashSet hashSet = new HashSet<>(); + hashSet.add(1); + testType.setSet(new Set[]{hashSet}); + return testType; + } + + @Test + void testFastUtilAdapter() throws Throwable { + final TestType testType = getTestType(); + final String json = GsonSerializer.INSTANCE.serialize(testType); + final TestType deserializeTestType = GsonSerializer.INSTANCE.deserialize(json, TestType.class); + assertNotNull(deserializeTestType); + } + + public static class TestType { + private int id; + private String name; + private IntSet intSet; + private FloatList floats; + private LongSet longSet; + private Set[] set; + + public TestType() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TestType(int id, String name) { + this.id = id; + this.name = name; + } + + public IntSet getIntSet() { + return intSet; + } + + public void setIntSet(IntSet intSet) { + this.intSet = intSet; + } + + public FloatList getFloats() { + return floats; + } + + public void setFloats(FloatList floats) { + this.floats = floats; + } + + public LongSet getLongSet() { + return longSet; + } + + public void setLongSet(LongSet longSet) { + this.longSet = longSet; + } + + public Set[] getSet() { + return set; + } + + public void setSet(Set[] set) { + this.set = set; + } + } +} \ No newline at end of file diff --git a/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java new file mode 100644 index 000000000..15654297d --- /dev/null +++ b/arex-instrumentation-foundation/src/test/java/io/arex/foundation/serializer/custom/GuavaRangeSerializerTest.java @@ -0,0 +1,85 @@ +package io.arex.foundation.serializer.custom; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.common.collect.Range; +import io.arex.foundation.serializer.GsonSerializer; +import io.arex.foundation.serializer.JacksonSerializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GuavaRangeSerializerTest { + static Range range1, range2, range3, range4; + @BeforeAll + static void setUp() { + range1 = Range.closed(1, 10); + range2 = Range.lessThan(2); + range3 = Range.greaterThan(3); + range4 = Range.openClosed(4, 40); + } + + @AfterAll + static void tearDown() { + range1 = null; + range2 = null; + range3 = null; + range4 = null; + } + + @Test + void testRangeSerializeWithGson() { + final String range1Json = GsonSerializer.INSTANCE.serialize(range1); + final Range deserializeRange1 = GsonSerializer.INSTANCE.deserialize(range1Json, Range.class); + assertEquals(range1.lowerEndpoint(), deserializeRange1.lowerEndpoint()); + assertEquals(range1.upperEndpoint(), deserializeRange1.upperEndpoint()); + + final String range2Json = GsonSerializer.INSTANCE.serialize(range2); + final Range deserializeRange2 = GsonSerializer.INSTANCE.deserialize(range2Json, Range.class); + assertFalse(range2.hasLowerBound()); + assertFalse(deserializeRange2.hasLowerBound()); + assertEquals(range2.upperEndpoint(), deserializeRange2.upperEndpoint()); + + final String range3Json = GsonSerializer.INSTANCE.serialize(range3); + final Range deserializeRange3 = GsonSerializer.INSTANCE.deserialize(range3Json, Range.class); + assertEquals(range3.lowerEndpoint(), deserializeRange3.lowerEndpoint()); + assertFalse(range3.hasUpperBound()); + assertFalse(deserializeRange3.hasUpperBound()); + + final String range4Json = GsonSerializer.INSTANCE.serialize(range4); + final Range deserializeRange4 = GsonSerializer.INSTANCE.deserialize(range4Json, Range.class); + assertEquals(range4.lowerEndpoint(), deserializeRange4.lowerEndpoint()); + assertEquals(range4.upperEndpoint(), deserializeRange4.upperEndpoint()); + assertEquals(range4.lowerBoundType(), deserializeRange4.lowerBoundType()); + assertEquals(range4.upperBoundType(), deserializeRange4.upperBoundType()); + } + + @Test + void testRangeSerializeWithJackson() throws Throwable { + final String range1Json = JacksonSerializer.INSTANCE.serialize(range1); + final Range deserializeRange1 = JacksonSerializer.INSTANCE.deserialize(range1Json, Range.class); + assertEquals(range1.lowerEndpoint(), deserializeRange1.lowerEndpoint()); + assertEquals(range1.upperEndpoint(), deserializeRange1.upperEndpoint()); + + final String range2Json = JacksonSerializer.INSTANCE.serialize(range2); + final Range deserializeRange2 = JacksonSerializer.INSTANCE.deserialize(range2Json, Range.class); + assertFalse(range2.hasLowerBound()); + assertFalse(deserializeRange2.hasLowerBound()); + assertEquals(range2.upperEndpoint(), deserializeRange2.upperEndpoint()); + + final String range3Json = JacksonSerializer.INSTANCE.serialize(range3); + final Range deserializeRange3 = JacksonSerializer.INSTANCE.deserialize(range3Json, Range.class); + assertEquals(range3.lowerEndpoint(), deserializeRange3.lowerEndpoint()); + assertFalse(range3.hasUpperBound()); + assertFalse(deserializeRange3.hasUpperBound()); + + final String range4Json = JacksonSerializer.INSTANCE.serialize(range4); + final Range deserializeRange4 = JacksonSerializer.INSTANCE.deserialize(range4Json, Range.class); + assertEquals(range4.lowerEndpoint(), deserializeRange4.lowerEndpoint()); + assertEquals(range4.upperEndpoint(), deserializeRange4.upperEndpoint()); + assertEquals(range4.lowerBoundType(), deserializeRange4.lowerBoundType()); + assertEquals(range4.upperBoundType(), deserializeRange4.upperBoundType()); + } +} \ No newline at end of file From 0c92a298d99b2839825d8d398c8bff8da4176cc8 Mon Sep 17 00:00:00 2001 From: Mark Zhang Date: Tue, 10 Oct 2023 11:52:33 +0800 Subject: [PATCH 16/17] feat: add config service host (#298) --- .../bootstrap/constants/ConfigConstants.java | 1 + .../arex/foundation/config/ConfigManager.java | 18 ++++++++++++++++++ .../foundation/services/ConfigService.java | 4 ++-- .../foundation/config/ConfigManagerTest.java | 5 +++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java index 944dabdf8..7d7222e25 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/constants/ConfigConstants.java @@ -6,6 +6,7 @@ private ConfigConstants() { public static final String ENABLE_DEBUG = "arex.enable.debug"; public static final String SERVICE_NAME = "arex.service.name"; public static final String STORAGE_SERVICE_HOST = "arex.storage.service.host"; + public static final String CONFIG_SERVICE_HOST = "arex.config.service.host"; public static final String CONFIG_PATH = "arex.config.path"; public static final String STORAGE_MODE = "local"; public static final String RECORD_RATE = "arex.rate.limit"; 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 54da9ac50..ed980d9d7 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 @@ -43,6 +43,7 @@ public class ConfigManager { private String agentVersion; private String serviceName; private String storageServiceHost; + private String configServiceHost; private String configPath; private String storageServiceMode; @@ -117,6 +118,21 @@ public void setStorageServiceHost(String storageServiceHost) { System.setProperty(STORAGE_SERVICE_HOST, storageServiceHost); } + public String getConfigServiceHost() { + if (StringUtil.isNotEmpty(configServiceHost)) { + return configServiceHost; + } + return storageServiceHost; + } + + public void setConfigServiceHost(String configServiceHost) { + if (StringUtil.isEmpty(configServiceHost)) { + return; + } + this.configServiceHost = configServiceHost; + System.setProperty(CONFIG_SERVICE_HOST, configServiceHost); + } + public void setRecordRate(int recordRate) { if (recordRate < 0) { return; @@ -218,6 +234,7 @@ void init() { setEnableDebug(System.getProperty(ENABLE_DEBUG)); setServiceName(StringUtil.strip(System.getProperty(SERVICE_NAME))); setStorageServiceHost(StringUtil.strip(System.getProperty(STORAGE_SERVICE_HOST))); + setConfigServiceHost(StringUtil.strip(System.getProperty(CONFIG_SERVICE_HOST))); configPath = StringUtil.strip(System.getProperty(CONFIG_PATH)); setRecordRate(DEFAULT_RECORDING_RATE); @@ -247,6 +264,7 @@ void readConfigFromFile(String configPath) { setEnableDebug(configMap.get(ENABLE_DEBUG)); setServiceName(configMap.get(SERVICE_NAME)); setStorageServiceHost(configMap.get(STORAGE_SERVICE_HOST)); + setConfigServiceHost(configMap.get(CONFIG_SERVICE_HOST)); setDynamicResultSizeLimit(configMap.get(DYNAMIC_RESULT_SIZE_LIMIT)); setTimeMachine(configMap.get(TIME_MACHINE)); setStorageServiceMode(configMap.get(STORAGE_SERVICE_MODE)); 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 c42c1d150..68d90f3b0 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 @@ -34,7 +34,7 @@ public class ConfigService { public static final ConfigService INSTANCE = new ConfigService(); private static final String CONFIG_LOAD_URI = - String.format("http://%s/api/config/agent/load", ConfigManager.INSTANCE.getStorageServiceHost()); + String.format("http://%s/api/config/agent/load", ConfigManager.INSTANCE.getConfigServiceHost()); private final AtomicBoolean firstLoad = new AtomicBoolean(false); private final AtomicBoolean reloadConfig = new AtomicBoolean(false); @@ -191,7 +191,7 @@ private static class AgentStatusService { private String prevLastModified; private static final String AGENT_STATUS_URI = - String.format("http://%s/api/config/agent/agentStatus", ConfigManager.INSTANCE.getStorageServiceHost()); + String.format("http://%s/api/config/agent/agentStatus", ConfigManager.INSTANCE.getConfigServiceHost()); private final AtomicBoolean shutdown = new AtomicBoolean(false); 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 81a5dd453..88fb4f73c 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 @@ -49,6 +49,11 @@ void initFromSystemPropertyTest() { assertEquals("test-your-service", configManager.getServiceName()); assertEquals("test-storage-service.host", configManager.getStorageServiceHost()); + assertEquals("test-storage-service.host", configManager.getConfigServiceHost()); + + System.setProperty("arex.config.service.host", "test-config-service.host"); + configManager.init(); + assertEquals("test-config-service.host", configManager.getConfigServiceHost()); } @Test From 0230131dde2c4619d93e9721b7d8725d140f38b7 Mon Sep 17 00:00:00 2001 From: YongwuHe <38196495+YongwuHe@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:06:46 +0800 Subject: [PATCH 17/17] fix: jackson deserialization is case-insensitive (#300) --- .../serializer/JacksonSerializer.java | 1 - .../serializer/JacksonSerializerTest.java | 33 +++++++++++++++++++ .../dynamic/common/DynamicClassExtractor.java | 3 +- .../common/DynamicClassExtractorTest.java | 8 +++++ 4 files changed, 43 insertions(+), 2 deletions(-) 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 ac7324e12..280f6ebc0 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 @@ -177,7 +177,6 @@ private void configMapper() { MAPPER.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false); MAPPER.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); MAPPER.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); } private void customTimeFormatSerializer(SimpleModule module) { 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 3b154184f..011623a7f 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 @@ -149,4 +149,37 @@ void testFastUtil() throws Throwable { assertNotNull(deserializeJackTestType); } + @Test + void testCaseSensitiveProperties() throws Throwable { + final CaseSensitive caseSensitive = new CaseSensitive(); + caseSensitive.setAmountPaid("100"); + caseSensitive.setAmountpaid("200"); + final String jackJson = JacksonSerializer.INSTANCE.serialize(caseSensitive); + final CaseSensitive deserializeJackTestType = JacksonSerializer.INSTANCE.deserialize(jackJson, CaseSensitive.class); + assertNotNull(deserializeJackTestType); + assertEquals("100", deserializeJackTestType.getAmountPaid()); + assertEquals("200", deserializeJackTestType.getAmountpaid()); + } + + static class CaseSensitive { + private String amountPaid; + private String amountpaid; + + public String getAmountPaid() { + return amountPaid; + } + + public void setAmountPaid(String amountPaid) { + this.amountPaid = amountPaid; + } + + public String getAmountpaid() { + return amountpaid; + } + + public void setAmountpaid(String amountpaid) { + this.amountpaid = amountpaid; + } + } + } \ 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 2904588ec..5bae2fa40 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 @@ -289,9 +289,10 @@ private boolean needRecord() { 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), methodSignatureKey)); + String.valueOf(size), String.valueOf(RESULT_SIZE_MAX), methodInfo)); return false; } } catch (Throwable e) { 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 bb6019324..b2b516dde 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 @@ -42,6 +42,7 @@ import java.util.stream.Stream; import org.mockito.stubbing.Answer; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -438,4 +439,11 @@ void invalidOperation() throws Throwable { final MockResult replay = extractor.replay(); assertEquals(MockResult.IGNORE_MOCK_RESULT, replay); } + + @Test + void emptyMethodKeyAndExceedSize() throws NoSuchMethodException { + Method testEmptyArgs = DynamicClassExtractorTest.class.getDeclaredMethod("invalidOperation"); + DynamicClassExtractor extractor = new DynamicClassExtractor(testEmptyArgs, new Object[0]); + assertDoesNotThrow(() -> extractor.recordResponse(new int[1001])); + } }