Skip to content

Component scanning fails against non-loadable annotation type with enum array on Java 25 #36432

@tkanafa-atlassian

Description

@tkanafa-atlassian

I've spotted inconsistent behaviour when running the attached code example.tar.gz on java 21 and 25.

Note that the class in the scanned directory is not used by the spring.
The class itself has annotated method, the used annotation and annotation value classes are "provided" scope - meaning they will be unavailable at runtime.

Our use case is that we are using spring in osgi environment where the annotated class is run in multiple environments - in some it's instantiated using custom code in other it's not used.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>test</groupId>
    <artifactId>spring7test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>7.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.atlassian.struts2</groupId>
            <artifactId>struts-support</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

SpringTestMain.java

package org.springtest;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTestMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring.xml");
        System.out.println(classPathXmlApplicationContext.getBeanDefinitionCount());
    }
}

TestClass.java

package org.springtest;

import com.atlassian.struts.httpmethod.HttpMethod;
import com.atlassian.struts.httpmethod.PermittedMethods;

public class TestClass {

    @PermittedMethods(HttpMethod.GET)
    public void executeIt() {
        System.out.println("Execute!");
    }
}

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springtest"/>

</beans>

When running the org.springtest.SpringTestMain#main on java 21 it executes without throwing an exception.
When running on the java 25 it throws

Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [/Users/tkanafa/git/spring7test/target/classes/org/springtest/TestClass.class]
	at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:504)
	at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:321)
	at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:276)
	at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:90)
	at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)
	at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1379)
	at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1360)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:176)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:147)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:94)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:518)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:398)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:345)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:317)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:216)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:187)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:249)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:131)
	at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:96)
	at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
	at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:720)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:592)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
	at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
	at org.springtest.SpringTestMain.main(SpringTestMain.java:7)
Caused by: java.lang.IllegalArgumentException: Could not find class [com.atlassian.struts.httpmethod.HttpMethod]
	at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:353)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.loadClass(ClassFileAnnotationMetadata.java:116)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.resolveArrayElementType(ClassFileAnnotationMetadata.java:168)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.parseArrayValue(ClassFileAnnotationMetadata.java:135)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.readAnnotationValue(ClassFileAnnotationMetadata.java:103)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.createMergedAnnotation(ClassFileAnnotationMetadata.java:72)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.lambda$createMergedAnnotations$0(ClassFileAnnotationMetadata.java:55)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
	at java.base/java.util.Collections$2.tryAdvance(Collections.java:5182)
	at java.base/java.util.Collections$2.forEachRemaining(Collections.java:5190)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:723)
	at org.springframework.core.type.classreading.ClassFileAnnotationMetadata.createMergedAnnotations(ClassFileAnnotationMetadata.java:57)
	at org.springframework.core.type.classreading.ClassFileMethodMetadata.lambda$of$2(ClassFileMethodMetadata.java:151)
	at java.base/java.util.Optional.map(Optional.java:260)
	at org.springframework.core.type.classreading.ClassFileMethodMetadata.of(ClassFileMethodMetadata.java:151)
	at org.springframework.core.type.classreading.ClassFileClassMetadata$Builder.method(ClassFileClassMetadata.java:295)
	at org.springframework.core.type.classreading.ClassFileClassMetadata.lambda$of$0(ClassFileClassMetadata.java:213)
	at java.base/java.util.ArrayList$Itr.forEachRemaining(ArrayList.java:1086)
	at java.base/java.util.Collections$UnmodifiableCollection$1.forEachRemaining(Collections.java:1087)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:803)
	at org.springframework.core.type.classreading.ClassFileClassMetadata.of(ClassFileClassMetadata.java:191)
	at org.springframework.core.type.classreading.ClassFileMetadataReader.<init>(ClassFileMetadataReader.java:45)
	at org.springframework.core.type.classreading.ClassFileMetadataReaderFactory.getMetadataReader(ClassFileMetadataReaderFactory.java:62)
	at org.springframework.core.type.classreading.CachingMetadataReaderFactory.getMetadataReader(CachingMetadataReaderFactory.java:127)
	at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:464)
	... 25 more
Caused by: java.lang.ClassNotFoundException: com.atlassian.struts.httpmethod.HttpMethod
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:547)
	at org.springframework.util.ClassUtils.forName(ClassUtils.java:302)
	at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:343)
	... 54 more

From my debugging on java 21 this classes are used:

abstract class MetadataReaderFactoryDelegate {

	static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) {
		return new SimpleMetadataReaderFactory(resourceLoader);
	}

	static MetadataReaderFactory create(@Nullable ClassLoader classLoader) {
		return new SimpleMetadataReaderFactory(classLoader);
	}
}

On java 25 these classes are used (from the versions in META-INF):

org.springframework.core.type.classreading.MetadataReaderFactoryDelegate
abstract class MetadataReaderFactoryDelegate {
    static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) {
        return new ClassFileMetadataReaderFactory(resourceLoader);
    }

    static MetadataReaderFactory create(@Nullable ClassLoader classLoader) {
        return new ClassFileMetadataReaderFactory(classLoader);
    }
}

Is it expected behaviour that processing the classes with annotations that are not present on the classpath results in an exception thrown?

Metadata

Metadata

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions