Spring Boot核心原理(上)--Spring基础

474 阅读17分钟
Spring Boot 框架的设计初衷:快速的启动 Spring 应用。

因而 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 Spring 生态圈的应用。

Spring Boot 有何魔法?

自动配置、起步依赖、Actuator、命令行界面(CLI) 是 Spring Boot 最重要的 4 大核心特性,其中 CLI 是 Spring Boot 的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它 3 种特性。


Spring IoC容器

可以把 Spring IoC 容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC 容器也是一样,你只需要告诉它需要某个bean,它就把对应的实例(instance)扔给你,至于这个bean是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。

作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC 容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。 

BeanDefinition对象就承担了这个责任:容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存bean对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。

原材料已经准备好(把 BeanDefinition 看着原料),开始做菜吧,等等,你还需要一份菜谱, BeanDefinitionRegistry和 BeanFactory就是这份菜谱,

BeanDefinitionRegistry 抽象出 bean 的注册逻辑,

而 BeanFactory 则抽象出了 bean 的管理逻辑

而各个 BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作。

它们之间的关系就如下图:

2019年一份来自大佬的Spring Boot核心知识清单


DefaultListableBeanFactory作为一个比较通用的 BeanFactory 实现,它同时也实现了 BeanDefinitionRegistry 接口,因此它就承担了 Bean 的注册管理工作。

从图中也可以看出,BeanFactory 接口中主要包含 getBean、containBean、getType、getAliases 等管理 bean 的方法,

而 BeanDefinitionRegistry 接口则包含 registerBeanDefinition、removeBeanDefinition、getBeanDefinition 等注册管理 BeanDefinition 的方法。

下面通过一段简单的代码来模拟 BeanFactory 底层是如何工作的:

// 默认容器实现
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
// 根据业务对象构造相应的
BeanDefinitionAbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true);
// 将bean定义注册到容器中
beanRegistry.registerBeanDefinition("beanName",definition);
// 如果有多个bean,还可以指定各个bean之间的依赖关系...

// 从容器中获取这个bean的实例
// 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转,单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的
BeanFactory container = (BeanFactory)beanRegistry;Business business = (Business)container.getBean("beanName");

这段代码仅为了说明 BeanFactory 底层的大致工作流程,实际情况会更加复杂,比如 bean 之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC 容器的整个工作流程大致可以分为两个阶段:

①、容器启动阶段

容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如: BeanDefinitionReader,BeanDefinitionReader会对加载的 ConfigurationMetaData进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。

来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系:

// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例
BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory(); 
// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry);
// 加载配置文件
beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml");
// 从容器中获取bean实例
BeanFactory container = (BeanFactory)beanRegistry;
Business business = (Business)container.getBean("beanName");

②、Bean的实例化阶段

经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。

BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。

而在实际场景下,我们更多的使用另外一种类型的容器: ApplicationContext,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。

Spring容器扩展机制

IoC 容器负责管理容器中所有bean的生命周期,而在 bean 生命周期的不同阶段,Spring 提供了不同的扩展点来改变 bean 的命运。

在容器的启动阶段,BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作,比如修改 bean 定义的某些属性或者增加其他信息等。

如果要自定义扩展类,通常需要实现 org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,与此同时,因为容器中可能有多个BeanFactoryPostProcessor,可能还需要实现 org.springframework.core.Ordered接口,以保证BeanFactoryPostProcessor按照顺序执行。Spring提供了为数不多的BeanFactoryPostProcessor实现,我们以 PropertyPlaceholderConfigurer来说明其大致的工作流程。

在Spring项目的XML配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的properties文件,这样可以将散落在不同XML文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由PropertyPlaceholderConfigurer负责实现的。

根据前文,当BeanFactory在第一阶段加载完所有配置信息时,BeanFactory中保存的对象的属性还是以占位符方式存在的,比如 ${jdbc.mysql.url}。

当PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor被应用时,它会使用properties配置文件中的值来替换相应的BeanDefinition中占位符所表示的属性值。

当需要实例化bean时,bean定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。

与之相似的,还有 BeanPostProcessor,其存在于对象实例化阶段。跟BeanFactoryPostProcessor类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor处理bean的定义,而BeanPostProcessor则处理bean完成实例化后的对象。BeanPostProcessor定义了两个接口:

public interface BeanPostProcessor { 
    // 前置处理 
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; 
    // 后置处理 
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}

为了理解这两个方法执行的时机,简单的了解下bean的整个生命周期:

2019年一份来自大佬的Spring Bootæ ¸å¿ƒçŸ¥è¯†æ¸…å•

postProcessBeforeInitialization()方法与 postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。

注解、AOP等功能的实现均大量使用了 BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作,想想是不是非常的简单?

再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的 ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。

当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。

看看代码吧:

