关于后端:三万字盘点SpringSpringBoot的那些常用扩展点

34次阅读

共计 28788 个字符,预计需要花费 72 分钟才能阅读完成。

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> {

@Override
public User getObject() throws Exception {User user = new User();
    System.out.println("调用 UserFactoryBean 的 getObject 办法生成 Bean:" + user);
    return user;
}

@Override
public 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 {

@Override
public 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 {

@Override
public 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 {

@Autowired
private User user;

public LifeCycle() {System.out.println("LifeCycle 对象被创立了");
}

/**
 * 实现的 Aware 回调接口
 *
 * @param applicationContext
 * @throws BeansException
 */
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {System.out.println("Aware 接口起作用,setApplicationContext 被调用了,此时 user=" + user);
}

@PostConstruct
public void postConstruct() {System.out.println("@PostConstruct 注解起作用,postConstruct 办法被调用了");
}

/**
 * 实现 InitializingBean 接口
 *
 * @throws Exception
 */
@Override
public 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 办法被调用了");
}

@PreDestroy
public 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
 */
@Override
public 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();
}

@Bean
public 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 {

@Override
public 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 {

@Override
public 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 {

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

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 {

@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 机制,这也是能扩大的重要起因。

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());
}


@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 配置文件格式的解析。

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> {

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

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

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/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…

正文完
 0