前言

原文地址:https://www.cnblogs.com/qnlcy/p/15905682.html

一、宗旨

在如日中天的 Spring 架构体系下,不管是什么样的组件,不管它采用的接入方式如何眼花缭乱,它们永远只有一个目的:

接入Spring容器

二、Spring 容器

Spring 容器内可以认为只有一种东西,那就是 bean,但是围绕 bean 的生命周期,Spring 添加了许多东西

2.1 bean 的生命周期

2.1.1 实例化 bean 实例

实例化 bean 实例是 spring 针对 bean 作的拓展最多的周期

它包括:

  1. bean 的扫描
  2. bean 的解析
  3. bean 实例化

常见扫描相关内容:

@Component@Service@Controller@ConfigurationapplicationContext.xml

spring/springboot 在启动的时候,会扫描到这些注解或配置文件修饰的类信息

根据拿到的类信息,经过第二步解析后,转换成 BeanDefintion 存入到 spring 容器当中,BeanDefintion 描述 bean 的 class、scop、beanName 等信息

在 bean 的解析过程中,我们常用到的 Properties 读取 、 @Configuration 配置类的处理 会在这一步完成

bean 的实例化实际有自动完成和调用 getBean() 时候完成,还有容器初始化完毕之后实例化 bean ,他们都是根据 bean 的定义 BeanDefintion 来反射目标 bean 类,并放到 bean 容器当中

这就是大名鼎鼎的 bean 容器,就是一个 Map

  1. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

2.1.2 设置实例属性

这一阶段是 @Value@Autowired@Resource 注解起作用的阶段

2.1.3 bean 前置处理

BeanPostProcessor 前置处理方法

2.1.4 bean init 处理

@PostConstruct 注解起作用的阶段

2.1.5 bean 后置处理

BeanPostProcessor 后置处理方法

2.1.6 正常使用

2.1.7 bean 销毁

@PreDestroy 注解起作用的阶段

bean 的销毁过程中,主要的作用就是释放一些需要手动释放的资源和一些收尾工作,如文件归并、连接池释放等

在了解了 Spring bean 的生命周期后,我们接下来介绍自建 Spring 组件的接入方式

三、使用简单配置类接入方式

使用配置类接入 Spring ,一般需要搭配 PostConstruct 来使用,并且要确保 Spring 能扫描到配置

如,在组件 quartz-configable 1.0 版本当中,就是使用的这种方式

quartz-configable 需要扫描用户自定义的 job 来注册到 quartz-configable 自动创建的调度器 Scheduler 当中,并启动调度器 Scheduler

在注册 Job 的过程当中,又添加了自定义的 TriggerListener 监听器,来监听配置的变动,以动态调整 Job 执行时机

  1. @Configuration
  2. public class QuartzInitConfig {
  3. @Autowired
  4. private Scheduler scheduler;
  5. @Autowired
  6. private CustomTriggerListener customTriggerListener;
  7. @PostConstruct
  8. public void init() {
  9. //先把所有jobDetail放到map里
  10. initJobMap();
  11. //添加自定义Trigger监听器,进行任务开关的监听和故障定位的配置
  12. addTriggerListener(scheduler, customTriggerListener);
  13. //添加任务到任务调度器中
  14. addJobToScheduler(scheduler);
  15. //启动任务调度器
  16. try {
  17. scheduler.start();
  18. } catch (SchedulerException e) {
  19. log.error("任务调度器启动失败", e);
  20. throw new RuntimeException("任务调度器启动失败");
  21. }
  22. log.info("任务调度器已启动");
  23. }
  24. private void initJobMap() {
  25. //省略部分代码
  26. }
  27. private void addJobToScheduler(Scheduler scheduler) {
  28. //省略部分代码
  29. }
  30. private void addTriggerListener(Scheduler scheduler, CustomTriggerListener customTriggerListener) {
  31. //省略部分代码
  32. }
  33. }

QuartzInitConfig 类的作用是把扫描到的任务类放入调度器当中,并添加自定义监听(用于动态修改 cron 表达式)

此类加载有两个过程:

  1. 注入组件初始化需要的资源
  2. 根据注入的资源初始化组件

步骤 1 所需要的功能与 Spring 的注入功能完美契合,而恰好 @Configuration 修饰的类也被当作了一个 Spring bean,所以才能顺利注入组件需要的资源

步骤 2 的初始化任务,极为契合 Spring bean 创建完毕后的初始化动作 @PostConstruct 当中,它同样是资源注入完毕后的初始化动作。

四、带有条件的简单配置类

