y0ngb1n

Aben Blog

欢迎来到我的技术小黑屋ヾ(◍°∇°◍)ノ゙
github

How to quickly implement a feature similar to auto-configuration in lower versions of Spring

The @Conditional and other conditional annotations were introduced only after Spring 4, which are the biggest contributors to automatic configuration in Spring Boot!
So the question arises: If we are still using the old version of Spring 3.x, how can we implement automatic configuration?

Code hosted on GitHub, welcome to Star 😘

Requirements and Issues#

Core Demands#

  • Existing system, no intention to refactor
  • Spring version is 3.x, no intention to upgrade or introduce Spring Boot
  • Expect to enhance functionality with minimal code changes

For example:

  • Hope to uniformly add logging across the entire site (e.g., summary information for RPC framework web calls, summary information for database access layer), which is actually a common feature.
  • We have referenced some infrastructure and want to further enhance the functionality of these infrastructures, which should be addressed from the framework level.

Problems Faced#

  • Spring 3.x lacks conditional annotations

    Without conditional annotations, we are unclear when we need or do not need to configure these things.

  • Unable to automatically locate the required automatic configuration

    At this point, we cannot let the framework automatically load our configuration like Spring Boot's automatic configuration; we need to use some other means to allow Spring to load these customized functionalities.

Core Solution Approach#

Conditional Judgment#

  • Judging through BeanFactoryPostProcessor

Spring provides us with an extension point, and we can use BeanFactoryPostProcessor to solve the conditional judgment problem. It allows us to perform some post-processing on our Bean definitions after the BeanFactory is defined and before the Beans are initialized. At this time, we can judge our Bean definitions to see which Beans are present or missing, and we can also add some Bean definitions—adding some custom Beans.

Configuration Loading#

  • Write Java Config class
  • Introduce configuration class
    • Through component-scan
    • Through XML file import

We can consider writing our own Java Config class and adding it to component-scan, then finding a way to ensure that the current system's component-scan includes our written Java Config class; we can also write an XML file. If the current system uses XML, we should check if our XML file can be loaded from the current system's loading path; if not, we can manually import this file.

Two Extension Points Provided by Spring#

BeanPostProcessor#

  • For Bean instances
  • Provides custom logic callbacks after Bean creation

BeanFactoryPostProcessor#

  • For Bean definitions
  • Obtains configuration metadata before the container creates Beans
  • In Java Config, it needs to be defined as a static method (if not defined, Spring will issue a warning at startup; you can try it out)

Some Customizations for Beans#

Since the two extension points of Spring have been mentioned above, let's expand on some ways to customize Beans.

Lifecycle Callback#

  • InitializingBean / @PostConstruct / init-method

    This part is about initialization, allowing for some customization after the Bean is initialized. There are three ways:

    • Implement the InitializingBean interface
    • Use the @PostConstruct annotation
    • Specify an init-method in the Bean definition's XML file; or specify init-method when using the @Bean annotation

    All of these allow us to call specific methods after the Bean is created.

  • DisposableBean / @PreDestroy / destroy-method

    This part involves operations we should perform when the Bean is recycled. We can specify that this Bean, upon destruction, if:

    • It implements the DisposableBean interface, then Spring will call its corresponding method.
    • We can also add the @PreDestroy annotation to a method, which will be called upon destruction.
    • Specify a destroy-method in the Bean definition's XML file; or specify destroy-method when using the @Bean annotation, which will be called upon destruction.

XxxAware Interfaces#

  • ApplicationContextAware

    The entire ApplicationContext can be injected through the interface, allowing us to obtain a complete ApplicationContext within this Bean.

  • BeanFactoryAware

    Similar to ApplicationContextAware.

  • BeanNameAware

    Allows the Bean's name to be injected into this instance.

If you are interested in the source code, see: org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean
If the current Bean has a method named close or shutdown, it will be treated by Spring as a destroy-method and will be called upon destruction.

Some Common Operations#

Check if a Class Exists#

  • ClassUtils.isPresent()

Call Spring's ClassUtils.isPresent() to check if a class exists in the current Class Path.

Check if a Bean is Defined#

  • ListableBeanFactory.containsBeanDefinition(): Check if a Bean is defined.
  • ListableBeanFactory.getBeanNamesForType(): You can see which names of certain types of Beans have been defined.

