【Jenkins】Pipeline 语法解析(声明式Pipeline)
文章目录
Declarative Pipeline在Pipeline子系统之上提供了一种更简化和规范化的语法。
所有有效的Declarative Pipeline必须包含在pipeline块中,例如:
pipeline {
/* 在此处插入Declarative Pipeline */
}
在Declarative Pipeline中,有效的基本语句和表达式遵循与Groovy语法相同的规则,但有以下例外:
- Pipeline的顶层必须是一个块,具体为:pipeline { }。
- 不使用分号作为语句分隔符。每个语句必须单独占据一行。
- 块只能由Sections、Directives、Steps或赋值语句组成。
- 属性引用语句被视为无参数方法调用。因此,例如,input被视为input()。
一、Sections
声明式Pipeline中的部分通常包含一个或多个指令或步骤
1、agent(代理)
agent指定整个Pipeline或特定stage将在Jenkins环境中执行的位置,具体取决于agent的位置。
该部分必须在pipeline块的顶层定义,但stage级别的使用是可选的。
pipeline {
agent any
options {
// Timeout counter starts AFTER agent is allocated
timeout(time: 1, unit: 'SECONDS')
}
stages {
stage('Example1') {
steps {
echo 'Hello World'
}
}
}
stages {
stage('Example2') {
agent any // 接stage中定义
steps {
echo 'Hello World'
}
}
}
}
agent 参数值
agent支持几种不同类型的参数。这些参数可以应用于pipeline块的顶层,也可以应用于每个阶段指令中。
下面列出一些常用参数:
any
在任何可用的代理上执行Pipeline或阶段。例如:agent any
none
当应用于pipeline块的顶层时,不会为整个Pipeline运行分配全局代理,并且每个阶段部分都需要包含自己的agent部分。例如:agent none
label
使用提供的标签在Jenkins环境中可用的代理上执行Pipeline或阶段。例如:agent { label ‘my-defined-label’ }
也可以使用标签条件:例如:agent { label ‘my-label1 && my-label2’ } 或 agent { label ‘my-label1 || my-label2’ }
node
agent { node { label ‘labelName’ } } 与 agent { label ‘labelName’ } 相同,但node允许使用其他选项(例如customWorkspace)。
2、post
post部分定义了一个或多个额外的步骤,在Pipeline或阶段运行完成后运行(取决于post部分在Pipeline中的位置)。post可以支持以下任何后置条件块:always
、changed
、fixed
、regression
、aborted
、failure
、success
、unstable
、unsuccessful
和cleanup
。这些条件块允许根据Pipeline或阶段的完成状态执行每个条件内的步骤。条件块按照下面的顺序执行。
always
无论Pipeline或阶段的运行完成状态如何,都运行post部分中的步骤。
changed
仅当当前Pipeline的运行完成状态与上一次运行不同时,才运行post部分中的步骤。
fixed
仅当当前Pipeline的运行成功且上一次运行失败或不稳定时,才运行post部分中的步骤。
regression
仅当当前Pipeline的运行状态为失败、不稳定或中止,并且上一次运行成功时,才运行post部分中的步骤。
aborted
仅当当前Pipeline的运行状态为"aborted"时才运行post部分中的步骤,通常是由于手动中止Pipeline导致的。在Web界面中通常以灰色表示。
failure
仅当当前Pipeline或阶段的运行状态为"failed"时才运行post部分中的步骤,通常在Web界面中以红色表示。
success
仅当当前Pipeline或阶段的运行状态为"success"时才运行post部分中的步骤,通常在Web界面中以蓝色或绿色表示。
unstable
仅当当前Pipeline的运行状态为"unstable"时才运行post部分中的步骤,通常由测试失败、代码违规等引起。在Web界面中通常以黄色表示。
unsuccessful
仅当当前Pipeline或阶段的运行状态不为"success"时才运行post部分中的步骤。在Web界面中根据前面提到的状态进行表示(对于阶段,如果构建本身不稳定,可能会触发此条件)。
cleanup
在评估了其他所有后置条件后,无论Pipeline或阶段的状态如何,都运行此后置条件中的步骤。
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
post {
always {
echo 'I will always say Hello again!'
}
}
}
3、stages(阶段)
包含一个或多个stage指令的序列,stages部分是Pipeline描述的“工作”的主要部分所在的位置。至少,建议stages包含至少一个stage指令,例如构建、测试和部署。
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}
4、steps(步骤)
steps定义了在给定的stage指令中要执行的一组一个或多个步骤。
二、Directives 指令
1、environment 环境变量
environment
指令指定了一组键值对,这些键值对将根据environment
指令在Pipeline中的位置,定义为所有步骤或特定阶段的步骤的环境变量。
该指令支持一个特殊的辅助方法credentials()
,它可以用于通过Jenkins环境中的标识符访问预定义的凭据。
credentials()
支持的凭据类型:
Secret Text
指定的环境变量将设置为Secret Text的内容。
Secret File
指定的环境变量将设置为临时创建的File文件的位置。
Username and password
指定的环境变量将设置为username:password
,并自动定义两个附加的环境变量:MYVARNAME_USR
和MYVARNAME_PSW
。
SSH with Private Key
指定的环境变量将设置为临时创建的SSH密钥文件的位置,并自动定义两个附加的环境变量:MYVARNAME_USR
和MYVARNAME_PSW
(包含密码)。
pipeline {
agent any
environment {
CC = 'clang'
}
stages {
stage('Example Username/Password') {
environment {
SERVICE_CREDS = credentials('my-predefined-username-password')
}
steps {
sh 'echo "Service user is $SERVICE_CREDS_USR"'
sh 'echo "Service password is $SERVICE_CREDS_PSW"'
sh 'curl -u $SERVICE_CREDS https://myservice.example.com'
}
}
stage('Example SSH Username with private key') {
environment {
SSH_CREDS = credentials('my-predefined-ssh-creds')
}
steps {
sh 'echo "SSH private key is located at $SSH_CREDS"'
sh 'echo "SSH user is $SSH_CREDS_USR"'
sh 'echo "SSH passphrase is $SSH_CREDS_PSW"'
}
}
}
}
2、options 配置选项
options
指令允许从Pipeline内部配置特定于Pipeline的选项。Pipeline提供了一系列选项,例如buildDiscarder
,也可以由插件提供,例如timestamps
。
可用的选项 options
buildDiscarder
保留最近运行的特定数量的Pipeline的构件和控制台输出。例如:options { buildDiscarder(logRotator(numToKeepStr: '1')) }
checkoutToSubdirectory
在工作区的一个子目录中执行自动源代码控制检出。例如:options { checkoutToSubdirectory('foo') }
disableConcurrentBuilds
禁止并发执行Pipeline。可以防止同时访问共享资源等。例如:options { disableConcurrentBuilds() }
将构建排队,当Pipeline中已经有一个正在执行的构建时,或 options { disableConcurrentBuilds(abortPrevious: true) }
以中止正在运行的构建并开始新的构建。
disableResume
如果控制器重新启动,则不允许管道恢复。例如:options { disableResume() }
newContainerPerStage
与docker或dockerfile顶级代理一起使用。指定时,每个阶段将在同一节点上的新容器实例中运行,而不是所有阶段在同一容器实例中运行。
overrideIndexTriggers
允许覆盖分支索引触发器的默认处理。如果在多分支或组织标签处禁用分支索引触发器,options { overrideIndexTriggers(true) }
将仅为此作业启用它们。否则,options { overrideIndexTriggers(false) }
将仅为此作业禁用分支索引触发器。
preserveStashes
保留已完成构建的存储库,以用于阶段重新启动。例如:options { preserveStashes() }
以保留最近完成的构建的存储库,或 options { preserveStashes(buildCount: 5) }
以保留最近完成的五个构建的存储库。
quietPeriod
设置Pipeline的静默时间(以秒为单位),覆盖全局默认值。例如:options { quietPeriod(30) }
retry
在失败时,重试整个Pipeline指定的次数。例如:options { retry(3) }
skipDefaultCheckout
在agent指令中默认跳过从源代码控制中检出代码。例如:options { skipDefaultCheckout() }
skipStagesAfterUnstable
一旦构建状态变为UNSTABLE,则跳过阶段。例如:options { skipStagesAfterUnstable() }
timeout
设置Pipeline运行的超时时间,在此之后Jenkins应中止Pipeline。例如:options { timeout(time: 1, unit: 'HOURS') }
timestamps
在Pipeline运行生成的所有控制台输出前面加上该行发出的时间。例如:options { timestamps() }
parallelsAlwaysFailFast
为管道中的所有后续并行阶段设置failfast为true。例如:options { parallelsAlwaysFailFast() }
disableRestartFromStage
完全禁用经典Jenkins UI和Blue Ocean中可见的“从阶段重新启动”选项。例如:options { disableRestartFromStage() }
。此选项不能在阶段内部使用。
pipeline {
agent any
options {
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('Example1') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
echo 'Hello World'
}
}
stage('Example2') {
steps {
echo 'Hello World'
}
}
}
}
3、parameters 参数
parameters
指令提供了在触发Pipeline时用户应提供的参数列表。这些用户指定的参数的值可以通过params对象在Pipeline步骤中使用。
每个参数都有一个名称和值,具体取决于参数类型。当构建开始时,这些信息会作为环境变量导出,允许构建配置的后续部分访问这些值。例如,在像bash
和ksh
这样的POSIX shell
中使用${PARAMETER_NAME}
语法,在PowerShell
中使用${Env:PARAMETER_NAME}
语法,在Windows的cmd.exe
中使用%PARAMETER_NAME%
语法。
可用的参数 Parameters
string
一个字符串类型的参数,例如:parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
。
text
一个文本参数,可以包含多行,例如:parameters { text(name: 'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description: '') }
。
booleanParam
一个布尔类型的参数,例如:parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
。
choice
一个选择参数,例如:parameters { choice(name: 'CHOICES', choices: ['one', 'two', 'three'], description: '') }
。第一个值是默认值。
password
一个密码参数,例如:parameters { password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password') }
。
pipeline {
agent any
parameters {
string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
text(name: 'BIOGRAPHY', defaultValue: '', description: 'Enter some information about the person')
booleanParam(name: 'TOGGLE', defaultValue: true, description: 'Toggle this value')
choice(name: 'CHOICE', choices: ['One', 'Two', 'Three'], description: 'Pick something')
password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'Enter a password')
}
stages {
stage('Example') {
steps {
echo "Hello ${params.PERSON}"
echo "Biography: ${params.BIOGRAPHY}"
echo "Toggle: ${params.TOGGLE}"
echo "Choice: ${params.CHOICE}"
echo "Password: ${params.PASSWORD}"
}
}
}
}
4、triggers 触发器
triggers
指令定义了Pipeline应该重新触发的自动化方式。目前可用的触发器有cron、pollSCM和upstream。
cron
接受一个cron风格的字符串来定义Pipeline应该重新触发的定期间隔,例如:triggers { cron('H */4 * * 1-5') }
pollSCM
接受一个cron风格的字符串来定义Jenkins应该检查新的源代码更改的定期间隔。如果存在新的更改,Pipeline将重新触发。例如:triggers { pollSCM('H */4 * * 1-5') }
upstream
接受一个逗号分隔的作业字符串和一个阈值。当字符串中的任何作业以最小阈值完成时,Pipeline将重新触发。例如:triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }
// Declarative //
pipeline {
agent any
triggers {
cron('H */4 * * 1-5')
}
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}
Jenkins cron 语法
Jenkins的cron语法遵循cron实用程序的语法(有一些小的差异)。具体而言,每行由5个由TAB或空格分隔的字段组成:
- 分钟,MINUTE,0-59
- 小时,HOUR,0-23
- 月份中的日期,DOM,-31
- 月份,MONTH,1-12
- 星期几,DOW,0-7(0和7表示星期日)
为了在一个字段中指定多个值,可以使用以下运算符。按优先顺序排列:
- * 表示所有有效值
- M-N 表示一个值范围
- M-N/X 或 */X 表示以X为间隔在指定范围或整个有效范围内进行步进
- A,B,…,Z 枚举多个值
为了使定时任务在系统上产生均匀的负载,应尽可能使用符号H
(代表“哈希”)。例如,对于十几个每天执行的作业,使用0 0 * * *
将在午夜产生大量的负载峰值。相比之下,使用H H * * *
仍然每天执行每个作业一次,但不会同时执行,更好地利用有限的资源。
H
符号可以与范围一起使用。例如,H H(0-7) * * *
表示在凌晨12:00到上午7:59之间的某个时间。您还可以在H
中使用步进间隔,可以有范围也可以没有范围。
H
符号可以被视为范围内的随机值,但实际上它是作业名称的哈希值,而不是随机函数,因此对于任何给定的项目,该值保持稳定。
请注意,在月份的日期字段中,短周期(例如*/3
或H/3
)在大多数月份的末尾可能无法保持一致,这是由于月份长度的变化。例如,*/3
将在长月份的1日、4日、……31日运行,然后在下个月的下一天再次运行。哈希值始终选择在1-28范围内,因此H/3
将在月底产生3到6天之间的运行间隔。较长的周期也会有不一致的长度,但影响可能相对较不明显。
空行和以#
开头的行将被忽略为注释。
此外,还支持@yearly
、@annually
、@monthly
、@weekly
、@daily
、@midnight
和@hourly
作为别名。这些别名使用哈希系统进行自动平衡。例如,@hourly
与H * * * *
相同,表示在整点的任何时间可能执行。@midnight
实际上表示在凌晨12:00 AM和2:59 AM之间的某个时间。
每隔十五分钟执行一次(可能在:07、:22、:37、:52)
triggers{ cron('H/15 * * * *') }
每隔十分钟在每小时的前半部分执行一次(三次,可能在:04、:14、:24)
triggers{ cron('H(0-29)/10 * * * *') }
每隔两个小时的45分钟执行一次,从上午9:45开始,每个工作日结束于下午3:45。
triggers{ cron('45 9-16/2 * * 1-5') }
在工作日的上午9点到下午5点之间的每两个小时时间段内执行一次(可能在上午10:38、下午12:38、下午2:38、下午4:38)
triggers{ cron('H H(9-16)/2 * * 1-5') }
每个月的1日和15日执行一次,除了12月
triggers{ cron('H H 1,15 1-11 *') }
5、stage(阶段)
stage
指令位于stages部分,并应包含一个steps部分、一个可选的agent部分或其他特定于阶段的指令。实际上,Pipeline执行的所有实际工作都将包含在一个或多个stage指令中。
6、tools(工具)
tools
指令定义了自动安装并放置在PATH上的工具。如果指定了agent none,则会忽略这个部分。
支持的tools
- maven
- jdk
- gradle
工具名称必须在Jenkins的“管理Jenkins”→“工具”下进行预配置
7、input (输入)
在一个stage上使用input指令可以提示输入。在应用任何选项之后,在进入该阶段的agent块或评估该阶段的when条件之前,该阶段将暂停。如果输入被批准,阶段将继续执行。作为输入提交的任何参数将在整个阶段的环境中可用。
构建过程中由用户输入相关数据,以进行下一步操作(不常用)
8、when
when
指令允许Pipeline根据给定的条件确定是否应执行该阶段。when
指令必须包含至少一个条件。如果when
指令包含多个条件,所有子条件必须返回true才能执行该阶段。这与将子条件嵌套在allOf
条件中的效果相同(参考下面的示例)。如果使用anyOf
条件,请注意,一旦找到第一个“true”条件,条件将跳过剩余的测试。
可以使用嵌套条件构建更复杂的条件结构:not
、allOf
或anyOf
。嵌套条件可以嵌套到任意深度。
内置when条件
branch
当正在构建的分支与给定的分支模式(ANT样式路径通配符)匹配时执行该阶段,例如:when { branch 'master' }
。请注意,这仅适用于多分支流水线。
可选参数比较器可以在属性后面添加,以指定如何评估匹配的任何模式:
EQUALS
用于简单的字符串比较GLOB
(默认值)用于ANT样式路径通配符(与例如changeset
相同)REGEXP
用于正则表达式匹配
例如:when { branch pattern: "release-\\d+", comparator: "REGEXP"}
buildingTag
当构建正在构建一个标签时执行该阶段。例如:when { buildingTag() }
changelog
如果构建的SCM更改日志包含给定的正则表达式模式,则执行该阶段,例如:when { changelog '.*^\\[DEPENDENCY\\] .+$' }
changeset
如果构建的SCM更改集包含一个或多个与给定模式匹配的文件,则执行该阶段。示例:when { changeset "**/*.js" }
可选参数比较器可以在属性后面添加,以指定如何评估匹配的任何模式:
EQUALS
用于简单的字符串比较GLOB
(默认值)用于ANT样式路径通配符(不区分大小写,可以使用caseSensitive参数关闭)。REGEXP
用于正则表达式匹配
例如:when { changeset pattern: ".TEST\\.java", comparator: "REGEXP" } 或 when { changeset pattern: "*/*TEST.java", caseSensitive: true }
changeRequest
如果当前构建是一个“变更请求”(在GitHub和Bitbucket上称为Pull Request,在GitLab上称为Merge Request,在Gerrit上称为Change等),则执行该阶段。当没有传递参数时,该阶段在每个变更请求上运行,例如:when { changeRequest() }。
通过在变更请求中添加带有参数的filter属性,可以使该阶段仅在匹配的变更请求上运行。可能的属性有id、target、branch、fork、url、title、author、authorDisplayName
和authorEmail
。每个属性对应一个CHANGE_*
环境变量,例如:when { changeRequest target: 'master' }。
可选参数比较器可以在属性后面添加,以指定如何评估匹配的任何模式:
EQUALS
用于简单的字符串比较(默认值)GLOB
用于ANT样式路径通配符(与changeset
相同)REGEXP
用于正则表达式匹配
示例:when { changeRequest authorEmail: "[\\w_-.]+@example.com", comparator: 'REGEXP' }
environment
当指定的环境变量设置为给定值时执行该阶段,例如:when { environment name: 'DEPLOY_TO', value: 'production' }
equals
当期望值等于实际值时执行该阶段,例如:when { equals expected: 2, actual: currentBuild.number }
expression
当指定的Groovy表达式求值为true时执行该阶段,例如:when { expression { return params.DEBUG_BUILD } }
tag
如果TAG_NAME变量与给定的模式匹配,则执行该阶段。例如:when { tag "release-*" }
如果提供了空模式,则该阶段将在TAG_NAME
变量存在时执行(与buildingTag()
相同)。
可选参数比较器可以在属性后面添加,以指定如何评估匹配的任何模式:
EQUALS
用于简单的字符串比较(默认值)GLOB
用于ANT样式路径通配符(与changeset
相同)REGEXP
用于正则表达式匹配
例如:when { tag pattern: "release-\\d+", comparator: "REGEXP"}
not
当嵌套条件为false时执行该阶段。必须包含一个条件。例如:when { not { branch 'master' } }
allOf
当所有嵌套条件都为true时执行该阶段。必须至少包含一个条件。例如:when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
anyOf
当至少一个嵌套条件为true时执行该阶段。必须至少包含一个条件。例如:when { anyOf { branch 'master'; branch 'staging' } }
triggeredBy
当当前构建由给定的参数触发时执行该阶段。例如:
when { triggeredBy 'SCMTrigger' }
when { triggeredBy 'TimerTrigger' }
when { triggeredBy 'BuildUpstreamCause' }
when { triggeredBy cause: "UserIdCause", detail: "vlinde" }
多个条件
pipeline {
agent any
stages {
stage('Example Build') {
steps {
echo 'Hello World'
}
}
stage('Example Deploy') {
when {
branch 'production'
environment name: 'DEPLOY_TO', value: 'production'
}
steps {
echo 'Deploying'
}
}
}
}
嵌套条件
pipeline {
agent any
stages {
stage('Example Build') {
steps {
echo 'Hello World'
}
}
stage('Example Deploy') {
when {
expression { BRANCH_NAME ==~ /(production|staging)/ }
branch 'production'
anyOf {
environment name: 'DEPLOY_TO', value: 'production'
environment name: 'DEPLOY_TO', value: 'staging'
}
}
steps {
echo 'Deploying'
}
}
}
}
四、Parallel(并行)
声明式Pipeline里面可以包含Parallel
并行部分,Parallel
可以包含多个并行的stage。
此外,您可以通过将failFast true
添加到包含并行项的阶段中,强制所有并行阶段在其中任何一个失败时都被中止。
添加failfast的另一种方法是向pipeline定义中添加一个选项:parallelsAlwaysFailFast()
。
Parallel Stages,failFast true
pipeline {
agent any
stages {
stage('Non-Parallel Stage') {
steps {
echo 'This stage will be executed first.'
}
}
stage('Parallel Stage') {
when {
branch 'master'
}
failFast true
parallel {
stage('Branch A') {
agent {
label "for-branch-a"
}
steps {
echo "On Branch A"
}
}
stage('Branch B') {
agent {
label "for-branch-b"
}
steps {
echo "On Branch B"
}
}
stage('Branch C') {
agent {
label "for-branch-c"
}
stages {
stage('Nested 1') {
steps {
echo "In stage Nested 1 within Branch C"
}
}
stage('Nested 2') {
steps {
echo "In stage Nested 2 within Branch C"
}
}
}
}
}
}
}
}
parallelsAlwaysFailFast
pipeline {
agent any
options {
parallelsAlwaysFailFast()
}
stages {
stage('Non-Parallel Stage') {
steps {
echo 'This stage will be executed first.'
}
}
stage('Parallel Stage') {
when {
branch 'master'
}
parallel {
stage('Branch A') {
agent {
label "for-branch-a"
}
steps {
echo "On Branch A"
}
}
stage('Branch B') {
agent {
label "for-branch-b"
}
steps {
echo "On Branch B"
}
}
stage('Branch C') {
agent {
label "for-branch-c"
}
stages {
stage('Nested 1') {
steps {
echo "In stage Nested 1 within Branch C"
}
}
stage('Nested 2') {
steps {
echo "In stage Nested 2 within Branch C"
}
}
}
}
}
}
}
}
五、script(脚本)
script
步骤接受一个Scripted Pipeline的代码块,并在Declarative Pipeline中执行该代码块。对于大多数用例,Declarative Pipeline中不需要使用script步骤,但它可以提供一个有用的“逃生口”。如果脚本块的大小和/或复杂性较大,应将其移入共享库中。
Scripted Pipeline
Scripted Pipeline和Declarative Pipeline一样,都是基于底层的Pipeline子系统构建的。与Declarative不同,Scripted Pipeline实际上是使用Groovy构建的通用DSL。Groovy语言提供的大多数功能都可以供Scripted Pipeline的用户使用,这意味着它可以是一个非常表达力和灵活的工具,用于编写持续交付流水线。
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
script {
def browsers = ['chrome', 'firefox']
for (int i = 0; i < browsers.size(); ++i) {
echo "Testing the ${browsers[i]} browser"
}
}
}
}
}
}
六、Flow Control(流程控制)
Scripted Pipeline从Jenkinsfile的顶部向下顺序执行,就像Groovy或其他语言中的大多数传统脚本一样。因此,提供流程控制依赖于Groovy表达式,例如if/else
条件语句,例如:
node {
stage('Example') {
if (env.BRANCH_NAME == 'master') {
echo 'I only execute on the master branch'
} else {
echo 'I execute elsewhere'
}
}
}
Scripted Pipeline的另一种流程控制方式是使用Groovy的异常处理支持。当步骤由于任何原因失败时,它们会抛出异常。在错误处理中,必须使用Groovy中的try/catch/finally
块,例如:
node {
stage('Example') {
try {
sh 'exit 1'
}
catch (exc) {
echo 'Something failed, I should sound the klaxons!'
throw
}
}
}
参考
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!