有时候,我们希望通过开关或者特定的配置来启用应用内具备的功能,这时候,我们可以使用 @ConditionalOnProperty 来解决问题

risk 组件扫描出符合规则的切点,在切点执行之前,去执行发送风控数据到风控平台的动作

  1. @Configuration
  2. @ConditionalOnProperty({"risk.expression", "risk.appid", "risk.appsecret", "risk.url"})
  3. public class RiskAspectConfig {
  4. //项目内配置
  5. @Value("${risk.expression}")
  6. private String riskExpression;
  7. @Bean
  8. public DefaultPointcutAdvisor defaultPointcutAdvisor() {
  9. SpringBeans springBeans = springBeans();
  10. RiskSenderDelegate riskSenderDelegate = new RiskSenderDelegate(springBeans);
  11. GrjrMethodInterceptor grjrMethodInterceptor = new GrjrMethodInterceptor(riskSenderDelegate);
  12. JdkRegexpMethodPointcut jdkRegexpMethodPointcut = new JdkRegexpMethodPointcut();
  13. jdkRegexpMethodPointcut.setPattern(riskExpression);
  14. log.info("切面准备完毕,切点为{}", riskExpression);
  15. return new DefaultPointcutAdvisor(jdkRegexpMethodPointcut, grjrMethodInterceptor);
  16. }
  17. //省略部分代码
  18. }

虽然类 RiskAspectConfig 是一个 Spring 配置类,方法 defaultPointcutAdvisor() 创建了一个切点顾问,用来在切点方法处实现风控的功能,但是,并不是应用启动之后,切点就会生效,这是因为有 @ConditionalOnProperty 的存在

@ConditionalOnProperty 的作用:

根据提供的条件判断对应的属性是否存在,存在,则加载此配置类,不存在,则忽略。

当应用中存在如下配置时:

  1. grjr:
  2. risk:
  3. expression: xxxx
  4. appid: xxx
  5. appsecret: xxx
  6. url: xxx

RiskAspectConfig 配置类才会被加载,才会生成切点顾问 DefaultPointcutAdvisor,因此切点就会生效

当需要的配置逐渐增多的时候,一条条添加进 @ConditionalOnProperty 显得比较冗长复杂,这时候该如何处理呢?

五、使用对应的 Properties 配置类来封装配置

在项目 fastdfs-spring-boot-starter 当中,像上述需要的配置有很多,那么它是怎么处理的呢?

它把需要的配置放到了一个 Java 类里

  1. @ConfigurationProperties(prefix = "fastdfs.boot")
  2. public class FastDfsProperties {
  3. private String trackerServerHosts;
  4. private int trackerHttpPort = 80;
  5. private int connectTimeout = 5000;
  6. private int networkTimeout = 30000;
  7. private boolean antiStealToken = false;
  8. private String charset = "ISO8859-1";
  9. private String secretKey;
  10. //省略字段 get set 方法
  11. }

其中, @ConfigurationProperties 指定了配置的 prefix ,上述配置相当于

  1. fastdfs:
  2. boot:
  3. trackerServerHosts: xxx
  4. trackerHttpPort: 80
  5. connectTimeout: 5000
  6. networkTimeout: 30000
  7. antiStealToken: false
  8. charset: ISO8859-1
  9. secretKey: xxx

这种类到现在为止还不可以和 Spring 结合起来,尚需要把它声明为 Spring bean 才生效

声明为 Spring bean 有两种形式

  • 在类本身上添加 @Component 注解,标识这是一个 Spring bean
  • @Configuration 类上使用 @EnableConfigurationProperties 来启用配置

通常的,在开发组件的时候,我们使用第二种方式,把 Properties 的启用,交给 @Configuration 配置类来管理,大家可以想想为什么

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnMissingBean(FastDfsClient.class) // 当 Spring 容器中不存在 FastDfsClient 时才加载这个类
  3. @EnableConfigurationProperties(FastDfsProperties.class) //启用上面的 FastDfsProperties
  4. public class FastDfsAutoConfiguration {
  5. /**
  6. * 创建 FastDfsClient 放到 Spring 容器当中
  7. */
  8. @Bean
  9. @ConditionalOnProperty("fastdfs.boot.trackerServerHosts")
  10. FastDfsClient fastDFSClient(FastDfsProperties fastDfsProperties) {
  11. globalInit(fastDfsProperties);
  12. return new FastDfsClient();
  13. }
  14. /**
  15. * 根据 properties 来配置 fastdfs
  16. */
  17. private void globalInit(FastDfsProperties fastDFSProperties) {
  18. // 省略部分代码
  19. }
  20. //省略部分代码
  21. }

