0

I am in the process of developing a common java library with reusable logic to interact with some AWS services, that will in turn be used by several consumer applications. For reasons outlined here, and the fact that Spring Boot seems to provide a lot of boilerplate free code for things like SQS integration, I have decided to implement this common library as a custom spring boot starter with auto configuration.

I am also completely new to the Spring framework and as a result, have run into a problem where my auto-configured class's instance variables are not getting initialized via the AutoWired annotation.

To better explain this, here is a very simplified version of my common dependency.

CommonCore.java

@Component
public class CommonCore { 

   @AutoWired
   ReadProperties readProperties;

   @AutoWired
   SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging 

   public CommonCore() {
       Properties props = readProperties.loadCoreProperties();
       //initialize stuff
   }

   processEvents(){
     // starts processing events from a kinesis stream.
   }
}

ReadProperties.java

@Component
public class ReadProperties {

    @Value("${some.property.from.application.properties}")
    private String someProperty;

    public Properties loadCoreProperties() {
      Properties properties = new Properties();
      properties.setProperty("some.property", someProperty);
      
      return properties;
    }
 }

CoreAutoConfiguration.java

@Configuration
public class CommonCoreAutoConfiguration {

    @Bean
    public CommonCore getCommonCore() {  
        return new CommonCore();
    }
}

The common dependency will be used by other applications like so:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class SampleConsumerApp implements ApplicationRunner {

    @Autowired
    CommonCore commonCore;

    public SampleConsumerApp() {
    }

    public static void main(String[] args) {
        SpringApplication.run(SampleConsumerApp.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {

        try {
            commonCore.processEvents();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The main problem I have like I mentioned, is the AutoWired objects in the CommonCore instance are not getting initialized as expected. However, I think the actual problems are more deeply rooted; but due to my lack of understanding of the Spring framework, I am finding it difficult to debug this on my own.

I am hoping for a few pointers along these points

  1. Does this approach of developing a custom starter make sense for my use case?
  2. What is the reason for the AutoWired dependencies to not get initialized with this approach?
Priyath Gregory
  • 927
  • 1
  • 11
  • 37
  • Hard to say like that, but perhaps they are not instantiated at all. I would create a no-argument constructor for one of those @Component classes like `ReadProperties ` for example and put a log line or a debugger breakpoint there and see if it is constructed at all. Perhaps it's to do with your component scan scope – Tarmo Oct 28 '20 at 12:02
  • Thanks for the response. I tried that, the constructor does not get called. The reason I wanted to do auto configuration is to avoid having to define custom component scan paths for the applications that uses the common library. If I auto-wire within the CommonCoreAutoConfiguration class itself, it works. But not within the CommonCore class which is what I need – Priyath Gregory Oct 28 '20 at 12:21
  • Could you share your META-INF/spring.factories file from the core jar? – Rohit Oct 28 '20 at 13:23

1 Answers1

1

Wild guess, but I think it's because of the order of how things are constructed. I am talking about this class:

@Component
public class CommonCore { 

   @AutoWired
   ReadProperties readProperties;

   @AutoWired
   SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging 

   public CommonCore() {
       Properties props = readProperties.loadCoreProperties();
       //initialize stuff
   }

   processEvents(){
     // starts processing events from a kinesis stream.
   }
}

You are trying to use a Spring injected component in a constructor, but constructor is called before Spring can do its @Autowire magic.

So one option is to autowire as a constructor argument

Something like this (untested):

@Component
public class CommonCore { 

   private final ReadProperties readProperties;

   private final SqsListener sqsListener; // this will be based on spring-cloud-starter-aws-messaging 
   @AutoWired 
   public CommonCore(SqsListener sqsListener, ReadProperties readProperties) {
       this.readProperties = readPropertis;
       this.sqsListener = sqsListener;
       Properties props = readProperties.loadCoreProperties();
       //initialize stuff
   }

   processEvents(){
     // starts processing events from a kinesis stream.
   }
}

Sidenote: I prefer to use dependency injection via constructor arguments always, wherever possible. This also makes unit testing a lot easier without any Spring specific testing libraries.

Tarmo
  • 3,851
  • 2
  • 24
  • 41
  • Thanks for the response. With this approach, how should I initialize the `CommonCore` instance during auto-configuration in the `CommonCoreAutoConfiguration` class? – Priyath Gregory Oct 28 '20 at 12:41
  • Why do you need to initialise it. It's already a `@Component` and therefor initialized by Spring. I don't think you need that `@Bean` method at all – Tarmo Oct 28 '20 at 12:44
  • 1
    Or option two is not to make it as `@Component`. Instead create it manually in you `@Bean` method and put `SqsListener` and `ReadProperties` as arguments to this method. Then remove `@Component` and `@AutoWired` anntotations from `@ CommonCore`. – Tarmo Oct 28 '20 at 12:47
  • Got it thanks! I think with the latter approach, Anyone using the library would need to use @ComponentScan in their applications to discover the components in the library. I wanted to avoid this, hence I followed the auto configuration approach as described here: https://stackoverflow.com/questions/48808752/spring-boot-auto-configuration-with-dependency-and-without-componentscan – Priyath Gregory Oct 28 '20 at 12:55
  • What I was aiming for was to not have those values (ReadPropertiese, SqsListener) as constructor arguments because those are implementation details contained within the `CommonCore` instance, and instead wire them up using the AutoWired annotations with a no args constructor. But looks thats a different problem that I need to re-think about. – Priyath Gregory Oct 28 '20 at 12:59
  • 1
    This goes to the land of "opinionated", but when object depends on another object then giving this a dependency at construction time does not break OOP principles. Exposing those parameters for instance users would break encapsulation principles. Leave Spring aside, if a third person would like to build CommonCore then she would like to know what it takes to build one. Hiding this for them does not accomplish much IMO. Plus you can always leave your constructor with package or protected visibility. – Tarmo Oct 28 '20 at 13:05
  • Thats a good point. But in this case, the only information "CommonCore" needs externally are a bunch of configuration details (for the SQS queue for example) which the user specifies in their application.properties file. So a no-args constructor would trigger the internal initialization of "CommonCore" (loading configs from application.properties, initialize SQS listeners etc). Once "CommonCore" is initialized, the only remaining concern for the user has is to trigger the `processEvents` method. Everything else is taken care of within the "CommonCore" instance. Just a bit more context. – Priyath Gregory Oct 28 '20 at 13:12