SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
SpringBoot中文注释项目Github地址:
https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE
本篇接 SpringApplication对象是如何构建的? SpringBoot源码(八)
1 温故而知新
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringApplication对象的构建过程及SpringBoot自己实现的一套SPI机制,现将关键步骤再浓缩总结下:
SpringApplication
对象的构造过程其实就是给SpringApplication
类的6个成员变量赋值;- SpringBoot通过以下步骤实现自己的SPI机制:
- 1)首先获取线程上下文类加载器;
- 2)然后利用上下文类加载器从
spring.factories
配置文件中加载所有的SPI扩展实现类并放入缓存中; - 3)根据SPI接口从缓存中取出相应的SPI扩展实现类;
- 4)实例化从缓存中取出的SPI扩展实现类并返回。
2 引言
在SpringBoot启动过程中,每个不同的启动阶段会分别广播不同的内置生命周期事件,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作比如ConfigFileApplicationListener
会监听onApplicationEnvironmentPreparedEvent
事件来加载配置文件application.properties
的环境变量等。
因此本篇内容将来分析下SpringBoot的事件监听机制的源码。
3 SpringBoot广播内置生命周期事件流程分析
为了探究SpringBoot广播内置生命周期事件流程,我们再来回顾一下SpringBoot的启动流程代码:
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
SpringApplicationRunListeners listeners = getRunListeners(args);
// 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
// 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
listeners.started(context);
callRunners(context, applicationArguments);
}
// 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
可以看到SpringBoot在启动过程中首先会先新建一个SpringApplicationRunListeners
对象用于发射SpringBoot启动过程中的各种生命周期事件,比如发射ApplicationStartingEvent
,ApplicationEnvironmentPreparedEvent
和ApplicationContextInitializedEvent
等事件,然后相应的监听器会执行一些SpringBoot启动过程中的初始化逻辑。那么,监听这些SpringBoot的生命周期事件的监听器们是何时被加载实例化的呢?还记得上篇文章在分析SpringApplication
的构建过程吗?没错,这些执行初始化逻辑的监听器们正是在SpringApplication
的构建过程中根据ApplicationListener
接口去spring.factories
配置文件中加载并实例化的。
3.1 为广播SpringBoot内置生命周期事件做前期准备
3.1.1 加载ApplicationListener监听器实现类
我们再来回顾下SpringApplication对象是如何构建的? SpringBoot源码(八)一文中讲到在构建SpringApplication
对象时的setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
这句代码。
这句代码做的事情就是从spring.factories
中加载出ApplicationListener
事件监听接口的SPI扩展实现类然后添加到SpringApplication
对象的listeners
集合中,用于后续监听SpringBoot启动过程中的事件,来执行一些初始化逻辑工作。
SpringBoot启动时的具体监听器们都实现了ApplicationListener
接口,其在spring.factories
部分配置如下:
不过在调试时,会从所有的spring.factories配置文件中加载监听器,最终加载了10个监听器。如下图:
3.1.2 加载SPI扩展类EventPublishingRunListener
前面讲到,在SpringBoot的启动过程中首先会先新建一个SpringApplicationRunListeners
对象用于发射SpringBoot启动过程中的生命周期事件,即我们现在来看下SpringApplicationRunListeners listeners = getRunListeners(args);
这句代码:
// SpringApplication.java
private SpringApplicationRunListeners getRunListeners(String[] args) {
// 构造一个由SpringApplication.class和String[].class组成的types
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
// 1) 根据SpringApplicationRunListener接口去spring.factories配置文件中加载其SPI扩展实现类
// 2) 构建一个SpringApplicationRunListeners对象并返回
return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
SpringApplicationRunListener.class, types, this, args));
}
我们将重点放到getSpringFactoriesInstances( SpringApplicationRunListener.class, types, this, args)
这句代码,getSpringFactoriesInstances
这个方法我们已经很熟悉,在上一篇分析SpringBoot的SPI机制时已经详细分析过这个方法。可以看到SpringBoot此时又是根据SpringApplicationRunListener
这个SPI接口去spring.factories
中加载相应的SPI扩展实现类,我们直接去spring.factories
中看看SpringApplicationRunListener
有哪些SPI实现类:
由上图可以看到,SpringApplicationRunListener
只有EventPublishingRunListener
这个SPI实现类
EventPublishingRunListener
这个哥们在SpringBoot的启动过程中尤其重要,由其在SpringBoot启动过程的不同阶段发射不同的SpringBoot的生命周期事件,即SpringApplicationRunListeners
对象没有承担广播事件的职责,而最终是委托EventPublishingRunListener
这个哥们来广播事件的。
因为从spring.factories
中加载EventPublishingRunListener
类后还会实例化该类,那么我们再跟进EventPublishingRunListener
的源码,看看其是如何承担发射SpringBoot生命周期事件这一职责的?
// EventPublishingRunListener.java
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
private final SpringApplication application;
private final String[] args;
/**
* 拥有一个SimpleApplicationEventMulticaster事件广播器来广播事件
*/
private final SimpleApplicationEventMulticaster initialMulticaster;
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
// 新建一个事件广播器SimpleApplicationEventMulticaster对象
this.initialMulticaster = new SimpleApplicationEventMulticaster();
// 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
for (ApplicationListener<?> listener : application.getListeners()) {
// 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
this.initialMulticaster.addApplicationListener(listener);
}
}
@Override
public int getOrder() {
return 0;
}
// 》》》》》发射【ApplicationStartingEvent】事件
@Override
public void starting() {
this.initialMulticaster.multicastEvent(
new ApplicationStartingEvent(this.application, this.args));
}
// 》》》》》发射【ApplicationEnvironmentPreparedEvent】事件
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
this.application, this.args, environment));
}
// 》》》》》发射【ApplicationContextInitializedEvent】事件
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
this.initialMulticaster.multicastEvent(new ApplicationContextInitializedEvent(
this.application, this.args, context));
}
// 》》》》》发射【ApplicationPreparedEvent】事件
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
for (ApplicationListener<?> listener : this.application.getListeners()) {
if (listener instanceof ApplicationContextAware) {
((ApplicationContextAware) listener).setApplicationContext(context);
}
context.addApplicationListener(listener);
}
this.initialMulticaster.multicastEvent(
new ApplicationPreparedEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationStartedEvent】事件
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationStartedEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationReadyEvent】事件
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationReadyEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationFailedEvent】事件
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
ApplicationFailedEvent event = new ApplicationFailedEvent(this.application,
this.args, context, exception);
if (context != null && context.isActive()) {
// Listeners have been registered to the application context so we should
// use it at this point if we can
context.publishEvent(event);
}
else {
// An inactive context may not have a multicaster so we use our multicaster to
// call all of the context's listeners instead
if (context instanceof AbstractApplicationContext) {
for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
.getApplicationListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}
this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
this.initialMulticaster.multicastEvent(event);
}
}
// ...省略非关键代码
}
可以看到EventPublishingRunListener
类实现了SpringApplicationRunListener
接口,SpringApplicationRunListener
接口定义了SpringBoot启动时发射生命周期事件的接口方法,而EventPublishingRunListener
类正是通过实现SpringApplicationRunListener
接口的starting
,environmentPrepared
和contextPrepared
等方法来广播SpringBoot不同的生命周期事件,我们直接看下SpringApplicationRunListener
接口源码好了:
// SpringApplicationRunListener.java
public interface SpringApplicationRunListener {
void starting();
void environmentPrepared(ConfigurableEnvironment environment);
void contextPrepared(ConfigurableApplicationContext context);
void contextLoaded(ConfigurableApplicationContext context);
void started(ConfigurableApplicationContext context);
void running(ConfigurableApplicationContext context);
void failed(ConfigurableApplicationContext context, Throwable exception);
}
我们再接着分析EventPublishingRunListener
这个类,可以看到其有一个重要的成员属性initialMulticaster
,该成员属性是SimpleApplicationEventMulticaster
类对象,该类正是承担了广播SpringBoot启动时生命周期事件的职责,即EventPublishingRunListener
对象没有承担广播事件的职责,而最终是委托SimpleApplicationEventMulticaster
这个哥们来广播事件的。 从EventPublishingRunListener
的源码中也可以看到在starting
,environmentPrepared
和contextPrepared
等方法中也正是通过调用SimpleApplicationEventMulticaster
类对象的multicastEvent
方法来广播事件的。
思考 SpringBoot启动过程中发射事件时事件广播者是层层委托职责的,起初由
SpringApplicationRunListeners
对象承担,然后SpringApplicationRunListeners
对象将广播事件职责委托给EventPublishingRunListener
对象,最终EventPublishingRunListener
对象将广播事件的职责委托给SimpleApplicationEventMulticaster
对象。为什么要层层委托这么做呢? 这个值得大家思考。
前面讲到从spring.factories
中加载出EventPublishingRunListener
类后会实例化,而实例化必然会通过EventPublishingRunListener
的构造函数来进行实例化,因此我们接下来分析下EventPublishingRunListener
的构造函数源码:
// EventPublishingRunListener.java
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
// 新建一个事件广播器SimpleApplicationEventMulticaster对象
this.initialMulticaster = new SimpleApplicationEventMulticaster();
// 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
for (ApplicationListener<?> listener : application.getListeners()) {
// 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
this.initialMulticaster.addApplicationListener(listener);
}
}
可以看到在EventPublishingRunListener
的构造函数中有一个for
循环会遍历之前从spring.factories
中加载的监听器们,然后添加到集合中缓存起来,用于以后广播各种事件时直接从这个集合中取出来即可,而不用再去spring.factories
中加载,提高效率。
3.2 广播SpringBoot的内置生命周期事件
从spring.factories
配置文件中加载并实例化EventPublishingRunListener
对象后,那么在在SpringBoot的启动过程中会发射一系列SpringBoot内置的生命周期事件,我们再来回顾下SpringBoot启动过程中的源码:
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
SpringApplicationRunListeners listeners = getRunListeners(args);
// 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
// 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
listeners.started(context);
callRunners(context, applicationArguments);
}
// 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
可以看到在SpringBoot的启动过程中总共会发射7种不同类型的生命周期事件,来标志SpringBoot的不同启动阶段,同时,这些生命周期事件的监听器们也会执行一些启动过程中的初始化逻辑,关于这些监听器的初始化逻辑将在下一篇内容中会分析。以下是SpringBoot启动过程中要发射的事件类型,其中ApplicationFailedEvent
在SpringBoot启动过程中遇到异常才会发射:
ApplicationStartingEvent
ApplicationEnvironmentPreparedEvent
ApplicationContextInitializedEvent
ApplicationPreparedEvent
ApplicationStartedEvent
ApplicationFailedEvent
ApplicationReadyEvent
我们以listeners.starting();
这句代码为例,看看EventPublishingRunListener
对象发射事件的源码:
// SpringApplicationRunListeners.java
public void starting() {
// 遍历listeners集合,这里实质取出的就是刚才从spring.factories中取出的SPI实现类EventPublishingRunListener
// 而EventPublishingRunListener对象承担了SpringBoot启动过程中负责广播不同的生命周期事件
for (SpringApplicationRunListener listener : this.listeners) {
// 调用EventPublishingRunListener的starting方法来广播ApplicationStartingEvent事件
listener.starting();
}
}
继续跟进listener.starting();
的源码:
EventPublishingRunListener.java
// 》》》》》发射【ApplicationStartingEvent】事件
public void starting() {
// EventPublishingRunListener对象将发布ApplicationStartingEvent这件事情委托给了initialMulticaster对象
// 调用initialMulticaster的multicastEvent方法来发射ApplicationStartingEvent事件
this.initialMulticaster.multicastEvent(
new ApplicationStartingEvent(this.application, this.args));
}
可以看到,EventPublishingRunListener
对象将发布ApplicationStartingEvent
这件事情委托给了SimpleApplicationEventMulticaster
对象initialMulticaster
,
,而initialMulticaster
对象最终会调用其multicastEvent
方法来发射ApplicationStartingEvent
事件。关于SimpleApplicationEventMulticaster
类如何广播事件,笔者已经在Spring是如何实现事件监听机制的? Spring源码(二)这篇文章已经详细分析,这里不再赘述。
关于SpringBoot启动过程中发射其他生命周期事件的源码这里不再分析
4 SpringBoot的内置生命周期事件总结
好了,前面已经分析了SpringBoot启动过程中要发射的各种生命周期事件,下面列一个表格总结下:
5 小结
SpringBoot启动过程中广播生命周期事件的源码分析就到此结束了,下一篇会继续介绍监听这些生命周期事件的监听器们。我们再回顾本篇内容总结下关键点:
SpringBoot启动过程中会发射7种类型的生命周期事件,标志不同的启动阶段,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作;
【源码笔记】Github源码分析项目上线啦!!!下面是笔记的Github地址:
https://github.com/yuanmabiji/Java-SourceCode-Blogs
点赞和转发是对笔者最大的激励哦!
公众号【源码笔记】,专注于Java后端系列框架的源码分析。
SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)的更多相关文章
- Spring 事件监听机制及原理分析
简介 在JAVA体系中,有支持实现事件监听机制,在Spring 中也专门提供了一套事件机制的接口,方便我们实现.比如我们可以实现当用户注册后,给他发送一封邮件告诉他注册成功的一些信息,比如用户订阅的主 ...
- SpringBoot事件监听机制及观察者模式/发布订阅模式
目录 本篇要点 什么是观察者模式? 发布订阅模式是什么? Spring事件监听机制概述 SpringBoot事件监听 定义注册事件 注解方式 @EventListener定义监听器 实现Applica ...
- JAVA 图形开发之计算器设计(事件监听机制)
/*文章中用到的代码只是一部分,需要源码的可通过邮箱联系我 1978702969@qq.com*/ 前段时间刚帮同学用MFC写了个计算器,现在学到JAVA的图形开发,就试着水了一个计算器出来.(可以说 ...
- .NET事件监听机制的局限与扩展
.NET中把“事件”看作一个基本的编程概念,并提供了非常优美的语法支持,对比如下C#和Java代码可以看出两种语言设计思想之间的差异. // C#someButton.Click += OnSomeB ...
- 关于事件监听机制的总结(Listener和Adapter)
记得以前看过事件监听机制背后也是有一种设计模式的.(设计模式的名字记不清了,只记得背后实现的数据结构是数组.) 附上事件监听机制的分析图: 一个事件源可以承载多个事件(只要这个事件源支持这个事件就可以 ...
- Java中的事件监听机制
鼠标事件监听机制的三个方面: 1.事件源对象: 事件源对象就是能够产生动作的对象.在Java语言中所有的容器组件和元素组件都是事件监听中的事件源对象.Java中根据事件的动作来区分不同的事件源对象,动 ...
- java Gui编程 事件监听机制
1. GUI编程引言 以前的学习当中,我们都使用的是命令交互方式: 例如:在DOS命令行中通过javac java命令启动程序. 软件的交互的方式: 1. 命令交互方式 图书管理系统 ...
- 7_3.springboot2.x启动配置原理_3.事件监听机制
事件监听机制配置在META-INF/spring.factories ApplicationContextInitializer SpringApplicationRunListenerioc容器中的 ...
- Halo 开源项目学习(六):事件监听机制
基本介绍 Halo 项目中,当用户或博主执行某些操作时,服务器会发布相应的事件,例如博主登录管理员后台时发布 "日志记录" 事件,用户浏览文章时发布 "访问文章" ...
随机推荐
- [Dynamic Programming]动态规划之背包问题
动态规划之背包问题 例题 现有4样物品n = ['a', 'b', 'c', 'd'],重量分别为w = [2, 4, 5, 3],价值分别为v = [5, 4, 6, 2].背包最大承重c = 9. ...
- python报错:ERROR: No matching distribution found for dns.resolver
可能有的小伙伴在安装dns.resolver的时候会遇到这个问题: 我百度的时候别人是: pip install dns-python 但是我这样安装也还是错误.有些时候是这个包改名了所以你没有搜索到 ...
- Vue中使用axios发送ajax请求
作为前后端交互的重要技巧--发送ajax请求,在Vue中我们使用axio来完成这一需求: 首先是下载axios的依赖, npm install --save axios vue-axios 然后在ma ...
- 拜托,别再问我什么是 B+ 树了
前言 每当我们执行某个 SQL 发现很慢时,都会下意识地反应是否加了索引,那么大家是否有想过加了索引为啥会使数据查找更快呢,索引的底层一般又是用什么结构存储的呢,相信大家看了标题已经有答案了,没错!B ...
- Apache Druid 底层存储设计(列存储与全文检索)
导读:首先你将通过这篇文章了解到 Apache Druid 底层的数据存储方式.其次将知道为什么 Apache Druid 兼具数据仓库,全文检索和时间序列的特点.最后将学习到一种优雅的底层数据文件结 ...
- 骑士cms-通读全文-代码审计
版本号:3.5.1 下载地址:http://103.45.101.75:66/2/201412/74cms.rar 1.审计方法 通读审计 1.1查看文件结构 首先需要看看有哪些文件和文件夹,寻找名称 ...
- 初识js(第一篇)
初识javascript js是前端中作交互控制的语言,有了它,我们的前端页面才能"活"起来.学好这么语言显得非常重要,但是存在一定难度,所以一定要认真学习,充满耐心. js书写规 ...
- algorithm++:一个整数称为是:【幸运数】,如果这个整数的各位数字的平方和为1或者反复计算各位数字的平方和为1 例如 19 是个幸运数
1):一个整数称为是:[幸运数],如果这个整数的各位数字的平方和为1或者反复计算各位数字的平方和为1 例如 19 是个幸运数 coding:java程序实现 import org.junit.Test ...
- iOS 图片圆角性能
通常设置圆角方式 imageView.clipsToBounds = YES; imageView.layer.cornerRadius = 50; 这样设置会触发离屏渲染,比较消耗性能.比如当一个页 ...
- 一次作业过程及其问题的记录:mysql建立数据库、建表、查询和插入等
前言 这次的作业需要我建立一个小的数据库. 这次作业我使用了mysql,进行了建库.建表.查询.插入等操作. 以下是对本次作业相关的mysql操作过程及过程中出现的问题的记录. 正文 作业中对数据库的 ...