@EnableConfigurationProperties(FastDfsProperties.class) 启用了括号内的 Properties 类,并把它们注入到 Spring 容器当中,使其可以被其他 Spring bean 导入

六、使用 META-INF/spring.factories 文件来代替扫描

有时候,我们开发的组件的类路径和应用的类路径不同,比如,应用类路径常常为 com.xxx.xxx,而组件的类路径常常为 com.xxx.yyy,这时候,经常需要为 Spring 指定扫描路径,才能把我们的组件加载进去,如果在自己项目当中加载上述 quartz-configable 组件,组件类路径为 com.xxx.yyy

  1. @ComponentScan({"com.xxx.xxx", "com.xxx.yyy"})
  2. @SpringBootApplication
  3. public class GrjrFundBatch {
  4. public static void main(String[] args) {
  5. SpringApplication.run(GrjrFundBatch.class);
  6. }
  7. }

如果新增了类似这样的 quartz-configable 组件,就需要改动 @ComponentScan 代码,这对启动类是有侵入性的,也是繁琐的,也极有可能写错,当组件路径有改动的时候也需要跟着改动

怎样避免这种硬编码形式的注入呢?

Springboot 在加载类的时候,会扫描 classpath 下的 META-INF/spring.factories 文件,当发现了 spring.factories 文件后,根据文件中的配置来加载类

其中一项配置为 org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.xxx.xxx.xxxx ,它声明了 Springboot 要加载的自动配置类,Springboot根据配置自动去加载配置类

借用这个规则,现在来升级我们的 quartz-configable 组件

我们在组件项目 resources 目录下添加 META-INF/spring.factories 文件,文件内容如下

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.grjr.quartz.config.GjSchedulerAutoConfiguration

然后在应用启动类当中删除已经无用的 @Component 注解即可

  1. @SpringBootApplication
  2. public class Application {
  3. public static void main(String[] args) {
  4. SpringApplication.run(Application.class);
  5. }
  6. }

此时,quartz-configable 依然能生效

使用 META-INF/spring.factories 虽然带来了简洁和便利,但是它总是去自动加载配置类,所以我们在设计组件的时候,一定要搭配 @ConditionOnxxxx 注解,有条件的去加载我们的组件

七、使用自定义 @EnableXxxx 注解的形式开启组件功能

就像上面说的一样,使用 META-INF/spring.factories 总会去加载配置类,自定义扫描路径有可能会写错类路径,那么,还有没有其他方式呢?

有,使用自定义注解来注入自己的组件,就像 dubbo 的 starter 组件一样,我们自己造一个 @EnableXxx 注解

7.1 自定义注解的核心

自定义注解的核心是 Spring 的 @Import 注解,它基于 @Import 注解来注入组件自身需要的资源和初始化组件自身

7.2 @Import 注解解析

@Import 注解是 Spring 用来注入 Spring bean 的一种方式,可以用来修饰别的注解,也可以直接在 Springboot 配置类上使用。

它只有一个value属性需要设置,来看一下源码

  1. public @interface Import {
  2. Class<?>[] value();
  3. }

这里的 value属性只接受三种类型的Class:

  • @Configuration 修饰的配置类
  • 接口 org.springframework.context.annotation.ImportBeanDefinitionRegistrar 的实现类
  • 接口 org.springframework.context.annotation.ImportSelector 的实现类

下面针对三种类型的 Class 分别做简单介绍,中间穿插自定义注解与外部配置的结合使用方式。

7.2.1 被@Configuration 修饰的配置类

像 Springboot 中的配置类一样正常使用,需要注意的是,如果该类的包路径已在 Springboot 启动类上配置的扫描路径下,则不需要再重新使用 @Import 导入了,因为 @Import 的目的是注入 bean,但是 Springboot 启动类自动扫描已经可以注入你想通过 @Import 导入的 bean 了。

7.2.2 接口 org.springframework.context.annotation.ImportBeanDefinitionRegistrar 的实现类

@Import 修饰自定义注解时候,通常会导入这个接口的实现类。

来看一下接口定义

  1. public interface ImportBeanDefinitionRegistrar {
  2. default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
  3. BeanNameGenerator importBeanNameGenerator) {
  4. registerBeanDefinitions(importingClassMetadata, registry);
  5. }
  6. /**
  7. * importingClassMetadata 被@Import修饰的自定义注解的元信息,可以获得属性集合
  8. * registry Spring bean注册中心
  9. **/
  10. default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  11. }

通过这种方式,我们可以根据自定义注解配置的属性值来注入 Spring Bean 信息。

