Skip to content

Spring JPA repository methods returning Sets fail in native mode with Spring Boot 4.0.0 #4094

@danthe1st

Description

@danthe1st

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 CrudRepository with a method querying these elements as a Set:
public interface TestRepo extends CrudRepository<Person, Long> {
	Set<Person> getAllByName(String name);
}
  • Call that method somewhere
  • The method call succeeds with mvn spring-boot:run but fails with mvn -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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions