Enabling Non-Spring Libraries to Access Spring Boot Components: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(23 intermediate revisions by the same user not shown)
Line 5: Line 5:
=Overview=
=Overview=


This article describes a possible approach to give non-Spring libraries access to Spring Boot runtime components.
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=
=Playground Example=
Line 13: Line 13:
=Approach=
=Approach=


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 write code in it. The dedicated class in the dependency package is conventionally named SpringApplicationContextAccess:
==Spring Boot Application Changes==
 
From the Spring Boot application's perspective, the (only) active step required to enable [[Spring_Dependency_Injection_and_Inversion_of_Control_Container_Concepts#Component_Scanning|component scanning]] for the dependency package in order to detect the Spring components that come in the dependency library, is to add an extra [[Spring_Dependency_Injection_and_Inversion_of_Control_Container_Concepts#.40ComponentScan|@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:


<syntaxhighlight lang='java'>
<syntaxhighlight lang='java'>
package playground.springboot.dependency;
@SpringBootApplication
@ComponentScans(@ComponentScan(basePackages = "some.experimental.dependency"))
public class MainApplication {
...
}
</syntaxhighlight>


import org.springframework.context.ApplicationContext;
Alternatively, we can get rid of even this requirement and remove the @ComponentScans annotation, making the Spring Boot application completely oblivious to the presence of the Spring-aware dependency library, if we add a Spring metadata file and a @Configuration class in the library, as described in the "[[#How_to_Make_Dependency_Library_Autoconfigurable_by_Spring|How to Make Dependency Library Autoconfigurable by Spring]]" section.


public class SpringApplicationContextAccess {
==Dependency Library Changes==


  private static ApplicationContext APPLICATION_CONTEXT;
The dependency library contains Spring components it wants managed by the container, while it is bootstrapped by the SpringBoot application in a non-Spring way: Spring Boot application invokes <tt>new</tt> on one of the dependency library's classes.  The situation is encountered when a Spring Boot application instantiates a JPA Converter class specified in a third party library. We can write the dependency library in such a way that it bootstraps itself, as shown in [[#How_to_Write_the_Spring_Component|How to Write the Spring Component]]. 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: [[Gradle_Spring_dependency-management_Plugin#Overview|Spring dependency-management Plugin for Gradle]].
   
 
  public static void installApplicationContext(ApplicationContext ac) {
<syntaxhighlight lang='groovy'>
       
dependencies {
    APPLICATION_CONTEXT = ac;
  }


  /**
    compileOnly('org.springframework:spring-beans')
  * Use this method to explicitly pull the bean from the context.
     compileOnly('org.springframework:spring-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 SpringApplicationContextAccess.APPLICATION_CONTEXT.getBean(type);
  }
}
}
</syntaxhighlight>
</syntaxhighlight>


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: [[Gradle_Spring_dependency-management_Plugin#Overview|Spring dependency-management Plugin for Gradle]].


The Spring Boot runtime is supposed to configured 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:
===How to Write Bootstrapping Code===


<syntaxhighlight lang='java'>
<syntaxhighlight lang='java'>
@Component
package some.experimental.dependency;
public class SpringApplicationContextConfiguratorForDependencies {
 
/**
* This is a dependency class that is invoked into by the SpringBoot framework directly, without an assumption of
* Spring component availability: the Dependency class instance is created with new or by reflection. The situation
* is similar to a Spring Boot application instantiating a JPA Converter class specified in a third party library.
* The Dependency instance gets the Spring-managed singleton instance via a static method call.
*/
public class Dependency {
 
  private DependencySpringComponent springComponent;
 
  public Dependency() {
 
    //
    // The DependencySpringComponent singleton is initialized by Spring by now and installed into the application
    // context; we get it by calling into DependencySpringComponent static method:
    //
    springComponent = DependencySpringComponent.getSpringBeanInstance();
  }


  @Autowired
   public void run() {
   public SpringApplicationContextConfiguratorForDependencies(ApplicationContext applicationContext) {


    SpringApplicationContextAccess.installApplicationContext(applicationContext);
    springComponent.run();
   }
   }
}
}
</syntaxhighlight>
</syntaxhighlight>


The SpringApplicationContextConfiguratorForDependencies component should be autowired into the Spring Boot application to trigger configuration.
===How to Write the Spring Component===
 
The Spring component exposes itself via a static member variable, during its initialization cycle:


<syntaxhighlight lang='java'>
<syntaxhighlight lang='java'>
@SpringBootApplication
package some.experimental.dependency;
public class MainApplication  {
...
/**
* This is a Spring bean that is identified by component scanning performed by the main application and is installed
* into the Spring application context. As part of its initialization cycle, it retains a reference to the application
* context and it is exposing itself via a static method that has access to the application context.
*/
@Component
public class DependencySpringComponent {


    @Autowired
  private static ApplicationContext APPLICATION_CONTEXT;
    private SpringApplicationContextConfiguratorForDependencies springBootstrap;


