盘点10个Spring实际开发中最常用扩展点

概述

Spring的核心思想就是容器,具有 控制反转(IoC)面向切面(AOP) 两大核心;

Spring Boot更是封装了Spring,遵循 约定大于配置,加上自动装配的机制。很多时候我们只要引用了一个依赖,几乎是零配置就能完成一个功能的装配。

一个高级开发者如果想精通Spring,不能仅仅局限于对框架的使用,而是需要对Spring进行深入理解,了解SpringBean是如何进行管理的,熟知Spring的各个扩展点。

在网上搜索Spring扩展点,发现很少有博文说的很全的,只有一些常用的扩展点的说明。

因此想通过本篇文章,详细总结一下Spring&Spring Boot所有的扩展接口,以及各个扩展点的使用场景。通过不同的实例和图片给每一个扩展点加以说明,并且整理出了一个beanSpring内部从被加载到最后初始化完成所有可扩展点的顺序调用图。从而我们也能窥探到bean是如何一步步加载到Spring容器中的。

👨‍🎓废话不多说,下面直接进入今天的主题。

1. 容器刷新之回调ApplicationContextInitializer

ApplicationContextInitializerSpring框架原有的东西,这个类的主要作用就是在ConfigurableApplicationContext类型(或者子类型)的ApplicationContextrefresh刷新之前,允许我们对ConfiurableApplicationContext的实例做进一步的设置和处理。它是在Spring容器刷新之前执行的一个回调函数。是在ConfigurableApplicationContext#refresh()之前调用(当Spring框架内部执行 ConfigurableApplicationContext#refresh()方法的时候或者在Spring Bootrun()执行时),作用是初始化Spring ConfigurableApplicationContext的回调接口。

简单理解就是:用于在刷新容器之前初始化Spring的回调接口,通常用于Spring/SpringBoot容器中注入属性。

ApplicationContextInitializer的作用

通常用于需要对应用程序上下文进行编程初始化的web应用程序中。例如,根据上下文环境注册属性源或激活概要文件。

ApplicationContextInitializer扩展

自定义ApplicationContextInitializerExtension扩展类

@Slf4j
public class ApplicationContextInitializerExtension implements ApplicationContextInitializer {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {

        ConfigurableEnvironment env = applicationContext.getEnvironment();
        HashMap<String, Object> map = new HashMap<>();
        map.put("name","austin");

        MapPropertySource mapPropertySource = new MapPropertySource("applicationContextInitializerExtension", map);
        env.getPropertySources().addLast(mapPropertySource);

        log.info("[ApplicationContextInitializerExtension] Loading ...");
    }
}
复制代码

手动在启动类添加initialier

@SpringBootApplication
public class SpringExtendPointsApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(SpringExtendPointsApplication.class);
        springApplication.addInitializers(new ApplicationContextInitializerExtension());
        springApplication.run(args);
    }
}
复制代码

run方法是SpringApplication的静态方法,其中会生成SpringApplication实例对象,真正执行的是实例对象的run方法。SpringFactoriesLoader加载ApplicationContextInitializer的过程就发生在生成 SpringApplication实例的过程中。类加载完毕,且生成了实例,那这些初始化器什么时候生效呢?如下是run 方法执行流程。

ApplicationContextInitializer是在准备Application的上下文阶段被执行的。我们知道,Spring是在刷新上下文的时候开始通过BeanFactory加载Bean,所以,ApplicationContextInitializer的执行发生在 Bean加载之前,但是此时的Environment已经初始化完毕,我们可以在该阶段获得Environment的实例,方便增加或修改一些值;此时ApplicationContext实例也创建好了,可以预先在上下文中加入一些监听器,处理器等。

启动项目,访问http://localhost:36878/getAttributes 查看控制台输出:

2. 获取Spring容器对象

Spring通过依赖注入方式能自动装配Bean,在我们日常开发中,经常需要从Spring容器中获取Bean,获取Spring对象主要提供了三种扩展实现:

  • FactoryBean接口
  • BeanFactoryAware接口
  • ApplicationContextAware接口

FactoryBean接口

BeanFactoryBean的工厂,可以帮我们生成想要的Bean,而FactoryBean就是一种Bean的类型。当往容器中注入class类型为FactoryBean的类型的时候,最终生成的Bean是通过FactoryBeangetObject获取的。

定义一个UserService(FactoryBean)

定义一个UserFactoryBeanExtension,实现FactoryBean接口,getObject方法返回一个User对象:

public class UserFactoryBeanExtension implements FactoryBean<User> {

    private BeanFactory beanFactory;

    @Override
    public User getObject() throws Exception {
        User user = new User();
        user.setId("123");
        user.setUsername("austin");
        user.setEmail("austin@gmail.com");
        user.setAge(26);
        System.out.println("调用[UserFactoryBeanExtension]的getObject方法生成Bean:" + user);
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        // 这个UserFactoryBeanExtension返回的Bean的类型
        return User.class;
    }
}
复制代码

启动测试类

