三万字盘点Spring/Boot的那些常用扩展点(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Spring对于每个Java后端程序员来说肯定不陌生,日常开发和面试必备的。本文就来盘点Spring/SpringBoot常见的扩展点,同时也来看看常见的开源框架是如何基于这些扩展点跟Spring/SpringBoot整合的。 话不多说,直接进入正题。

大家好,我是三友。

Spring对于每个Java后端程序员来说肯定不陌生,日常开发和面试必备的。本文就来盘点Spring/SpringBoot常见的扩展点,同时也来看看常见的开源框架是如何基于这些扩展点跟Spring/SpringBoot整合的。

话不多说,直接进入正题。

由于本文太长,所以就分为上下两部分,上部分可关注微信公众号三友的java日记,在主页中查看

image.png

image.png

Spring SPI机制

SPI全称为 (Service Provider Interface),是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离, 让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

JDK有内置的SPI机制的实现ServiceLoader,Dubbo也有自己的SPI机制的实现ExtensionLoader,但是这里我们都不讲。。

image.png

但是,我之前写过相关的文章,面试常问的dubbo的spi机制到底是什么?(上),文章的前半部分有对比三者的区别。

这里我们着重讲一下Spring的SPI机制的实现SpringFactoriesLoader。

SpringFactoriesLoader

Spring的SPI机制规定,配置文件必须在classpath路径下的META-INF文件夹内,文件名必须为spring.factories,文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名。但是键和值可以没有任何关系,当然想有也可以有。

show me the code

这里我自定义一个类,MyEnableAutoConfiguration作为键,值就是User

public class MyEnableAutoConfiguration {
   
   
}

spring.factories文件

com.sanyou.spring.extension.spi.MyEnableAutoConfiguration=com.sanyou.spring.extension.User

然后放在META-INF底下

image.png

测试:

public class Application {
   
   

    public static void main(String[] args) {
   
   
        List<String> classNames = SpringFactoriesLoader.loadFactoryNames(MyEnableAutoConfiguration.class, MyEnableAutoConfiguration.class.getClassLoader());
        classNames.forEach(System.out::println);
    }

}

结果:

com.sanyou.spring.extension.User

可以看出,通过SpringFactoriesLoader的确可以从spring.factories文件中拿到MyEnableAutoConfiguration键对应的值。

到这你可能说会,这SPI机制也没啥用啊。的确,我这个例子比较简单,拿到就是遍历,但是在Spring中,如果Spring在加载类的话使用SPI机制,那我们就可以扩展,接着往下看。

SpringBoot启动扩展点

SpringBoot项目在启动的过程中有很多扩展点,这里就来盘点一下几个常见的扩展点。

1、自动装配

说到SpringBoot的扩展点,第一时间肯定想到的就是自动装配机制,面试贼喜欢问,但是其实就是一个很简单的东西。当项目启动的时候,会去从所有的spring.factories文件中读取@EnableAutoConfiguration键对应的值,拿到配置类,然后根据一些条件判断,决定哪些配置可以使用,哪些不能使用。

spring.factories文件?键值?不错,自动装配说白了就是SPI机制的一种运用场景。

@EnableAutoConfiguration注解:

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   
   
    //忽略
}

我擦,这个注解也是使用@Import注解,而且配置类还实现了ImportSelector接口,跟前面也都对上了。在SpringBoot中,@EnableAutoConfiguration是通过@SpringBootApplication来使用的。

在AutoConfigurationImportSelector中还有这样一段代码

image.png

所以,这段代码也明显地可以看出,自动装配也是基于SPI机制实现的。

那么我想实现自动装配怎么办呢?很简单,只需两步。

第一步,写个配置类:

@Configuration
public class UserAutoConfiguration {
   
   

    @Bean
    public UserFactoryBean userFactoryBean() {
   
   
        return new UserFactoryBean();
    }

}

这里我为了跟前面的知识有关联,配置了一个UserFactoryBean。

第二步,往spring.factories文件配置一下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sanyou.spring.extension.springbootextension.UserAutoConfiguration

