Spring对于每个Java后端程序员来说必定不生疏,日常开发和面试必备的。本文就来盘点Spring/SpringBoot常见的扩大点,同时也来看看常见的开源框架是如何基于这些扩大点跟Spring/SpringBoot整合的。
话不多说,间接进入正题。
FactoryBean
提起FactoryBean,就有一道“驰名”的面试题“说一说FactoryBean和BeanFactory的区别”。其实这两者除了名字有点像,没有半毛钱关系。。
BeanFactory是Bean的工厂,能够帮咱们生成想要的Bean,而FactoryBean就是一种Bean的类型。当往容器中注入class类型为FactoryBean的类型的时候,最终生成的Bean是用过FactoryBean的getObject获取的。

来个FactoryBean的Demo
定义一个UserFactoryBean,实现FactoryBean接口,getObject办法返回一个User对象
public class UserFactoryBean implements FactoryBean<User> {

@Overridepublic User getObject() throws Exception {    User user = new User();    System.out.println("调用 UserFactoryBean 的 getObject 办法生成 Bean:" + user);    return user;}@Overridepublic Class<?> getObjectType() {    // 这个 FactoryBean 返回的Bean的类型    return User.class;}

}
复制代码
测试类:
public class Application {

public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    //将 UserFactoryBean 注册到容器中    applicationContext.register(UserFactoryBean.class);    applicationContext.refresh();    System.out.println("获取到的Bean为" + applicationContext.getBean(User.class));}

}
复制代码
后果:
调用 UserFactoryBean 的 getObject 办法生成 Bean:com.sanyou.spring.extension.User@396e2f39
获取到的Bean为com.sanyou.spring.extension.User@396e2f39
复制代码
从后果能够看出,明明注册到Spring容器的是UserFactoryBean,然而却能从容器中获取到User类型的Bean,User这个Bean就是通过UserFactoryBean的getObject办法返回的。
FactoryBean在开源框架中的应用
1、 在Mybatis中的应用
Mybatis在整合Spring的时候,就是通过FactoryBean来实现的,这也就是为什么在Spring的Bean中能够注入Mybatis的Mapper接口的动静代理对象的起因。
代码如下,省略了不重要的代码。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

// mapper的接口类型
private Class<T> mapperInterface;

@Override
public T getObject() throws Exception {

// 通过SqlSession获取接口的动静搭理对象return getSqlSession().getMapper(this.mapperInterface);

}

@Override
public Class<T> getObjectType() {

return this.mapperInterface;

}

}
复制代码
getObject办法的实现就是返回通过SqlSession获取到的Mapper接口的动静代理对象。
而@MapperScan注解的作用就是将每个接口对应的MapperFactoryBean注册到Spring容器的。
2、在OpenFeign中的应用
FeignClient接口的动静代理也是通过FactoryBean注入到Spring中的。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {

// FeignClient接口类型private Class<?> type;@Override

public Object getObject() throws Exception {

  return getTarget();

}

@Override

public Class<?> getObjectType() {

  return type;

}
}
复制代码
getObject办法是调用getTarget办法来返回的动静代理。
@EnableFeignClients注解的作用就是将每个接口对应的FeignClientFactoryBean注入到Spring容器的。

一般来说,FactoryBean 比拟适宜那种简单Bean的构建,在其余框架整合Spring的时候用的比拟多。\

@Import注解
@Import注解在我的项目中可能不常见,然而上面这两个注解必定常见。
@Import({SchedulingConfiguration.class})
public @interface EnableScheduling {
}

