Backend Development 15 min read

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.

macrozheng
macrozheng
macrozheng
Master 10 Essential Spring Boot Extension Points for Robust Backend Development

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&amp;b=0

shows a detailed error page:

Exception response example
Exception response example

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

@RestControllerAdvice

solves 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

HttpServletRequest

and

HttpServletResponse

. The core interface

HandlerInterceptor

defines three methods:

preHandle

: executed before the target method.

postHandle

: executed after the target method.

afterCompletion

: executed after request completion.

Typically we extend

HandlerInterceptorAdapter

to 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

@Import

annotation 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

@Bean

methods.

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

ApplicationRunner

or

CommandLineRunner

to 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

BeanFactoryPostProcessor

to 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

@PostConstruct

annotation.

Implement

InitializingBean

interface.

<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

BeanPostProcessor

to 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

singleton

and

prototype

by 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.

Javabackend developmentException HandlingSpring BootInterceptorbean lifecycleCustom Scope
macrozheng
Written by

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.

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.