Backend Development 40 min read

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.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Unlock Spring’s Hidden Extension Points: From FactoryBean to Custom Namespaces

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() .

FactoryBean diagram
FactoryBean diagram

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 registration diagram
Bean registration diagram

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>&lt;xsd:schema xmlns="http://sanyou.com/schema/sanyou"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://sanyou.com/schema/sanyou"&gt;
    &lt;xsd:import namespace="http://www.w3.org/XML/1998/namespace"/&gt;
    &lt;xsd:complexType name="Bean"&gt;
        &lt;xsd:attribute name="class" type="xsd:string" use="required"/&gt;
    &lt;/xsd:complexType&gt;
    &lt;xsd:element name="mybean" type="Bean"/&gt;
&lt;/xsd:schema&gt;
</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>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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">
    &lt;sanyou:mybean class="com.sanyou.spring.extension.User"/>
&lt;/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.

BackendJavaSpringdependency injectionExtension Points
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.