@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {

//疏忽

}
复制代码
@EnableScheduling和@EnableAsync两个注解,一个是开启定时工作,一个是开启异步执行。通过这两个注解能够看出,他们都应用了@Import注解,所以真正起作用的是@Import注解。并且在很多状况下,@EnbaleXXX这种格局的注解,都是通过@Import注解起作用的,代表开启了某个性能。
@Import注解导入的配置类的分类
@Import注解导入的配置类能够分为三种状况:
第一种:配置类实现了 ImportSelector 接口
public interface ImportSelector {

String[] selectImports(AnnotationMetadata importingClassMetadata);

@Nullable
default Predicate<String> getExclusionFilter() {

  return null;

}

}
复制代码
当配置类实现了 ImportSelector 接口的时候,就会调用 selectImports 办法的实现,获取一批类的全限定名,最终这些类就会被注册到Spring容器中。
UserImportSelector实现了ImportSelector,selectImports办法返回User的全限定名,代表吧User这个类注册容器中
public class UserImportSelector implements ImportSelector {

@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {    System.out.println("调用 UserImportSelector 的 selectImports 办法获取一批类限定名");    return new String[]{"com.sanyou.spring.extension.User"};}

}
复制代码
测试:
// @Import 注解导入 UserImportSelector
@Import(UserImportSelector.class)
public class Application {

public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    //将 Application 注册到容器中    applicationContext.register(Application.class);    applicationContext.refresh();    System.out.println("获取到的Bean为" + applicationContext.getBean(User.class));}

}
复制代码
后果:
调用 UserImportSelector 的 selectImports 办法获取一批类限定名
获取到的Bean为com.sanyou.spring.extension.User@282003e1
复制代码
所以能够看出,确实胜利往容器中注入了User这个Bean
第二种:配置类实现了 ImportBeanDefinitionRegistrar 接口
public interface ImportBeanDefinitionRegistrar {

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {

   registerBeanDefinitions(importingClassMetadata, registry);

}

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

}
复制代码
当配置类实现了 ImportBeanDefinitionRegistrar 接口,你就能够自定义往容器中注册想注入的Bean。这个接口相比与 ImportSelector 接口的次要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,然而 ImportBeanDefinitionRegistrar 是能够本人注入 BeanDefinition,能够增加属性之类的。
来个demo:
实现ImportBeanDefinitionRegistrar接口
public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {    //构建一个 BeanDefinition , Bean的类型为 User    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)            // 设置 User 这个Bean的属性username的值为三友的java日记            .addPropertyValue("username", "三友的java日记")            .getBeanDefinition();    System.out.println("往Spring容器中注入User");    //把 User 这个Bean的定义注册到容器中    registry.registerBeanDefinition("user", beanDefinition);}

}
复制代码
测试:
// 导入 UserImportBeanDefinitionRegistrar
@Import(UserImportBeanDefinitionRegistrar.class)
public class Application {

public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    //将 Application 注册到容器中    applicationContext.register(Application.class);    applicationContext.refresh();    User user = applicationContext.getBean(User.class);    System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());}

}
复制代码
后果:
往Spring容器中注入User
获取到的Bean为com.sanyou.spring.extension.User@6385cb26,属性username值为:三友的java日记
复制代码
第三种:配置类什么接口都没实现
这种就不演示了,就是一个普普通通的类。
总结

其实不论是什么样的配置类,次要的作用就是往Spring容器中注册Bean,只不过注入的形式不同罢了。
这种形式有什么益处呢?
ImportSelector和ImportBeanDefinitionRegistrar的办法是有入参的,也就是注解的一些属性的封装,所以就能够依据注解的属性的配置,来决定应该返回样的配置类或者是应该往容器中注入什么样的类型的Bean,能够看一下 @EnableAsync 的实现,看看是如何依据@EnableAsync注解的属性来决定往容器中注入什么样的Bean。

@Import的核心作用就是导入配置类,并且还能够依据配合(比方@EnableXXX)应用的注解的属性来决定应该往Spring中注入什么样的Bean。\

Bean的生命周期
第一节讲的FactoryBean是一种非凡的Bean的类型,@Import注解是往Spring容器中注册Bean。其实不论是@Import注解,还是@Component、@Bean等注解,又或是xml配置,甚至是demo中的register办法,其实次要都是做了一件事,那就是往Spring容器去注册Bean。