  ...
  @Autowired
}
  public DependencySpringComponent(ApplicationContext applicationContext) {
</syntaxhighlight>


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.
    APPLICATION_CONTEXT = applicationContext;
  }


<syntaxhighlight lang='java'>
  public static DependencySpringComponent getSpringBeanInstance() {
public class Dependency {


  private DependencySpringComponentA springComponent;
    if (APPLICATION_CONTEXT == null) {


  public Dependency() {
      throw new IllegalStateException("access to Spring ApplicationContext has not been configured");
    }


     configureAccessToSpringComponent();
     return APPLICATION_CONTEXT.getBean(DependencySpringComponent.class);
   }
   }


   public void run() {
   public void run() {
    ...
  }
}
</syntaxhighlight>


    System.out.println(this + " is running with Spring component " + springComponent);;
DependencySpringComponent must exist and be a valid Spring component, properly annotated with @Component or similar. It is detected by Spring because we [[#Spring_Boot_Application_Changes|configured the Spring Boot application context to @ComponentScan the dependency package]].
  }


/**
===How to Make Dependency Library Autoconfigurable by Spring===
  * Use this method in the constructor to explicitly pull the bean from the context.
  *
  * @throws IllegalStateException if we encounter bad state because the initialization was not performed.
  */
private void configureAccessToSpringComponent() throws IllegalStateException {


  if (SpringApplicationContextAccess.APPLICATION_CONTEXT == null) {
Instead of adding a @ComponentScan annotation that points to the dependency library package to the Spring Boot main application, we  can make the dependency library autoconfigurable, by adding a META-INF/spring.factories to the dependency library JAR:


    throw new IllegalStateException("access to Spring ApplicationContext has not been configured");
<syntaxhighlight lang='text'>
  }
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  some.experimental.dependency.DependencyAutoConfiguration
</syntaxhighlight>
 
We also need to provide a @Configuration class that creates the beans for Spring, as referred from the content of the META-INF/spring.factories file:
 
<syntaxhighlight lang='java'>
package some.experimental.dependency;


  springComponent = SpringApplicationContextAccess.APPLICATION_CONTEXT.getBean(DependencySpringComponentA.class);
@Configuration
public class DependencyAutoConfiguration {


   if (springComponent == null) {
   @Bean
  public DependencySpringComponent dependencySpringComponent(ApplicationContext applicationContext) {


      throw new IllegalStateException(
    return new DependencySpringComponent(applicationContext);
            "a component of type " + DependencySpringComponentA.class + " not found in application context");
  }
  }
}
}
}
</syntaxhighlight>
</syntaxhighlight>
Obviously, DependencySpringComponentA must exist and be a valid Spring component.

Latest revision as of 22:36, 7 November 2018

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 {
...
}

Alternatively, we can get rid of even this requirement and remove the @ComponentScans annotation, making the Spring Boot application completely oblivious to the presence of the Spring-aware dependency library, if we add a Spring metadata file and a @Configuration class in the library, as described in the "How to Make Dependency Library Autoconfigurable by Spring" section.

Dependency Library Changes

The dependency library contains Spring components it wants managed by the container, while it is bootstrapped by the SpringBoot application in a non-Spring way: Spring Boot application invokes new on one of the dependency library's classes. The situation is encountered when a Spring Boot application instantiates a JPA Converter class specified in a third party library. We can write the dependency library in such a way that it bootstraps itself, as shown in How to Write the Spring Component. 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.

dependencies {

    compileOnly('org.springframework:spring-beans')
    compileOnly('org.springframework:spring-context')
}


How to Write Bootstrapping Code

package some.experimental.dependency;

/**
 * This is a dependency class that is invoked into by the SpringBoot framework directly, without an assumption of
 * Spring component availability: the Dependency class instance is created with new or by reflection. The situation 
 * is similar to a Spring Boot application instantiating a JPA Converter class specified in a third party library.
 * The Dependency instance gets the Spring-managed singleton instance via a static method call. 
 */
public class Dependency {

  private DependencySpringComponent springComponent;

  public Dependency() {

    //
    // The DependencySpringComponent singleton is initialized by Spring by now and installed into the application
    // context; we get it by calling into DependencySpringComponent static method:
    //
    springComponent = DependencySpringComponent.getSpringBeanInstance();
  }

  public void run() {

    springComponent.run();
  }
}

How to Write the Spring Component

The Spring component exposes itself via a static member variable, during its initialization cycle:

package some.experimental.dependency;
...
/**
 * This is a Spring bean that is identified by component scanning performed by the main application and is installed
 * into the Spring application context. As part of its initialization cycle, it retains a reference to the application
 * context and it is exposing itself via a static method that has access to the application context.
 */
@Component
public class DependencySpringComponent {

  private static ApplicationContext APPLICATION_CONTEXT;

  @Autowired
  public DependencySpringComponent(ApplicationContext applicationContext) {

    APPLICATION_CONTEXT = applicationContext;
  }

  public static DependencySpringComponent getSpringBeanInstance() {

    if (APPLICATION_CONTEXT == null) {

      throw new IllegalStateException("access to Spring ApplicationContext has not been configured");
    }

    return APPLICATION_CONTEXT.getBean(DependencySpringComponent.class);
  }

  public void run() {
    ...
  }
}

DependencySpringComponent 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.

How to Make Dependency Library Autoconfigurable by Spring

Instead of adding a @ComponentScan annotation that points to the dependency library package to the Spring Boot main application, we can make the dependency library autoconfigurable, by adding a META-INF/spring.factories to the dependency library JAR:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  some.experimental.dependency.DependencyAutoConfiguration

We also need to provide a @Configuration class that creates the beans for Spring, as referred from the content of the META-INF/spring.factories file:

package some.experimental.dependency;

@Configuration
public class DependencyAutoConfiguration {

  @Bean
  public DependencySpringComponent dependencySpringComponent(ApplicationContext applicationContext) {

    return new DependencySpringComponent(applicationContext);
  }
}