From 6045ec55a8fc3efca35bb62523e03d5a3d6d2177 Mon Sep 17 00:00:00 2001 From: cookie-meringue Date: Tue, 19 May 2026 05:46:44 +0900 Subject: [PATCH] Optimize ClassNameReader.getClassName via direct ASM API getClassName now calls ClassReader.getClassName() directly instead of routing through the visitor-based getClassInfo. Previously, it allocated a List and a ClassVisitor and decoded super_class and every interface name only to discard all but the first element. The method is on the hot path of every CGLIB proxy class definition, so this change significantly lowers its per-call processing cost. Signed-off-by: cookie-meringue --- .../cglib/core/ClassNameReaderBenchmark.java | 133 ++++++++++++++++++ .../cglib/core/ClassNameReader.java | 4 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 spring-core/src/jmh/java/org/springframework/cglib/core/ClassNameReaderBenchmark.java diff --git a/spring-core/src/jmh/java/org/springframework/cglib/core/ClassNameReaderBenchmark.java b/spring-core/src/jmh/java/org/springframework/cglib/core/ClassNameReaderBenchmark.java new file mode 100644 index 000000000000..7f6ddd513473 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/cglib/core/ClassNameReaderBenchmark.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.cglib.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.asm.ClassReader; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; + +/** + * Benchmarks for {@link ClassNameReader#getClassName(ClassReader)}. + * Comparing the legacy {@code getClassInfo(r)[0]} delegation against a direct {@code ClassReader.getClassName()} call. + * + * @author Daehyeon Kim + */ +public class ClassNameReaderBenchmark { + + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 2) + @Measurement(iterations = 5, time = 2) + public static class WithoutInterfaces { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + public ClassReader classReader; + + @Setup(Level.Trial) + public void setup() { + this.classReader = generateProxyReader(new Class[0]); + } + } + + @Benchmark + public String viaGetClassInfo(BenchmarkState state) { + return ClassNameReader.getClassInfo(state.classReader)[0]; + } + + @Benchmark + public String viaGetClassName(BenchmarkState state) { + return state.classReader.getClassName().replace('/', '.'); + } + } + + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Warmup(iterations = 5, time = 2) + @Measurement(iterations = 5, time = 2) + public static class WithInterfaces { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + public ClassReader classReader; + + @Setup(Level.Trial) + public void setup() { + this.classReader = generateProxyReader( + new Class[]{Cloneable.class, Comparable.class, Runnable.class} + ); + } + } + + @Benchmark + public String viaGetClassInfo(BenchmarkState state) { + return ClassNameReader.getClassInfo(state.classReader)[0]; + } + + @Benchmark + public String viaGetClassName(BenchmarkState state) { + return state.classReader.getClassName().replace('/', '.'); + } + } + + private static ClassReader generateProxyReader(Class[] interfaces) { + CapturingGeneratorStrategy strategy = new CapturingGeneratorStrategy(); + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(ProxyTarget.class); + enhancer.setCallbackType(MethodInterceptor.class); + enhancer.setUseCache(false); + enhancer.setInterfaces(interfaces); + enhancer.setStrategy(strategy); + enhancer.createClass(); + return strategy.createClassReader(); + } + + public static class ProxyTarget { + } + + private static class CapturingGeneratorStrategy implements GeneratorStrategy { + + private static final GeneratorStrategy delegate = DefaultGeneratorStrategy.INSTANCE; + + private byte[] captured; + + @Override + public byte[] generate(ClassGenerator cg) throws Exception { + this.captured = delegate.generate(cg); + return this.captured; + } + + private ClassReader createClassReader() { + return new ClassReader(this.captured); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ClassNameReader.java b/spring-core/src/main/java/org/springframework/cglib/core/ClassNameReader.java index 4371fcdc3a4b..0e5034c1a3d7 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ClassNameReader.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ClassNameReader.java @@ -34,9 +34,11 @@ private ClassNameReader() { private static class EarlyExitException extends RuntimeException { } + // SPRING PATCH BEGIN public static String getClassName(ClassReader r) { - return getClassInfo(r)[0]; + return r.getClassName().replace('/', '.'); } + // SPRING PATCH END public static String[] getClassInfo(ClassReader r) { final List array = new ArrayList<>();