From cf4f1903e0b03b4759e0d55d64260241da45627e Mon Sep 17 00:00:00 2001 From: kuleat <303148062@qq.com> Date: Wed, 8 Apr 2026 01:08:20 +0800 Subject: [PATCH 1/3] perf: add reflection cache --- .../health/ReadinessAutoConfiguration.java | 5 +- ...lectionCacheEndpointAutoConfiguration.java | 44 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ReadinessAutoConfigurationTests.java | 4 +- ...adinessEndpointAutoConfigurationTests.java | 4 +- ...onCacheEndpointAutoConfigurationTests.java | 62 +++ .../RpcActuatorAutoConfigurationTests.java | 4 +- .../health/HealthIndicatorProcessor.java | 14 +- .../reflection/ReflectionCacheEndpoint.java | 49 ++ .../ReflectionCacheEndpointTests.java | 63 +++ .../ReflectionCacheAutoConfiguration.java | 40 ++ .../reflection/ReflectionCacheProperties.java | 42 ++ .../DataSourceAutoConfiguration.java | 5 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ReflectionCacheAutoConfigurationTests.java | 59 ++ .../DataSourceAutoConfigurationTests.java | 4 +- .../DataSourceBeanPostProcessor.java | 16 +- .../sofa/boot/constant/SofaBootConstants.java | 2 +- .../sofa/boot/reflection/ReflectionCache.java | 512 ++++++++++++++++++ .../boot/reflection/ReflectionCacheTests.java | 134 +++++ .../ReflectionCacheEndpointWebTests.java | 61 +++ 21 files changed, 1116 insertions(+), 10 deletions(-) create mode 100644 sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java create mode 100644 sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java create mode 100644 sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java create mode 100644 sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java create mode 100644 sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java create mode 100644 sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java create mode 100644 sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java create mode 100644 sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java create mode 100644 sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java create mode 100644 sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java index 4678ce909..4ba5e7840 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java @@ -25,6 +25,7 @@ import com.alipay.sofa.boot.autoconfigure.condition.OnVirtualThreadStartupDisableCondition; import com.alipay.sofa.boot.constant.SofaBootConstants; import com.alipay.sofa.boot.log.SofaBootLoggerFactory; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.common.thread.NamedThreadFactory; import com.alipay.sofa.common.thread.SofaThreadPoolExecutor; import com.alipay.sofa.common.thread.virtual.SofaVirtualThreadFactory; @@ -97,9 +98,11 @@ public HealthCheckerProcessor healthCheckerProcessor(HealthProperties healthChec @Bean @ConditionalOnMissingBean public HealthIndicatorProcessor healthIndicatorProcessor(HealthProperties healthCheckProperties, - ExecutorService readinessHealthCheckExecutor) { + ExecutorService readinessHealthCheckExecutor, + ReflectionCache reflectionCache) { HealthIndicatorProcessor healthIndicatorProcessor = new HealthIndicatorProcessor(); healthIndicatorProcessor.setHealthCheckExecutor(readinessHealthCheckExecutor); + healthIndicatorProcessor.setReflectionCache(reflectionCache); healthIndicatorProcessor.initExcludedIndicators(healthCheckProperties .getExcludedIndicators()); healthIndicatorProcessor.setParallelCheck(healthCheckProperties.isParallelCheck()); diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java new file mode 100644 index 000000000..42f11e55b --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.actuator.autoconfigure.reflection; + +import com.alipay.sofa.boot.actuator.reflection.ReflectionCacheEndpoint; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; +import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +/** + * Configures {@link ReflectionCacheEndpoint}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +@AutoConfiguration(after = ReflectionCacheAutoConfiguration.class) +@ConditionalOnBean(ReflectionCache.class) +@ConditionalOnAvailableEndpoint(endpoint = ReflectionCacheEndpoint.class) +public class ReflectionCacheEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ReflectionCacheEndpoint reflectionCacheEndpoint(ReflectionCache reflectionCache) { + return new ReflectionCacheEndpoint(reflectionCache); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6dbe5466a..76852ac4c 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1,6 @@ com.alipay.sofa.boot.actuator.autoconfigure.beans.IsleBeansEndpointAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.components.ComponentsEndpointAutoConfiguration +com.alipay.sofa.boot.actuator.autoconfigure.reflection.ReflectionCacheEndpointAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.health.ReadinessAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.health.ReadinessIsleAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.health.ReadinessEndpointAutoConfiguration diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java index caf3ada77..28e816109 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java @@ -22,6 +22,7 @@ import com.alipay.sofa.boot.actuator.health.ModuleHealthChecker; import com.alipay.sofa.boot.actuator.health.ReadinessCheckCallbackProcessor; import com.alipay.sofa.boot.actuator.health.ReadinessCheckListener; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; import com.alipay.sofa.boot.autoconfigure.isle.SofaModuleAutoConfiguration; import com.alipay.sofa.boot.autoconfigure.runtime.SofaRuntimeAutoConfiguration; import com.alipay.sofa.boot.isle.ApplicationRuntimeModel; @@ -49,7 +50,8 @@ public class ReadinessAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration( AutoConfigurations - .of(ReadinessAutoConfiguration.class)) + .of(ReflectionCacheAutoConfiguration.class, + ReadinessAutoConfiguration.class)) .withPropertyValues( "management.endpoints.web.exposure.include=readiness"); diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessEndpointAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessEndpointAutoConfigurationTests.java index 7960e9166..8fbc40e26 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessEndpointAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessEndpointAutoConfigurationTests.java @@ -19,6 +19,7 @@ import com.alipay.sofa.boot.actuator.health.ReadinessEndpoint; import com.alipay.sofa.boot.actuator.health.ReadinessEndpointWebExtension; import com.alipay.sofa.boot.actuator.health.ReadinessHttpCodeStatusMapper; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; @@ -38,7 +39,8 @@ public class ReadinessEndpointAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations - .of(ReadinessEndpointAutoConfiguration.class, + .of(ReflectionCacheAutoConfiguration.class, + ReadinessEndpointAutoConfiguration.class, ReadinessAutoConfiguration.class, HealthEndpointAutoConfiguration.class)); diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java new file mode 100644 index 000000000..e35fb5ca2 --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.actuator.autoconfigure.reflection; + +import com.alipay.sofa.boot.actuator.reflection.ReflectionCacheEndpoint; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectionCacheEndpointAutoConfiguration}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +public class ReflectionCacheEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(ReflectionCacheAutoConfiguration.class, + ReflectionCacheEndpointAutoConfiguration.class)); + + @Test + void runShouldHaveEndpointBean() { + this.contextRunner.withPropertyValues( + "management.endpoints.web.exposure.include=reflection-cache") + .run((context) -> assertThat(context) + .hasSingleBean(ReflectionCacheEndpoint.class)); + } + + @Test + void runWhenNotExposedShouldNotHaveEndpointBean() { + this.contextRunner.run( + (context) -> assertThat(context).doesNotHaveBean(ReflectionCacheEndpoint.class)); + } + + @Test + void runWhenReflectionCacheBeanMissingShouldNotHaveEndpointBean() { + new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(ReflectionCacheEndpointAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=reflection-cache") + .run((context) -> assertThat(context) + .doesNotHaveBean(ReflectionCacheEndpoint.class)); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/rpc/RpcActuatorAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/rpc/RpcActuatorAutoConfigurationTests.java index a72f858c0..ffab33129 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/rpc/RpcActuatorAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/rpc/RpcActuatorAutoConfigurationTests.java @@ -20,6 +20,7 @@ import com.alipay.sofa.boot.actuator.rpc.HealthCheckProviderConfigDelayRegisterChecker; import com.alipay.sofa.boot.actuator.rpc.RpcAfterHealthCheckCallback; import com.alipay.sofa.boot.actuator.rpc.SofaRpcEndpoint; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; import com.alipay.sofa.rpc.boot.context.RpcStartApplicationListener; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -46,7 +47,8 @@ public class RpcActuatorAutoConfigurationTests { @Test void runShouldHaveHealthCheckProviderConfigDelayRegisterChecker() { this.contextRunner - .withUserConfiguration(ReadinessAutoConfiguration.class) + .withConfiguration(AutoConfigurations.of(ReflectionCacheAutoConfiguration.class, + ReadinessAutoConfiguration.class)) .withBean(RpcStartApplicationListener.class) .run((context) -> assertThat(context) .hasSingleBean(HealthCheckProviderConfigDelayRegisterChecker.class)); diff --git a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/health/HealthIndicatorProcessor.java b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/health/HealthIndicatorProcessor.java index 74dbb833b..cef265bd8 100644 --- a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/health/HealthIndicatorProcessor.java +++ b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/health/HealthIndicatorProcessor.java @@ -18,6 +18,7 @@ import com.alipay.sofa.boot.log.ErrorCode; import com.alipay.sofa.boot.log.SofaBootLoggerFactory; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.boot.startup.BaseStat; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -98,6 +99,8 @@ public class HealthIndicatorProcessor implements ApplicationContextAware { private long parallelCheckTimeout; + private ReflectionCache reflectionCache; + static { REACTOR_CLASS_EXIST = ClassUtils.isPresent(REACTOR_CLASS, null); } @@ -133,7 +136,7 @@ public void initExcludedIndicators(List excludes) { excludedIndicators = new HashSet<>(); for (String exclude : excludes) { try { - Class c = Class.forName(exclude); + Class c = loadClass(exclude); excludedIndicators.add(c); } catch (Throwable e) { logger.warn("Unable to find excluded HealthIndicator class {}, just ignore it.", @@ -295,6 +298,15 @@ public void setParallelCheckTimeout(long parallelCheckTimeout) { this.parallelCheckTimeout = parallelCheckTimeout; } + public void setReflectionCache(ReflectionCache reflectionCache) { + this.reflectionCache = reflectionCache; + } + + private Class loadClass(String className) throws ClassNotFoundException { + return reflectionCache != null ? reflectionCache.forName(className) : Class + .forName(className); + } + public Map getHealthIndicatorConfig() { return healthIndicatorConfig; } diff --git a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java new file mode 100644 index 000000000..5c1b84c52 --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.actuator.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; + +/** + * Endpoint for viewing and clearing the reflection cache. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +@Endpoint(id = "reflection-cache") +public class ReflectionCacheEndpoint { + + private final ReflectionCache reflectionCache; + + public ReflectionCacheEndpoint(ReflectionCache reflectionCache) { + this.reflectionCache = reflectionCache; + } + + @ReadOperation + public ReflectionCache.CacheStats stats() { + return reflectionCache.getStats(); + } + + @WriteOperation + public ReflectionCache.CacheStats clear() { + reflectionCache.clear(); + return reflectionCache.getStats(); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java new file mode 100644 index 000000000..2b4a09d3d --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.actuator.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectionCacheEndpoint}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +public class ReflectionCacheEndpointTests { + + @Test + void statsShouldExposeReflectionCacheState() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(); + ReflectionCacheEndpoint endpoint = new ReflectionCacheEndpoint(reflectionCache); + + reflectionCache.forName(String.class.getName()); + reflectionCache.forName(String.class.getName()); + + ReflectionCache.CacheStats stats = endpoint.stats(); + assertThat(stats.isEnabled()).isTrue(); + assertThat(stats.getClassHitCount()).isEqualTo(1); + assertThat(stats.getClassMissCount()).isEqualTo(1); + assertThat(stats.getClassCacheSize()).isEqualTo(1); + } + + @Test + void clearShouldResetReflectionCacheState() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(); + ReflectionCacheEndpoint endpoint = new ReflectionCacheEndpoint(reflectionCache); + + reflectionCache.forName(String.class.getName()); + reflectionCache.findMethod(String.class, "trim"); + reflectionCache.getDeclaredConstructor(String.class, String.class); + + ReflectionCache.CacheStats stats = endpoint.clear(); + assertThat(stats.getTotalHitCount()).isZero(); + assertThat(stats.getTotalMissCount()).isZero(); + assertThat(stats.getClassCacheSize()).isZero(); + assertThat(stats.getMethodCacheSize()).isZero(); + assertThat(stats.getConstructorCacheSize()).isZero(); + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java new file mode 100644 index 000000000..56664ccf4 --- /dev/null +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.autoconfigure.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Configures {@link ReflectionCache}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +@AutoConfiguration +@EnableConfigurationProperties(ReflectionCacheProperties.class) +public class ReflectionCacheAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ReflectionCache reflectionCache(ReflectionCacheProperties reflectionCacheProperties) { + return new ReflectionCache(reflectionCacheProperties.isEnabled()); + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java new file mode 100644 index 000000000..5e92c470b --- /dev/null +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.autoconfigure.reflection; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for {@link ReflectionCacheAutoConfiguration}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +@ConfigurationProperties("sofa.boot.reflection.cache") +public class ReflectionCacheProperties { + + /** + * Enables the reflection cache. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java index e29785dc5..3a4da957a 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java @@ -17,6 +17,7 @@ package com.alipay.sofa.boot.autoconfigure.tracer.datasource; import com.alipay.common.tracer.core.configuration.SofaTracerConfiguration; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.boot.tracer.datasource.DataSourceBeanPostProcessor; import com.alipay.sofa.tracer.plugins.datasource.SmartDataSource; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -41,10 +42,12 @@ public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean - public static DataSourceBeanPostProcessor dataSourceBeanPostProcessor(Environment environment) { + public static DataSourceBeanPostProcessor dataSourceBeanPostProcessor(Environment environment, + ReflectionCache reflectionCache) { String appName = environment.getProperty(SofaTracerConfiguration.TRACER_APPNAME_KEY); DataSourceBeanPostProcessor dataSourceBeanPostProcessor = new DataSourceBeanPostProcessor(); dataSourceBeanPostProcessor.setAppName(appName); + dataSourceBeanPostProcessor.setReflectionCache(reflectionCache); return dataSourceBeanPostProcessor; } } diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sofa-boot-project/sofa-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b934929e7..51860fe5d 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ +com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration com.alipay.sofa.boot.autoconfigure.runtime.SofaRuntimeAutoConfiguration com.alipay.sofa.boot.autoconfigure.isle.SofaModuleAutoConfiguration com.alipay.sofa.boot.autoconfigure.rpc.SofaRpcAutoConfiguration diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java new file mode 100644 index 000000000..7645ec2b9 --- /dev/null +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.autoconfigure.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectionCacheAutoConfiguration}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +public class ReflectionCacheAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(ReflectionCacheAutoConfiguration.class)); + + @Test + void registerReflectionCacheByDefault() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ReflectionCache.class); + assertThat(context.getBean(ReflectionCache.class).isEnabled()).isTrue(); + }); + } + + @Test + void disabledPropertyShouldCreateDisabledReflectionCache() { + this.contextRunner.withPropertyValues("sofa.boot.reflection.cache.enabled=false") + .run((context) -> assertThat(context.getBean(ReflectionCache.class).isEnabled()).isFalse()); + } + + @Test + void backOffWhenCustomReflectionCacheExists() { + ReflectionCache reflectionCache = new ReflectionCache(false); + this.contextRunner.withBean(ReflectionCache.class, () -> reflectionCache) + .run((context) -> assertThat(context.getBean(ReflectionCache.class)) + .isSameAs(reflectionCache)); + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java index a54013d44..a7d4b482d 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java @@ -16,6 +16,7 @@ */ package com.alipay.sofa.boot.autoconfigure.tracer.datasource; +import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; import com.alipay.sofa.boot.tracer.datasource.DataSourceBeanPostProcessor; import com.alipay.sofa.tracer.plugins.datasource.SmartDataSource; import org.junit.jupiter.api.Test; @@ -35,7 +36,8 @@ public class DataSourceAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations - .of(DataSourceAutoConfiguration.class)); + .of(ReflectionCacheAutoConfiguration.class, + DataSourceAutoConfiguration.class)); @Test public void registerDataSourcePostProcessors() { diff --git a/sofa-boot-project/sofa-boot-core/tracer-sofa-boot/src/main/java/com/alipay/sofa/boot/tracer/datasource/DataSourceBeanPostProcessor.java b/sofa-boot-project/sofa-boot-core/tracer-sofa-boot/src/main/java/com/alipay/sofa/boot/tracer/datasource/DataSourceBeanPostProcessor.java index 2898653e1..e4ddd8d6c 100644 --- a/sofa-boot-project/sofa-boot-core/tracer-sofa-boot/src/main/java/com/alipay/sofa/boot/tracer/datasource/DataSourceBeanPostProcessor.java +++ b/sofa-boot-project/sofa-boot-core/tracer-sofa-boot/src/main/java/com/alipay/sofa/boot/tracer/datasource/DataSourceBeanPostProcessor.java @@ -17,6 +17,7 @@ package com.alipay.sofa.boot.tracer.datasource; import com.alipay.sofa.boot.context.processor.SingletonSofaPostProcessor; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.tracer.plugins.datasource.SmartDataSource; import com.alipay.sofa.tracer.plugins.datasource.utils.DataSourceUtils; import org.springframework.beans.BeansException; @@ -42,7 +43,9 @@ @SingletonSofaPostProcessor public class DataSourceBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { - private String appName; + private String appName; + + private ReflectionCache reflectionCache; @Override public Object postProcessAfterInitialization(Object bean, String beanName) @@ -73,7 +76,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) } try { - Method urlMethod = ReflectionUtils.findMethod(bean.getClass(), getUrlMethodName); + Method urlMethod = findMethod(bean.getClass(), getUrlMethodName); urlMethod.setAccessible(true); url = (String) urlMethod.invoke(bean); } catch (Throwable throwable) { @@ -100,4 +103,13 @@ public int getOrder() { public void setAppName(String appName) { this.appName = appName; } + + public void setReflectionCache(ReflectionCache reflectionCache) { + this.reflectionCache = reflectionCache; + } + + private Method findMethod(Class targetClass, String methodName) { + return reflectionCache != null ? reflectionCache.findMethod(targetClass, methodName) + : ReflectionUtils.findMethod(targetClass, methodName); + } } diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/constant/SofaBootConstants.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/constant/SofaBootConstants.java index e5e304ec7..4f9add769 100644 --- a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/constant/SofaBootConstants.java +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/constant/SofaBootConstants.java @@ -88,7 +88,7 @@ public class SofaBootConstants { /** * Default exposure web endpoint list */ - public static final String SOFA_DEFAULT_ENDPOINTS_WEB_EXPOSURE_VALUE = "info,health,readiness,startup,beans,components,rpc,isle,threadpool,sbom"; + public static final String SOFA_DEFAULT_ENDPOINTS_WEB_EXPOSURE_VALUE = "info,health,readiness,startup,beans,components,rpc,isle,threadpool,sbom,reflection-cache"; /** * CPU core diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java new file mode 100644 index 000000000..fb03a8930 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.reflection; + +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +/** + * Caches repeated reflection lookups. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +public class ReflectionCache { + + private static final Class[] EMPTY_PARAMETER_TYPES = new Class[0]; + + private final LookupStore classLookup = new LookupStore<>(); + + private final LookupStore methodLookup = new LookupStore<>(); + + private final LookupStore constructorLookup = new LookupStore<>(); + + private volatile boolean enabled; + + public ReflectionCache() { + this(true); + } + + public ReflectionCache(boolean enabled) { + this.enabled = enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (!enabled) { + clear(); + } + } + + public boolean isEnabled() { + return enabled; + } + + public Class forName(String className) throws ClassNotFoundException { + return forName(className, null); + } + + public Class forName(String className, @Nullable ClassLoader classLoader) + throws ClassNotFoundException { + if (!enabled) { + return doForName(className, classLoader); + } + + ClassLookupResult result = classLookup.get(new ClassKey(className, classLoader), cacheKey -> { + try { + return ClassLookupResult.found(doForName(cacheKey.className, cacheKey.classLoader)); + } catch (ClassNotFoundException exception) { + return ClassLookupResult.missed(); + } + }); + if (result.isMiss()) { + throw new ClassNotFoundException(className); + } + return result.type; + } + + @Nullable + public Method findMethod(Class targetClass, String methodName, Class... parameterTypes) { + Class[] normalizedParameterTypes = normalizeParameterTypes(parameterTypes); + if (!enabled) { + return doFindMethod(targetClass, methodName, normalizedParameterTypes); + } + + MethodLookupResult result = methodLookup.get(new MethodKey(targetClass, methodName, + normalizedParameterTypes), cacheKey -> new MethodLookupResult( + doFindMethod(cacheKey.targetClass, cacheKey.methodName, cacheKey.parameterTypes))); + return result.method; + } + + public Constructor getDeclaredConstructor(Class targetClass, + Class... parameterTypes) + throws NoSuchMethodException { + Class[] normalizedParameterTypes = normalizeParameterTypes(parameterTypes); + if (!enabled) { + return doGetDeclaredConstructor(targetClass, normalizedParameterTypes); + } + + ConstructorLookupResult result = constructorLookup.get(new ConstructorKey(targetClass, + normalizedParameterTypes), cacheKey -> { + try { + Constructor constructor = doGetDeclaredConstructor(cacheKey.targetClass, + cacheKey.parameterTypes); + return ConstructorLookupResult.found(constructor); + } catch (NoSuchMethodException exception) { + return ConstructorLookupResult.missed(); + } + }); + if (result.isMiss()) { + throw new NoSuchMethodException(buildConstructorSignature(targetClass, + normalizedParameterTypes)); + } + return castConstructor(result.constructor); + } + + public CacheStats getStats() { + LookupStats classStats = classLookup.getStats(); + LookupStats methodStats = methodLookup.getStats(); + LookupStats constructorStats = constructorLookup.getStats(); + return new CacheStats(enabled, classStats.hitCount, classStats.missCount, + classStats.cacheSize, classStats.missCacheSize, methodStats.hitCount, + methodStats.missCount, methodStats.cacheSize, methodStats.missCacheSize, + constructorStats.hitCount, constructorStats.missCount, constructorStats.cacheSize, + constructorStats.missCacheSize); + } + + public void clear() { + classLookup.clear(); + methodLookup.clear(); + constructorLookup.clear(); + } + + private Class doForName(String className, @Nullable ClassLoader classLoader) + throws ClassNotFoundException { + if (classLoader == null) { + return Class.forName(className); + } + return Class.forName(className, true, classLoader); + } + + @Nullable + private Method doFindMethod(Class targetClass, String methodName, Class[] parameterTypes) { + return ReflectionUtils.findMethod(targetClass, methodName, parameterTypes); + } + + private Constructor doGetDeclaredConstructor(Class targetClass, + Class[] parameterTypes) + throws NoSuchMethodException { + Constructor constructor = targetClass.getDeclaredConstructor(parameterTypes); + ReflectionUtils.makeAccessible(constructor); + return constructor; + } + + private String buildConstructorSignature(Class targetClass, Class[] parameterTypes) { + String parameterTypeNames = Arrays.stream(parameterTypes).map(Class::getName).reduce( + (left, right) -> left + "," + right).orElse(""); + return targetClass.getName() + ".(" + parameterTypeNames + ")"; + } + + private Class[] normalizeParameterTypes(@Nullable Class[] parameterTypes) { + if (parameterTypes == null || parameterTypes.length == 0) { + return EMPTY_PARAMETER_TYPES; + } + return parameterTypes.clone(); + } + + @SuppressWarnings("unchecked") + private Constructor castConstructor(Constructor constructor) { + return (Constructor) constructor; + } + + public static final class CacheStats { + + private final boolean enabled; + + private final long classHitCount; + + private final long classMissCount; + + private final int classCacheSize; + + private final int classMissCacheSize; + + private final long methodHitCount; + + private final long methodMissCount; + + private final int methodCacheSize; + + private final int methodMissCacheSize; + + private final long constructorHitCount; + + private final long constructorMissCount; + + private final int constructorCacheSize; + + private final int constructorMissCacheSize; + + public CacheStats(boolean enabled, long classHitCount, long classMissCount, + int classCacheSize, int classMissCacheSize, long methodHitCount, + long methodMissCount, int methodCacheSize, int methodMissCacheSize, + long constructorHitCount, long constructorMissCount, + int constructorCacheSize, int constructorMissCacheSize) { + this.enabled = enabled; + this.classHitCount = classHitCount; + this.classMissCount = classMissCount; + this.classCacheSize = classCacheSize; + this.classMissCacheSize = classMissCacheSize; + this.methodHitCount = methodHitCount; + this.methodMissCount = methodMissCount; + this.methodCacheSize = methodCacheSize; + this.methodMissCacheSize = methodMissCacheSize; + this.constructorHitCount = constructorHitCount; + this.constructorMissCount = constructorMissCount; + this.constructorCacheSize = constructorCacheSize; + this.constructorMissCacheSize = constructorMissCacheSize; + } + + public boolean isEnabled() { + return enabled; + } + + public long getClassHitCount() { + return classHitCount; + } + + public long getClassMissCount() { + return classMissCount; + } + + public int getClassCacheSize() { + return classCacheSize; + } + + public int getClassMissCacheSize() { + return classMissCacheSize; + } + + public long getMethodHitCount() { + return methodHitCount; + } + + public long getMethodMissCount() { + return methodMissCount; + } + + public int getMethodCacheSize() { + return methodCacheSize; + } + + public int getMethodMissCacheSize() { + return methodMissCacheSize; + } + + public long getConstructorHitCount() { + return constructorHitCount; + } + + public long getConstructorMissCount() { + return constructorMissCount; + } + + public int getConstructorCacheSize() { + return constructorCacheSize; + } + + public int getConstructorMissCacheSize() { + return constructorMissCacheSize; + } + + public long getTotalHitCount() { + return classHitCount + methodHitCount + constructorHitCount; + } + + public long getTotalMissCount() { + return classMissCount + methodMissCount + constructorMissCount; + } + + public double getHitRate() { + long total = getTotalHitCount() + getTotalMissCount(); + if (total == 0) { + return 0D; + } + return (double) getTotalHitCount() / total; + } + } + + private interface LookupResult { + + boolean isMiss(); + } + + private static final class LookupStore { + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + private final AtomicLong hitCount = new AtomicLong(); + + private final AtomicLong missCount = new AtomicLong(); + + private V get(K key, Function loader) { + AtomicBoolean cacheMiss = new AtomicBoolean(false); + V result = cache.computeIfAbsent(key, cacheKey -> { + cacheMiss.set(true); + return loader.apply(cacheKey); + }); + if (cacheMiss.get()) { + missCount.incrementAndGet(); + } else { + hitCount.incrementAndGet(); + } + return result; + } + + private LookupStats getStats() { + return new LookupStats(hitCount.get(), missCount.get(), cache.size(), + (int) cache.values().stream().filter(LookupResult::isMiss).count()); + } + + private void clear() { + cache.clear(); + hitCount.set(0); + missCount.set(0); + } + } + + private static final class LookupStats { + + private final long hitCount; + + private final long missCount; + + private final int cacheSize; + + private final int missCacheSize; + + private LookupStats(long hitCount, long missCount, int cacheSize, int missCacheSize) { + this.hitCount = hitCount; + this.missCount = missCount; + this.cacheSize = cacheSize; + this.missCacheSize = missCacheSize; + } + } + + private static final class ClassLookupResult implements LookupResult { + + private final Class type; + + private final boolean miss; + + private ClassLookupResult(Class type, boolean miss) { + this.type = type; + this.miss = miss; + } + + private static ClassLookupResult found(Class type) { + return new ClassLookupResult(type, false); + } + + private static ClassLookupResult missed() { + return new ClassLookupResult(null, true); + } + + @Override + public boolean isMiss() { + return miss; + } + } + + private static final class MethodLookupResult implements LookupResult { + + private final Method method; + + private MethodLookupResult(Method method) { + this.method = method; + } + + @Override + public boolean isMiss() { + return method == null; + } + } + + private static final class ConstructorLookupResult implements LookupResult { + + private final Constructor constructor; + + private final boolean miss; + + private ConstructorLookupResult(Constructor constructor, boolean miss) { + this.constructor = constructor; + this.miss = miss; + } + + private static ConstructorLookupResult found(Constructor constructor) { + return new ConstructorLookupResult(constructor, false); + } + + private static ConstructorLookupResult missed() { + return new ConstructorLookupResult(null, true); + } + + @Override + public boolean isMiss() { + return miss; + } + } + + private static final class ClassKey { + + private final String className; + + private final ClassLoader classLoader; + + private ClassKey(String className, @Nullable ClassLoader classLoader) { + this.className = className; + this.classLoader = classLoader; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof ClassKey that)) { + return false; + } + return Objects.equals(className, that.className) && classLoader == that.classLoader; + } + + @Override + public int hashCode() { + return 31 * className.hashCode() + System.identityHashCode(classLoader); + } + } + + private static final class MethodKey { + + private final Class targetClass; + + private final String methodName; + + private final Class[] parameterTypes; + + private MethodKey(Class targetClass, String methodName, Class[] parameterTypes) { + this.targetClass = targetClass; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof MethodKey that)) { + return false; + } + return Objects.equals(targetClass, that.targetClass) + && Objects.equals(methodName, that.methodName) + && Arrays.equals(parameterTypes, that.parameterTypes); + } + + @Override + public int hashCode() { + return Objects.hash(targetClass, methodName, Arrays.hashCode(parameterTypes)); + } + } + + private static final class ConstructorKey { + + private final Class targetClass; + + private final Class[] parameterTypes; + + private ConstructorKey(Class targetClass, Class[] parameterTypes) { + this.targetClass = targetClass; + this.parameterTypes = parameterTypes; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof ConstructorKey that)) { + return false; + } + return Objects.equals(targetClass, that.targetClass) + && Arrays.equals(parameterTypes, that.parameterTypes); + } + + @Override + public int hashCode() { + return Objects.hash(targetClass, Arrays.hashCode(parameterTypes)); + } + } +} diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java new file mode 100644 index 000000000..0f04aa9e4 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.reflection; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ReflectionCache}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +public class ReflectionCacheTests { + + @Test + void cacheHitsShouldBeRecordedForSuccessfulLookups() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(); + + Class type = reflectionCache.forName(SampleTarget.class.getName()); + Class cachedType = reflectionCache.forName(SampleTarget.class.getName()); + Method method = reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + Method cachedMethod = reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + Constructor constructor = reflectionCache.getDeclaredConstructor( + SampleTarget.class, String.class); + Constructor cachedConstructor = reflectionCache.getDeclaredConstructor( + SampleTarget.class, String.class); + + assertThat(type).isSameAs(SampleTarget.class); + assertThat(cachedType).isSameAs(type); + assertThat(method).isNotNull(); + assertThat(cachedMethod).isSameAs(method); + assertThat(constructor).isNotNull(); + assertThat(cachedConstructor).isSameAs(constructor); + assertThat(constructor.newInstance("SOFA").greet("Boot")).isEqualTo("SOFABoot"); + + ReflectionCache.CacheStats stats = reflectionCache.getStats(); + assertThat(stats.isEnabled()).isTrue(); + assertThat(stats.getClassHitCount()).isEqualTo(1); + assertThat(stats.getClassMissCount()).isEqualTo(1); + assertThat(stats.getClassCacheSize()).isEqualTo(1); + assertThat(stats.getMethodHitCount()).isEqualTo(1); + assertThat(stats.getMethodMissCount()).isEqualTo(1); + assertThat(stats.getMethodCacheSize()).isEqualTo(1); + assertThat(stats.getConstructorHitCount()).isEqualTo(1); + assertThat(stats.getConstructorMissCount()).isEqualTo(1); + assertThat(stats.getConstructorCacheSize()).isEqualTo(1); + assertThat(stats.getHitRate()).isEqualTo(0.5D); + } + + @Test + void cacheMissesShouldAlsoBeCached() { + ReflectionCache reflectionCache = new ReflectionCache(); + + assertThatThrownBy( + () -> reflectionCache.forName("com.alipay.sofa.boot.reflection.DoesNotExist")) + .isInstanceOf(ClassNotFoundException.class); + assertThatThrownBy( + () -> reflectionCache.forName("com.alipay.sofa.boot.reflection.DoesNotExist")) + .isInstanceOf(ClassNotFoundException.class); + assertThat(reflectionCache.findMethod(SampleTarget.class, "missingMethod")).isNull(); + assertThat(reflectionCache.findMethod(SampleTarget.class, "missingMethod")).isNull(); + assertThatThrownBy( + () -> reflectionCache.getDeclaredConstructor(SampleTarget.class, Integer.class)) + .isInstanceOf(NoSuchMethodException.class); + assertThatThrownBy( + () -> reflectionCache.getDeclaredConstructor(SampleTarget.class, Integer.class)) + .isInstanceOf(NoSuchMethodException.class); + + ReflectionCache.CacheStats stats = reflectionCache.getStats(); + assertThat(stats.getClassHitCount()).isEqualTo(1); + assertThat(stats.getClassMissCount()).isEqualTo(1); + assertThat(stats.getClassMissCacheSize()).isEqualTo(1); + assertThat(stats.getMethodHitCount()).isEqualTo(1); + assertThat(stats.getMethodMissCount()).isEqualTo(1); + assertThat(stats.getMethodMissCacheSize()).isEqualTo(1); + assertThat(stats.getConstructorHitCount()).isEqualTo(1); + assertThat(stats.getConstructorMissCount()).isEqualTo(1); + assertThat(stats.getConstructorMissCacheSize()).isEqualTo(1); + } + + @Test + void disabledCacheShouldBypassStatistics() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(false); + + assertThat(reflectionCache.isEnabled()).isFalse(); + assertThat(reflectionCache.forName(SampleTarget.class.getName())).isSameAs( + SampleTarget.class); + assertThat(reflectionCache.findMethod(SampleTarget.class, "greet", String.class)) + .isNotNull(); + assertThat(reflectionCache.getDeclaredConstructor(SampleTarget.class, String.class)) + .isNotNull(); + + ReflectionCache.CacheStats stats = reflectionCache.getStats(); + assertThat(stats.isEnabled()).isFalse(); + assertThat(stats.getTotalHitCount()).isZero(); + assertThat(stats.getTotalMissCount()).isZero(); + assertThat(stats.getClassCacheSize()).isZero(); + assertThat(stats.getMethodCacheSize()).isZero(); + assertThat(stats.getConstructorCacheSize()).isZero(); + } + + static class SampleTarget { + + private final String prefix; + + private SampleTarget(String prefix) { + this.prefix = prefix; + } + + public String greet(String suffix) { + return prefix + suffix; + } + } +} diff --git a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java new file mode 100644 index 000000000..2dffa63ac --- /dev/null +++ b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.smoke.tests.actuator.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import com.alipay.sofa.smoke.tests.actuator.ActuatorSofaBootApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@code /actuator/reflection-cache}. + * + * @author xiaosiyuan + * @since 4.5.0 + */ +@SpringBootTest(classes = ActuatorSofaBootApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "management.endpoints.web.exposure.include=reflection-cache" }) +public class ReflectionCacheEndpointWebTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ReflectionCache reflectionCache; + + @Test + public void reflectionCacheEndpoint() throws Exception { + reflectionCache.forName(String.class.getName()); + reflectionCache.forName(String.class.getName()); + + ResponseEntity response = restTemplate.getForEntity("/actuator/reflection-cache", + String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("\"enabled\":true").contains("\"classHitCount\":1") + .contains("\"classMissCount\":1").contains("\"classCacheSize\":1"); + + response = restTemplate.postForEntity("/actuator/reflection-cache", null, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("\"totalHitCount\":0") + .contains("\"totalMissCount\":0").contains("\"classCacheSize\":0"); + } +} From 1485434ab1a9a4a2112203c713e6d7850d4fe653 Mon Sep 17 00:00:00 2001 From: kuleat <303148062@qq.com> Date: Wed, 8 Apr 2026 10:26:21 +0800 Subject: [PATCH 2/3] remove @since comments --- .../reflection/ReflectionCacheEndpointAutoConfiguration.java | 1 - .../ReflectionCacheEndpointAutoConfigurationTests.java | 1 - .../sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java | 1 - .../boot/actuator/reflection/ReflectionCacheEndpointTests.java | 1 - .../reflection/ReflectionCacheAutoConfiguration.java | 1 - .../boot/autoconfigure/reflection/ReflectionCacheProperties.java | 1 - .../reflection/ReflectionCacheAutoConfigurationTests.java | 1 - .../java/com/alipay/sofa/boot/reflection/ReflectionCache.java | 1 - .../com/alipay/sofa/boot/reflection/ReflectionCacheTests.java | 1 - .../actuator/reflection/ReflectionCacheEndpointWebTests.java | 1 - 10 files changed, 10 deletions(-) diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java index 42f11e55b..ae0ebd2c6 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfiguration.java @@ -29,7 +29,6 @@ * Configures {@link ReflectionCacheEndpoint}. * * @author xiaosiyuan - * @since 4.5.0 */ @AutoConfiguration(after = ReflectionCacheAutoConfiguration.class) @ConditionalOnBean(ReflectionCache.class) diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java index e35fb5ca2..02e8229fa 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/reflection/ReflectionCacheEndpointAutoConfigurationTests.java @@ -28,7 +28,6 @@ * Tests for {@link ReflectionCacheEndpointAutoConfiguration}. * * @author xiaosiyuan - * @since 4.5.0 */ public class ReflectionCacheEndpointAutoConfigurationTests { diff --git a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java index 5c1b84c52..bda83bbe2 100644 --- a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java +++ b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java @@ -25,7 +25,6 @@ * Endpoint for viewing and clearing the reflection cache. * * @author xiaosiyuan - * @since 4.5.0 */ @Endpoint(id = "reflection-cache") public class ReflectionCacheEndpoint { diff --git a/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java index 2b4a09d3d..4aaf674cf 100644 --- a/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java +++ b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpointTests.java @@ -25,7 +25,6 @@ * Tests for {@link ReflectionCacheEndpoint}. * * @author xiaosiyuan - * @since 4.5.0 */ public class ReflectionCacheEndpointTests { diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java index 56664ccf4..c262c908c 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfiguration.java @@ -26,7 +26,6 @@ * Configures {@link ReflectionCache}. * * @author xiaosiyuan - * @since 4.5.0 */ @AutoConfiguration @EnableConfigurationProperties(ReflectionCacheProperties.class) diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java index 5e92c470b..32bfcec0a 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheProperties.java @@ -22,7 +22,6 @@ * Properties for {@link ReflectionCacheAutoConfiguration}. * * @author xiaosiyuan - * @since 4.5.0 */ @ConfigurationProperties("sofa.boot.reflection.cache") public class ReflectionCacheProperties { diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java index 7645ec2b9..0219d82ab 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/reflection/ReflectionCacheAutoConfigurationTests.java @@ -27,7 +27,6 @@ * Tests for {@link ReflectionCacheAutoConfiguration}. * * @author xiaosiyuan - * @since 4.5.0 */ public class ReflectionCacheAutoConfigurationTests { diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java index fb03a8930..12f7fbb38 100644 --- a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java @@ -32,7 +32,6 @@ * Caches repeated reflection lookups. * * @author xiaosiyuan - * @since 4.5.0 */ public class ReflectionCache { diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java index 0f04aa9e4..578cdb698 100644 --- a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java @@ -28,7 +28,6 @@ * Tests for {@link ReflectionCache}. * * @author xiaosiyuan - * @since 4.5.0 */ public class ReflectionCacheTests { diff --git a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java index 2dffa63ac..d9b8699c3 100644 --- a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java +++ b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java @@ -31,7 +31,6 @@ * Integration tests for {@code /actuator/reflection-cache}. * * @author xiaosiyuan - * @since 4.5.0 */ @SpringBootTest(classes = ActuatorSofaBootApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { "management.endpoints.web.exposure.include=reflection-cache" }) public class ReflectionCacheEndpointWebTests { From 5e20b6d6fbe4eec601ccc265d66b25cf8eaf15c6 Mon Sep 17 00:00:00 2001 From: kuleat <303148062@qq.com> Date: Fri, 24 Apr 2026 22:13:11 +0800 Subject: [PATCH 3/3] fix: improve reflection cache --- .../health/ReadinessAutoConfiguration.java | 5 +- .../ReadinessAutoConfigurationTests.java | 15 ++++ .../reflection/ReflectionCacheEndpoint.java | 3 +- .../DataSourceAutoConfiguration.java | 5 +- .../DataSourceAutoConfigurationTests.java | 10 +++ .../sofa/boot/reflection/ReflectionCache.java | 23 ++++-- .../boot/reflection/ReflectionCacheTests.java | 74 +++++++++++++++++++ ...tionCacheEndpointUnrestrictedWebTests.java | 57 ++++++++++++++ .../ReflectionCacheEndpointWebTests.java | 7 +- 9 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointUnrestrictedWebTests.java diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java index 4ba5e7840..4be3eb9ed 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfiguration.java @@ -30,6 +30,7 @@ import com.alipay.sofa.common.thread.SofaThreadPoolExecutor; import com.alipay.sofa.common.thread.virtual.SofaVirtualThreadFactory; import org.slf4j.Logger; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -99,10 +100,10 @@ public HealthCheckerProcessor healthCheckerProcessor(HealthProperties healthChec @ConditionalOnMissingBean public HealthIndicatorProcessor healthIndicatorProcessor(HealthProperties healthCheckProperties, ExecutorService readinessHealthCheckExecutor, - ReflectionCache reflectionCache) { + ObjectProvider reflectionCacheProvider) { HealthIndicatorProcessor healthIndicatorProcessor = new HealthIndicatorProcessor(); healthIndicatorProcessor.setHealthCheckExecutor(readinessHealthCheckExecutor); - healthIndicatorProcessor.setReflectionCache(reflectionCache); + reflectionCacheProvider.ifAvailable(healthIndicatorProcessor::setReflectionCache); healthIndicatorProcessor.initExcludedIndicators(healthCheckProperties .getExcludedIndicators()); healthIndicatorProcessor.setParallelCheck(healthCheckProperties.isParallelCheck()); diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java index 28e816109..843b28136 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/health/ReadinessAutoConfigurationTests.java @@ -26,6 +26,7 @@ import com.alipay.sofa.boot.autoconfigure.isle.SofaModuleAutoConfiguration; import com.alipay.sofa.boot.autoconfigure.runtime.SofaRuntimeAutoConfiguration; import com.alipay.sofa.boot.isle.ApplicationRuntimeModel; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.runtime.spi.component.SofaRuntimeContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnJre; @@ -66,6 +67,20 @@ void runShouldHaveReadinessBeans() { .hasBean(ReadinessCheckListener.READINESS_HEALTH_CHECK_EXECUTOR_BEAN_NAME)); } + @Test + void runWithoutReflectionCacheAutoConfigurationShouldHaveReadinessBeans() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReadinessAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=readiness") + .run((context) -> assertThat(context) + .doesNotHaveBean(ReflectionCache.class) + .hasSingleBean(ReadinessCheckListener.class) + .hasSingleBean(HealthCheckerProcessor.class) + .hasSingleBean(HealthIndicatorProcessor.class) + .hasSingleBean(ReadinessCheckCallbackProcessor.class) + .hasBean(ReadinessCheckListener.READINESS_HEALTH_CHECK_EXECUTOR_BEAN_NAME)); + } + @Test void runWhenNotExposedShouldNotHaveReadinessBeans() { this.contextRunner diff --git a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java index bda83bbe2..b1fce0b69 100644 --- a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java +++ b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/reflection/ReflectionCacheEndpoint.java @@ -17,6 +17,7 @@ package com.alipay.sofa.boot.actuator.reflection; import com.alipay.sofa.boot.reflection.ReflectionCache; +import org.springframework.boot.actuate.endpoint.Access; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; @@ -26,7 +27,7 @@ * * @author xiaosiyuan */ -@Endpoint(id = "reflection-cache") +@Endpoint(id = "reflection-cache", defaultAccess = Access.READ_ONLY) public class ReflectionCacheEndpoint { private final ReflectionCache reflectionCache; diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java index 3a4da957a..b4c2b5b91 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfiguration.java @@ -20,6 +20,7 @@ import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.boot.tracer.datasource.DataSourceBeanPostProcessor; import com.alipay.sofa.tracer.plugins.datasource.SmartDataSource; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -43,11 +44,11 @@ public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public static DataSourceBeanPostProcessor dataSourceBeanPostProcessor(Environment environment, - ReflectionCache reflectionCache) { + ObjectProvider reflectionCacheProvider) { String appName = environment.getProperty(SofaTracerConfiguration.TRACER_APPNAME_KEY); DataSourceBeanPostProcessor dataSourceBeanPostProcessor = new DataSourceBeanPostProcessor(); dataSourceBeanPostProcessor.setAppName(appName); - dataSourceBeanPostProcessor.setReflectionCache(reflectionCache); + reflectionCacheProvider.ifAvailable(dataSourceBeanPostProcessor::setReflectionCache); return dataSourceBeanPostProcessor; } } diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java index a7d4b482d..cc0287fd8 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/datasource/DataSourceAutoConfigurationTests.java @@ -17,6 +17,7 @@ package com.alipay.sofa.boot.autoconfigure.tracer.datasource; import com.alipay.sofa.boot.autoconfigure.reflection.ReflectionCacheAutoConfiguration; +import com.alipay.sofa.boot.reflection.ReflectionCache; import com.alipay.sofa.boot.tracer.datasource.DataSourceBeanPostProcessor; import com.alipay.sofa.tracer.plugins.datasource.SmartDataSource; import org.junit.jupiter.api.Test; @@ -46,6 +47,15 @@ public void registerDataSourcePostProcessors() { .hasSingleBean(DataSourceBeanPostProcessor.class)); } + @Test + public void registerDataSourcePostProcessorsWithoutReflectionCacheAutoConfiguration() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(ReflectionCache.class) + .hasSingleBean(DataSourceBeanPostProcessor.class)); + } + @Test public void noDataSourcePostProcessorsWhenSmartDataSourceClassNotExist() { this.contextRunner.withClassLoader(new FilteredClassLoader(SmartDataSource.class)) diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java index 12f7fbb38..90b54d390 100644 --- a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/reflection/ReflectionCache.java @@ -25,6 +25,7 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -305,20 +306,31 @@ private interface LookupResult { private static final class LookupStore { - private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - private final AtomicLong hitCount = new AtomicLong(); + private final AtomicLong hitCount = new AtomicLong(); - private final AtomicLong missCount = new AtomicLong(); + private final AtomicLong missCount = new AtomicLong(); + + private final AtomicInteger missCacheSize = new AtomicInteger(); private V get(K key, Function loader) { + V result = cache.get(key); + if (result != null) { + hitCount.incrementAndGet(); + return result; + } + AtomicBoolean cacheMiss = new AtomicBoolean(false); - V result = cache.computeIfAbsent(key, cacheKey -> { + result = cache.computeIfAbsent(key, cacheKey -> { cacheMiss.set(true); return loader.apply(cacheKey); }); if (cacheMiss.get()) { missCount.incrementAndGet(); + if (result.isMiss()) { + missCacheSize.incrementAndGet(); + } } else { hitCount.incrementAndGet(); } @@ -327,13 +339,14 @@ private V get(K key, Function loader) { private LookupStats getStats() { return new LookupStats(hitCount.get(), missCount.get(), cache.size(), - (int) cache.values().stream().filter(LookupResult::isMiss).count()); + missCacheSize.get()); } private void clear() { cache.clear(); hitCount.set(0); missCount.set(0); + missCacheSize.set(0); } } diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java index 578cdb698..72f938e82 100644 --- a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/reflection/ReflectionCacheTests.java @@ -118,6 +118,80 @@ void disabledCacheShouldBypassStatistics() throws Exception { assertThat(stats.getConstructorCacheSize()).isZero(); } + @Test + void setEnabledFalseShouldClearCacheState() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(); + + reflectionCache.forName(SampleTarget.class.getName()); + reflectionCache.forName(SampleTarget.class.getName()); + reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + reflectionCache.getDeclaredConstructor(SampleTarget.class, String.class); + reflectionCache.getDeclaredConstructor(SampleTarget.class, String.class); + + assertThat(reflectionCache.getStats().getTotalHitCount()).isEqualTo(3); + assertThat(reflectionCache.getStats().getTotalMissCount()).isEqualTo(3); + + reflectionCache.setEnabled(false); + + assertThat(reflectionCache.isEnabled()).isFalse(); + ReflectionCache.CacheStats disabledStats = reflectionCache.getStats(); + assertThat(disabledStats.getTotalHitCount()).isZero(); + assertThat(disabledStats.getTotalMissCount()).isZero(); + assertThat(disabledStats.getClassCacheSize()).isZero(); + assertThat(disabledStats.getMethodCacheSize()).isZero(); + assertThat(disabledStats.getConstructorCacheSize()).isZero(); + + assertThat(reflectionCache.forName(SampleTarget.class.getName())).isSameAs( + SampleTarget.class); + assertThat(reflectionCache.getStats().getTotalMissCount()).isZero(); + + reflectionCache.setEnabled(true); + + assertThat(reflectionCache.isEnabled()).isTrue(); + assertThat(reflectionCache.forName(SampleTarget.class.getName())).isSameAs( + SampleTarget.class); + assertThat(reflectionCache.getStats().getClassMissCount()).isEqualTo(1); + } + + @Test + void clearShouldResetCacheState() throws Exception { + ReflectionCache reflectionCache = new ReflectionCache(); + + reflectionCache.forName(SampleTarget.class.getName()); + reflectionCache.forName(SampleTarget.class.getName()); + reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + reflectionCache.findMethod(SampleTarget.class, "greet", String.class); + reflectionCache.getDeclaredConstructor(SampleTarget.class, String.class); + reflectionCache.getDeclaredConstructor(SampleTarget.class, String.class); + assertThatThrownBy( + () -> reflectionCache.forName("com.alipay.sofa.boot.reflection.DoesNotExist")) + .isInstanceOf(ClassNotFoundException.class); + assertThatThrownBy( + () -> reflectionCache.forName("com.alipay.sofa.boot.reflection.DoesNotExist")) + .isInstanceOf(ClassNotFoundException.class); + assertThat(reflectionCache.findMethod(SampleTarget.class, "missingMethod")).isNull(); + assertThat(reflectionCache.findMethod(SampleTarget.class, "missingMethod")).isNull(); + assertThatThrownBy( + () -> reflectionCache.getDeclaredConstructor(SampleTarget.class, Integer.class)) + .isInstanceOf(NoSuchMethodException.class); + assertThatThrownBy( + () -> reflectionCache.getDeclaredConstructor(SampleTarget.class, Integer.class)) + .isInstanceOf(NoSuchMethodException.class); + + reflectionCache.clear(); + + ReflectionCache.CacheStats stats = reflectionCache.getStats(); + assertThat(stats.getTotalHitCount()).isZero(); + assertThat(stats.getTotalMissCount()).isZero(); + assertThat(stats.getClassCacheSize()).isZero(); + assertThat(stats.getMethodCacheSize()).isZero(); + assertThat(stats.getConstructorCacheSize()).isZero(); + assertThat(stats.getClassMissCacheSize()).isZero(); + assertThat(stats.getMethodMissCacheSize()).isZero(); + assertThat(stats.getConstructorMissCacheSize()).isZero(); + } + static class SampleTarget { private final String prefix; diff --git a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointUnrestrictedWebTests.java b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointUnrestrictedWebTests.java new file mode 100644 index 000000000..d261216ed --- /dev/null +++ b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointUnrestrictedWebTests.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.smoke.tests.actuator.reflection; + +import com.alipay.sofa.boot.reflection.ReflectionCache; +import com.alipay.sofa.smoke.tests.actuator.ActuatorSofaBootApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for unrestricted {@code /actuator/reflection-cache} access. + * + * @author xiaosiyuan + */ +@SpringBootTest(classes = ActuatorSofaBootApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { + "management.endpoints.web.exposure.include=reflection-cache", + "management.endpoint.reflection-cache.access=unrestricted" }) +public class ReflectionCacheEndpointUnrestrictedWebTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ReflectionCache reflectionCache; + + @Test + public void reflectionCacheEndpointShouldClearWhenAccessIsUnrestricted() throws Exception { + reflectionCache.forName(String.class.getName()); + reflectionCache.forName(String.class.getName()); + + ResponseEntity response = restTemplate.postForEntity("/actuator/reflection-cache", + null, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("\"totalHitCount\":0") + .contains("\"totalMissCount\":0").contains("\"classCacheSize\":0"); + } +} diff --git a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java index d9b8699c3..4b9f686e9 100644 --- a/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java +++ b/sofa-boot-tests/sofa-boot-smoke-tests/sofa-boot-smoke-tests-actuator/src/test/java/com/alipay/sofa/smoke/tests/actuator/reflection/ReflectionCacheEndpointWebTests.java @@ -42,7 +42,8 @@ public class ReflectionCacheEndpointWebTests { private ReflectionCache reflectionCache; @Test - public void reflectionCacheEndpoint() throws Exception { + public void reflectionCacheEndpointShouldExposeStatsWithReadOnlyAccessByDefault() + throws Exception { reflectionCache.forName(String.class.getName()); reflectionCache.forName(String.class.getName()); @@ -53,8 +54,6 @@ public void reflectionCacheEndpoint() throws Exception { .contains("\"classMissCount\":1").contains("\"classCacheSize\":1"); response = restTemplate.postForEntity("/actuator/reflection-cache", null, String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("\"totalHitCount\":0") - .contains("\"totalMissCount\":0").contains("\"classCacheSize\":0"); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED); } }