Spring Framework currently shades ASM for several features: 1. bytecode generation with cglib 2. reading class metadata from bytecode

Our usage of ASM is not a problem these days and we tend to allow use of up-to-date JDK versions in Spring apps thanks to swift upgrades of our shaded version. Still, we should consider our options with the new ClassFile API.

Spring Framework maintains two implementations of metadata reading: one based on Class reflection (org.springframework.core.type.StandardClassMetadata) and another one based on bytecode reading with ASM (org.springframework.core.type.classreading.SimpleMetadataReaderFactory). This API does not expose any ASM type and is a good use case for the ClassFile API.

The ClassFile API JEP 457 is currently in preview mode but should be out of preview for JDK 24.

With this issue, we should:

  • [x] prototype an implementation variant of SimpleMetadataReaderFactory based on ClassFile
  • [x] consider how this variant should be involved. As a feature flag only? Should it be the default on JDK24+?
  • [x] decide which Spring Framework version we should target; while not user facing, this is an important change that we should schedule accordingly

Comment From: bclozel

I've extended the existing test suite and got a working prototype with a ClassFileMetadataReader that passes all tests on Java 24 early access.

So far I chose to add new static factory methods to MetadataReaderFactory - instantiating the SimpleMetadataReaderFactory on JDK < 24 and ClassFileMetadataReaderFactory on JDK 24+. This is the easiest way to test things so far, but we could consider a feature flag in SpringProperties.

Comment From: liach

Should it be the default on JDK24+?

The ClassFile API aims to reduce the hussles with ASM dependency bumps when Java version is bumped - for example, to parse Java 23 class files, you need to bump ASM to 9.7.1. The ClassFile API, being part of the JDK, naturally parses new class files, and thus improves the compatibility with future Java versions.

Comment From: bclozel

@liach Thanks for your input. So you're saying this should be the default for Java 24+? You can't think of a case where falling back to ASM would be preferable? Why?

Comment From: liach

I recommend so. I think ClassFile API already covers whatever ASM parses; feel free to keep ASM as a fallback, though I cannot think of a use case (except custom attribute reading/writing with Code offsets or explicit max stack and locals, but I don't think metadata handling needs those). Feel free to check out https://github.com/raphw/asm-jdk-bridge for how ASM functionalities are rerouted to JDK, but this for sure will be less efficient than using the JDK APIs directly.

Comment From: bclozel

Temporarily reverting these commits since the latest Java 24 early access version fails with Gradle:


error: An unhandled exception was thrown by the Error Prone static analysis plugin.
     Please report this at https://github.com/google/error-prone/issues/new and include the following:

     error-prone version: 2.9.0
     BugPattern: (see stack trace)
     Stack Trace:
     java.lang.NoSuchFieldError: Class com.sun.tools.javac.code.TypeTag does not have member field 'com.sun.tools.javac.code.TypeTag UNKNOWN'
        at com.google.errorprone.util.ASTHelpers.<clinit>(ASTHelpers.java:1122)
        at com.google.errorprone.SuppressionInfo$1.visitClass(SuppressionInfo.java:117)
        at com.google.errorprone.SuppressionInfo$1.visitClass(SuppressionInfo.java:114)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCClassDecl.accept(JCTree.java:900)
        at jdk.compiler/com.sun.source.util.SimpleTreeVisitor.visit(SimpleTreeVisitor.java:80)
        at jdk.compiler/com.sun.source.util.SimpleTreeVisitor.visit(SimpleTreeVisitor.java:94)
        at com.google.errorprone.SuppressionInfo.forCompilationUnit(SuppressionInfo.java:121)
        at com.google.errorprone.scanner.Scanner.updateSuppressions(Scanner.java:89)
        at com.google.errorprone.scanner.Scanner.scan(Scanner.java:56)
        at com.google.errorprone.scanner.ErrorProneScannerTransformer.apply(ErrorProneScannerTransformer.java:43)
        at com.google.errorprone.ErrorProneAnalyzer.finished(ErrorProneAnalyzer.java:152)
        at jdk.compiler/com.sun.tools.javac.api.MultiTaskListener.finished(MultiTaskListener.java:133)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1432)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1379)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:964)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.lambda$doCall$0(JavacTaskImpl.java:104)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.invocationHelper(JavacTaskImpl.java:152)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:100)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:94)
        at org.gradle.internal.compiler.java.IncrementalCompileTask.call(IncrementalCompileTask.java:92)
        at org.gradle.api.internal.tasks.compile.AnnotationProcessingCompileTask.call(AnnotationProcessingCompileTask.java:94)
        at org.gradle.api.internal.tasks.compile.ResourceCleaningCompilationTask.call(ResourceCleaningCompilationTask.java:57)
        at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:78)
        at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:46)
        at org.gradle.api.internal.tasks.compile.daemon.AbstractIsolatedCompilerWorkerExecutor$CompilerWorkAction.execute(AbstractIsolatedCompilerWorkerExecutor.java:78)
        at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
        at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:54)
        at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:48)
        at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
        at org.gradle.workers.internal.AbstractClassLoaderWorker.executeInClassLoader(AbstractClassLoaderWorker.java:48)
        at org.gradle.workers.internal.FlatClassLoaderWorker.run(FlatClassLoaderWorker.java:32)
        at org.gradle.workers.internal.FlatClassLoaderWorker.run(FlatClassLoaderWorker.java:22)
        at org.gradle.workers.internal.WorkerDaemonServer.run(WorkerDaemonServer.java:103)
        at org.gradle.workers.internal.WorkerDaemonServer.run(WorkerDaemonServer.java:72)
        at org.gradle.process.internal.worker.request.WorkerAction$1.call(WorkerAction.java:152)
        at org.gradle.process.internal.worker.child.WorkerLogEventListener.withWorkerLoggingProtocol(WorkerLogEventListener.java:41)
        at org.gradle.process.internal.worker.request.WorkerAction.lambda$run$1(WorkerAction.java:149)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
        at org.gradle.process.internal.worker.request.WorkerAction.run(WorkerAction.java:141)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
        at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1095)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:619)
        at java.base/java.lang.Thread.run(Thread.java:1447)
1 error

Comment From: liach

See google/error-prone#4656