带着问题去分析:Spring Bean 生命周期 | 京东物流技术团队
1: Bean在Spring容器中是如何存储和定义的
Bean在Spring中的定义是_org.springframework.beans.factory.config.BeanDefinition_接口,BeanDefinition里面存储的就是我们编写的Java类在Spring中的元数据,包括了以下主要的元数据信息:
1:Scope(Bean类型):包括了单例Bean(Singleton)和多实例Bean(Prototype)
2:BeanClass: Bean的Class类型
3:LazyInit:Bean是否需要延迟加载
4:AutowireMode:自动注入类型
5:DependsOn:Bean所依赖的其他Bean的名称,Spring会先初始化依赖的Bean
6:PropertyValues:Bean的成员变量属性值
7:InitMethodName:Bean的初始化方法名称
8:DestroyMethodName:Bean的销毁方法名称
同时BeanDefinition是存储到_org.springframework.beans.factory.support.DefaultListableBeanFactory类中维护的BeanDefinitionMap_中的,源码如下:
Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
了解了BeanDefinition的基础信息和存储位置后,接下来看看创建好的Bean实例是存储在什么地方的,创建好的Bean是存储在:_org.springframework.beans.factory.support.DefaultSingletonBeanRegistry_类中的
_singletonObjects_中的,Key是Bean的名称,Value就是创建好的Bean实例:
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
了解了基本信息之后,就可以带着下面两个关键问题去分析Spring Bean的生命周期了:
1:Java类是如何被 Spring 扫描从而变成 BeanDefinition 的?
2:BeanDefinition是如何被 Spring 加工创建成我们可以直接使用的 Bean实例的?
2:Java类是如何被Spring扫描成为BeanDefinition的?
在Spring中定义Bean的方式有非常多,例如使用XML文件、注解,包括:@Component,@Service,@Configuration等,下面就以@Component注解为例来探究Spring是如何扫描我们的Bean的。我们知道使用@Component注解来标记Bean是需要配合@ComponentScan注解来使用的,而我们的主启动类上标注的@SpringBootApplication注解中就默认继承了@ComponentScan注解
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication
所以最初的问题就转化成了**@ComponentScan**注解是如何在Spring中运作的
2.1 @ComponentScan注解是如何运作的
在Spring框架中,这个注解对应的处理类是_ComponentScanAnnotationParser,这个类的parse_方法是主要的处理逻辑,这个方法简要处理逻辑如下:
1:获取@ComponentScan注解中的basePackage属性,若没有则默认为该注解所标注类所在的包路径
2:使用ClassPathBeanDefinitionScanner的scanCandidateComponents方法扫描classpath:+basePackage+/*.class**下的所有类资源文件
3:最后循环判断扫描的所有类资源文件,判断是否包含@Component注解,若有则将这些类注册到beandefinitionMap中
自此,我们代码里写的Java类,就被Spring扫描成BeanDefinition存储到了BeanDefinitionMap中了,扫描的细节大家可以去看看这个类的源码
3:Spring如何创建我们的Bean实例的
Spring把我们编写的Java类扫描成BeanDefinition之后,就会开始创建我们的Bean实例了,Spring将创建Bean的方法交给了_org.springframework.beans.factory.support.AbstractBeanFactory#getBean_方法,接下来就来看看getBean方法是如何创建Bean的
getBean方法的调用逻辑如下:getBean–> doGetBean --> createBean --> doCreateBean,最终Spring会使用org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean方法来创建Bean,创建Bean实例的主要逻辑分为了四个部分:创建Bean实例,填充Bean属性,初始化Bean,销毁Bean,接下来我们分别对这个四个部分进行探究
3.1 创建Bean实例
创建Bean实例的方法入口如下:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#__createBeanInstance
if (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);
}
这个方法的主要逻辑是:推断出创建该Bean的构造器方法和参数,然后使用Java反射去创建Bean实例
3.2 填充Bean属性值populateBean方法
在这个方法中,主要是解析Bean需要注入的成员属性,然后将这些属性注入到该Bean中,如果该Bean有依赖的其他Bean则会优先去创建依赖的Bean,然后返回来继续创建该Bean,注意这里就会产生Bean创建的循环依赖问题,在本文的第6节中会详细说明
3.4:初始化Bean(initializeBean方法)
初始化Bean主要包括了四个部分:
3.4.1:invokeAwareMethods
在这个方法中主要调用实现的Aware接口中的方法,包括了BeanNameAware.setBeanName,BeanClassLoaderAware.setBeanClassLoader,BeanFactoryAware.setBeanFactory,这三个方法
Aware接口的功能:通过调用Aware接口中的set方法,将Spring容器中对应的Bean注入到正在创建的Bean中
3.4.2:调用前置处理方法:applyBeanPostProcessorsBeforeInitialization
在这个方法中主要是获取Spring容器中所有实现了_org.springframework.beans.factory.config.BeanPostProcessor接口的的实现类,然后循环调用postProcessBeforeInitialization_方法来加工正在创建的Bean
所以在这个方法中我们可以自定义_BeanPostProcessor_来扩展Bean的功能,实现自己的加工逻辑
3.4.3:调用Bean相关的初始化方法:
3.4.3.1 如果是InitializingBean则调用afterPropertiesSet方法
在这个流程中,Spring框架会判断正在创建的Bean是否实现了InitializingBean接口,如果实现了就会调用_afterPropertiesSet_方法来执行代码逻辑。
3.4.3.2 调用自定义初始化方法:initMethod
在这个流程中主要调用我们自定义的初始化方法,例如在xml文件中配置的_init-method和destory-method或者使用注解配置的@Bean(initMethod = “initMethod”, destroyMethod = “destroyMethod”)_ 方法
3.4.3.3:调用后置处理方法:applyBeanPostProcessorsAfterInitialization
在这个方法中主要是获取Spring容器中所有实现了_org.springframework.beans.factory.config.BeanPostProcessor接口的的实现类,然后循环调用postProcessAfterInitialization_来加工正在创建的Bean
在这个方法中我们可以自定义_BeanPostProcessor_来扩展Bean的功能,实现自己的加工逻辑
4:注册Bean销毁方法
在这里主要是注册Bean销毁时Spring回掉的方法例如:
1:xml文件中配置的destroy-method方法或者_@Bean注解中配置的destroyMethod_方法
2:_org.springframework.beans.factory.DisposableBean接口中的destory_方法
5:总结
到这里,从我们编写的Java类到Spring容器中可使用的Bean实例的创建过程就完整的梳理完成了,了解Bean的创建过程能够使我们更加熟悉Bean的使用方法,同时我们也可以在创建Bean的过程中新增自己的处理逻辑,从而实现将自己的组件接入Spring框架
6:Spring循环依赖的解决方法
Spring在创建Bean实例的时候,有时避免不了我们编写的Java类存在互相依赖的情况,如果Spring对这种互相依赖的情况不做处理,那么就会产生创建Bean实例的死循环问题,所以Spring对于这种情况必须特殊处理,下面就来探究Spring是如何巧妙处理Bean之间的循环依赖问题
6.1 暴露钩子方法getEarlyBeanReference
首先对于单实例类型的Bean来说,Spring在创建Bean的时候,会提前暴露一个钩子方法来获取这个正在创建中的Bean的地址引用,其代码如下:
如上面的代码所示,此时会在_singletonFactories这个Map中提前储存这个钩子方法singletonFactory_,从而能够提前对外暴露这个Bean的地址引用,那么为什么获取地址引用需要包装成复杂的方法呢?下面会解释
6.2 其他Bean获取提前暴露的Bean的地址引用
当其他Bean需要依赖正在创建中的Bean的时候,就会调用getSingleton方法来获取需要的Bean的地址引用
如上诉代码所示,在获取Bean的时候会从三个地方来获取
1:singletonObjects :这个是存放已经完全创建完成的Bean实例的Map
2:earlySingletonObjects :这个是存放用提前暴露的钩子方法创建好的Bean实例的Map
3:singletonFactories :这个是用来存放钩子方法的Map
当获取依赖的Bean的时候,就会调用钩子方法getEarlyBeanReference来获取提前暴露的Bean的引用,这个方法的源码如下:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;
}
如上面的源码所示,这个方法主要是需要调用SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法来提前处理一下尚未创建完成的Bean,而getEarlyBeanReference方法有逻辑的实现类只有一个**org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator,**这个类就是创建Aop代理的类,其代码如下:
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = this.getCacheKey(bean.getClass(), beanName);//提前标记这个bean已经创建过代理对象了this.earlyProxyReferences.put(cacheKey, bean);//按条件创建代理对象return this.wrapIfNecessary(bean, beanName, cacheKey);
}
如上面的代码所示,这段代码的主要目标就是判断提前暴露的Bean是否需要做动态代理,需要的话就会返回提前暴露的Bean的动态代理对象
那么这里为什么要去判断是否需要动态代理呢?考虑下面这种情况
1:如果这里不返回这个Bean的动态代理对象,但是这个Bean在后续的初始化流程中会存在动态代理:
举例:这里假设A依赖B,B又依赖A,此时B正在获取A提前暴露的引用,如果这时将A本身的地址引用返回给B,那么B里面就会保存A原始的地址引用,当B创建完成后,程序返回去创建A时,结果A在初始化的流程(initializingBean)中发生了动态代理,那么这时Spring容器中实际使用的是A的动态代理对象,而B却持有了原始A的引用,那么这时容器中就会存在A原始的引用以及A的动态代理的引用,从而产生歧义,这就是为什么需要提前去判断是否需要创建动态代理的原因,__这个原因的问题在于填充属性populateBean流程在初始化流程(initializingBean)之前,而创建动态代理的过程在初始化流程中
6.3 判断Bean的地址是否发生变化
Spring在Bean初始化之后,又判断了一下Bean初始化之后的地址是否发生了变化,其代码逻辑如下所示:
if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);//判断是否触发了提前创建bean的逻辑(getEarlyBeanReference)//如果有其他bean触发了提前创建bean的逻辑,那么这里就不为nullif (earlySingletonReference != null) {//判断引用地址是否发生了变化if (exposedObject == bean) {exposedObject = earlySingletonReference;}}
}
那么这里为什么需要在初始化之后继续判断Bean的地址是否发生了变化呢?
这是因为,如果存在循环依赖,同时Bean在初始化的流程(initializingBean)中又发生了额外的动态代理,例如,除了在**getEarlyBeanReference中发生的动态代理之外,还有额外的动态代理发生了,也就是发生了两次动态代理,那么这时Bean的地址与getEarlyBeanReference流程中产生的Bean的地址就不一样了,**这时如果不处理这种情况,又会出现Spring容器中同时存在两种不同的引用对象,又会造成歧义,所以Spring需要避免这种情况的存在
6.4 如果Bean地址发生变化则判断是否存在强依赖的Bean
Spring在Bean的创建过程中如果出现了上诉6.3节的情况时,Spring采取了下面的方法进行处理:
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {//获取该Bean依赖的BeanString[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);//去除因为类型检查而创建的Bean(doGetBean方法typeCheckOnly参数来控制)for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}//如果去除因为类型检查而创建的bean之外还存在依赖的bean//(这些剩下的bean就是spring实际需要使用的)那么就会抛出异常,阻止问题出现if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");}
}
上面这段代码就是Spring处理上诉情况的逻辑,首先明确的是Spring不允许上诉情况发生,Spring对于Bean的引用地址发生变化的情况,Spring首先会判断依赖的Bean是否已经完全创建完毕,如果存在完全创建完成的Bean就会直接抛出异常,因为这些完全创建完成的依赖Bean中持有的引用已经是旧地址引用了
具体的处理逻辑是:先拿到该Bean所有依赖的Bean,然后从这些Bean中排除仅仅是因为类型检查而创建的Bean,如果排除这些Bean之后,还有依赖的Bean,那么这些Bean就是可能存在循环依赖并且是强依赖的Bean(这些Bean中持有的引用地址是老地址,所以会存在问题),Spring就会及时抛出异常,避免发生问题
作者:京东物流 钟磊
来源:京东云开发者社区 自猿其说Tech 转载请注明来源
相关文章:

带着问题去分析:Spring Bean 生命周期 | 京东物流技术团队
1: Bean在Spring容器中是如何存储和定义的 Bean在Spring中的定义是_org.springframework.beans.factory.config.BeanDefinition_接口,BeanDefinition里面存储的就是我们编写的Java类在Spring中的元数据,包括了以下主要的元数据信息: 1&…...
C语言修行之函数篇(一)tolower —— 转换为小写字母
文章目录 函数说明函数声明函数返回值函数实现函数实例 函数说明 对于大写字母,如果在当前语言环境中存在小写表示形式,则tolower()返回其小写等效物。否则,tolower()函数执行相同的任务。 函数声明 #include <ctype.h> int tolower(…...

【JavaSE专栏55】Java集合类HashTable解析
🌲Java集合类HashTable解析 🌲Java集合类HashTable解析摘要引言Hashtable是什么?Hashtable vs. HashMap:何时使用Hashtable?多线程环境:历史遗留系统:不需要进行特殊操作: Hashtable…...

Apollo上机实践:一次对自动驾驶技术的亲身体验
上机实践 概述自动驾驶通信分布式系统开发模式开发工具 自动驾驶感知传感器特性感知流程及算法部署感知模型 自动驾驶决策规划决策规划流程和算法使用 Dreamview 进行控制在环的规划调试开发规划场景和算法 福利活动 主页传送门:📀 传送 概述 Apollo 是…...
QTcpServer简单的TCP服务器连接
1、简介 简单实现控制TCP服务器获取连接的套接字。点击断开服务器即可关闭所有连接,最大连接数量为5个。 声明源文件 #include "mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) {//设置固定大小setFixedSize(1024,600);b…...

LeetCode热题100——双指针
双指针 1.移动零2.盛最多水的容器3.三数之和 1.移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 // 题解:使用双指针,其中快指针指向非零元素,慢指针指向首个零元素下…...

Ubuntu ARMv8编译Qt源码以及QtCreator
最近需要在NVIDIA小盒子上面跑一个程序,一开始想着在Ubuntu x64下交叉编译一版,后来发现libqxcb.so 这个库在configure时就会一直报错,多方查找怀疑可能是由于硬件不支持在x64环境下编译AMR架构的xcb库。 所以最后在ARM下直接编译Qt源码了&am…...
虚机Centos忘记密码如何重置
1进入开机前的页面,选中第一个,按“e”键,进入编辑模式 2找到ro crashkernel项,将ro替换成 rw initsysroot/bin/sh 3 Ctrlx mount -o remount, rw / chroot /sysroot chroot /sysroot passwd root 输入两次密码 touch /.a…...
OpenGL_Learn02
1. 监听窗口,绑定回调函数 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream>void framebuffer_size_callback(GLFWwindow* window, int width, int height) {glViewport(0, 0, width, height);std::cout << "变了…...

基于STC系列单片机实现外部中断0控制按键调节定时器0产生PWM(脉宽调制)的功能
#define uchar unsigned char//自定义无符号字符型为uchar #define uint unsigned int//自定义无符号整数型为uint sbit PwmOut P1^0;//位定义脉宽调制输出为单片机P1.0脚 uchar PwmTimeCount;//声明脉宽调制时间计数变量 uchar PwmDutyCycle;//声明脉宽调制占空比变量 void Ti…...
vue3中 reactive和ref的区别
在Vue 3中,reactive和ref都是用于响应式数据的API。它们的主要区别在于使用方式和返回值类型。 reactive: reactive函数用于将一个对象转换为响应式对象。它接收一个普通的JavaScript对象,并返回一个被代理的响应式对象。这意味着当响应式对…...

docker的安装部署nginx和mysql
小白自己整理,如有错误请指示! 自我理解:docker就是把应用程序所用的依赖程序,函数库等相关文件打包成镜像文件,类似系统光盘,然后可以在任意电脑上安装使用(方便运维人员部署程序)…...

测试C#调用Aplayer播放视频(1:加载Aplayer控件)
微信公众号“Dotnet跨平台”的文章《开源精品,使用 C# 开发的 KTV 点歌项目》中使用了迅雷开源APlayer播放引擎。最近在学习有哪些能拿来播放视频的组件或控件,于是准备试试,根据文章中的介绍,在迅雷APlayer播放引擎网站中下载了A…...

二叉树的遍历+二叉树的基本操作
文章目录 二叉树的操作一、 二叉树的存储1.二叉树的存储结构 二、 二叉树的基本操作1.前置创建一棵二叉树:1. 定义结点 2.简单的创建二叉树 2.二叉数的遍历1.前序遍历2.中序遍历3.后序遍历4.层序遍历 3.二叉树的操作1.获取树中节点的个数2.获取叶子节点的个数3.获取…...

Go 语言gin框架的web
节省时间与精力,更高效地打造稳定可靠的Web项目:基于Go语言和Gin框架的完善Web项目骨架。无需从零开始,直接利用这个骨架,快速搭建一个功能齐全、性能优异的Web应用。充分发挥Go语言和Gin框架的优势,轻松处理高并发、大…...
Docker底层原理:Cgroup V2的使用
文章目录 检查 cgroup2 文件系统是否已加载检查系统是否已挂载 cgroup2 文件系统创建 cgroup2 层次结构查看 cgroup2 开启的资源控制类型启用 cgroup2 资源控制设置 cgroup2 资源限制加入进程到 cgroup2 检查 cgroup2 文件系统是否已加载 cat /proc/filesystems | grep cgroup…...

历年上午真题笔记(2014年)
解析:A 网络设计的三层模型 : 接入层:Layer 2 Switching,最终用户被许可接入网络的点,用户通过接入层可以访问网络设备。 汇聚层:Layer2/3 Switching,访问层设备的汇聚点,负责汇接配线单元,利用二、三层技术实现工作组分段及网络故障的隔离,以免对核心层网络设备造…...

数据库软考知识
分布式数据库透明性 封锁 加上共享锁之后只能加共享锁,加上排他锁之后,啥锁都不能加。 分布式数据库特性 伪传递定理 SQL函数定义,有点冷 来了奥,更冷 存储过程 很重要,下午第二大题也是数据库...

学习笔记|配对样本均数T检验|SPSS常用的快捷键|规范表达|《小白爱上SPSS》课程:SPSS第六讲 | 配对样本均数T检验
目录 学习目的软件版本原始文档配对样本均数T检验一、实战案例二、案例解析三、统计策略四、SPSS操作1、正态性检验2、配对样本T检验 五、结果解读六、规范报告1、规范表格2、规范文字 划重点Tips:SPSS常用的快捷键 学习目的 SPSS第六讲 | 配对样本均数T检验 软件版本 IBM S…...
python内置模块smtplib、email 发送电子邮件
一、简介 smtplib 是 Python 的标准库之一,用于发送电子邮件。它提供了一个简单的接口来连接到 SMTP(Simple Mail Transfer Protocol)服务器,并通过该服务器发送电子邮件。 email 是 Python 的标准库之一,用于处理电子…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
深度解析云存储:概念、架构与应用实践
在数据爆炸式增长的时代,传统本地存储因容量限制、管理复杂等问题,已难以满足企业和个人的需求。云存储凭借灵活扩展、便捷访问等特性,成为数据存储领域的主流解决方案。从个人照片备份到企业核心数据管理,云存储正重塑数据存储与…...

五、jmeter脚本参数化
目录 1、脚本参数化 1.1 用户定义的变量 1.1.1 添加及引用方式 1.1.2 测试得出用户定义变量的特点 1.2 用户参数 1.2.1 概念 1.2.2 位置不同效果不同 1.2.3、用户参数的勾选框 - 每次迭代更新一次 总结用户定义的变量、用户参数 1.3 csv数据文件参数化 1、脚本参数化 …...