-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
When creating a Spring JPA repository with a query method returning a Set, it runs fine in JVM mode. However, when compiling it with native-image, it fails with a java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.util.Set.
Reproducer
- Create a project with Spring Boot 4, Spring JPA and native-image (using H2 as an example here)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>- Create an entity with some non-unique field:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters, setters, equals and hashCode omitted
}- Create a
CrudRepositorywith a method querying these elements as aSet:
public interface TestRepo extends CrudRepository<Person, Long> {
Set<Person> getAllByName(String name);
}- Call that method somewhere
- The method call succeeds with
mvn spring-boot:runbut fails withmvn -Pnative native:compile && ./target/the-app
For a full reproducer, visit https://github.com/danthe1st/spring-jpa-native-ClassCastException-repro and build the project with mvn -Pnative native:compile and run the generated binary (./target/spring-jpa-native-cce) or check the GitHub Actions build log.
Additional information
Stack trace/logs:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.0.0)
2025-11-22T10:13:42.261Z INFO 2665 --- [ main] i.g.d.spring_demo.SpringDemoApplication : Starting AOT-processed SpringDemoApplication using Java 25.0.1 with PID 2665 (/home/runner/work/spring-jpa-native-ClassCastException-repro/spring-jpa-native-ClassCastException-repro/target/spring-jpa-native-cce started by runner in /home/runner/work/spring-jpa-native-ClassCastException-repro/spring-jpa-native-ClassCastException-repro)
2025-11-22T10:13:42.261Z INFO 2665 --- [ main] i.g.d.spring_demo.SpringDemoApplication : No active profile set, falling back to 1 default profile: "default"
2025-11-22T10:13:42.275Z INFO 2665 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-22T10:13:42.278Z INFO 2665 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 7.1.8.Final
2025-11-22T10:13:42.289Z INFO 2665 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2025-11-22T10:13:42.291Z INFO 2665 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:0f990fee-7a17-4f37-ae8b-ca8252e1b228 user=SA
2025-11-22T10:13:42.291Z INFO 2665 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2025-11-22T10:13:42.291Z INFO 2665 --- [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [jdbc:h2:mem:0f990fee-7a17-4f37-ae8b-ca8252e1b228]
Database driver: H2 JDBC Driver
Database dialect: H2Dialect
Database version: 2.4.240
Default catalog/schema: 0F990FEE-7A17-4F37-AE8B-CA8252E1B228/PUBLIC
Autocommit mode: undefined/unknown
Isolation level: READ_COMMITTED [default READ_COMMITTED]
JDBC fetch size: 100
Pool: DatasourceConnectionProviderImpl
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-22T10:13:42.301Z INFO 2665 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-22T10:13:42.303Z INFO 2665 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-11-22T10:13:42.309Z INFO 2665 --- [ main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
2025-11-22T10:13:42.321Z INFO 2665 --- [ main] i.g.d.spring_demo.SpringDemoApplication : Started SpringDemoApplication in 0.071 seconds (process running for 0.076)
Application run failed
java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.util.Set
at io.github.danthe1st.spring_demo.TestRepoImpl__AotRepository.getAllByName(TestRepoImpl__AotRepository.java:36)
at java.base@25.0.1/java.lang.reflect.Method.invoke(Method.java:565)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:278)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:169)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:545)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:290)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:708)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:171)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:146)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:370)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:166)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy4/jdk.proxy4.$Proxy/sdfc2056a.getAllByName(Unknown Source)
at io.github.danthe1st.spring_demo.SpringDemoApplication.run(SpringDemoApplication.java:22)
at org.springframework.boot.SpringApplication.lambda$callRunner$1(SpringApplication.java:801)
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82)
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60)
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:809)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800)
at org.springframework.boot.SpringApplication.lambda$callRunners$0(SpringApplication.java:785)
at java.base@25.0.1/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
at java.base@25.0.1/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357)
at java.base@25.0.1/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:571)
at java.base@25.0.1/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
at java.base@25.0.1/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
at java.base@25.0.1/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
at java.base@25.0.1/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
at java.base@25.0.1/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:785)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:328)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1374)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
at io.github.danthe1st.spring_demo.SpringDemoApplication.main(SpringDemoApplication.java:17)
at java.base@25.0.1/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
2025-11-22T10:13:42.355Z INFO 2665 --- [ main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-11-22T10:13:42.355Z INFO 2665 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2025-11-22T10:13:42.356Z INFO 2665 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
This works with Spring Boot 3.5.5 so I'd consider this to be a regression.
Operating System: Linux, (K)Ubuntu 25.10
JDK:
java version "25" 2025-09-16 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 25+37.1 (build 25+37-LTS-jvmci-b01)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 25+37.1 (build 25+37-LTS-jvmci-b01, mixed mode, sharing)
The (faulty) generated implementation of the repository is the following:
package io.github.danthe1st.spring_demo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import java.lang.String;
import java.util.Set;
import org.springframework.aot.generate.Generated;
import org.springframework.data.jpa.repository.aot.AotRepositoryFragmentSupport;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
/**
* AOT generated JPA repository implementation for {@link TestRepo}.
*/
@Generated
public class TestRepoImpl__AotRepository extends AotRepositoryFragmentSupport {
private final RepositoryFactoryBeanSupport.FragmentCreationContext context;
private final EntityManager entityManager;
public TestRepoImpl__AotRepository(EntityManager entityManager,
RepositoryFactoryBeanSupport.FragmentCreationContext context) {
super(QueryEnhancerSelector.DEFAULT_SELECTOR, context);
this.entityManager = entityManager;
this.context = context;
}
/**
* AOT generated implementation of {@link TestRepo#getAllByName(java.lang.String)}.
*/
public Set<Person> getAllByName(String name) {
String queryString = "SELECT p FROM Person p WHERE p.name = :name";
Query query = this.entityManager.createQuery(queryString);
query.setParameter("name", name);
return (Set<Person>) query.getResultList();
}
}The (Set<Person>) query.getResultList() cast doesn't seem correct and it should probably match the non-native implementation and actually return a Set instead of casting a List to one.
Originally reported in spring-projects/spring-boot#48243