来看如下案例,我们通过一个注解,启动 RocketMq 的消息发送器:

  1. @SpringBootApplication
  2. @EnableMqProducer(group="xxx")
  3. public class App {
  4. public static void main(String[] args) {
  5. SpringApplication.run(App.class);
  6. }
  7. }

这是一个服务项目的启动类,这个服务开启了 RocketMq 的一个发送器,并且分到 xxx 组里。

来看一下 @EnableMqProducer 注解

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target({ElementType.TYPE})
  3. @Documented
  4. @Import({XXXRegistrar.class,XXXConfig.class})
  5. public @interface EnableMqProducer {
  6. String group() default "DEFAULT_PRODUCER_GROUP";
  7. String instanceName() default "defaultProducer";
  8. boolean retryAnotherBrokerWhenNotStoreOK() default true;
  9. }

这里使用 @Import 导入了两个配置类,第一个是接口 org.springframework.context.annotation.ImportBeanDefinitionRegistrar 的实现类,第二个是被 @Configuration 修饰的配置类

我们看第一个类 XXXRegistrar,这个类的功能是注入一个自定义的 DefaultMQProducer 到Spring 容器中,使业务方可以直接通过 @Autowired 注入 DefaultMQProducer 使用

  1. public class XXXRegistrar implements ImportBeanDefinitionRegistrar {
  2. @Override
  3. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  4. //获取注解里配置的属性
  5. AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableMqProducer.class.getName()));
  6. //根据配置的属性注入自定义 bean 到 spring 容器当中
  7. registerBeanDefinitions(attributes, registry);
  8. }
  9. private void registerBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {
  10. //获取配置
  11. String group = attributes.getString("group");
  12. //省略部分代码...
  13. //添加要注入的类的字段值
  14. Map<String, Object> values = new HashMap<>();
  15. //这里有的同学可能不清楚为什么key是这个
  16. //这里的key就是DefaultMQProducer的字段名
  17. values.put("producerGroup", group);
  18. //省略部分代码
  19. //注册到Spring中
  20. BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, DefaultMQProducer.class.getName(), DefaultMQProducer.class, values);
  21. }

到这里,我们已经注入了一个 DefaultMQProducer 的实例到 Spring 容器中,但是这个实例,还不完整,比如:

  • 还没有启动
  • nameServer地址还没有配置
  • 外部配置的属性还没有覆盖实例已有的值(nameServer地址建议外部配置)。

但是好消息是,我们已经可以通过注入来使用这个未完成的实例了。

上面遗留的问题,就是第二个类接下来要做的事。

