当前位置: 首页 > news >正文

Spring源码(五)— 解析XML配置文件(一) bean标签解析流程

前面几章的内容已经介绍了BeanFactory创建前的准备工作,以及加载XML配置文件前的准备的步骤。本章会着重介绍解析XML的步骤。

registerBeanDefinitions

前几个方法不做过多的赘述,着重看registerBeanDefinitions方法中解析XML的步骤。

	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {// 对xml的beanDefinition进行解析BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();int countBefore = getRegistry().getBeanDefinitionCount();// 完成具体的解析过程documentReader.registerBeanDefinitions(doc, createReaderContext(resource));return getRegistry().getBeanDefinitionCount() - countBefore;}

doRegisterBeanDefinitions

之前已经将XML转换成了Document对象,所以Element 元素就是XML配置文件中的最外层标签< beans >,并且看beans标签中是否配置了default-lazy-init,default-merge等属性,如果配置了则进行赋值。其中defaultNamespace就是XML配置文件中beans标签的xmlns属性中所对应的。主要是用来在网上对应地址进行校验XML。
在这里插入图片描述

protected void doRegisterBeanDefinitions(Element root) {// Any nested <beans> elements will cause recursion in this method. In// order to propagate and preserve <beans> default-* attributes correctly,// keep track of the current (parent) delegate, which may be null. Create// the new (child) delegate with a reference to the parent for fallback purposes,// then ultimately reset this.delegate back to its original (parent) reference.// this behavior emulates a stack of delegates without actually necessitating one.BeanDefinitionParserDelegate parent = this.delegate;//创建Delegate,并且看beans标签中是否配置了default-lazy-init,default-merge等属性,设置默认值this.delegate = createDelegate(getReaderContext(), root, parent);//defaultNamespace = http://www.springframework.org/schema/beansif (this.delegate.isDefaultNamespace(root)) {String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);if (StringUtils.hasText(profileSpec)) {String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);// We cannot use Profiles.of(...) since profile expressions are not supported// in XML config. See SPR-12458 for details.if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {return;}}}preProcessXml(root);parseBeanDefinitions(root, this.delegate);postProcessXml(root);this.delegate = parent;}

parseBeanDefinitions
解析XML的具体操作,从beans标签节点开始逐层像里面遍历

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {if (delegate.isDefaultNamespace(root)) {//获取childNodeNodeList nl = root.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);if (node instanceof Element) {Element ele = (Element) node;if (delegate.isDefaultNamespace(ele)) {parseDefaultElement(ele, delegate);}else {delegate.parseCustomElement(ele);}}}}else {delegate.parseCustomElement(root);}}

parseDefaultElement

默认namespace,则走parseDefaultElement方法进行解析,只有Spring默认的标签,才会执行这个解析方式,默认标签共有import、alias、bean和beans四种。

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {importBeanDefinitionResource(ele);}else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {processAliasRegistration(ele);}else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {processBeanDefinition(ele, delegate);}else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {// recursedoRegisterBeanDefinitions(ele);}}

processBeanDefinition

每种对应的解析方式也不同,以bean标签为例。创建一个BeanDefinitionHolder 对象用来封装

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {// beanDefinitionHolder是beanDefinition对象的封装类,封装了BeanDefinition,bean的名字和别名,用它来完成向IOC容器的注册// 得到这个beanDefinitionHolder就意味着beandefinition是通过BeanDefinitionParserDelegate对xml元素的信息按照spring的bean规则进行// 解析得到的BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);try {// Register the final decorated instance.// 向ioc容器注册解析得到的beandefinition的地方BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {}// Send registration event.// 在beandefinition向ioc容器注册完成之后,发送消息getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}}

解析bean标签

获取bean标签中id属性和name属性,并检查唯一性,调用parseBeanDefinitionElement进一步解析bean下其他节点属性。
parseBeanDefinitionElement

	@Nullablepublic BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {// 解析id属性String id = ele.getAttribute(ID_ATTRIBUTE);// 解析name属性String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);// 如果bean有别名的话,那么将别名分割解析List<String> aliases = new ArrayList<>();if (StringUtils.hasLength(nameAttr)) {String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);aliases.addAll(Arrays.asList(nameArr));}String beanName = id;if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {beanName = aliases.remove(0);}if (containingBean == null) {checkNameUniqueness(beanName, aliases, ele);}// 对bean元素的详细解析AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);if (beanDefinition != null) {if (!StringUtils.hasText(beanName)) {try {// 如果不存在beanname,那么根据spring中提供的命名规则为当前bean生成对应的beanNameif (containingBean != null) {beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true);}else {beanName = this.readerContext.generateBeanName(beanDefinition);// Register an alias for the plain bean class name, if still possible,// if the generator returned the class name plus a suffix.// This is expected for Spring 1.2/2.0 backwards compatibility.String beanClassName = beanDefinition.getBeanClassName();if (beanClassName != null &&beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {aliases.add(beanClassName);}}}catch (Exception ex) {error(ex.getMessage(), ele);return null;}}String[] aliasesArray = StringUtils.toStringArray(aliases);return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);}return null;}

parseBeanDefinitionElement
createBeanDefinition中创建的是GenericBeanDefinition,将解析好的属性值放到GenericBeanDefinition中,并封装到BeanDefinitionHolder中。

public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean) {this.parseState.push(new BeanEntry(beanName));// 解析class属性String className = null;if (ele.hasAttribute(CLASS_ATTRIBUTE)) {className = ele.getAttribute(CLASS_ATTRIBUTE).trim();}// 解析parent属性String parent = null;if (ele.hasAttribute(PARENT_ATTRIBUTE)) {parent = ele.getAttribute(PARENT_ATTRIBUTE);}try {// 创建装在bean信息的AbstractBeanDefinition对象,实际的实现是GenericBeanDefinitionAbstractBeanDefinition bd = createBeanDefinition(className, parent);// 解析bean标签的各种其他属性parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);// 设置description信息bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));// 解析元数据parseMetaElements(ele, bd);// 解析lookup-method属性parseLookupOverrideSubElements(ele, bd.getMethodOverrides());// 解析replaced-method属性parseReplacedMethodSubElements(ele, bd.getMethodOverrides());// 解析构造函数参数parseConstructorArgElements(ele, bd);// 解析property子元素parsePropertyElements(ele, bd);// 解析qualifier子元素parseQualifierElements(ele, bd);bd.setResource(this.readerContext.getResource());bd.setSource(extractSource(ele));return bd;} finally {this.parseState.pop();}return null;}

此时,整个bean标签已经全部加载完成,并封装到了一个BeanDefinitionHolder中,接下来,再回到BeanDefinitionHolder的操作。看解析完之后做了些什么。parseBeanDefinitionElement方法已经执行完,接下来是registerBeanDefinition。

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {// beanDefinitionHolder是beanDefinition对象的封装类,封装了BeanDefinition,bean的名字和别名,用它来完成向IOC容器的注册// 得到这个beanDefinitionHolder就意味着beandefinition是通过BeanDefinitionParserDelegate对xml元素的信息按照spring的bean规则进行// 解析得到的BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);try {// Register the final decorated instance.// 向ioc容器注册解析得到的beandefinition的地方BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to register bean definition with name '" +bdHolder.getBeanName() + "'", ele, ex);}// Send registration event.// 在beandefinition向ioc容器注册完成之后,发送消息getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}}

registerBeanDefinition
这个方法主要流程是获取封装进eanDefinitionHolder的beanName,注册到BeanFactory容器中。

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)throws BeanDefinitionStoreException {// Register bean definition under primary name.// 使用beanName做唯一标识注册String beanName = definitionHolder.getBeanName();registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());// Register aliases for bean name, if any.// 注册所有的别名String[] aliases = definitionHolder.getAliases();if (aliases != null) {for (String alias : aliases) {registry.registerAlias(beanName, alias);}}}

registerBeanDefinition
beanDefinitionMap:ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap类型(key:beanName,value:BeanDefinition)。
beanDefinitionNames:ArrayList< String > beanDefinitionNames类型。
如果beanDefinitionMap中key存在,证明加载解析过,则看allowBeanDefinitionOverriding是否允许类被覆盖(创建BeanFactory是设置的值),可以则覆盖,不可以则抛异常。
如果没加载过,则放到beanDefinitionMap和beanDefinitionNames中。

@Overridepublic void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)throws BeanDefinitionStoreException {if (beanDefinition instanceof AbstractBeanDefinition) {try {// 注册前的最后一个校验,这里的检验不同于之前的xml文件校验,主要是对应abstractBeanDefinition属性的methodOverrides校验,// 检验methodOverrides是否与工厂方法并存或者methodoverrides对应的方法根本不存在((AbstractBeanDefinition) beanDefinition).validate();}//异常,省略。。}BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);// 处理注册已经注册的beanName情况if (existingDefinition != null) {// 如果对应的beanName已经注册且在配置中配置了bean不允许被覆盖,则抛出异常if (!isAllowBeanDefinitionOverriding()) {throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);}//LOG,省略。。this.beanDefinitionMap.put(beanName, beanDefinition);}else {if (hasBeanCreationStarted()) {// Cannot modify startup-time collection elements anymore (for stable iteration)synchronized (this.beanDefinitionMap) {this.beanDefinitionMap.put(beanName, beanDefinition);List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);updatedDefinitions.addAll(this.beanDefinitionNames);updatedDefinitions.add(beanName);this.beanDefinitionNames = updatedDefinitions;removeManualSingletonName(beanName);}}else {// Still in startup registration phase// 注册beanDefinitionthis.beanDefinitionMap.put(beanName, beanDefinition);// 记录beanNamethis.beanDefinitionNames.add(beanName);removeManualSingletonName(beanName);}this.frozenBeanDefinitionNames = null;}if (existingDefinition != null || containsSingleton(beanName)) {// 重置所有beanName对应的缓存resetBeanDefinition(beanName);}else if (isConfigurationFrozen()) {clearByTypeCache();}}

至此,XML加载整个bean标签流程结束。
在这里插入图片描述

相关文章:

Spring源码(五)— 解析XML配置文件(一) bean标签解析流程

前面几章的内容已经介绍了BeanFactory创建前的准备工作&#xff0c;以及加载XML配置文件前的准备的步骤。本章会着重介绍解析XML的步骤。 registerBeanDefinitions 前几个方法不做过多的赘述&#xff0c;着重看registerBeanDefinitions方法中解析XML的步骤。 public int regi…...

隐私政策声明

http://lxfamn.cn/tools 我们注重对您个人隐私的保护。有时候我们需要某些信息才能为您提供您请求的服务&#xff0c;本隐私声明解释了这些情况下的数据收集和使用情况。本隐私声明适用于本网站的所有相关服务。如果您访问本网站、使用本网站的任何服务&#xff0c;那么您便接受…...

Flutter 最佳实践和编码准则

Flutter 最佳实践和编码准则 视频 前言 最佳实践是一套既定的准则&#xff0c;可以提高代码质量、可读性和可靠性。它们确保遵循行业标准&#xff0c;鼓励一致性&#xff0c;并促进开发人员之间的合作。通过遵循最佳实践&#xff0c;代码变得更容易理解、修改和调试&#xff…...

LangChain Agents深入剖析及源码解密上(一)

LangChain Agents深入剖析及源码解密上(一) LangChain Agents深入剖析及源码解密上 Agent工作原理详解 本节会结合AutoGPT的案例,讲解LangChain代理(Agent)为核心的内容。我们前面已经谈了代理本身的很多内容,也看了绝大部分的源代码,例如:ReAct的源代码,还有mrkl的源代…...

css定义超级链接a标签里面的title的样式

效果: 代码: 总结:此css 使用于任何元素,不仅仅是a标签!...

hcip——路由策略

要求&#xff1a; 基础配置 AR1 [R1]int g 0/0/0 [R1-GigabitEthernet0/0/0]ip add 12.0.0.1 24[R1-GigabitEthernet0/0/0]int g 0/0/1 [R1-GigabitEthernet0/0/1]ip add 14.0.0.1 24[R1]int loop0 [R1-LoopBack0]ip add 1.1.1.1 24[R1]rip 1 [R1-rip-1]vers 2 [R1-rip-1]net…...

ReID网络:MGN网络(1) - 概述

Start MGN 1. 序言 现代基于感知的信息中&#xff0c;视觉信息占了80~85%。基于视觉信息的处理和分析被应用到诸如安防、电力、汽车等领域。 以安防市场为例&#xff0c;早在2017年&#xff0c;行业咨询公司IHS Market&#xff0c;我国在公共和私人领域安装有摄像头约1.76亿…...

C++数据结构笔记(10)递归实现二叉树的三序遍历

对于三种遍历方式来说&#xff0c;均为先左后右&#xff01;区别在于根结点的位置顺序 先序遍历&#xff1a;根——左——右 中序遍历&#xff1a;左——根——右 后序遍历&#xff1a;左——右——根 &#xff08;所谓先中后的顺序&#xff0c;是指根结点D先于子树还是后于…...

hMailServer-5.3.3-B1879.exe

hMailServer-5.3.3-B1879.exe...

后端校验JSR303

目录 一、导入依赖 二、实现步骤 三、分组校验 四、自定义校验 一、导入依赖 <dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version></dependency> 二…...

vmware磁盘组使用率100%处理

今天在外办事时&#xff0c;有客户发过来一个截图&#xff0c;问vmware 磁盘组空间使用率100%咋办&#xff1f;如下图&#xff1a; 直接回复&#xff1a; 1、首先删除iso文件等 2、若不存在ISO文件等&#xff0c;找个最不重要的虚拟机直接删除&#xff0c;删除后稍等就会释放…...

Redis实战(3)——缓存模型与缓存更新策略

1 什么是缓存? 缓存就是数据交换的缓冲区&#xff0c; 是存贮数据的临时区&#xff0c;一般读写性能较高 \textcolor{red}{是存贮数据的临时区&#xff0c;一般读写性能较高} 是存贮数据的临时区&#xff0c;一般读写性能较高。缓存可在多个场景下使用 以一次 w e b 请求为例…...

python与深度学习(十):CNN和cifar10二

目录 1. 说明2. cifar10的CNN模型测试2.1 导入相关库2.2 加载数据和模型2.3 设置保存图片的路径2.4 加载图片2.5 图片预处理2.6 对图片进行预测2.7 显示图片 3. 完整代码和显示结果4. 多张图片进行测试的完整代码以及结果 1. 说明 本篇文章是对上篇文章训练的模型进行测试。首…...

剑指offer12 矩阵中的路径 13 机器人的运动范围 34.二叉树中和为某一值得路径

class Solution { public:bool exist(vector<vector<char>>& board, string word) {int rowboard.size(),colboard[0].size();int index0,i0,j0;if(word.size()>row*col) return 0;//vector<vector<int>> visit[row][col];//标记当前位置有没有…...

Pushgateway+Prometheus监控Flink

思路方案 FlinkMtrics->pushgateway->prometheus->grafnana->altermanager 方案 : Flink任务先将数据推到pushgateway。然后pushgateway将值推送到prometheus,最后grafana展示prometheus中的值, 去这个 https://prometheus.io/download/ 下载最新的 Prometheu…...

OpenCV图像处理-视频分割静态背景-MOG/MOG2/GMG

视频分割背景 1.概念介绍2. 函数介绍MOG算法MOG2算法GMG算法 原视频获取链接 1.概念介绍 视频背景扣除原理&#xff1a;视频是一组连续的帧&#xff08;一幅幅图组成&#xff09;&#xff0c;帧与帧之间关系密切(GOP/group of picture)&#xff0c;在GOP中&#xff0c;背景几乎…...

nginx 反向代理浅谈

前言 通常情况下&#xff0c;客户端向Web服务器发送请求&#xff0c;Web服务器响应请求并返回数据。而在反向代理中&#xff0c;客户端的请求不直接发送到Web服务器&#xff0c;而是发送到反向代理服务器。反向代理服务器会将请求转发给真实的Web服务器&#xff0c;Web服务器响…...

【概率预测】对风力发电进行短期概率预测的分析研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

原型设计模式go实现尝试

文章目录 前言代码结果总结 前言 本文章尝试使用go实现“原型”。 代码 package mainimport ("fmt" )// 不同原型标志枚举 type Type intconst (PROTOTYPE_1 Type iotaPROTOTYPE_2 )// 原型接口 type IPrototype interface {Clone() IPrototypeMethod(value int)P…...

链表是否有环、环长度、环起点

问题引入 如何检测一个链表是否有环&#xff0c;如果有&#xff0c;那么如何确定环的长度及起点。 引自博客&#xff1a;上述问题是一个经典问题&#xff0c;经常会在面试中被问到。我之前在杭州一家网络公司的电话面试中就很不巧的问到&#xff0c;当时是第一次遇到那个问题&…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

Spring Boot面试题精选汇总

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配

AI3D视觉的工业赋能者 迁移科技成立于2017年&#xff0c;作为行业领先的3D工业相机及视觉系统供应商&#xff0c;累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成&#xff0c;通过稳定、易用、高回报的AI3D视觉系统&#xff0c;为汽车、新能源、金属制造等行…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...