为什么须要去注册Bean?
当然是为了让Spring晓得要为咱们生成Bean,并且须要依照我的要求来生成Bean,比如说,我要@Autowired一个对象,那么你在创立Bean的过程中,就得给我@Autowired一个对象,这就是一个IOC的过程。所以这就波及了Bean的创立,销毁的过程,也就是面试常问的Bean的生命周期。我之前写过 Spring bean到底是如何创立的?(上)  、Spring bean到底是如何创立的?(下) 两篇文章,来分析Bean的生命周期的源码,有须要的小伙伴能够看一下。
本节来着重看一下,一个Bean在创立的过程中,有哪些常见的操作Spring在Bean的创立过程中给咱们实现,并且操作的程序是什么样的。
话不多说,间接测试,基于后果来剖析。
Bean生命周期的回调
先来测试
创立LifeCycle类
创立了一个LifeCycle,实现了 InitializingBean、ApplicationContextAware、DisposableBean接口,加了@PostConstruct、@PreDestroy注解,注入了一个User对象。
public class LifeCycle implements InitializingBean, ApplicationContextAware, DisposableBean {

@Autowiredprivate User user;public LifeCycle() {    System.out.println("LifeCycle对象被创立了");}/** * 实现的 Aware 回调接口 * * @param applicationContext * @throws BeansException */@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {    System.out.println("Aware接口起作用,setApplicationContext被调用了,此时user=" + user);}@PostConstructpublic void postConstruct() {    System.out.println("@PostConstruct注解起作用,postConstruct办法被调用了");}/** * 实现 InitializingBean 接口 * * @throws Exception */@Overridepublic void afterPropertiesSet() throws Exception {    System.out.println("InitializingBean接口起作用,afterPropertiesSet办法被调用了");}/** * 通过 {@link Bean#initMethod()}来指定 * * @throws Exception */public void initMethod() throws Exception {    System.out.println("@Bean#initMethod()起作用,initMethod办法被调用了");}@PreDestroypublic void preDestroy() throws Exception {    System.out.println("@PreDestroy注解起作用,preDestroy办法被调用了");}/** * 通过 {@link Bean#destroyMethod()}来指定 * * @throws Exception */public void destroyMethod() throws Exception {    System.out.println("@Bean#destroyMethod()起作用,destroyMethod办法被调用了");}/** * 实现 DisposableBean 注解 * * @throws Exception */@Overridepublic void destroy() throws Exception {    System.out.println("DisposableBean接口起作用,destroy办法被调用了");}

}
复制代码
申明LifeCycle
通过@Bean申明了LifeCycle,并且initMethod和destroyMethod属性别离指定到了LifeCycle类的initMethod办法和destroyMethod办法
@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
public LifeCycle lifeCycle() {

return new LifeCycle();

}
复制代码
测试
public class Application {

public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    //将 LifeCycle 注册到容器中    applicationContext.register(Application.class);    applicationContext.refresh();    // 敞开上下文,触发销毁操作    applicationContext.close();}@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")public LifeCycle lifeCycle() {    return new LifeCycle();}@Beanpublic User user() {    return new User();}

}
复制代码
执行后果:
LifeCycle对象被创立了
Aware接口起作用,setApplicationContext被调用了,此时user=com.sanyou.spring.extension.User@57d5872c
@PostConstruct注解起作用,postConstruct办法被调用了
InitializingBean接口起作用,afterPropertiesSet办法被调用了
@Bean#initMethod()起作用,initMethod办法被调用了
@PreDestroy注解起作用,preDestroy办法被调用了
DisposableBean接口起作用,destroy办法被调用了
@Bean#destroyMethod()起作用,destroyMethod办法被调用了
复制代码
剖析后果
通过测试的后果能够看出,Bean在创立和销毁的过程当咱们实现了某些接口或者加了某些注解,Spring就会回调咱们实现的接口或者执行的办法。
同时,在执行setApplicationContext的时候,能打印出User对象,阐明User曾经被注入了,阐明注入产生在setApplicationContext之前。
这里画张图总结一下Bean创立和销毁过程中调用的程序。