@Slf4j
@SpringBootApplication
public class SpringExtendPointsApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringExtendPointsApplication.class, args);

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(UserFactoryBeanExtension.class);
        applicationContext.refresh();

        log.info("获取到的Bean为:" + applicationContext.getBean(User.class));
    }
}
复制代码

控制台打印:

调用[UserFactoryBeanExtension]的getObject方法生成Bean:User(id=123, username=austin, email=austin@gmail.com, age=26)
[main] c.a.s.SpringExtendPointsApplication: 获取到的Bean为:User(id=123, username=austin, email=austin@gmail.com, age=26)
复制代码

从结果可以看出,明明注册到Spring容器的是UserFactoryBeanExtension,但是却能从容器中获取到User类型的BeanUser这个Bean就是通过UserFactoryBeanExtensiongetObject方法返回的。

3. Spring Aware接口

🔥几种常用的Aware接口如下:

  • ApplicationContextAware能获取
  • Application Context`调用容器的服务
  • ApplicationEventPublisherAware`应用事件发布器,可以用来发布事件
  • BeanClassLoaderAware能获取加载当前Bean`的类加载器
  • BeanFactoryAware能获取Bean Factory`调用容器的服务
  • BeanNameAware`能获取当前Bean的名称
  • EnvironmentAware`能获取当前容器的环境属性信息
  • MessageSourceAware`能获取国际化文本信息
  • ResourceLoaderAware`获取资源加载器读取资源文件
  • ServletConfigAware`能获取到
  • ServletConfigServletContextAware能获取到ServletContext`

Spring为了扩展性,不仅提供了容器刷新时会处理的后置处理器,而且提供了运行时感知的Aware来提供访问容器资源的接口。让开发者根据需求访问容器资源。

如果Spring检测到一个bean实现了Aware接口,则能在bean中获取相应的Spring资源;如果某个对象实现了某个Aware接口,比如需要依赖Spring的上下文容器(ApplicationContext),则可以实现ApplicationContextAware接口。

SpringBean进行初始化(注意与实例化的区别)之前,会将依赖的ApplicationContext对象通过调用ApplicationContextAware#setApplicationContext方法实现注入。

4. 自定义拦截器

拦截器的本质是面向切面编程,符合横切关注点的功能都可以放在拦截器中实现,主要的使用场景包括:

  • 日志记录
  • 登录授权、验证
  • 权限检查
  • 性能检测(记录业务执行的耗时)

一般可以通过实现HandlerInterceptor接口,重新preHandle、postHandle、afterCompletion方法来实现,也可以通过继承HandlerInterceptorAdapter抽象类实现,但是最新版本的Spring该类已经废弃。

代码示例:

/**
 * 自定义HandlerInterceptor扩展点,从5.3开始支持直接实现HandlerInterceptor和/或AsyncHandlerInterceptor,HandlerInterceptorAdapter被弃用
 *
 * @author: austin
 * @since: 2023/2/14 3:58
 */
@Slf4j
@Component
public class InterceptorExtension implements HandlerInterceptor {

    /**
     * 预处理,controller方法执行前
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("[CustomInterceptor] preHandle...");
        return true;
    }

    /**
     * 后处理方法,controller方法执行后,在success执行之前,可以通过给定的ModelAndView向视图公开其他模型对象
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("[CustomInterceptor] postHandle...");
    }

    /**
     * 方法执行完成后
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("[CustomInterceptor] afterCompletion...");
    }
}
复制代码

5. 自定义工具获取Spring容器对象

举个例子🌰:SpringContextUtils通过实现ApplicationContextAware类,实现根据名称getBean()获取IOCBean对象,或者根据类Class去获取Bean对象。

@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ConfigurableApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = (ConfigurableApplicationContext) context;
    }

    /**
     * 获取ApplicationContext对象
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 根据bean的名称获取bean
     */
    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    /**
     * 根据bean的class来查找对象
     */
    public static <T> T getBeanByClass(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    /**
     * 根据bean的class来查找所有的对象(包括子类)
     */
    public static <T> Map<String, T> getBeansByClass(Class<T> c) {
        return applicationContext.getBeansOfType(c);
    }

    /**
     * 获取HttpServletRequest
     */
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getRequest();
    }

    /**
     * 获取HttpSession
     */
    public static HttpSession getSession() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getRequest().getSession();
    }
}
复制代码

6. 初始化方法

Spring中使用比较多的初始化bean的方法有:

  • 使用@PostConstruct注解
  • 实现InitializingBean接口

@PostConstruct

源码所在位置:javax.annotation.PostConstruct

初始化方法应该算是Spring的一个扩展点之一,@PostConstruct注解的方法在项目启动的时候执行这个方法,也可以理解为在Spring容器启动的时候执行,可作为一些数据的常规化加载,比如:数据预热、字段数据加载、区域信息初始化 等等,被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

@PostConstruct注解的方法将会在依赖注入完成后被自动调用。对应的执行顺序是:Construcutor > @Autowired > @PostConstruct

举个示例🌰:

@Component
public class DataRreHeating {

