Approaches for Dynamically Registering Beans in Spring Boot
Test Environment & Use Case
- Runtime: Java 8
- Framework version: Spring Boot 2.5.14
- Demo scenario: Dynamically register
ProxyServletto implement Nginx-style reverse proxy functionality
In Spring Boot's bean lifecycle, bean definition loading always precedes bean instantiation. To register beans dynamically, you only need to inject custom bean definitions during the window after standard bean definitions are loaded and before instantiation starts. This is achieved via the postProcessBeanDefinitionRegistry method of the BeanDefinitionRegistryPostProcessor interface.
Official source code note: Modify the application context’s internal bean definition registry after its standard initialization. All regular bean definitions will have been loaded, but no beans will have been instantiated yet. This allows for adding further bean definitions before the next post-processing phase kicks in.
General Dynamic Registration Method
Note for bean loading order: If you define three separate beans that implement only BeanDefinitionRegistryPostProcessor, ApplicationContextAware, and EnvironmentAware respectively, their default loading and method execution order matches the order listed above. If a bean implements an Aware interface with higher loading priority than ApplicationContextAware or EnvironmentAware, its corresponding setter method will execute first, as Spring detects all implemented Aware interfaces and invokes their setters before the formal bean initialization process.
This general approach is applicable for all dynamic bean registration scenarios in Spring Boot, by registering custom bean definitions through BeanDefinitionRegistryPostProcessor:
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DynamicBeanRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment runtimeEnv;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinitionBuilder servletRegBuilder = BeanDefinitionBuilder.genericBeanDefinition(ServletRegistrationBean.class);
servletRegBuilder.addConstructorArgValue(new ProxyServlet());
servletRegBuilder.addConstructorArgValue(runtimeEnv.getProperty("proxy.servletUrl"));
Map<String, String> servletInitParams = new HashMap<>();
servletInitParams.put("targetUri", runtimeEnv.getProperty("proxy.targetUrl"));
servletInitParams.put("log", "true");
servletRegBuilder.addPropertyValue("initParameters", servletInitParams);
servletRegBuilder.addPropertyValue("name", runtimeEnv.getProperty("proxy.name"));
registry.registerBeanDefinition("customProxyServlet", servletRegBuilder.getBeanDefinition());
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// No custom processing required for this scenario
}
@Override
public void setEnvironment(Environment environment) {
this.runtimeEnv = environment;
}
}
As an alternative to implementing the
EnvironmentAwareinterface, you can also inject theEnvironmentobject via constructor to ensure its fully initialized when the registration method runs.
Servlet-Specific Dynamic Registration Method
For servlet registration scenarios, you can directly use the ServletContext to register servlets dynamically without going through the general bean definition process:
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import java.util.Map;
@Configuration
public class CustomProxyServletRegistrar implements ServletContextInitializer {
private final Environment runtimeEnv;
public CustomProxyServletRegistrar(Environment runtimeEnv) {
this.runtimeEnv = runtimeEnv;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
String proxyName = runtimeEnv.getProperty("proxy.name");
String targetEndpoint = runtimeEnv.getProperty("proxy.targetUrl");
createAndRegisterProxyServlet(servletContext, proxyName, targetEndpoint);
}
private void createAndRegisterProxyServlet(ServletContext servletContext, String proxyAlias, String targetUri) {
ProxyServlet reverseProxyServlet = new ProxyServlet();
ServletRegistration.Dynamic servletReg = servletContext.addServlet(proxyAlias + "ReverseProxyServlet", reverseProxyServlet);
servletReg.setLoadOnStartup(1);
servletReg.addMapping("/" + proxyAlias + "/*");
servletReg.setInitParameters(Map.of(
"targetUri", targetUri,
"log", "true"
));
}
}