来看第二个配置类

  1. @Configuration
  2. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  3. @EnableConfigurationProperties(XxxProperties.class) //Spring提供的配置自动映射功能,配置后可直接注入
  4. public class XXXConfig {
  5. @Resource //直接注入
  6. private XxxProperties XxxProperties;
  7. @Autowired //注入上一步生成的实例
  8. private DefaultMQProducer producer;
  9. @PostConstruct
  10. public void init() {
  11. //省略部分代码
  12. //获取外部配置的值
  13. String nameServer = XxxProperties.getNameServer();
  14. //修改实例
  15. producer.setNamesrvAddr(nameServer);
  16. //启动实例
  17. try {
  18. this.producer.start();
  19. } catch (MQClientException e) {
  20. throw new RocketMqException("mq消息发送实例启动失败", e);
  21. }
  22. }
  23. @PreDestroy
  24. public void destroy() {
  25. producer.shutdown();
  26. }

到这里,通过自定义注解和外部配置的结合,一个完整的消息发送器就可以使用了,但方式有取巧之嫌,因为在消息发送器启动之前,不知道还有没有别的类使用了这个实例,这是不安全的。

7.2.3 接口org.springframework.context.annotation.ImportSelector的实现类

首先看一下接口

  1. public interface ImportSelector {
  2. /**
  3. * importingClassMetadata 注解元信息,可获取自定义注解的属性集合
  4. * 根据自定义注解的属性,或者没有属性,返回要注入Spring的Class全限定类名集合
  5. 如:XXX.class.getName(),Spring会自动注入XXX的一个实例
  6. */
  7. String[] selectImports(AnnotationMetadata importingClassMetadata);
  8. @Nullable
  9. default Predicate<String> getExclusionFilter() {
  10. return null;
  11. }
  12. }

这个接口的实现类如果没有进行 Spring Aware 接口拓展,功能比较单一,因为我们无法参与 Spring Bean 的构建过程,只是告诉 Spring 要注入的 Bean 的名字。不再详述。

八、总结

综上所述,我们一共聊了三种形式的组件创建方式

  • 相同路径下, @Configuration 修饰的配置类
  • 使用 META-INF/spring.factories 文件接入
  • 结合 @Import 注解注入

其中穿插了 @ConditionOnXxxx 选择性启动、Properties 封装的技术,快去试一下吧

教你写Spring组件的更多相关文章

  1. vue之手把手教你写日历组件

    ---恢复内容开始--- 1.日历组件 1.分析功能:日历基本功能,点击事件改变日期,样式的改变 1.结构分析:html 1.分为上下两个部分 2.上面分为左按钮,中间内容展示,右按钮 下面分为周几展 ...

  2. [原创]手把手教你写网络爬虫(4):Scrapy入门

    手把手教你写网络爬虫(4) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 上期我们理性的分析了为什么要学习Scrapy,理由只有一个,那就是免费,一分钱都不用花! 咦?怎么有人扔西红柿 ...

  3. [原创]手把手教你写网络爬虫(5):PhantomJS实战

    手把手教你写网络爬虫(5) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 大家好!从今天开始,我要与大家一起打造一个属于我们自己的分布式爬虫平台,同时也会对涉及到的技术进行详细介绍.大 ...

  4. 手把手教你写Kafka Streams程序

    本文从以下四个方面手把手教你写Kafka Streams程序: 一. 设置Maven项目 二. 编写第一个Streams应用程序:Pipe 三. 编写第二个Streams应用程序:Line Split ...

  5. 手写Spring事务框架

    Spring事务基于AOP环绕通知和异常通知 编程事务 声明事务 Spring事务底层使用编程事务+AOP进行包装的   = 声明事务 AOP应用场景:  事务 权限 参数验证 什么是AOP技术 AO ...

  6. Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)

    前言 在上一篇<Spring学习之——手写Spring源码(V1.0)>中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也 ...

  7. 手写spring

    体系结构 Spring 有可能成为所有企业应用程序的一站式服务点,然而,Spring 是模块化的,允许你挑选和选择适用于你的模块,不必要把剩余部分也引入.下面的部分对在 Spring 框架中所有可用的 ...

  8. 手把手教你写DI_0_DI是什么?

    DI是什么? Dependency Injection 常常简称为:DI. 它是实现控制反转(Inversion of Control – IoC)的一个模式. fowler 大大大神 "几 ...

  9. 手把手教你写Sublime中的Snippet

    手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...

随机推荐

  1. Vue项目中使用websocket

    <template> <div class="test"> </div> </template> <script> ex ...

  2. Windows 重装系统,配置 WSL,美化终端,部署 WebDAV 服务器,并备份系统分区

    最新博客文章链接 最近发现我 Windows11 上的 WSL 打不开了,一直提示我虚拟化功能没有打开,但我看了下配置,发现虚拟化功能其实是开着的.然后试了各种方法,重装了好几次系统,我一个软件一个软 ...

  3. 解读与部署(三):基于 Kubernetes 的微服务部署即代码

    在基于 Kubernetes 的基础设施即代码一文中,我概要地介绍了基于 Kubernetes 的 .NET Core 微服务和 CI/CD 动手实践工作坊使用的基础设施是如何使用代码描述的,以及它的 ...

  4. RocketMQ架构原理解析(一):整体架构

    RocketMQ架构原理解析(一):整体架构 RocketMQ架构原理解析(二):消息存储(CommitLog) RocketMQ架构原理解析(三):消息索引(ConsumeQueue & I ...

  5. 【Java】Collections

    文章目录 Collections reverse(List) shuffle(List) sort(List) sort(List,Comparator) swap(List,int, int) Ob ...

  6. Docker入门篇(一)安装docker

    Docker入门篇(一)安装docker Docker的来源 由dotCloud公司首创及正式命名,但是企业规模小,影响力不够,所以在快要坚持不住的时候,开始吃百家饭--开源了.不开则已,一开惊人.越 ...

  7. thanos的日志能不能打到文件里面去?

    不行. thanos/pkg/logging/logger.go: logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) if logF ...

  8. centos6.6手动安装mysql5.5并配置主从同步

    0.实验环境 主机IP(Master) 192.168.61.150 centos6.6 从机IP(Slave)   192.168.61.157 centos6.6 1.查看centos系统版本 [ ...

  9. elasticsearch启动流程

    本文基于ES2.3.2来描述.通过结合源码梳理出ES实例的启动过程. elasticsearch的启动过程是根据配置和环境组装需要的模块并启动的过程.这一过程就是通过guice注入各个功能模块并启动这 ...

  10. lua之自索引

    Father={ a=100, b=200 } function Father:dis() print(self.a,self.b) end Father.__index=Father Son= { ...