找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3726

积分

1

好友

513

主题
发表于 2026-2-12 20:36:25 | 查看: 30| 回复: 0

一提到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 后,用户会看到类似下图的堆栈信息:

Java Web应用Whitelabel错误页面截图

直接把如此详细的错误堆栈抛给用户,体验无疑非常糟糕。你可能会想,在每个接口里用 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层,可以直接获取 HttpServletRequestHttpServletResponse 等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提供了两个非常方便的接口:CommandLineRunnerApplicationRunner

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的初始化方法(@PostConstructafterPropertiesSet)被调用之前之后,插入一些全局性的处理逻辑,该怎么办?实现 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 等注解之所以能生效,正是依靠 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 这两个内置的 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 方法。通常会同时实现 InitializingBeanDisposableBean 来完成初始化和销毁的闭环。当然,使用 @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 BootJVM的话题,欢迎来云栈社区交流分享。




上一篇:MySQL 5.7与8.0升级兼容性指南及性能问题排查
下一篇:xAI核心团队动荡:联合创始人离职、Grok安全争议与组织管理挑战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 12:58 , Processed in 0.834861 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表