红色局部产生在Bean的创立过程,灰色局部产生在Bean销毁的过程中,在容器敞开的时候,就会销毁Bean。
这里说一下图中的Aware接口指的是什么。其余的其实没什么好说的,就是依照这种形式配置,Spring会调用对应的办法而已。
Aware接口是指以Aware结尾的一些Spring提供的接口,当你的Bean实现了这些接口的话,在创立过程中会回调对应的set办法,并传入响应的对象。
这里列举几个Aware接口以及它们的作用

接口作用ApplicationContextAware注入ApplicationContextApplicationEventPublisherAware注入ApplicationEventPublisher事件公布器BeanFactoryAware注入BeanFactoryBeanNameAware注入Bean的名称
有了这些回调,比如说我的Bean想拿到ApplicationContext,不仅能够通过@Autowired注入,还能够通过实现ApplicationContextAware接口拿到。
通过下面的例子咱们晓得了比如说@PostConstruct注解、@Autowired注解、@PreDestroy注解的作用,然而它们是如何在不同的阶段实现的呢?接着往下看。
BeanPostProcessor
BeanPostProcessor,中文名 Bean的后置处理器,在Bean创立的过程中起作用。
BeanPostProcessor是Bean在创立过程中一个十分重要的扩大点,因为每个Bean在创立的各个阶段,都会回调BeanPostProcessor及其子接口的办法,传入正在创立的Bean对象,这样如果想对Bean创立过程中某个阶段进行自定义扩大,那么就能够自定义BeanPostProcessor来实现。
说得简略点,BeanPostProcessor就是在Bean创立过程中留的口子,通过这个口子能够对正在创立的Bean进行扩大。只不过Bean创立的阶段比拟多,而后BeanPostProcessor接口以及他的子接口InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多办法,能够使得在不同的阶段都能够拿到正在创立的Bean进行扩大。
来个Demo
当初须要实现一个这样的需要,如果Bean的类型是User,那么就设置这个对象的username属性为 ”三友的java日记“。
那么就能够这么写:
public class UserBeanPostProcessor implements BeanPostProcessor {

@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {    if (bean instanceof User) {        //如果以后的Bean的类型是 User ,就把这个对象 username 的属性赋值为 三友的java日记        ((User) bean).setUsername("三友的java日记");    }    return bean;}

}
复制代码
测试:
public class Application {

public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    //将 UserBeanPostProcessor 和  User 注册到容器中    applicationContext.register(UserBeanPostProcessor.class);    applicationContext.register(User.class);    applicationContext.refresh();    User user = applicationContext.getBean(User.class);    System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());}

}
复制代码
测试后果:
获取到的Bean为com.sanyou.spring.extension.User@21a947fe,属性username值为:三友的java日记
复制代码
从后果能够看出,每个生成的Bean在执行到某个阶段的时候,都会回调UserBeanPostProcessor,而后UserBeanPostProcessor就会判断以后创立的Bean的类型,如果是User类型,那么就会将username的属性设置为 ”三友的java日记“。
Spring内置的BeanPostProcessor
这里我列举了常见的一些BeanPostProcessor的实现以及它们的作用

BeanPostProcessor作用AutowiredAnnotationBeanPostProcessor解决@Autowired、@Value注解CommonAnnotationBeanPostProcessor解决@Resource、@PostConstruct、@PreDestroy注解AnnotationAwareAspectJAutoProxyCreator解决一些注解或者是AOP切面的动静代理ApplicationContextAwareProcessor解决Aware接口注入的AsyncAnnotationBeanPostProcessor解决@Async注解ScheduledAnnotationBeanPostProcessor解决@Scheduled注解
通过列举的这些BeanPostProcessor的实现能够看出,Spring Bean的很多注解的解决都是依附BeanPostProcessor及其子类的实现来实现的,这也答复了上一大节的疑难,解决@Autowired、@PostConstruct、@PreDestroy注解是如何起作用的,其实就是通过BeanPostProcessor,在Bean的不同阶段来调用对应的办法起作用的。
BeanPostProcessor在Dubbo中的应用
在Dubbo中能够通过@DubboReference(@Reference)来援用生产者提供的接口,这个注解的解决也是依附ReferenceAnnotationBeanPostProcessor,也就是 BeanPostProcessor 的扩大来实现的。
public class ReferenceAnnotationBeanPostProcessor

   extends AbstractAnnotationBeanPostProcessor    implements ApplicationContextAware, BeanFactoryPostProcessor {    // 疏忽

}
复制代码
当Bean在创立的某一阶段,走到了ReferenceAnnotationBeanPostProcessor这个类,就会依据反射找出这个类有没有@DubboReference(@Reference)注解,有的话就构建一个动静搭理注入就能够了。

BeanPostProcessor在Spring Bean的扩大中扮演着重要的角色,是Spring Bean生命周期中很重要的一部分。正是因为有了BeanPostProcessor,你就能够在Bean创立过程中的任意一个阶段扩大本人想要的货色。\

BeanFactoryPostProcessor
通过下面一节咱们晓得 BeanPostProcessor 是对Bean的解决,那么BeanFactoryPostProcessor很容易就猜到是对BeanFactory,也就是Spring容器的解决。
举个例子,如果咱们想禁止循环依赖,那么就能够这么写。
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {    // 禁止循环依赖    ((DefaultListableBeanFactory) beanFactory).setAllowCircularReferences(false);}

}
复制代码
前面只须要将注入到Spring容器中就会失效。

BeanFactoryPostProcessor是能够对Spring容器做解决的,办法的入参就是Spring的容器,通过这个接口,就对容器进行随心所欲的操作。\

Spring SPI机制
SPI全称为 (Service Provider Interface),是一种动静替换发现的机制,一种解耦十分优良的思维,SPI能够很灵便的让接口和实现拆散, 让api提供者只提供接口,第三方来实现,而后能够应用配置文件的形式来实现替换或者扩大,在框架中比拟常见,进步框架的可扩展性。
JDK有内置的SPI机制的实现ServiceLoader,Dubbo也有本人的SPI机制的实现ExtensionLoader,我之前写过相干的文章,面试常问的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底下

测试:
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中还有这样一段代码

\
所以,这段代码也显著地能够看出,主动拆卸也是基于SPI机制实现的。
那么我想实现主动拆卸怎么办呢?很简略,只需两步。
第一步,写个配置类:
@Configuration
public class UserAutoConfiguration {

@Beanpublic 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。

Mybatis整合SpringBoot的spring.factories文件
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结尾的配置文件

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

所以能够看出,要想实现json格局的反对,只须要本人实现能够用来解析json格局的配置文件的PropertySourceLoader就能够了。
入手来一个。
实现能够读取json格局的配置文件
实现这个性能,只须要两步就能够了。
第一步:自定义一个PropertySourceLoader
JsonPropertySourceLoader,实现PropertySourceLoader接口
public class JsonPropertySourceLoader implements PropertySourceLoader {

@Overridepublic String[] getFileExtensions() {    //这个办法表明这个类反对解析以json结尾的配置文件    return new String[]{"json"};}@Overridepublic 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机制,这也是能扩大的重要起因。

SPI机制加载PropertySourceLoader实现

spring.factories文件配置PropertySourceLoader
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的配置文件

application.json配置文件
革新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());}@Beanpublic User user() {    return new User();}

}
复制代码
运行后果:
获取到的Bean为com.sanyou.spring.extension.User@481ba2cf,属性username值为:三友的java日记
复制代码
胜利将json配置文件的属性注入到User对象中。
至此,SpringBoot就反对了以json为结尾的配置文件格式。
Nacos对于PropertySourceLoader的实现
如果你的我的项目正在用Nacos作为配置核心,那么刚刚好,Nacos曾经实现json配置文件格式的解析。

Nacos不仅实现了json格局的解析,也实现了对于xml格局的配置文件的解析,并且优先级会比SpringBoot默认的xml格式文件解析的优先级高。至于Nacos为啥须要实现PropertySourceLoader?其实很简略,因为Nacos作为配置核心,不仅反对properties和yaml格局的文件,还反对json格局的配置文件,那么客户端拿到这些配置就须要解析,SpringBoot曾经反对了properties和yaml格局的文件的解析,那么Nacos只须要实现SpringBoot不反对的就能够了。
3、ApplicationContextInitializer
ApplicationContextInitializer也是SpringBoot启动过程的一个扩大点。

在SpringBoot启动过程,会回调这个类的实现initialize办法,传入ConfigurableApplicationContext。
那怎么用呢?
仍然是SPI。

SPI加载ApplicationContextInitializer
而后遍历所有的实现,顺次调用

调用initialize
这里就不演示了,实现接口,依照如下这种配置就行了

然而这里须要留神的是,此时传入的ConfigurableApplicationContext并没有调用过refresh办法,也就是外面是没有Bean对象的,个别这个接口是用来配置ConfigurableApplicationContext,而不是用来获取Bean的。
4、EnvironmentPostProcessor
EnvironmentPostProcessor在SpringBoot启动过程中,也会调用,也是通过SPI机制来加载扩大的。

EnvironmentPostProcessor是用来解决ConfigurableEnvironment的,也就是一些配置信息,SpringBoot所有的配置都是存在这个对象的。
说这个类的次要起因,次要不是说扩大,而是他的一个实现类很要害。

这个类的作用就是用来解决内部化配置文件的,也就是这个类是用来解决配置文件的,通过后面提到的PropertySourceLoader解析配置文件,放到ConfigurableEnvironment外面。
5、ApplicationRunner和CommandLineRunner
ApplicationRunner和CommandLineRunner都是在SpringBoot胜利启动之后会调用,能够拿到启动时的参数。
那怎么扩大呢?
当然又是SPI了。

这两个其实不是通过SPI机制来扩大,而是间接从容器中获取的,这又是为啥呢?
因为调用ApplicationRunner和CommandLineRunner时,SpringBoot曾经启动胜利了,Spring容器都筹备好了,须要什么Bean间接从容器中查找多不便。
而后面说的几个须要SPI机制的扩大点,是因为在SpringBoot启动的时候,Spring容器还没有启动好,也就是无奈从Spring容器获取到这些扩大的对象,为了兼顾扩展性,所以就通过SPI机制来实现获取到实现类。

所以要想扩大这个点,只须要实现接口,增加到Spring容器就能够了。
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> {

@Overridepublic void onApplicationEvent(FireEvent event) {    System.out.println("打119");}

}
复制代码
救人的火灾事件的监听器:
public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {

@Overridepublic 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> {@Overridepublic 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的父类)事件的监听

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

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 {

@Overridepublic 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/sany...
复制代码
通过spring.handlers配置文件,就晓得sanyou命名空间应该找SanYouNameSpaceHandler进行解析
spring.schemas文内容
http://sanyou.com/schema/sany...
复制代码
spring.schemas配置xsd文件的门路
文件都有了,只须要放到classpath下的META-INF文件夹就行了。

到这里,就实现了扩大,接下来进行测试
测试
先构建一个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的扩大

这个就是来扫描指定门路的mapper接口的,解决 scan 标签,跟@MapperScan注解的作用是一样的。
2、dubbo的扩大
应用dubbo可能写过如下的配置
<dubbo:registry address="zookeeper://192.168.10.119:2181" />
复制代码
这个dubbo命名空间必定就是扩大的Spring的,也有对应的dubbo实现的NameSpaceHandler。

不得不说,dubbo解析的标签可真的多啊,不过性能也是真的多。
总结
到这,本文就靠近序幕了,这里画两张图来总结一下本文讲了Spring的哪些扩大点。

通过学习Spring的这些扩大点,既能够帮忙咱们应答日常的开发,还能够帮忙咱们更好地看懂Spring的源码。
最初,本文前前后后花了一周多的工夫实现,如果对你有点帮忙,还请帮忙点赞、在看、转发、非常感谢。
哦,差点忘了,本文所有demo代码都在这了

github.com/sanyou3/spr…