一提到Spring,或许你首先想到的是IOC(控制反转)和AOP(面向切面编程)。它们确实是Spring框架的基石,也正是凭借其出色的设计,Spring才能在众多优秀框架中脱颖而出。
Spring的强大之处在于其卓越的扩展性,许多第三方应用程序如RocketMQ、MyBatis、Redis等都能轻松集成。今天,我们就来深入探讨一下Spring框架中最常用、最核心的十个扩展点。
1. 全局异常处理
在开发接口时,如果发生异常,直接返回给用户的默认错误信息通常不够友好,甚至可能暴露内部细节。
设想一个简单的除法接口,如果没有异常处理:
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/division")
public String division(@RequestParam("a") int a, @RequestParam("b") int b) {
return String.valueOf(a / b);
}
}
访问 127.0.0.1:8080/test/division?a=10&b=0 后,用户会看到类似下图的堆栈信息:

直接把如此详细的错误堆栈抛给用户,体验无疑非常糟糕。你可能会想,在每个接口里用 try-catch 包裹不就行了?
@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;
}
改造后,用户只会看到 “params error”,体验好了不少。
但问题是,如果一个项目有成百上千个接口,难道要在每个接口都重复这种异常处理代码吗?这显然是不现实的。此时,Spring提供的全局异常处理扩展点 @RestControllerAdvice 就派上用场了。
@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;
}
}
只需在一个地方集中处理各种异常,业务接口就可以放心编写核心逻辑,无需再关心异常捕获,所有异常都将遵循这里定义的统一规则返回给前端。
2. 自定义拦截器
与Spring拦截器相比,Spring MVC拦截器更贴近Web层,可以直接获取 HttpServletRequest 和 HttpServletResponse 等Web对象。
Spring MVC拦截器的顶级接口是 HandlerInterceptor,它定义了三个关键方法:
preHandle:在目标方法执行前执行。
postHandle:在目标方法执行后执行。
afterCompletion:在整个请求完成时执行。
为了方便,我们通常会使用其适配器类 HandlerInterceptorAdapter。自定义拦截器非常适用于权限校验、日志记录、请求统计等场景。
第一步,继承 HandlerInterceptorAdapter 定义你自己的拦截器:
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;
}
}
第二步,将这个拦截器注册到Spring容器中:
@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
@Bean
public AuthInterceptor getAuthInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor());
}
}
完成配置后,所有匹配的接口请求都会自动经过这个拦截器进行权限验证。
3. 获取 Spring 容器对象
在开发中,我们有时需要从Spring容器中主动获取Bean。你知道如何拿到这个容器对象本身吗?Spring提供了两个核心接口。
3.1 BeanFactoryAware 接口
实现此接口,Spring会在Bean初始化后将 BeanFactory 实例注入进来。
@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");
}
}
3.2 ApplicationContextAware 接口
这个接口更常用,它提供的是功能更丰富的 ApplicationContext。
@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");
}
}
4. 导入配置
有时我们需要在一个配置类中引入其他类,并将这些类也纳入Spring容器管理。@Import 注解就是为此而生。它支持导入多种类型的类。
4.1 导入普通类
这是最简单的方式,被导入的类会被实例化为一个Bean。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}
之后,你就可以在其他地方通过 @Autowired 直接注入A的实例,无需在其类上添加任何注解。
4.2 导入带有@Configuration注解的配置类
这种方式功能强大,因为被导入的配置类本身可能又通过 @Import、@ImportResource、@PropertySource 等注解引入了更多配置,这些都会一并生效。
public class A {
}
public class B {
}
@Import(B.class)
@Configuration
public class AConfiguration {
@Bean
public A a() {
return new A();
}
}
@Import(AConfiguration.class)
@Configuration
public class TestConfiguration {
}
最终,A和B都会被注册到Spring容器中。
4.3 ImportSelector
当需要根据条件动态决定导入哪些类时,可以实现 ImportSelector 接口。
public class AImportSelector implements ImportSelector {
private static final String CLASS_NAME = "com.demo.cache.service.A";
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{CLASS_NAME};
}
}
@Import(AImportSelector.class)
@Configuration
public class TestConfiguration {
}
selectImports 方法返回一个字符串数组,非常适合批量导入。
4.4 ImportBeanDefinitionRegistrar
这是最灵活的方式,允许你以编程方式直接向容器注册BeanDefinition,可以精细控制Bean的元数据。
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 {
}
5. 项目启动时的附加功能
有时我们需要在Spring Boot应用启动完成后立即执行一些逻辑,比如加载系统参数、初始化资源、预热本地缓存等。Spring Boot提供了两个非常方便的接口:CommandLineRunner 和 ApplicationRunner。
以 ApplicationRunner 为例:
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 在这里编写项目启动时需要执行的代码
System.out.println("项目启动时执行附加功能,加载系统参数...");
// 假设这里从配置文件中加载系统参数并进行处理
Properties properties = new Properties();
try (InputStream inputStream = new FileInputStream("application.properties")) {
properties.load(inputStream);
String systemParam = properties.getProperty("system.param");
System.out.println("加载的系统参数值为:" + systemParam);
} catch (IOException e) {
e.printStackTrace();
}
}
}
实现接口并重写 run 方法即可。当应用启动完成后,Spring Boot会自动执行所有这类Runner。
这两个接口的主要区别在于参数:ApplicationRunner 使用 ApplicationArguments 对象,它提供了对应用参数的更结构化访问;而 CommandLineRunner 接收的是原始的字符串数组 String[] args。
6. 修改 BeanDefinition
在Spring IOC容器实例化一个Bean对象之前,它会先将该Bean的所有配置信息(如类名、作用域、属性值等)读取并封装到一个 BeanDefinition 对象中。如果你想在Bean实例化之前修改这些元数据,可以实现 BeanFactoryPostProcessor 接口。
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);
beanDefinitionBuilder.addPropertyValue("id", 123);
beanDefinitionBuilder.addPropertyValue("name", "Dylan Smith");
defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
}
}
在 postProcessBeanFactory 方法中,你可以获取并修改任何已注册Bean的 BeanDefinition,甚至可以动态注册新的BeanDefinition。这是实现一些高级功能(如动态数据源、根据配置生成Bean)的关键扩展点。
7. 初始化方法
Bean被实例化和依赖注入完成后,如果需要执行一些初始化逻辑,Spring提供了两种主流方式。
7.1 使用@PostConstruct注解
这是JSR-250标准注解,使用起来非常简洁。
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===Initializing===");
}
}
7.2 实现InitializingBean接口
这是Spring原生提供的接口。
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===Initializing===");
}
}
两种方式效果类似,@PostConstruct 注解更受推崇,因为它减少了与Spring框架的耦合。
8. 在初始化 Bean 前后添加逻辑
如果你想在每个Bean的初始化方法(@PostConstruct 或 afterPropertiesSet)被调用之前或之后,插入一些全局性的处理逻辑,该怎么办?实现 BeanPostProcessor 接口。
这个接口有两个方法:
postProcessBeforeInitialization:在初始化方法之前调用。
postProcessAfterInitialization:在初始化方法之后调用。
例如,下面的处理器会在所有User类型的Bean初始化完成后,自动设置其userName属性:
@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;
}
}
实际上,我们日常使用的 @Autowired、@Value、@Resource、@PostConstruct 等注解之所以能生效,正是依靠 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 这两个内置的 BeanPostProcessor 实现的。
9. 在关闭容器之前添加操作
与初始化相对应,我们有时需要在Spring容器关闭、销毁所有Bean之前,执行一些清理工作,比如关闭网络连接、释放文件句柄、停止后台线程等。这时可以实现 DisposableBean 接口。
@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");
}
}
容器销毁时,会自动调用其 destroy 方法。通常会同时实现 InitializingBean 和 DisposableBean 来完成初始化和销毁的闭环。当然,使用 @PreDestroy 注解是另一种更解耦的销毁方式。
10. 自定义作用域
我们都知道Spring默认支持两种作用域(Scope):
singleton:单例,整个容器中只有一个实例。
prototype:原型,每次获取都创建新实例。
Spring Web又扩展了两种:
RequestScope:请求作用域,一次请求内单例。
SessionScope:会话作用域,一次会话内单例。
但如果这些都不满足需求呢?比如,我们希望在同一线程内获取的Bean是同一个实例(类似ThreadLocal),这就需要自定义Scope。
第一步,实现 Scope 接口:
public class ThreadLocalScope implements Scope {
private static final ThreadLocal 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;
}
}
第二步,将这个自定义的Scope注册到Spring容器:
我们可以复用前面提到的 BeanFactoryPostProcessor。
@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
}
}
第三步,使用自定义的Scope:
@Scope("threadLocalScope")
@Service
public class CService {
public void add() {
}
}
现在,在同一个线程内,无论你从Spring容器中获取多少次 CService,拿到的都是同一个实例;不同线程间则互不影响。这为某些特定场景(如线程级事务、线程级缓存)提供了极大便利。

掌握这些扩展点,能让你更深入地理解Spring的运作机制,从“会用”框架进阶到“懂”框架。当遇到复杂业务场景时,你就能灵活运用这些“钩子”,定制出符合需求的解决方案,而不是被框架限制住思路。希望这十个扩展点能成为你Java进阶路上的得力助手。如果你想深入探讨更多关于Spring Boot或JVM的话题,欢迎来云栈社区交流分享。