Register Bean Definitions#

  • BeanDefinitionRegistry.registerBeanDefinition()
    • GenericBeanDefinition
  • BeanFactory.registerSingleton()

Roll Up Your Sleeves and Get to Work#

The theory has been covered, now let's start practicing.
In the current example, we assume the current environment is: not using Spring Boot and high versions of Spring.

Step 1: Simulate a Low Version of Spring Environment

Here, we simply introduced the spring-context dependency and did not actually use Spring 3.x, nor did we use any features from Spring 4 or above.

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
  <dependency>
    <groupId>io.github.y0ngb1n.samples</groupId>
    <artifactId>custom-starter-core</artifactId>
    <scope>provided</scope>
  </dependency>
</dependencies>

Step 2: Example of Implementing the BeanFactoryPostProcessor Interface

@Slf4j
public class GreetingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
    throws BeansException {

    // Check if the required GreetingApplicationRunner class exists in the current Class Path
    boolean hasClass = ClassUtils
      .isPresent("io.github.y0ngb1n.samples.greeting.GreetingApplicationRunner",
        GreetingBeanFactoryPostProcessor.class.getClassLoader());

    if (!hasClass) {
      // Class does not exist
      log.info("GreetingApplicationRunner is NOT present in CLASSPATH.");
      return;
    }

    // Check if there is a Bean definition with id greetingApplicationRunner
    boolean hasDefinition = beanFactory.containsBeanDefinition("greetingApplicationRunner");
    if (hasDefinition) {
      // The current context already has a greetingApplicationRunner
      log.info("We already have a greetingApplicationRunner bean registered.");
      return;
    }

    register(beanFactory);
  }

  private void register(ConfigurableListableBeanFactory beanFactory) {

    if (beanFactory instanceof BeanDefinitionRegistry) {
      GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
      beanDefinition.setBeanClass(GreetingApplicationRunner.class);

      ((BeanDefinitionRegistry) beanFactory)
        .registerBeanDefinition("greetingApplicationRunner", beanDefinition);
    } else {

      beanFactory.registerSingleton("greetingApplicationRunner", new GreetingApplicationRunner());
    }
  }
}

Register our Bean (see CustomStarterAutoConfiguration), with the following points to note:

  • The method here is defined as static
  • When using, if the two projects are not in the same package, you need to actively add the current class to the project's component-scan
@Configuration
public class CustomStarterAutoConfiguration {

  @Bean
  public static GreetingBeanFactoryPostProcessor greetingBeanFactoryPostProcessor() {
    return new GreetingBeanFactoryPostProcessor();
  }
}

Step 3: Verify if the Automatic Configuration is Effective

Add dependencies in other projects:

<dependencies>
  ...
  <dependency>
    <groupId>io.github.y0ngb1n.samples</groupId>
    <artifactId>custom-starter-spring-lt4-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>io.github.y0ngb1n.samples</groupId>
    <artifactId>custom-starter-core</artifactId>
  </dependency>
  ...
</dependencies>

Start the project and observe the logs (see custom-starter-examples), to verify if the automatic configuration is effective:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.0.RELEASE)

2019-05-02 20:47:27.692  INFO 11460 --- [           main] i.g.y.s.d.AutoconfigureDemoApplication   : Starting AutoconfigureDemoApplication on HP with PID 11460 ...
2019-05-02 20:47:27.704  INFO 11460 --- [           main] i.g.y.s.d.AutoconfigureDemoApplication   : No active profile set, falling back to default profiles: default
2019-05-02 20:47:29.558  INFO 11460 --- [           main] i.g.y.s.g.GreetingApplicationRunner      : Initializing GreetingApplicationRunner.
2019-05-02 20:47:29.577  INFO 11460 --- [           main] i.g.y.s.d.AutoconfigureDemoApplication   : Started AutoconfigureDemoApplication in 3.951 seconds (JVM running for 14.351)
2019-05-02 20:47:29.578  INFO 11460 --- [           main] i.g.y.s.g.GreetingApplicationRunner      : Hello everyone! We all like Spring!

At this point, we have successfully implemented automatic configuration-like functionality in lower versions of Spring. 👏


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.