Enabling Non-Spring Libraries to Access Spring Boot Components

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

Internal

Overview

This article describes a possible approach to give a non-Spring library runtime access to Spring Boot runtime components and inject its own Spring components into the Spring Boot runtime application context.

Playground Example

https://github.com/ovidiuf/playground/tree/master/spring/spring-boot/spring-boot-with-dependency

Approach

Spring Boot Application Changes

From the Spring Boot application's perspective, the (only) active step required to enable component scanning for the dependency package in order to detect the Spring components that come in the dependency library, is to add an extra @ComponentScan annotation in its main class, configured with the dependency library package. Note that @ComponentScans (plural) is required because the Spring Boot application implicitly declares a @ComponentScan for its own package, as part of the @SpringBootApplication annotation:

@SpringBootApplication
@ComponentScans(@ComponentScan(basePackages = "some.experimental.dependency"))
public class MainApplication {
...
}

Dependency Library Changes

The Spring Boot runtime should implement a Spring component whose job is to configure a dedicated class in the dependency package. This solution assumes that the dependency package "cooperates" and we can code in it. The dedicated class in the dependency package is conventionally named SpringApplicationContextAccess:

package playground.springboot.dependency;

import org.springframework.context.ApplicationContext;

public class SpringApplicationContextAccess {

  private static ApplicationContext APPLICATION_CONTEXT;
    
  public static void installApplicationContext(ApplicationContext ac) {
        
    APPLICATION_CONTEXT = ac;
  }

  /**
   * Use this method to explicitly pull the bean from the context.
   *
   * @return may return null if no such bean exists in the application context.
   *
   * @throws IllegalStateException if we encounter bad state because the initialization was not performed.
   */
  public static <T> T getBean(Class<T> type) throws IllegalStateException {
      
    if (APPLICATION_CONTEXT == null) {
        
      throw new IllegalStateException("access to Spring ApplicationContext has not been configured");
    }
    
    return APPLICATION_CONTEXT.getBean(type);
  }
}

Note that the dependency project should be configured with "compile only" access to Spring Framework API packages "org.springframework:spring-beans" and "org.springframework:spring-context". A way to do this that does not involve Spring Boot dependency management is described here: Spring dependency-management Plugin for Gradle.

The Spring Boot runtime is supposed to configure SpringApplicationContextAccess for the dependency, via a SpringApplicationContextConfiguratorForDependencies component., early in its life cycle. The simplest way to do this is when the component is initialized so it has access to the application context:

@Component
public class SpringApplicationContextConfiguratorForDependencies {

  @Autowired
  public SpringApplicationContextConfiguratorForDependencies(ApplicationContext applicationContext) {

     SpringApplicationContextAccess.installApplicationContext(applicationContext);
  }
}

Because SpringApplicationContextConfiguratorForDependencies is a @Component, thus by default a singleton, its instance will be initialized upon Spring Boot application start and as part of its own initialization sequence, it will install the active application context into a static member variable of the dependency package.

The Spring Boot application should also configure the component scan of its application context to detect components in the dependency:

@SpringBootApplication
//
// this is necessary to enable component scan in the dependency package
//
@ComponentScan(basePackageClasses = {MainApplication.class, Dependency.class})
public class MainApplication  {

   ...
}

Finally, the non-spring dependency should explicitly pull the component it needs from the application context, with getBean(). That will trigger bean initialization and will kick off necessary dependency injection.

package playground.springboot.dependency;

public class Dependency {

  private DependencySpringComponentA springComponent;

  public Dependency() {

    springComponent = SpringApplicationContextAccess.getBean(DependencySpringComponentA.class); 
  }

  public void run() {

    System.out.println(this + " is running with Spring component " + springComponent);

    springComponent.run();
  }
}

Obviously, DependencySpringComponentA must exist and be a valid Spring component, properly annotated with @Component or similar. It is detected by Spring because we configured the Spring Boot application context to @ComponentScan the dependency package.