前言

原文地址: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

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 执行时机

@Configuration
public class QuartzInitConfig { @Autowired
private Scheduler scheduler; @Autowired
private CustomTriggerListener customTriggerListener; @PostConstruct
public void init() {
//先把所有jobDetail放到map里
initJobMap(); //添加自定义Trigger监听器,进行任务开关的监听和故障定位的配置
addTriggerListener(scheduler, customTriggerListener); //添加任务到任务调度器中
addJobToScheduler(scheduler); //启动任务调度器
try {
scheduler.start();
} catch (SchedulerException e) {
log.error("任务调度器启动失败", e);
throw new RuntimeException("任务调度器启动失败");
}
log.info("任务调度器已启动");
} private void initJobMap() {
//省略部分代码
} private void addJobToScheduler(Scheduler scheduler) {
//省略部分代码
} private void addTriggerListener(Scheduler scheduler, CustomTriggerListener customTriggerListener) {
//省略部分代码
}
}

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

此类加载有两个过程:

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

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

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

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

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

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

@Configuration
@ConditionalOnProperty({"risk.expression", "risk.appid", "risk.appsecret", "risk.url"})
public class RiskAspectConfig { //项目内配置
@Value("${risk.expression}")
private String riskExpression; @Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor() {
SpringBeans springBeans = springBeans();
RiskSenderDelegate riskSenderDelegate = new RiskSenderDelegate(springBeans);
GrjrMethodInterceptor grjrMethodInterceptor = new GrjrMethodInterceptor(riskSenderDelegate);
JdkRegexpMethodPointcut jdkRegexpMethodPointcut = new JdkRegexpMethodPointcut();
jdkRegexpMethodPointcut.setPattern(riskExpression);
log.info("切面准备完毕,切点为{}", riskExpression);
return new DefaultPointcutAdvisor(jdkRegexpMethodPointcut, grjrMethodInterceptor);
} //省略部分代码 }

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

@ConditionalOnProperty 的作用:

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

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

grjr:
risk:
expression: xxxx
appid: xxx
appsecret: xxx
url: xxx

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

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

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

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

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

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

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

fastdfs:
boot:
trackerServerHosts: xxx
trackerHttpPort: 80
connectTimeout: 5000
networkTimeout: 30000
antiStealToken: false
charset: ISO8859-1
secretKey: xxx

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

声明为 Spring bean 有两种形式

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

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

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(FastDfsClient.class) // 当 Spring 容器中不存在 FastDfsClient 时才加载这个类
@EnableConfigurationProperties(FastDfsProperties.class) //启用上面的 FastDfsProperties
public class FastDfsAutoConfiguration { /**
* 创建 FastDfsClient 放到 Spring 容器当中
*/
@Bean
@ConditionalOnProperty("fastdfs.boot.trackerServerHosts")
FastDfsClient fastDFSClient(FastDfsProperties fastDfsProperties) {
globalInit(fastDfsProperties);
return new FastDfsClient();
} /**
* 根据 properties 来配置 fastdfs
*/
private void globalInit(FastDfsProperties fastDFSProperties) {
// 省略部分代码
} //省略部分代码
}

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

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

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

@ComponentScan({"com.xxx.xxx", "com.xxx.yyy"})
@SpringBootApplication
public class GrjrFundBatch { public static void main(String[] args) {
SpringApplication.run(GrjrFundBatch.class);
}
}

如果新增了类似这样的 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 文件,文件内容如下

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

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

@SpringBootApplication
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

此时,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属性需要设置,来看一下源码

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

这里的 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 修饰自定义注解时候,通常会导入这个接口的实现类。

来看一下接口定义

public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
} /**
* importingClassMetadata 被@Import修饰的自定义注解的元信息,可以获得属性集合
* registry Spring bean注册中心
**/
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

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

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

@SpringBootApplication
@EnableMqProducer(group="xxx")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class);
}
}

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

来看一下 @EnableMqProducer 注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({XXXRegistrar.class,XXXConfig.class})
public @interface EnableMqProducer { String group() default "DEFAULT_PRODUCER_GROUP"; String instanceName() default "defaultProducer"; boolean retryAnotherBrokerWhenNotStoreOK() default true;
}

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

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

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

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

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

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

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

来看第二个配置类

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@EnableConfigurationProperties(XxxProperties.class) //Spring提供的配置自动映射功能,配置后可直接注入
public class XXXConfig { @Resource //直接注入
private XxxProperties XxxProperties; @Autowired //注入上一步生成的实例
private DefaultMQProducer producer; @PostConstruct
public void init() {
//省略部分代码
//获取外部配置的值
String nameServer = XxxProperties.getNameServer();
//修改实例
producer.setNamesrvAddr(nameServer);
//启动实例
try {
this.producer.start();
} catch (MQClientException e) {
throw new RocketMqException("mq消息发送实例启动失败", e);
}
} @PreDestroy
public void destroy() {
producer.shutdown();
}

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

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

首先看一下接口

public interface ImportSelector {

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

这个接口的实现类如果没有进行 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. 用软碟通UltraISO刻录Win 10 1909 到U盘,只有1个G左右,安装不了系统

    之前一直用软碟通刻录WIN10的ISO镜像到U盘.最近想到用最新版的WIN10 1909 来做一个U盘系统,刻录也成功了.就是安装系统的时候总报错,找了很久原因,终于发现刻录后占用U盘的空间只有1G左 ...

  2. vue重置data

    Object.assign(this.$data, this.$options.data()) 解析:1.Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象. ...

  3. 复盘报告:心跳数据丢失,从发现到解决历经一年多的bug

    时间线 大约在2020年10月,内网测试服服务端更新,发现进程A重启后,与其他进程之间的心跳协议不通,不能正常的提供服务.重启后,就正常了. 这个情况持续了很长时间.只在重启时才会出现,且发生概率很低 ...

  4. kubernetes运行应用2之DaemonSet详解

    kubernetes运行应用1之Deployment详解   查看daemonset 如下,k8s自身的 DaemonSet kube-flannel-ds和kube-proxy分别负责在每个结点上运 ...

  5. Ubuntu下使用VS Code创建Spring Boot工程

    目的 我们将在Ubuntu桌面系统下,使用VS Code(Visual Studio Code)编辑器从零开始创建一个Spring Boot工程,并实现一个简单的RESTful风格接口.使用这套流程的 ...

  6. Solon Web 开发,十四、与Spring、Jsr330的常用注解对比

    Solon Web 开发 一.开始 二.开发知识准备 三.打包与运行 四.请求上下文 五.数据访问.事务与缓存应用 六.过滤器.处理.拦截器 七.视图模板与Mvc注解 八.校验.及定制与扩展 九.跨域 ...

  7. gin中设置和获取cookie

    package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { router := ...

  8. Clang-Format 个人常用配置

    Clang-Format 个人常用配置 本文记录 Clang-Format 个人常用配置. 欲了解更多配置选项,可查阅 官方文档. BasedOnStyle: Google AccessModifie ...

  9. 关于一键提取QQ群成员信息的记录

    问题情境 昨天晚上回宿舍看到舍友在吐槽:天哪,我要把70多个人的QQ号全统计出来,只能一个一个地在咱们学院的学院群里找.吐了. 当时我第一反应是用python写个脚本之类的去做,因此查阅了一下资料,找 ...

  10. pod 详解

    静态pod是由kubelet进行管理的仅存在于特定的node上的pod. pod容器共享volume同一个pod中的多个容器能够共享pod级别的存储卷volume pod的配置管理  应用配置管理方案 ...