Unlock Spring’s Hidden Extension Points: From FactoryBean to Custom Namespaces
Explore the core extension mechanisms of Spring, including FactoryBean, @Import, BeanPostProcessor, BeanFactoryPostProcessor, SPI, SpringBoot startup hooks, event handling, and custom XML namespaces, with detailed code demos and practical examples that reveal how to integrate and extend Spring in real-world applications.
FactoryBean
When talking about FactoryBean , a famous interview question is "Explain the difference between FactoryBean and BeanFactory". They are unrelated despite the similar names.
BeanFactory creates beans, while FactoryBean is a special bean type. When a class implementing FactoryBean is registered, the actual bean returned is the object obtained via FactoryBean#getObject() .
Example:
<code>public class UserFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
User user = new User();
System.out.println("Calling UserFactoryBean#getObject to create Bean:" + user);
return user;
}
@Override
public Class<?> getObjectType() {
// The type of bean this FactoryBean creates
return User.class;
}
}
</code>Test class:
<code>public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(UserFactoryBean.class);
ctx.refresh();
System.out.println("Obtained Bean: " + ctx.getBean(User.class));
}
}
</code>Result shows that although UserFactoryBean is registered, the bean retrieved is of type User , created via getObject() .
FactoryBean in Open Source Frameworks
1. MyBatis
MyBatis integrates with Spring using FactoryBean to inject mapper interface proxies.
<code>public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class<T> getObjectType() {
return this.mapperInterface;
}
}
</code>The @MapperScan annotation registers each mapper interface as a MapperFactoryBean in the container.
2. OpenFeign
Feign client interfaces are also injected via a FactoryBean implementation.
<code>class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
private Class<?> type;
@Override
public Object getObject() throws Exception {
return getTarget();
}
@Override
public Class<?> getObjectType() {
return type;
}
}
</code>@Import Annotation
Two common annotations ( @EnableScheduling and @EnableAsync ) rely on @Import to import configuration classes.
<code>@Import({SchedulingConfiguration.class})
public interface EnableScheduling {}
</code> <code>@Import({AsyncConfigurationSelector.class})
public interface EnableAsync {}
</code>Both ultimately trigger @Import to register beans.
Classification of @Import Imported Configurations
1. Config class implements ImportSelector
<code>public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
default Predicate<String> getExclusionFilter() { return null; }
}
</code>Implementation returns fully qualified class names to be registered.
<code>public class UserImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("Calling UserImportSelector#selectImports");
return new String[]{"com.sanyou.spring.extension.User"};
}
}
</code>Test shows the bean is successfully imported.
2. Config class implements ImportBeanDefinitionRegistrar
<code>public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {}
}
</code>This allows custom BeanDefinition registration.
<code>public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
.addPropertyValue("username", "三友的java日记")
.getBeanDefinition();
System.out.println("Injecting User into Spring container");
registry.registerBeanDefinition("user", beanDefinition);
}
}
</code>3. Config class implements no special interface
Simply a regular class; Spring registers it as a bean.
Bean Lifecycle
FactoryBean, @Import, @Component , @Bean , and XML all serve to register beans in the container.
Bean creation involves callbacks from interfaces and annotations such as Aware , @PostConstruct , InitializingBean , @PreDestroy , and DisposableBean . The order is demonstrated with a test class that prints each callback.
<code>public class LifeCycle implements InitializingBean, ApplicationContextAware, DisposableBean {
@Autowired
private User user;
public LifeCycle() { System.out.println("LifeCycle object created"); }
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
System.out.println("Aware setApplicationContext called, user=" + user);
}
@PostConstruct
public void postConstruct() { System.out.println("@PostConstruct called"); }
@Override
public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean afterPropertiesSet called"); }
public void initMethod() throws Exception { System.out.println("@Bean initMethod called"); }
@PreDestroy
public void preDestroy() throws Exception { System.out.println("@PreDestroy called"); }
public void destroyMethod() throws Exception { System.out.println("@Bean destroyMethod called"); }
@Override
public void destroy() throws Exception { System.out.println("DisposableBean destroy called"); }
}
</code>Result order:
<code>LifeCycle object created
Aware setApplicationContext called, user=com.sanyou.spring.extension.User@57d5872c
@PostConstruct called
InitializingBean afterPropertiesSet called
@Bean initMethod called
@PreDestroy called
DisposableBean destroy called
@Bean destroyMethod called
</code>BeanPostProcessor
BeanPostProcessor allows custom logic at any bean creation stage.
<code>public class UserBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof User) {
((User) bean).setUsername("三友的java日记");
}
return bean;
}
}
</code>Test shows the username is set automatically.
Spring Built‑in BeanPostProcessors
AutowiredAnnotationBeanPostProcessor – processes @Autowired and @Value
CommonAnnotationBeanPostProcessor – processes @Resource , @PostConstruct , @PreDestroy
AnnotationAwareAspectJAutoProxyCreator – handles AOP proxies
ApplicationContextAwareProcessor – injects Aware interfaces
AsyncAnnotationBeanPostProcessor – processes @Async
ScheduledAnnotationBeanPostProcessor – processes @Scheduled
BeanFactoryPostProcessor
Allows manipulation of the BeanFactory before beans are instantiated. Example disables circular references:
<code>public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
((DefaultListableBeanFactory) beanFactory).setAllowCircularReferences(false);
}
}
</code>Spring SPI Mechanism
SPI uses files under META-INF named spring.factories containing key‑value pairs where the key is an interface and the value is one or more implementation class names.
SpringFactoriesLoader Demo
<code>public class MyEnableAutoConfiguration {}
// spring.factories entry:
// com.sanyou.spring.extension.spi.MyEnableAutoConfiguration=com.sanyou.spring.extension.User
public class Application {
public static void main(String[] args) {
List<String> classNames = SpringFactoriesLoader.loadFactoryNames(MyEnableAutoConfiguration.class,
MyEnableAutoConfiguration.class.getClassLoader());
classNames.forEach(System.out::println);
}
}
</code>Output shows the User class is loaded.
SpringBoot Startup Extension Points
1. Auto‑configuration
SpringBoot reads spring.factories for the key EnableAutoConfiguration and registers the listed configuration classes. Example creates UserAutoConfiguration that defines a UserFactoryBean , then adds it to spring.factories . Running the application prints the bean creation via the factory.
2. PropertySourceLoader
SpringBoot supports .properties and .yaml . To add .json , implement PropertySourceLoader :
<code>public class JsonPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() { return new String[]{"json"}; }
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
ReadableByteChannel channel = resource.readableChannel();
ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength());
channel.read(buffer);
String content = new String(buffer.array());
JSONObject json = JSON.parseObject(content);
Map<String, Object> map = new HashMap<>();
for (String key : json.keySet()) {
map.put(key, json.getString(key));
}
return Collections.singletonList(new MapPropertySource("jsonPropertySource", map));
}
}
</code>Add to spring.factories under org.springframework.boot.env.PropertySourceLoader . After placing application.json and injecting a @Value field, the JSON values are available as bean properties.
3. ApplicationContextInitializer
Loaded via SPI, receives a ConfigurableApplicationContext before it is refreshed, allowing programmatic configuration.
4. EnvironmentPostProcessor
Processes ConfigurableEnvironment early in startup, used by SpringBoot to load external configuration files.
5. ApplicationRunner / CommandLineRunner
Executed after the context is fully started; beans can be injected directly.
Spring Event System
Implements the observer pattern. Core classes:
ApplicationEvent – base class for events.
ApplicationListener<E extends ApplicationEvent> – handles a specific event type.
ApplicationEventPublisher – publishes events (implemented by ApplicationContext ).
Example fire event:
<code>public class FireEvent extends ApplicationEvent {
public FireEvent(String source) { super(source); }
}
public class Call119FireEventListener implements ApplicationListener<FireEvent> {
@Override
public void onApplicationEvent(FireEvent event) { System.out.println("Call 119"); }
}
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {
@Override
public void onApplicationEvent(FireEvent event) { System.out.println("Rescue person"); }
}
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Call119FireEventListener.class, SavePersonFireEventListener.class);
ctx.refresh();
ctx.publishEvent(new FireEvent("Fire!");
}
}
</code>Output:
<code>Call 119
Rescue person
</code>Spring Built‑in Events
ContextRefreshedEvent – after refresh()
ContextStartedEvent – after start()
ContextStoppedEvent – after stop()
ContextClosedEvent – after close()
Listeners can react to these lifecycle moments.
Event Propagation Across Parent‑Child Contexts
When a child context publishes an event, the parent context also receives it. Demonstrated by registering a listener in the parent and another in the child, then publishing from the child.
<code>public class EventPropagateApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
parent.register(Call119FireEventListener.class);
parent.refresh();
AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
child.register(SavePersonFireEventListener.class);
child.refresh();
child.setParent(parent);
child.publishEvent(new FireEvent("Fire!");
}
}
</code>Result shows both listeners are invoked.
Custom XML Namespace
Spring allows custom XML tags via a namespace handler.
Step 1 – Define XSD (META-INF/sanyou.xsd)
<code><xsd:schema xmlns="http://sanyou.com/schema/sanyou"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://sanyou.com/schema/sanyou">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:complexType name="Bean">
<xsd:attribute name="class" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:element name="mybean" type="Bean"/>
</xsd:schema>
</code>Step 2 – Implement NamespaceHandler
<code>public class SanYouNameSpaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("mybean", new SanYouBeanDefinitionParser());
}
private static class SanYouBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected boolean shouldGenerateId() { return true; }
@Override
protected String getBeanClassName(Element element) { return element.getAttribute("class"); }
}
}
</code>Step 3 – Register via SPI files
spring.handlers :
<code>http\://sanyou.com/schema/sanyou=com.sanyou.spring.extension.namespace.SanYouNameSpaceHandler
</code>spring.schemas :
<code>http\://sanyou.com/schema/sanyou.xsd=META-INF/sanyou.xsd
</code>Test XML
<code><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:sanyou="http://sanyou.com/schema/sanyou"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://sanyou.com/schema/sanyou
http://sanyou.com/schema/sanyou.xsd">
<sanyou:mybean class="com.sanyou.spring.extension.User"/>
</beans>
</code>Running a ClassPathXmlApplicationContext loads the User bean successfully.
Conclusion
The article covered numerous Spring extension points—FactoryBean, @Import, BeanPostProcessor, BeanFactoryPostProcessor, SPI, SpringBoot auto‑configuration, PropertySourceLoader, ApplicationContextInitializer, EnvironmentPostProcessor, runners, event system, and custom XML namespaces—illustrating how they enable powerful customization and integration of third‑party frameworks.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.