Master 10 Essential Spring Boot Extension Points for Robust Backend Development
This guide walks through ten crucial Spring Boot extension points—including global exception handling, custom interceptors, container access, configuration imports, startup runners, bean definition tweaks, initialization hooks, bean post‑processing, graceful shutdown, and custom scopes—providing clear explanations and ready‑to‑use code samples for building resilient backend services.
1. Global Exception Handling
When an exception occurs in a controller method, the raw stack trace can be exposed to the user, leading to a poor experience. By catching the exception and returning a friendly message, we improve usability.
<code>@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/division")
public String division(@RequestParam("a") int a, @RequestParam("b") int b) {
return String.valueOf(a / b);
}
}</code>Calling
127.0.0.1:8080/test/division?a=10&b=0shows a detailed error page:
To avoid exposing details, we wrap the logic in a try‑catch block and return a generic error message.
<code>@GetMapping("/division")
public String division(@RequestParam("a") int a, @RequestParam("b") int b) {
String result = "";
try {
result = String.valueOf(a / b);
} catch (ArithmeticException e) {
result = "params error";
}
return result;
}</code>For many endpoints, adding such code manually is impractical; a global handler using
@RestControllerAdvicesolves this.
<code>@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
if (e instanceof ArithmeticException) {
return "params error";
}
if (e instanceof Exception) {
return "Internal server exception";
}
return null;
}
}</code>All controllers now benefit from unified error handling without individual try‑catch blocks.
2. Custom Interceptor
Spring MVC interceptors can access
HttpServletRequestand
HttpServletResponse. The core interface
HandlerInterceptordefines three methods:
preHandle: executed before the target method.
postHandle: executed after the target method.
afterCompletion: executed after request completion.
Typically we extend
HandlerInterceptorAdapterto implement custom logic, such as permission checks.
<code>public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUrl = request.getRequestURI();
if (checkAuth(requestUrl)) {
return true;
}
return false;
}
private boolean checkAuth(String requestUrl) {
System.out.println("===Authority Verification===");
return true;
}
}</code>Register the interceptor in a configuration class.
<code>@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
@Bean
public AuthInterceptor getAuthInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor());
}
}</code>3. Access Spring Container Objects
Sometimes we need to obtain beans programmatically.
3.1 BeanFactoryAware
<code>@Service
public class StudentService implements BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public void add() {
Student student = (Student) beanFactory.getBean("student");
}
}</code>3.2 ApplicationContextAware
<code>@Service
public class StudentService2 implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void add() {
Student student = (Student) applicationContext.getBean("student");
}
}</code>4. Import Configuration
The
@Importannotation allows importing additional classes into the Spring container.
4.1 Import a Plain Class
<code>public class A {}
@Import(A.class)
@Configuration
public class TestConfiguration {}</code>Beans can be autowired without declaring
@Beanmethods.
4.2 Import a @Configuration Class
<code>public class A {}
public class B {}
@Import(B.class)
@Configuration
public class AConfiguration { }
@Import(AConfiguration.class)
@Configuration
public class TestConfiguration { }</code>4.3 ImportSelector
<code>public class AImportSelector implements ImportSelector {
private static final String CLASS_NAME = "com.demo.cache.service.A";
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{CLASS_NAME};
}
}
@Import(AImportSelector.class)
@Configuration
public class TestConfiguration { }</code>4.4 ImportBeanDefinitionRegistrar
<code>public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class);
registry.registerBeanDefinition("a", rootBeanDefinition);
}
}
@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
public class TestConfiguration { }</code>5. Additional Logic at Application Startup
Implement
ApplicationRunneror
CommandLineRunnerto execute code after the Spring context is ready.
<code>@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("Project startup: load system parameters...");
Properties properties = new Properties();
try (InputStream inputStream = new FileInputStream("application.properties")) {
properties.load(inputStream);
String systemParam = properties.getProperty("system.param");
System.out.println("Loaded system param: " + systemParam);
} catch (IOException e) {
e.printStackTrace();
}
}
}</code>6. Modify BeanDefinition
Implement
BeanFactoryPostProcessorto alter bean definitions before instantiation.
<code>@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);
beanDefinitionBuilder.addPropertyValue("id", 123);
beanDefinitionBuilder.addPropertyValue("name", "Dylan Smith");
defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
}
}</code>7. Initialization Methods
Two common ways to run initialization logic:
Use
@PostConstructannotation.
Implement
InitializingBeaninterface.
<code>@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===Initializing===");
}
}</code> <code>@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===Initializing===");
}
}</code>8. BeanPostProcessor Before/After Initialization
Implement
BeanPostProcessorto inject custom logic around bean initialization.
<code>@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof User) {
((User) bean).setUserName("Dylan Smith");
}
return bean;
}
}</code>9. Operations Before Container Shutdown
Implement
DisposableBean(often together with
InitializingBean) to run cleanup code.
<code>@Service
public class DService implements InitializingBean, DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean afterPropertiesSet");
}
}</code>10. Custom Scope
Spring supports only
singletonand
prototypeby default; custom scopes can be created for special needs.
Step 1: Implement Scope Interface
<code>public class ThreadLocalScope implements Scope {
private static final ThreadLocal<Object> THREAD_LOCAL_SCOPE = new ThreadLocal<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object value = THREAD_LOCAL_SCOPE.get();
if (value != null) {
return value;
}
Object object = objectFactory.getObject();
THREAD_LOCAL_SCOPE.set(object);
return object;
}
@Override
public Object remove(String name) {
THREAD_LOCAL_SCOPE.remove();
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {}
@Override
public Object resolveContextualObject(String key) { return null; }
@Override
public String getConversationId() { return null; }
}</code>Step 2: Register the New Scope
<code>@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
}
}</code>Step 3: Use the Custom Scope
<code>@Scope("threadLocalScope")
@Service
public class CService {
public void add() {}
}</code>Summary
This article covered ten frequently used Spring extension points, providing practical code examples for global exception handling, custom interceptors, container access, configuration imports, startup runners, bean definition modification, initialization hooks, bean post‑processing, graceful shutdown, and defining custom scopes, enabling developers to build more flexible and maintainable Spring Boot applications.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.