到这就已经实现了自动装配的扩展。

接下来进行测试:

@SpringBootApplication
public class Application {
   
   

    public static void main(String[] args) {
   
   
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);

        User user = applicationContext.getBean(User.class);

        System.out.println("获取到的Bean为" + user);
    }

}

运行结果:

调用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@3406472c
获取到的Bean为com.sanyou.spring.extension.User@3406472c

从运行结果可以看出,自动装配起了作用,并且虽然往容器中注入的Bean的class类型为UserFactoryBean,但是最终会调用UserFactoryBean的getObject的实现获取到User对象。

自动装配机制是SpringBoot的一个很重要的扩展点,很多框架在整合SpringBoot的时候,也都通过自动装配来的,实现项目启动,框架就自动启动的,这里我举个Mybatis整合SpringBoot。

image.png

2、PropertySourceLoader

PropertySourceLoader,这是干啥的呢?

我们都知道,在SpringBoot环境下,外部化的配置文件支持properties和yaml两种格式。但是,现在不想使用properties和yaml格式的文件,想使用json格式的配置文件,怎么办?

当然是基于该小节讲的PropertySourceLoader来实现的。

public interface PropertySourceLoader {
   
   

      //可以支持哪种文件格式的解析
      String[] getFileExtensions();

      // 解析配置文件,读出内容,封装成一个PropertySource<?>结合返回回去
      List<PropertySource<?>> load(String name, Resource resource) throws IOException;

}

对于PropertySourceLoader的实现,SpringBoot两个实现

PropertiesPropertySourceLoader:可以解析properties或者xml结尾的配置文件

image.png

YamlPropertySourceLoader:解析以yml或者yaml结尾的配置文件

image.png

所以可以看出,要想实现json格式的支持,只需要自己实现可以用来解析json格式的配置文件的PropertySourceLoader就可以了。

动手来一个。

实现可以读取json格式的配置文件

实现这个功能,只需要两步就可以了。

第一步:自定义一个PropertySourceLoader

JsonPropertySourceLoader,实现PropertySourceLoader接口

public class JsonPropertySourceLoader implements PropertySourceLoader {
   
   

    @Override
    public String[] getFileExtensions() {
   
   
        //这个方法表明这个类支持解析以json结尾的配置文件
        return new String[]{
   
   "json"};
    }

    @Override
    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
   
   

        ReadableByteChannel readableByteChannel = resource.readableChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate((int) resource.contentLength());

        //将文件内容读到 ByteBuffer 中
        readableByteChannel.read(byteBuffer);
        //将读出来的字节转换成字符串
        String content = new String(byteBuffer.array());
        // 将字符串转换成 JSONObject
        JSONObject jsonObject = JSON.parseObject(content);

        Map<String, Object> map = new HashMap<>(jsonObject.size());
        //将 json 的键值对读出来,放入到 map 中
        for (String key : jsonObject.keySet()) {
   
   
            map.put(key, jsonObject.getString(key));
        }

        return Collections.singletonList(new MapPropertySource("jsonPropertySource", map));
    }

}
第二步:配置PropertySourceLoader

JsonPropertySourceLoader 已经有了,那么怎么用呢?当然是SPI机制了,SpringBoot对于配置文件的处理,就是依靠SPI机制,这也是能扩展的重要原因。

image.png

image.png

SpringBoot会先通过SPI机制加载所有PropertySourceLoader,然后遍历每个PropertySourceLoader,判断当前遍历的PropertySourceLoader,通过getFileExtensions获取到当前PropertySourceLoader能够支持哪些配置文件格式的解析,让后跟当前需要解析的文件格式进行匹配,如果能匹配上,那么就会使用当前遍历的PropertySourceLoader来解析配置文件。

PropertySourceLoader其实就属于策略接口,配置文件的解析就是策略模式的运用。

所以,只需要按照这种格式,在spring.factories文件中配置一下就行了。