// 代码来自:
org.springframework.context.support.ApplicationContextAwareProcessor
// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法
private void invokeAwareInterfaces(Object bean) {
    if (bean instanceof EnvironmentAware) { 
        ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
    }if (bean instanceof ApplicationContextAware) {
        ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
    }
    // ......
}

JavaConfig与常见Annotation

JavaConfig

在最初,Spring使用XML配置文件的方式来描述bean的定义以及相互间的依赖关系。

因为Spring项目的所有业务类均以bean的形式配置在XML文件中,造成了大量的XML文件,使项目变得复杂且难以管理。
后来,基于纯Java Annotation依赖注入框架 Guice出世,其性能明显优于采用XML方式的Spring,甚至有部分人认为, Guice可以完全取代Spring( Guice仅是一个轻量级IOC框架,取代Spring还差的挺远)。正是这样的危机感,促使Spring及社区推出并持续完善了 JavaConfig子项目,它基于Java代码和Annotation注解来描述bean之间的依赖绑定关系。比如,下面是使用XML配置方式来描述bean的定义:

<bean id="bookService" class="cn.moondev.service.BookServiceImpl">
</bean>

而基于JavaConfig的配置形式是这样的:

@Configurationpublic 
class MoonBookConfiguration { 
// 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 
// 方法名默认成为该bean定义的id 
    @Bean public BookService bookService() { 
        return new BookServiceImpl();
     }
}

@ComponentScan

@ComponentScan注解对应XML配置形式中的 <context:component-scan>元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过 basePackages等属性来指定 @ComponentScan自动扫描的范围,如果不指定,默认从声明 @ComponentScan所在类的 package进行扫描。正因为如此,SpringBoot的启动类都默认在 src/main/java下。

@Import

@Import注解用于导入配置类,举个简单的例子:

@Configurationpublic class MoonBookConfiguration { 
    @Bean public BookService bookService() { 
        return new BookServiceImpl(); 
    }
}

现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?

借助 @Import即可:

@Configuration
// 可以同时导入多个配置类,比如:@Import({A.class,B.class})
@Import(MoonBookConfiguration.class)
public class MoonUserConfiguration { 
    @Bean public UserService userService(BookService bookService) { 
        return new BookServiceImpl(bookService); 
    }
}

@Conditional

@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解标识的类上面,或者由 @Bean标记的方法上。如果一个 @Configuration类标记了 @Conditional,则该类中所有标识了 @Bean的方法和 @Import注解导入的相关类将遵从这些条件。

在Spring里可以很方便的编写你自己的条件类,所要做的就是实现 Condition接口,并覆盖它的 matches()方法。举个例子,下面的简单条件类表示只有在 Classpath里存在 JdbcTemplate类时才生效:

public class JdbcTemplateCondition implements Condition { 
    @Override 
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { 
        try { 
             conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");
             return true; 
        } catch (ClassNotFoundException e) { 
            e.printStackTrace(); 
        } 
        return false; 
    }
}

@Conditional(JdbcTemplateCondition.class)
@Service
public MyService service() { 
    ......
}

这个例子中只有当 JdbcTemplateCondition类的条件成立时才会创建MyService这个bean。也就是说MyService这bean的创建条件是 classpath里面包含 JdbcTemplate,否则这个bean的声明就会被忽略掉。

@ConfigurationProperties与@EnableConfigurationProperties

当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,比如下面配置数据源的代码。

使用 @Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。我们可以通过下面的方式来改写上面的代码:

@Component
// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件
@ConfigurationProperties("jdbc.mysql")
// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项
pulic class JdbcConfig { 
    public String url; 
    public String username; 
    public String password;
}
@Configurationpublic
class HikariDataSourceConfiguration { 
    @AutoWired 
    public JdbcConfig config;
    @Bean public HikariDataSource dataSource() { 
        HikariConfig hikariConfig = new HikariConfig(); 
        hikariConfig.setJdbcUrl(config.url); 
        hikariConfig.setUsername(config.username); 
        hikariConfig.setPassword(config.password); 
        // 省略部分代码 return new HikariDataSource(hikariConfig); 
    }
}

@ConfigurationProperties对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:

#App
app.menus[0].title=Home
app.menus[0].name=Home
app.menus[0].path=/
app.menus[1].title=Login
app.menus[1].name=Login
app.menus[1].path=/login
app.compiler.timeout=5
app.compiler.output-folder=/temp/
app.error=/error/

可以定义如下配置类来接收这些属性

@Component@ConfigurationProperties("app")
public class AppProperties { 
    public String error;
    public List<Menu> menus = new ArrayList<>(); 
    public Compiler compiler = new Compiler(); 

    public static class Menu { 
        public String name; 
        public String path; 
        public String title;
    }
 
    public static class Compiler { 
        public String timeout;
        public String outputFolder;
    }
}

@EnableConfigurationProperties注解表示对 @ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加 @Component注解。

