Search code examples
javaspringspring-bootspring-mvcopencsv

Autowire Java Bean created with NewInstance from an external library


I am doing a Spring Boot Project and using the OpenCSV library to parse some csvs into POJOs to be persisted to db.

OpenCSV uses the annotation @CsvCustomBindByName to map a CSV field to a Java object.

The converter = DepartmentConverter.class is a custom converter that is instantiated with:

Class<? extends AbstractBeanField<T,K>>.newInstance() 

by the library, at runtime.

The problem is that because the custom field converter is instantiated reflectively by the OpenCSV library, it cant autowire beans because it is not registered in the Spring Context.

How can i make that dynamically instantiated converter be aware of the Spring context or the other way around. Some kind of interceptor? Thanks!

//Spring Managed class
public class Specialization { 
   
    @CsvCustomBindByName(required = true, converter = DepartmentConverter.class)
    private Department department;
    
    ....
}

In my DepartmentConverter i need to use a Spring JPARepository to retrieve some data. DepartmentRepository can not be autowired.

    @Component
public class DepartmentConverter extends AbstractBeanField<Department, String> {

    @Autowired
    private DepartmentRepository departmentRepository;

    public DepartmentConverter() {

    }

    @Override protected Object convert(String val) throws CsvConstraintViolationException, ResourceNotFoundException {
        //use departmentRepository
        ...
    }
}

Solution

  • The newInstance() call you're referring to is in the HeaderColumnNameMappingStrategy class, which calls the instantiateCustomConverter() method to do the newInstance() call.

    Create a subclass and override the method:

    @Override
    protected BeanField<T, K> instantiateCustomConverter(Class<? extends AbstractBeanField<T, K>> converter) throws CsvBadConverterException {
        BeanField<T, K> c = super.instantiateCustomConverter(converter);
        // TODO autowire here
        return c;
    }
    

    As can be seen in this answer to Spring @Autowired on a class new instance, you can do the autowiring as follows:

    autowireCapableBeanFactory.autowireBean(c);
    

    So the subclass would be something like:

    public class AutowiredConverterMappingStrategy extends HeaderColumnNameMappingStrategy {
    
        private final AutowireCapableBeanFactory beanFactory;
    
        public AutowiredConverterMappingStrategy(AutowireCapableBeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }
    
        @Override
        protected BeanField<T, K> instantiateCustomConverter(Class<? extends AbstractBeanField<T, K>> converter) throws CsvBadConverterException {
            BeanField<T, K> c = super.instantiateCustomConverter(converter);
            this.beanFactory.autowireBean(c);
            return c;
        }
    }
    

    To use it, you'd need something like this:

    @Component
    class MyComponent {
    
        @Autowired
        private AutowireCapableBeanFactory beanFactory;
    
        public <T> List<T> parseCsvToBean(Reader reader, Class<? extends T> type) {
            return new CsvToBeanBuilder(reader)
                    .withType(type)
                    .withMappingStrategy(new AutowiredConverterMappingStrategy(this.beanFactory))
                    .build()
                    .parse();
        }
    }
    

    That is of course just an example. Your CsvToBean setup may be more complex, but the key part is the withMappingStrategy() call, and that the code is itself in a Spring Bean, so it has access to the bean factory.