org.springframework.boot.env.PropertySourceLoader=\
com.sanyou.spring.extension.springbootextension.propertysourceloader.JsonPropertySourceLoader

到此,其实就扩展完了,接下来就来测试一下。

测试

先创建一个application.json的配置文件

image.png

改造User

public class User {
   
   
    // 注入配置文件的属性
    @Value("${sanyou.username:}")
    private String username;
}

启动项目

@SpringBootApplication
public class Application {
   
   

    public static void main(String[] args) {
   
   
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);

        User user = applicationContext.getBean(User.class);

        System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());
    }


    @Bean
    public User user() {
   
   
        return new User();
    }

}

运行结果:

获取到的Bean为com.sanyou.spring.extension.User@481ba2cf,属性username值为:三友的java日记

成功将json配置文件的属性注入到User对象中。

至此,SpringBoot就支持了以json为结尾的配置文件格式。

Nacos对于PropertySourceLoader的实现

如果你的项目正在用Nacos作为配置中心,那么刚刚好,Nacos已经实现json配置文件格式的解析。

image.png

Nacos不仅实现了json格式的解析,也实现了关于xml格式的配置文件的解析,并且优先级会比SpringBoot默认的xml格式文件解析的优先级高。至于Nacos为啥需要实现PropertySourceLoader?其实很简单,因为Nacos作为配置中心,不仅支持properties和yaml格式的文件,还支持json格式的配置文件,那么客户端拿到这些配置就需要解析,SpringBoot已经支持了properties和yaml格式的文件的解析,那么Nacos只需要实现SpringBoot不支持的就可以了。

3、ApplicationContextInitializer

ApplicationContextInitializer也是SpringBoot启动过程的一个扩展点。

image.png

在SpringBoot启动过程,会回调这个类的实现initialize方法,传入ConfigurableApplicationContext。

那怎么用呢?

依然是SPI。

image.png

然后遍历所有的实现,依次调用

image.png

这里就不演示了,实现接口,按照如下这种配置就行了

image.png

但是这里需要注意的是,此时传入的ConfigurableApplicationContext并没有调用过refresh方法,也就是里面是没有Bean对象的,一般这个接口是用来配置ConfigurableApplicationContext,而不是用来获取Bean的。

4、EnvironmentPostProcessor

EnvironmentPostProcessor在SpringBoot启动过程中,也会调用,也是通过SPI机制来加载扩展的。

image.png

EnvironmentPostProcessor是用来处理ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在这个对象的。

说这个类的主要原因,主要不是说扩展,而是他的一个实现类很关键。

image.png

这个类的作用就是用来处理外部化配置文件的,也就是这个类是用来处理配置文件的,通过前面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment里面。

5、ApplicationRunner和CommandLineRunner

ApplicationRunner和CommandLineRunner都是在SpringBoot成功启动之后会调用,可以拿到启动时的参数。

那怎么扩展呢?

当然又是SPI了。

image.png

这两个其实不是通过SPI机制来扩展,而是直接从容器中获取的,这又是为啥呢?

因为调用ApplicationRunner和CommandLineRunner时,SpringBoot已经启动成功了,Spring容器都准备好了,需要什么Bean直接从容器中查找多方便。

而前面说的几个需要SPI机制的扩展点,是因为在SpringBoot启动的时候,Spring容器还没有启动好,也就是无法从Spring容器获取到这些扩展的对象,为了兼顾扩展性,所以就通过SPI机制来实现获取到实现类。

image.png

image.png

所以要想扩展这个点,只需要实现接口,添加到Spring容器就可以了。

Spring Event 事件

Event 事件可以说是一种观察者模式的实现,主要是用来解耦合的。当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。

举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作。

image.png

什么是Spring Event 事件

那么是什么是Spring Event 事件,就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以完成事件的发布订阅

Spring提供的事件api:

ApplicationEvent

image.png

事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。

ApplicationListener

image.png

事件监听的接口,泛型是子类需要监听的事件类型,子类需要实现onApplicationEvent,参数就是事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件。

ApplicationEventPublisher

