The conditional annotations like
@Conditional
were introduced only after Spring 4, and they are the biggest contributors to achieving automatic configuration in Spring Boot!
So the question arises: If we are still using the old version of Spring 3.x, how can we achieve automatic configuration?
The code is hosted on GitHub, feel free to Star 😘
Requirements and Issues#
Core Demands#
- Existing system, no plans for refactoring
- Spring version is 3.x, no plans 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 this infrastructure, which should be solved from the framework level.
Problems Faced#
-
Spring 3.x does not have 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 other means to allow Spring to load our customized functionalities.
Core Solution Approach#
Conditional Judgments#
- Judging through
BeanFactoryPostProcessor
Spring provides us with an extension point, and we can use BeanFactoryPostProcessor
to solve the problem of conditional judgment. 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
- Through
We can consider writing our own Java Config class and adding it to component-scan
, then finding a way to make the current system's component-scan
include our written Java Config class; we can also write an XML file. If the current system uses XML, we can check if our XML file can be loaded in the 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 report awarning
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 some customization after the Bean's initialization. There are three ways:
- Implement the
InitializingBean
interface - Use the
@PostConstruct
annotation - Specify an
init-method
in the Bean definition's XML file; or specifyinit-method
when using the@Bean
annotation
All of these allow us to call specific methods after the Bean is created.
- Implement the
-
DisposableBean
/@PreDestroy
/destroy-method
This part involves operations we should perform when the Bean is being collected. We can specify that this Bean, when destroyed, 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 specifydestroy-method
when using the@Bean
annotation, which will call this method upon destruction
- It implements the
XxxAware Interfaces#
-
ApplicationContextAware
The entire
ApplicationContext
can be injected through the interface, allowing us to obtain a completeApplicationContext
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 namedclose
orshutdown
, it will be treated by Spring as adestroy-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 what names of certain types of Beans have already 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 higher versions of Spring.
Step 1: Simulate a Lower 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
), here are a few points to note:
- The method 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. 👏