Affects: \6.1.4

We have encountered an issue where RepositoryFactoryInformation is no longer autowired correctly.

E.g. @Service public class MyService { private final List<RepositoryFactoryInformation<?, ?>> repositoryFactoryInformations; private final List<JpaRepositoryFactoryBean<?, ?, ?>> jpaRepositoryFactoryBeans;

JpaRepositoryFactoryBean is found, but RepositoryFactoryInformation is not, although JpaRepositoryFactoryBean also implements RepositoryFactoryInformation.

A small project with a more complete description and reproduction is available here: https://github.com/Pinusar/RepositoryFactoryInformationBug

The issue might be related to this commit: https://github.com/spring-projects/spring-framework/commit/0e9eab55ceff1a7b4f8720f2c3725aaac2bedb48#diff-bc3adc4d2ff654e867247e2887b0f6e738b2d07bc55f6ff45328d406be53c74f

Comment From: jhoeller

Could you indicate more specifically between which versions you see this regression: from 5.3.x to 6.1.4, I suppose?

To be on the safe side, you can also try against 6.1.5 even though I do not expect a difference there. The current behavior is present since 6.0 already and has not changed recently.

Comment From: Pinusar

I just tried this also with Spring Framework 6.1.5 but behavior stayed the same.

And the regression seems to have appeared between versions 6.0.17 and 6.1.1

Comment From: quaff

I confirm it works with Spring Boot 3.1.9/Spring Framework 6.0.17 but failed with Spring Boot 3.2.3/Spring Framework 6.1.4. Here is a minimal reproducer:

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.test.context.ContextConfiguration;

import jakarta.persistence.Entity;

@DataJpaTest
@EnableJpaRepositories(basePackageClasses = InjectTests.TestEntityRepository.class, considerNestedRepositories = true)
@EntityScan(basePackageClasses = InjectTests.TestEntity.class)
@ContextConfiguration(classes = InjectTests.class)
class InjectTests {

    @Autowired
    private List<RepositoryFactoryInformation<?, ?>> repositoryFactoryInformations;

    @Autowired
    private List<JpaRepositoryFactoryBean<?, ?, ?>> jpaRepositoryFactoryBeans;

    @Test
    void test() {
        assertThat(repositoryFactoryInformations).isEqualTo(jpaRepositoryFactoryBeans);
    }

    interface TestEntityRepository extends JpaRepository<TestEntity, Long> {

    }

    @Entity
    static class TestEntity extends AbstractPersistable<Long> {

    }
}

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version 'latest.release'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'com.h2database:h2'
}

tasks.named('test') {
    useJUnitPlatform()
}

Comment From: murkaje

Created another reproducer using spring-context and replicating the behavior of spring-data-commons. The issue is present in spring-beans 6.0.0 until latest (6.1.5) and works fine in spring-beans 5.x versions e.g. 5.3.33

This issue started happening because spring-data-commons is generating bean definitions for FactoryBeans with targetType being set. This seems to be the commit that introduced the change: https://github.com/spring-projects/spring-data-commons/commit/98f20a4457ace08348a8e568eaed2c451e127699 Note the line

beanDefinition.setTargetType(getRepositoryInterface(configuration));

The reproducer:

@Configuration
public class Main {

  public interface GenericIntf<T> {}

  // Default bean definition doesn't have targetType set so won't reproduce
//  @Component
  public static class MyFactoryBean<T extends CharSequence> implements FactoryBean<T>, GenericIntf<T> {
    @Override
    public T getObject() { return (T) "bean"; }

    @Override
    public Class<?> getObjectType() { return String.class; }
  }

  @Component
  public record FactoryHolder(List<GenericIntf<?>> factoryBeans) {  // Inject site is not FactoryBean
    public FactoryHolder {
      System.out.println(factoryBeans);
    }
  }

  @Component  // Uncomment and use @Component on MyFactoryBean to see expected behavior
  public static class BFPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
      RootBeanDefinition rbd = new RootBeanDefinition();
      rbd.setBeanClass(MyFactoryBean.class);
      // Important: Replicate org.springframework.data.repository.config.RepositoryConfigurationDelegate#registerRepositoriesIn
      // behavior of setting targetType, required to hit other branch in org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver.checkGenericTypeMatch
      rbd.setTargetType(ResolvableType.forClassWithGenerics(MyFactoryBean.class, String.class));
      ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("myBean", rbd);
      System.out.println("Registered FactoryBean def: " + rbd);
    }
  }

  public static void main(String[] args) {
    new AnnotationConfigApplicationContext(Main.class).start();
  }
}

The issue seems to happen due to the change https://github.com/spring-projects/spring-framework/commit/0e9eab55ceff1a7b4f8720f2c3725aaac2bedb48#diff-bc3adc4d2ff654e867247e2887b0f6e738b2d07bc55f6ff45328d406be53c74f where the following check is done

if (resolvedClass != null && FactoryBean.class.isAssignableFrom(resolvedClass)) {
    Class<?> typeToBeMatched = dependencyType.resolve();
    if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched)) {
        targetType = targetType.getGeneric();

The bean candidate MyFactoryBean is a FactoryBean so it continues. The injection site expects the interface GenericIntf which doesn't extend FactoryBean. Now it sets targetType to the generic parameter type of MyFactoryBean<String>, so targetType = String.class. Afterwards the check dependencyType.isAssignableFrom(targetType) no longer passes as GenericIntf is not assignable from String. However without the assignment the check would read (GenericIntf).isAssignableFrom(MyFactoryBean) which would be true and the injection happens as in 5.x versions.

Comment From: snicoll

@murkaje I haven't tried it yet (it would be better to share something we can clone or download rather than having to copy paste code in text from a comment), but what the OP shared is working with Spring Framework 6.0.x. Yours might be a different incarnation or a totally different problem if it's broken since 6.0.0.

Comment From: snicoll

I can see that there is a change in Spring Data between Spring Boot 3.1.x and Spring Boot 3.2.x which explains my confusion as I didn't see any change in framework. We'll have a look with them and see if we want to handle that additional tricky case.