Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic interface on FactoryBean class not autowired in case of targetType mismatch #32489

Closed
Pinusar opened this issue Mar 19, 2024 · 6 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: regression A bug that is also a regression
Milestone

Comments

@Pinusar
Copy link

Pinusar commented Mar 19, 2024

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: 0e9eab5#diff-bc3adc4d2ff654e867247e2887b0f6e738b2d07bc55f6ff45328d406be53c74f

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 19, 2024
@jhoeller
Copy link
Contributor

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.

@Pinusar
Copy link
Author

Pinusar commented Mar 19, 2024

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

@quaff
Copy link
Contributor

quaff commented Mar 20, 2024

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()
}

@murkaje
Copy link

murkaje commented Mar 21, 2024

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: spring-projects/spring-data-commons@98f20a4
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 0e9eab5#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.

@snicoll
Copy link
Member

snicoll commented Mar 22, 2024

@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.

@snicoll
Copy link
Member

snicoll commented Mar 22, 2024

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.

@snicoll snicoll self-assigned this Mar 22, 2024
snicoll added a commit to snicoll/spring-framework that referenced this issue Mar 27, 2024
@snicoll snicoll added the in: core Issues in core modules (aop, beans, core, context, expression) label Apr 4, 2024
@jhoeller jhoeller self-assigned this Apr 8, 2024
@jhoeller jhoeller added type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 8, 2024
@jhoeller jhoeller changed the title RepositoryFactoryInformation bean not autowired correctly Generic interface on FactoryBean class not autowired in case of targetType mismatch Apr 8, 2024
@jhoeller jhoeller added the for: backport-to-6.0.x Marks an issue as a candidate for backport to 6.0.x label Apr 8, 2024
@github-actions github-actions bot added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-6.0.x Marks an issue as a candidate for backport to 6.0.x labels Apr 8, 2024
@jhoeller jhoeller added this to the 6.1.6 milestone Apr 8, 2024
snicoll added a commit that referenced this issue Apr 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

6 participants