    @PostConstruct
    public void customerFieldInit() {
        // 1.初始化区域信息
        initRegionData();
        
        // 2.加载字典数据
        loadDictionaryInfo();
    }

    private void loadDictionaryInfo() {
        // TODO do something
    }

    private void initRegionData() {
        // TODO do something
    }
}
复制代码

initializingBean接口实现初始化

通过实现InitializingBean接口,继而实现afterPropertiesSet的方法,项目启动的时候会执行afterPropertiesSet方法,从而实现数据的初始加载,实际上它跟@PostConstruct功能非常差不多。

举个例子🌰:

@Slf4j
@Service
public class AirportDataService implements InitializingBean {

    // 根据不同的语言来初始化机场信息
    @Override
    public void afterPropertiesSet() throws Exception {

        CompletableFuture.runAsync(() -> {
            String[] langs = new String[]{"en","es","pt","fr-ca"};
            for(String lang : langs) {
                try {
                    final String key = String.format(lockFormat, lang);
                    if(!lockService.grabLock(key, 30, TimeUnit.SECONDS)){
                        continue;
                    }
                    
                    doInit(lang);
                    
                    lockService.releaseLock(key);
                } catch (Exception e){
                    log.error("[{}, {}] Init airport data error", , lang, e);
                }
            }
        });
    }
}
复制代码

7. 当工程启动时

有时候我们需要在项目启动的时候自定义一些额外的功能,比如加载一些系统参数,完成初始化,预热本地缓存等。 我们应该做什么?

SpringBoot 提供了:

  • CommandLineRunner
  • ApplicationRunner

简单的以ApplicationRunner接口为🌰:

@Slf4j
@Component
public class ApplicationInit implements ApplicationRunner {
    
    @Resource
    private IAreaService iAreaService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        iAreaService.initArea();
    }
}
复制代码

只需要实现ApplicationRunner接口,重写run方法,在该方法中实现你的自定义需求。如果项目中有多个类实现了ApplicationRunner接口,如何指定它们的执行顺序?答案是使用@Order(n)注解,n的值越小越早执行。 当然,顺序也可以通过@Priority注解来指定。

8. 类的转换器

源码位置:org.springframework.cglib.core.Converter

如果接口中接收参数的实体对象中,有一个字段类型为Date,但实际传递的参数是字符串类型:2023-02-14 10:20:15,该如何处理?

Spring提供了一个扩展点,类型转换器Type Converter,具体分为3类:

  • Converter<S,T>: 将类型 S 的对象转换为类型 T 的对象
  • ConverterFactory<S, R>: 将 S 类型对象转换为 R 类型或其子类对象
  • GenericConverter:它支持多种源和目标类型的转换,还提供了源和目标类型的上下文。 此上下文允许根据注释或属性信息执行类型转换。
Component
public class StringToDateConverter implements Converter<String, Date> {
    public static final Logger logger = LoggerFactory.getLogger(StringToDateConverter.class);
    static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 时间格式转换
     */
    @Override
    public Date convert(String source) {
        Date date = new Date();
        try {
           date =  simpleDateFormat.parse(source);
        } catch (ParseException e) {
            logger.info(source + "转换异常");
            e.printStackTrace();
        }
        return date;
    }
}
复制代码

9. 容器初始化和容器销毁

有时候,我们需要在关闭Spring容器前,做一些额外的工作,比如:关闭资源文件等。这时可以实现DisposableBean接口,并且重写它的destroy方法:

@Component
public class Application 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");
    }
}
复制代码

运行项目,查看控制台打印,一开始项目启动的时候容器会初始化执行afterPropertiesSet()方法,当我们Stop Appcation的时候,会触发destroy销毁的方法:

[InitializingBean] afterPropertiesSet...
2023-02-14 17:11:05.024  INFO 14048 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 36878 (http) with context path ''
2023-02-14 17:11:05.631  INFO 14048 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2023-02-14 17:11:05.638  INFO 14048 --- [           main] c.a.s.SpringExtendPointsApplication      : Started SpringExtendPointsApplication in 2.63 seconds (JVM running for 3.345)
[DisposableBean] destroy...
复制代码

10. 事件之ApplicationListener

准确的说,这个应该不算Spring&Spring Boot当中的一个扩展点,ApplicationListener可以监听某个事件的event,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。但是Spring内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。

Spring事件是基于事件/监听器编程模型,在这个模型中,有几个重要的角色,事件ApplicationEvent,应用事件监听器ApplicationListener,以及事件发布者ApplicationContext

Spring其他所有的扩展接口

🤔思考: Spring还有很多可扩展的接口,在本文并没有提及到,如果有兴趣的,可以自行去了解一下对于接口的特性,可以思考一下不同的接口在适合什么情况下做扩展?

总结

通过本篇文章,我总结了Spring在实际开发中常用的扩展点,可以发现其实Spring的扩展能力是非常强的,正因为Spring自身的这个优势,让Spring拥有了强大的拓展能力,能让很多第三方应该轻松的接入Spring,与Spring做整合,比如:JDBC、MyBatis、Redis、RabbitMQ

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值