三万字盘点Spring/Boot的那些常用扩展点(上篇)
Spring Event 事件
Event 事件可以说是一种观察者模式的实现,主要是用来解耦合的。当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。
举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作。
什么是Spring Event 事件
那么是什么是Spring Event 事件,就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以完成事件的发布订阅
Spring提供的事件api:
ApplicationEvent
ApplicationEvent
事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。
ApplicationListener
ApplicationListener
事件监听的接口,泛型是子类需要监听的事件类型,子类需要实现onApplicationEvent,参数就是事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件。
ApplicationEventPublisher
ApplicationEventPublisher
事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。
ApplicationContext实现了ApplicationEventPublisher接口,所以通过ApplicationContext就可以发布事件。
那怎么才能拿到ApplicationContext呢?
前面Bean生命周期那节说过,可以通过ApplicationContextAware接口拿到,甚至你可以通过实现ApplicationEventPublisherAware直接获取到ApplicationEventPublisher,其实获取到的ApplicationEventPublisher也就是ApplicationContext,因为是ApplicationContext实现了ApplicationEventPublisher。
话不多说,上代码
就以上面的火灾为例
第一步:创建一个火灾事件类
火灾事件类继承ApplicationEvent
// 火灾事件
public class FireEvent extends ApplicationEvent {
public FireEvent(String source){
super(source);
}
}
第二步:创建火灾事件的监听器
打119的火灾事件的监听器:
public class Call119FireEventListener implements ApplicationListener<FireEvent> {
@Override
public void onApplicationEvent(FireEvent event){
System.out.println("打119");
}
}
救人的火灾事件的监听器:
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {
@Override
public void onApplicationEvent(FireEvent event){
System.out.println("救人");
}
}
事件和对应的监听都有了,接下来进行测试:
public class Application {
public static void main(String[] args){
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
//将 事件监听器 注册到容器中
applicationContext.register(Call119FireEventListener.class);
applicationContext.register(SavePersonFireEventListener.class);
applicationContext.refresh();
// 发布着火的事件,触发监听
applicationContext.publishEvent(new FireEvent("着火了"));
}
}
将两个事件注册到Spring容器中,然后发布FireEvent事件
运行结果:
打119
救人
控制台打印出了结果,触发了监听。
如果现在需要对火灾进行救火,那么只需要去监听FireEvent,实现救火的逻辑,注入到Spring容器中,就可以了,其余的代码根本不用动。
Spring内置的事件
Spring内置的事件很多,这里我罗列几个
事件类型 | 触发时机 |
ContextRefreshedEvent | 在调用ConfigurableApplicationContext 接口中的refresh()方法时触发 |
ContextStartedEvent | 在调用ConfigurableApplicationContext的start()方法时触发 |
ContextStoppedEvent | 在调用ConfigurableApplicationContext的stop()方法时触发 |
ContextClosedEvent | 当ApplicationContext被关闭时触发该事件,也就是调用close()方法触发 |
在Spring容器启动的过程中,Spring会发布这些事件,如果你需要这Spring容器启动的某个时刻进行什么操作,只需要监听对应的事件即可。
Spring事件的传播
Spring事件的传播是什么意思呢?
我们都知道,在Spring中有子父容器的概念,而Spring事件的传播就是指当通过子容器发布一个事件之后,不仅可以触发在这个子容器的事件监听器,还可以触发在父容器的这个事件的监听器。
上代码
public class EventPropagateApplication {
public static void main(String[] args){
// 创建一个父容器
AnnotationConfigApplicationContext parentApplicationContext = new AnnotationConfigApplicationContext();
//将 打119监听器 注册到父容器中
parentApplicationContext.register(Call119FireEventListener.class);
parentApplicationContext.refresh();
// 创建一个子容器
AnnotationConfigApplicationContext childApplicationContext = new AnnotationConfigApplicationContext();
//将 救人监听器 注册到子容器中
childApplicationContext.register(SavePersonFireEventListener.class);
childApplicationContext.refresh();
// 设置一下父容器
childApplicationContext.setParent(parentApplicationContext);
// 通过子容器发布着火的事件,触发监听
childApplicationContext.publishEvent(new FireEvent("着火了"));
}
}
创建了两个容器,父容器注册了打119的监听器,子容器注册了救人的监听器,然后将子父容器通过setParent关联起来,最后通过子容器,发布了着火的事件。
运行结果:
救人
打119
从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件。
源码验证
事件传播源码
从这段源码可以看出,如果父容器不为空,就会通过父容器再发布一次事件。
传播特性的一个坑
前面说过,在Spring容器启动的过程,会发布很多事件,如果你需要有相应的扩展,可以监听这些事件。但是,在SpringCloud环境下,你的这些Spring发布的事件的监听器可能会执行很多次。为什么会执行很多次呢?其实就是跟传播特性有关。
在SpringCloud的环境下,为了使像FeignClient和RibbonClient这些不同的服务的配置相互隔离,会创建很多的子容器,而这些子容器都有一个公共的父容器,那就是SpringBoot项目启动时创建的容器,事件的监听器都在这个容器中。而这些为了配置隔离创建的子容器,在容器启动的过程中,也会发布诸如ContextRefreshedEvent等这样的事件,如果你监听了这些事件,那么由于传播特性的关系,你的这个事件的监听器就会触发多次。
如何解决这个坑呢?
你可以进行判断这些监听器有没有执行过,比如加一个判断的标志;或者是监听类似的事件,比如ApplicationStartedEvent事件,这种事件是在SpringBoot启动中发布的事件,而子容器不是SpringBoot,所以不会多次发这种事件,也就会只执行一次。
Spring事件的运用举例
1、在Mybatis中的使用
又来以Mybatis举例了。。Mybatis的SqlSessionFactoryBean监听了ApplicationEvent,然后判断如果是ContextRefreshedEvent就进行相应的处理,这个类还实现了FactoryBean接口。。
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event){
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
}
说实话,这监听代码写的不太好,监听了ApplicationEvent,那么所有的事件都会回调这个类的onApplicationEvent方法,但是onApplicationEvent方法实现又是当ApplicationEvent是ContextRefreshedEvent类型才会往下走,那为什么不直接监听ContextRefreshedEvent呢?
可以给个差评。
膨胀了膨胀了。。
2、在SpringCloud的运用
在SpringCloud的中,当项目启动的时候,会自动往注册中心进行注册,那么是如何实现的呢?当然也是基于事件来的。当web服务器启动完成之后,就发布ServletWebServerInitializedEvent事件。
然后不同的注册中心的实现都只需要监听这个事件,就知道web服务器已经创建好了,那么就可以往注册中心注册服务实例了。如果你的服务没往注册中心,看看是不是web环境,因为只有web环境才会发这个事件。
SpringCloud提供了一个抽象类 AbstractAutoServiceRegistration,实现了对WebServerInitializedEvent(ServletWebServerInitializedEvent的父类)事件的监听
AbstractAutoServiceRegistration
一般不同的注册中心都会去继承这个类,监听项目启动,实现往注册中心服务端进行注册。
Nacos对于AbstractAutoServiceRegistration的继承
Spring Event事件在Spring内部中运用很多,是解耦合的利器。在实际项目中,你既可以监听Spring/Boot内置的一些事件,进行相应的扩展,也可以基于这套模型在业务中自定义事件和相应的监听器,减少业务代码的耦合。
命名空间
最后来讲一个可能没有留意,但是很神奇的扩展点--命名空间。起初我知道这个扩展点的时候,我都惊呆了,这玩意也能扩展?真的不得不佩服Spring设计的可扩展性。
回忆一下啥是命名空间?
先看一段配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/beans/spring-context.xsd
">
<context:component-scan base-package="com.sanyou.spring.extension"/>
</beans>
这一段xml配置想必都很熟悉,其中, context 标签就代表了一个命名空间。
也就说,这个标签是可以扩展的。
话不多说,来个扩展
接下来自定义命名空间 sanyou,总共分为3步。
第一步:定义一个xsd文件
如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- xmlns 和 targetNamespace 需要定义,结尾为sanyou,前面都一样的-->
<xsd:schema xmlns="http://sanyou.com/schema/sanyou"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://sanyou.com/schema/sanyou">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:complexType name="Bean">
<xsd:attribute name="class" type="xsd:string" use="required"/>
</xsd:complexType>
<!-- sanyou 便签的子标签,类型是Bean ,就会找到上面的complexType=Bean类型,然后处理属性 -->
<xsd:element name="mybean" type="Bean"/>
</xsd:schema>
这个xsd文件来指明sanyou这个命名空间下有哪些标签和属性。这里我只指定了一个标签 mybean,mybean标签里面有个class的属性,然后这个标签的目的就是将class属性指定的Bean的类型,注入到Spring容器中,作用跟spring的标签的作用是一样的。
xsd文件没有需要放的固定的位置,这里我放到 META-INF 目录下
第二步:解析这个命名空间
解析命名空间很简单,Spring都有配套的东西--NamespaceHandler接口,只要实现这个接口就行了。但一般我们不直接实现 NamespaceHandler 接口,我们可以继承 NamespaceHandlerSupport 类,这个类实现了 NamespaceHandler 接口。
public class SanYouNameSpaceHandler extends NamespaceHandlerSupport {
@Override
public void init(){
//注册解析 mybean 标签的解析器
registerBeanDefinitionParser("mybean", new SanYouBeanDefinitionParser());
}
private static class SanYouBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected boolean shouldGenerateId(){
return true;
}
@Override
protected String getBeanClassName(Element element){
return element.getAttribute("class");
}
}
}
SanYouNameSpaceHandler的作用就是将sanyou命名空间中的mybean这个标签读出来,拿到class的属性,然后将这个class属性指定的class类型注入到Spring容器中,至于注册这个环节的代码,都交给了SanYouBeanDefinitionParser的父类来做了。
第三步:创建并配置spring.handlers和spring.schemas文件
先创建spring.handlers和spring.schemas文件
spring.handlers文件内容
http\://sanyou.com/schema/sanyou=com.sanyou.spring.extension.namespace.SanYouNameSpaceHandler
通过spring.handlers配置文件,就知道sanyou命名空间应该找SanYouNameSpaceHandler进行解析
spring.schemas文内容
http\://sanyou.com/schema/sanyou.xsd=META-INF/sanyou.xsd
spring.schemas配置xsd文件的路径
文件都有了,只需要放到classpath下的META-INF文件夹就行了。
xsd、spring.handlers、spring.schema文件
到这里,就完成了扩展,接下来进行测试
测试
先构建一个applicationContext.xml文件,放到resources目录下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:sanyou="http://sanyou.com/schema/sanyou"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://sanyou.com/schema/sanyou
http://sanyou.com/schema/sanyou.xsd
">
<!--使用 sanyou 标签,配置一个 User Bean-->
<sanyou:mybean class="com.sanyou.spring.extension.User"/>
</beans>
再写个测试类
public class Application {
public static void main(String[] args){
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
applicationContext.refresh();
User user = applicationContext.getBean(User.class);
System.out.println(user);
}
}
运行结果:
com.sanyou.spring.extension.User@27fe3806
成功获取到User这个对象,说明自定义标签生效了。
Spring内置命名空间的扩展
NameSpaceHandler的spring实现
通过NameSpaceHandler接口的这些实现类的命名就可以看出来有哪些扩展和这些扩展的作用,比如有处理aop的,有处理mvc的等等之类的。
开源框架对命名空间的扩展
1、Mybatis的扩展
Mybatis的NameSpaceHandler实现
这个就是来扫描指定路径的mapper接口的,处理 scan 标签,跟@MapperScan注解的作用是一样的。
2、dubbo的扩展
使用dubbo可能写过如下的配置
<dubbo:registry address="zookeeper://192.168.10.119:2181" />
这个dubbo命名空间肯定就是扩展的Spring的,也有对应的dubbo实现的NameSpaceHandler。
DubboNamespaceHandler
不得不说,dubbo解析的标签可真的多啊,不过功能也是真的多。
总结
到这,本文就接近尾声了,这里画两张图来总结一下本文讲了Spring的哪些扩展点。
整体
SpringBoot启动扩展点
通过学习Spring的这些扩展点,既可以帮助我们应对日常的开发,还可以帮助我们更好地看懂Spring的源码。
最后,本文前前后后花了一周多的时间完成,如果对你有点帮助,还请帮忙点赞、转发、非常感谢。
哦,差点忘了,本文所有demo代码都在这了
https://github.com/sanyou3/spring-extension.git
文章转载自公众号: 三友的java日记