javaspring-bootspring-mvcspring-data-jpafactory-pattern

Repository not injected in service class when initialized through a factory class


I have multiple implementations for a method, so I decided to use Factory design pattern for it. Here is the implementation for factory class:

@Component
public class ProcessFileFactory {

    private static ApplicationContext applicationContext;

    public ProcessFileFactory() {
        applicationContext = new AnnotationConfigApplicationContext(
                ProcessDoctorFile.class,
                ProcessPatientFile.class
        );
    }

    public static ProcessImportFiles getInstance(String fileType) {
        if (ImportTypes.DOCTORS.toString().equals(fileType)) {
            return applicationContext.getBean("processDoctorFile", ProcessImportFiles.class);
        }
        if (ImportTypes.PATIENTS.toString().equals(fileType)) {
            return applicationContext.getBean("processPatientFile", ProcessImportFiles.class);
        }

        return null;
    }
}

ProcessImportFiles is the interface where I defined the method which has to be implemented differently. And here is one implementation of the method from the interface:

@Service
public class ProcessDoctorFile implements ProcessImportFiles {
    @Autowired
    private DoctorRepository doctorRepository;

    @Override
    public void processImportFile(String fileName, Sheet sheet) {
        System.out.println("doctors");
    }
}

Also, this is how I call the the method in the rest controller:

 ProcessFileFactory.getInstance(fileType).processImportFile(file.getName(), sheet);

But I receive this error:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'processFileFactory' defined in file [C:\Workspace\MedicalStatistics\target\classes\app\one\MedicalStatistics\importService\ProcessFileFactory.class]: Failed to instantiate [app.one.MedicalStatistics.importService.ProcessFileFactory]: Constructor threw exception
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1306) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1198) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:917) ~[spring-context-6.0.6.jar:6.0.6]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[spring-context-6.0.6.jar:6.0.6]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.0.4.jar:3.0.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-3.0.4.jar:3.0.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[spring-boot-3.0.4.jar:3.0.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-3.0.4.jar:3.0.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1304) ~[spring-boot-3.0.4.jar:3.0.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1293) ~[spring-boot-3.0.4.jar:3.0.4]
    at app.one.MedicalStatistics.MedicalStatisticsApplication.main(MedicalStatisticsApplication.java:16) ~[classes/:na]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [app.one.MedicalStatistics.importService.ProcessFileFactory]: Constructor threw exception
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:223) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:87) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1300) ~[spring-beans-6.0.6.jar:6.0.6]
    ... 17 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'processDoctorFile': Unsatisfied dependency expressed through field 'doctorRepository': No qualifying bean of type 'app.one.MedicalStatistics.repository.DoctorRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:712) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:692) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:133) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:481) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1408) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:598) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:917) ~[spring-context-6.0.6.jar:6.0.6]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[spring-context-6.0.6.jar:6.0.6]
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:93) ~[spring-context-6.0.6.jar:6.0.6]
    at app.one.MedicalStatistics.importService.ProcessFileFactory.<init>(ProcessFileFactory.java:24) ~[classes/:na]
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484) ~[na:na]
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:197) ~[spring-beans-6.0.6.jar:6.0.6]
    ... 19 common frames omitted
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'app.one.MedicalStatistics.repository.DoctorRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1812) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1371) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1325) ~[spring-beans-6.0.6.jar:6.0.6]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:709) ~[spring-beans-6.0.6.jar:6.0.6]
    ... 38 common frames omitted

If I comment the declaration for DoctorRepository the application starts and design pattern works as expected. The problem appeared when I defined a repo in my service for updating data in DB. I tried to create a constructor to initialize the repository, also multiple annotations, but nothing works. Any idea how to fix it?


Solution

  • Some details that could be important are missing in your question (packages of the classes involved in your problem, for example) but I guess that, for any reason, the ApplicationContext you instantiate in your ProcessFileFactory does not discover the DoctorRepository class.

    I suggest to change your factory this way :

        @Component
        public class ProcessFileFactory implements org.springframework.context.ApplicationContextAware {
    
            private static ApplicationContext applicationContext;
    
            public static ProcessImportFiles getInstance(String fileType) {
                if (ImportTypes.DOCTORS.toString().equals(fileType)) {
                    return applicationContext.getBean("processDoctorFile", ProcessImportFiles.class);
                }
                if (ImportTypes.PATIENTS.toString().equals(fileType)) {
                    return applicationContext.getBean("processPatientFile", ProcessImportFiles.class);
                }
    
                return null;
            }
    
            @Override
            public void setApplicationContext(ApplicationContext context)
                throws BeansException {
                this.applicationContext = context;
            }
        }
    

    When you implement ApplicationContextAware interface, you ask Spring to inject into your class the existing application context.

    But, to be honest, there is a solution that, I think, would be cleaner as it would just use Dependency Injection instead of getting beans from the application context.

    Change your ProcessImportFiles this way :

        public interface ProcessImportFiles {
    
          String getManagedFileType();
    
          void processImportFile(String fileName, Sheet sheet);
    
        }
    

    Implementation example of the interface for ProcessPatientFile:

    @Service
    public class ProcessPatientFile implements ProcessImportFiles {
    
        @Override
        public String getManagedFileType() {
            return ImportTypes.PATIENTS.toString();
        }
    
        @Override
        public void processImportFile(String fileName, Sheet sheet) {
    
            System.out.println("I'm processing a PATIENTS file");
    
        }
    }
    

    Then change your ProcessFileFactory class this way :

        @Component
        public class ProcessFileFactory {
    
          private Map<String, ProcessImportFiles> importFileProcessorsByType;
    
          public ProcessFileFactory(Collection<ProcessImportFiles> importFileProcessors) {
    
            importFileProcessorsByType = importFileProcessors.stream().collect(Collectors.toMap(
                ProcessImportFiles::getManagedFileType,
                processor -> processor
            ));
    
          }
    
          public ProcessImportFiles getInstance(String fileType) {
            return importFileProcessorsByType.get(fileType);
          }
    
        }
    

    Spring will automatically inject all beans implementing your interface into your factory. The internal map of your factory will retrieve the expected implementation according to the fileType.

    By the way, if one day, you need to code new implementations of the ProcessImportFiles interface, your factory will work without any change.