image.png

事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。

ApplicationContext实现了ApplicationEventPublisher接口,所以通过ApplicationContext就可以发布事件。

image.png

那怎么才能拿到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

从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件。

源码验证

image.png

从这段源码可以看出,如果父容器不为空,就会通过父容器再发布一次事件。

传播特性的一个坑

前面说过,在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呢?

可以给个差评。

image.png

膨胀了膨胀了。。

2、在SpringCloud的运用

在SpringCloud的中,当项目启动的时候,会自动往注册中心进行注册,那么是如何实现的呢?当然也是基于事件来的。当web服务器启动完成之后,就发布ServletWebServerInitializedEvent事件。

image.png

然后不同的注册中心的实现都只需要监听这个事件,就知道web服务器已经创建好了,那么就可以往注册中心注册服务实例了。如果你的服务没往注册中心,看看是不是web环境,因为只有web环境才会发这个事件。

SpringCloud提供了一个抽象类 AbstractAutoServiceRegistration,实现了对WebServerInitializedEvent(ServletWebServerInitializedEvent的父类)事件的监听

image.png

一般不同的注册中心都会去继承这个类,监听项目启动,实现往注册中心服务端进行注册。

image.png

Spring Event事件在Spring内部中运用很多,是解耦合的利器。在实际项目中,你既可以监听Spring/Boot内置的一些事件,进行相应的扩展,也可以基于这套模型在业务中自定义事件和相应的监听器,减少业务代码的耦合。

命名空间

最后来讲一个可能没有留意,但是很神奇的扩展点--命名空间。起初我知道这个扩展点的时候,我都惊呆了,这玩意也能扩展?真的不得不佩服Spring设计的可扩展性。

image.png

回忆一下啥是命名空间?

先看一段配置

<?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文件夹就行了。

image.png

到这里,就完成了扩展,接下来进行测试

测试

先构建一个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内置命名空间的扩展

image.png

通过NameSpaceHandler接口的这些实现类的命名就可以看出来有哪些扩展和这些扩展的作用,比如有处理aop的,有处理mvc的等等之类的。

开源框架对命名空间的扩展

1、Mybatis的扩展

image.png

这个就是来扫描指定路径的mapper接口的,处理 scan 标签,跟@MapperScan注解的作用是一样的。

2、dubbo的扩展

使用dubbo可能写过如下的配置

<dubbo:registry address="zookeeper://192.168.10.119:2181" />

这个dubbo命名空间肯定就是扩展的Spring的,也有对应的dubbo实现的NameSpaceHandler。

image.png

不得不说,dubbo解析的标签可真的多啊,不过功能也是真的多。

PS:如果觉得这篇文章对你有帮助,欢迎大家关注公众号三友的java日记、分享、点赞、在看,感谢支持。

由于本文太长,所以就分为上下两部分,剩下的可关注微信公众号三友的java日记,在主业中查看

本文的所有代码,在公众号三友的java日记回复扩展点即可获得

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

两万字盘点那些被玩烂了的设计模式

搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

相关文章
|
1月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
44 2
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
79 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
88 2
|
6月前
|
监控 Java 应用服务中间件
Spring Boot应用的部署与扩展
Spring Boot应用的部署与扩展
|
6月前
|
运维 Java 关系型数据库
Spring运维之boot项目bean属性的绑定读取与校验
Spring运维之boot项目bean属性的绑定读取与校验
61 2
|
6月前
|
存储 运维 Java
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
75 2
|
6月前
|
Java Maven
springboot项目打jar包后,如何部署到服务器
springboot项目打jar包后,如何部署到服务器
453 1
|
6月前
|
XML 运维 Java
Spring运维之boot项目打包jar和插件运行并且设置启动时临时属性和自定义配置文件
Spring运维之boot项目打包jar和插件运行并且设置启动时临时属性和自定义配置文件
59 1
|
5月前
|
监控 Java 应用服务中间件
Spring Boot应用的部署与扩展
Spring Boot应用的部署与扩展