Spring容器的事件监听机制

过去,事件监听机制多用于图形界面编程,比如:点击按钮、在文本框输入内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件;

而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。

Java提供了实现事件监听机制的两个基础类:自定义事件类型扩展自 java.util.EventObject、事件的监听器扩展自 java.util.EventListener。来看一个简单的实例:简单的监控一个方法的耗时。

首先定义事件类型,通常的做法是扩展EventObject,随着事件的发生,相应的状态通常都封装在此类中:

public class MethodMonitorEvent extends EventObject { 
    // 时间戳,用于记录方法开始执行的时间 
    public long timestamp; 
    public MethodMonitorEvent(Object source) { 
        super(source); 
    }
}

事件发布之后,相应的监听器即可对该类型的事件进行处理,

我们可以在方法开始执行之前发布一个begin事件,在方法执行结束之后发布一个end事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理:

// 1、定义事件监听接口
public interface MethodMonitorEventListener extends EventListener { 
    // 处理方法执行之前发布的事件 
    public void onMethodBegin(MethodMonitorEvent event); 
    // 处理方法结束时发布的事件 
    public void onMethodEnd(MethodMonitorEvent event);
}// 

2、事件监听接口的实现:如何处理
public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { 
    @Override public void onMethodBegin(MethodMonitorEvent event) { 
        // 记录方法开始执行时的时间 
        event.timestamp = System.currentTimeMillis(); 
    } 
    @Override 
    public void onMethodEnd(MethodMonitorEvent event) { 
        // 计算方法耗时 
        long duration = System.currentTimeMillis() - event.timestamp;
        System.out.println("耗时:" + duration);
     }
}

事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收MethodMonitorEvent参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。

通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器:

public class MethodMonitorEventPublisher { 
    private List<MethodMonitorEventListener> listeners = new ArrayList<MethodMonitorEventListener>();

    public void methodMonitor() { 
        MethodMonitorEvent eventObject = new MethodMonitorEvent(this); 
        publishEvent("begin",eventObject); 
        // 模拟方法执行:休眠5秒钟 
        TimeUnit.SECONDS.sleep(5); 
        publishEvent("end",eventObject);
    } 

    private void publishEvent(String status,MethodMonitorEvent event) { 
        // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作 
        List<MethodMonitorEventListener> copyListeners = new ArrayList<MethodMonitorEventListener>(listeners); 
        for (MethodMonitorEventListener listener : copyListeners) { 
            if ("begin".equals(status)) { 
                listener.onMethodBegin(event);
            } else { 
                listener.onMethodEnd(event); 
            }
        }
    }
    // 入口
    public static void main(String[] args) {
        MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher(); 
        publisher.addEventListener(new AbstractMethodMonitorEventListener());
        publisher.methodMonitor();
    } 

    // 省略实现 
    public void addEventListener(MethodMonitorEventListener listener) {}
    public void removeEventListener(MethodMonitorEventListener listener) {}
    public void removeAllListeners() {}

对于事件发布者(事件源)通常需要关注两点:

在合适的时机发布事件。此例中的methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布MethodMonitorEvent事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。

事件监听器的管理。publisher类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供remove方法,那么注册的监听器示例将一直被MethodMonitorEventPublisher引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。


Spring容器内的事件监听机制

Spring的ApplicationContext容器内部中的所有事件类型均继承自 org.springframework.context.AppliationEvent,容器中的所有监听器都实现 org.springframework.context.ApplicationListener接口,并且以bean的形式注册在容器中。一旦在容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。

ApplicationEvent继承自EventObject,Spring提供了一些默认的实现,比如: ContextClosedEvent表示容器在即将关闭时发布的事件类型, ContextRefreshedEvent表示容器在初始化或者刷新的时候发布的事件类型……

容器内部使用ApplicationListener作为事件监听器接口定义,它继承自EventListener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型的bean,一旦容器内有事件发布,将通知这些注册到容器的EventListener。

ApplicationContext接口继承了ApplicationEventPublisher接口,该接口提供了 voidpublishEvent(ApplicationEventevent)方法定义,不难看出,ApplicationContext容器担当的就是事件发布者的角色。

如果有兴趣可以查看 AbstractApplicationContext.publishEvent(ApplicationEventevent)方法的源码:ApplicationContext将事件的发布以及监听器的管理工作委托给 ApplicationEventMulticaster接口的实现类。在容器启动时,会检查容器内是否存在名为applicationEventMulticaster的ApplicationEventMulticaster对象实例。如果有就使用其提供的实现,没有就默认初始化一个SimpleApplicationEventMulticaster作为实现。

最后,如果我们业务需要在容器内部发布事件,只需要为其注入ApplicationEventPublisher依赖即可:实现ApplicationEventPublisherAware接口或者ApplicationContextAware接口(Aware接口相关内容请回顾上文)。