How to inject Dependencies dynamically in Spring Boot

In this article, let us discuss when we may need to inject dependencies dynamically and how we can implement this with an example in spring boot.

Introduction

Dependency Injection is a simple and efficient way of injecting dependencies into application code without having to actually maintain the object lifecycle of the dependency. This helps us keep the components loosely coupled while externalizing the object management (creation and disposal).

In Spring Boot, we have seen in different ways how we can register and inject dependencies into our application code – constructor, setter and property injection.

We have also seen the use of Autowiring, where the sub dependencies of a dependency are automatically resolved while injecting into the requested component.

When to Inject Dependencies Dynamically

Although Dependency Injection is useful, sometimes we may encounter cases when we are not sure what is the correct dependency to be injected until the runtime, when the application may require a specific implementation to be injected as required.

We may want to inject a dependency whose lifecycle is different from the requesting service (example, injecting a prototype dependency into a singleton service).

In such cases, we may want to have a mechanism where we can request a specific dependency to be injected dynamically. In this article, let us look into the different ways in which we can inject dependencies dynamically into our application in Spring Boot.

To illustrate this case, let us take the example of a simple service that stores text content into a target system based on the request type. Our service has multiple sinks to store data into – a database, a file or cloud.

Now each of these sinks have their own individual dependencies to resolve and logic. However, we don’t know which of these services to be used until a request is received. 

In such cases we can make use of this dynamic dependency injection.

Implementing Dynamic Dependency Injection

To implement this, l have declared an interface SinkService, that defines a single method store() which receives text content as argument and then returns a success or failure message.

package com.example.hello.services.sinks;

public interface SinkService { String store(String textContent); }

I have created 3 simple implementations of SinkService that will, for simplicity sake return different messages based on their type. All these services are annotated with @Service attribute, so that they can be used for Dependency Injection.

package com.example.hello.services.sinks;

import org.springframework.stereotype.Service;

@Service("db")
public class DatabaseSinkService implements SinkService {
   @Override
   public String store(String textContent) {
       return "Saved to database";
   }
}
package com.example.hello.services.sinks;

import org.springframework.stereotype.Service;

@Service("file")
public class FileSinkService implements SinkService {
   @Override
   public String store(String textContent) {
       return "Saved to file";
   }
}
package com.example.hello.services.sinks;

import org.springframework.stereotype.Service;

@Service("cloud")
public class CloudSinkService implements SinkService {
   @Override
   public String store(String textContent) {
       return "Saved to cloud";
   }
}

Now we have 3 different implementations of a Data Sink, which we have to inject based on the user’s input.

In Spring Boot, we can dynamically inject dependencies based on specific input criteria in 3 ways.

These are using ApplicationContext, ObjectProvider, and ObjectFactory.

Let us look at each of them briefly –

ApplicationContext

ApplicationContext object represents the IoC container that is provided by the Spring Boot framework. 

It has multiple uses –

  • Bean factory methods for accessing application components
  • load file resources
  • publish events to registered listeners
  • resolve messages

In our context, it provides a flexible way to retrieve beans by type or name. This is useful when the bean’s lifecycle is already managed by Spring, and you need dynamic access.

To get the object of a specific dependency that is already registered as a Spring Bean, you can do it by using below –

BlahService blahService = applicationContext.getBean(BlahService.class);

ObjectFactory

ObjectFactory interface is a lightweight, always-available mechanism to retrieve a bean instance. It defines a factory which can return an Object instance (possibly shared or independent) when invoked.

It is a generic interface, so takes a type argument of whatever dependency type you are looking for. It has a single method getObject() that returns an instance of the object managed by this factory. In case of creation errors, it throws a BeanException.

@Autowired
ObjectFactory<BlahService> blahServiceObjectFactory;

// usage
BlahService blahService = blahServiceObjectFactory.getObject();

ObjectProvider

ObjectProvider extends ObjectFactory and supports optional and lazy fetching of beans. It is designed specifically for injection points, allowing for programmatic optionality and lenient not-unique handling.

ObjectProvider provides multiple methods for various use cases and scenarios. It is also a generic interface that takes a type argument. Using ObjectProvider is ideal when the bean might not always be available or should only be initialized when explicitly needed.

@Autowired
private final ObjectProvider<BlahService> blahServiceObjectProvider;

// usage
BlahService blahService = blahServiceObjectProvider.getIfAvailable();
if(blahService == null) {
   System.out.println("Dependency not yet ready");
}
else {
   blahService.sayBlah();
}

Dynamically Injecting Dependencies via Factory

Back to our example of dynamically determining the correct sink to use, we can define a factory that will return the correct object. The factory looks like below –

package com.example.hello.services.sinks;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class SinkServiceFactory {
   private final ApplicationContext applicationContext;
   public SinkServiceFactory(ApplicationContext applicationContext) {
       this.applicationContext = applicationContext;
   }

   // Get bean by name
   public SinkService getSinkByName(String name) {
       if (applicationContext.containsBean(name)) {
           return applicationContext.getBean(name, SinkService.class);
       }
       return null;
   }

   // Get all available Sink services
   public Map<String, SinkService> getAllSinks() {
       return applicationContext.getBeansOfType(SinkService.class);
   }
}

The Controller method has two endpoints that will help store the content to different sinks. For simplicity sake I’m using a GET endpoint that will take the content as a query parameter.

package com.example.hello.controllers;

import com.example.hello.services.sinks.SinkService;
import com.example.hello.services.sinks.SinkServiceFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/sink")
public class SinkController {

    private final SinkServiceFactory sinkServiceFactory;

    public SinkController(SinkServiceFactory sinkServiceFactory) {
        this.sinkServiceFactory = sinkServiceFactory;
    }

    // Get all available sinks
    @GetMapping("/list")
    public ResponseEntity<List<String>> listAllServices() {
        return ResponseEntity.ok(sinkServiceFactory.getAllSinks().keySet().stream().toList());
    }

    @GetMapping("/store/{serviceName}")
    public ResponseEntity<String> storeByName(@PathVariable String serviceName, @RequestParam String message) {
        SinkService service = sinkServiceFactory.getSinkByName(serviceName);
        if (service != null) {
            String result = service.store(message);
            return ResponseEntity.ok(result);
        } else {
            return ResponseEntity.badRequest().body("Invalid sink type");
        }
    }
}

First, the list endpoint will return the list of all registered SinkService objects by their bean names – which we have specified within the @Service annotation.

GET http://localhost:8080/api/sink/list

["cloud","db","file"]

Next, when we hit the store endpoint with different sink names we got from the list endpoint, the return message changes – meaning that different SinkService implementations have been dynamically called.

GET http://localhost:8080/api/sink/store/db?message=Hello%20World!%20This%20is%20me!

Saved to database

Conclusion

In this article, we have discussed the scenarios when we may need to dynamically inject dependencies into our application code.

Scenarios such as –

  • Conditional Dependency Injection Based on Runtime Parameters
  • Fetching heavy dependencies when needed
  • Injecting prototype or scoped beans into singletons
  • Dynamic configuration based injections etc.

In Spring Boot, we can dynamically inject Dependencies into our application in 3 different ways – ApplicationContext, ObjectFactory and ObjectProvider. 

The following table summarizes their use cases: 

ApplicationContextTo dynamically fetch beans by their type or name
ObjectFactoryFor simple, on-demand creation of bean instances
ObjectProviderTo lazily fetch a bean or retrieve optional dependencies
Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *