Jenkins(Pipeline job)

文章目录

Pipeline job

Pipeline是什么

从某种抽象层次上讲,部署流水线(Deployment pipeline)是指从软件版本控制库到用户手中这一过程的自动化表现形式

Jenkins原本就支持pipeline,只不过最初该功能被称为"任务"

  • Jenkins 1.x仅支持界面手动配置流水线(Freestyle Job),而2.x则实现了"流水线即代码"(pipeline as a code)
  • 使用代码而非UI完成pipeline定义的意义在于
    • 更好地版本化:支持版本控制
    • 更好地协作:pipeline的修改对所有人可见,支持代码审查
    • 更好的可重用性

在Jenkins 2.x中,用于保存pipeline代码并可被Jenkins加载的文件称为Jenkinsfile

  • 流水线既可以在pipeline类型的任务中创建,也可以定义在一个称为Jenkinsfile的外部文件中,它可以同代码

    保存在一起

  • Jenkinsfile就是一个文本文件,它是部署流水线概念在Jenkins中的表现形式

流水线语法初步(脚本式、声明式)

Jenkins 2.x支持两种pipeline语法:脚本式语法声明式语法

  • 脚本式语法:没有固定关键字,纯用 Groovy 语法写,想加什么逻辑就加什么;

  • 声明式语法:格式固定,用 pipelineagentstagesstagesteps 这些固定关键字,像填表格一样写;

groovy 复制代码
//脚本式流水线: node用于脚本式流水线,从技术层面上来说
//它是一个步骤,代表可以用于流水线中执行活动的资源
node('node01') {
    stages {
        stage('Build') {
            steps {
                echo 'Building...'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing...'
            }
        }
    }
}



//声明式流水线:agent用于声明式流水线,它作为一个指令用于分配节点;
pipeline {
    agent {
        label 'node01'  // 节点标签,对应 Jenkins 中配置的节点名称
    }
    stages {
        stage('Build') {
            steps {
                echo 'Building...'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing...'
            }
        }
    }
}

Pipeline的组成

  • pipeline实际上就是基于Groovy语言实现的一种DSL(Domain-Specific Language),用于描述代码编译到打包发布的整个流水线是如何进行的
  • 一个标准pipeline流水线基本组成元素
元素名称 核心角色 是否必选 关键说明
pipeline 流水线最外层结构 包裹整条流水线的完整逻辑,是声明式 Pipeline 的固定入口,所有配置需嵌套其中
stages 所有功能阶段(stage)的容器 流水线中只允许有一个stages 用于统一管理多个 stage,确保流水线按阶段顺序执行(不可单独存在,需嵌套在 pipeline 内)
stage 流水线的独立功能阶段(如编译、测试、部署) 每个 stage 对应一个具体功能,需嵌套在 stages 内,可包含 steps、post 等子配置
steps 单个 stage 内的具体执行步骤集合 定义完成该阶段功能的操作(如 sh 执行 Shell 命令、echo 输出日志),与其他配置分隔
agent 指定流水线的执行位置 分配执行资源(物理机、虚拟机、容器等),通过 label 关联 Jenkins 节点(如 agent { label 'node01' }
post 流水线 / 单个 stage 执行完成后的附加步骤(如通知、清理) 唯一可省略的核心元素,但实际应用中建议配置(如执行成功 / 失败的告警、工作目录清理)

创建一个pipeline项目

创建pipeline项目时,将首先打开一个基于Web表单形式的新项目配置页面

  • 每个主要的配置部分都有一个对应的选项卡

  • 底部还会有一个用于输入pipeline code的文本框,这是Jenkins内置的流水线编辑器

Pipline基础及代码示例

groovy 复制代码
pipeline {
    agent any

    stages {
        stage('Hello') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

通过左边菜单中的"Build Now"便可触发pipeline的一次运行

  • 蓝色条纹:运行中
  • 白色:stage尚未执行
  • 红色条纹:steps执行失败
  • 绿色:stage执行成功
  • 浅红色:stage执行成功,但是下游某个stage出现失败

Pipline语法

声明式Pipeline的结构(区块、指令、步骤)

groovy 复制代码
pipeline{
    agent any
    stages{
        stage('<stage_name>'){
            steps{...}
        }
    }
}

Pipeline声明结构由区块指令 组成,每一个区块 又可包含其他的区块指令步骤 ,以及一些条件的定义

类型 核心定义 作用说明 具体示例
Section(区块) 用于将某一个时间点需一同运行的条目组织在一起 划分 Pipeline 的功能模块,明确不同逻辑单元的执行范围和关联关系 1. agent:指定运行代码的节点(如 agent { label 'node01' }) 2. stages:组织所有 stage(如 stages { stage('Build') { ... } }); 3. steps:组织具体执行步骤(如 steps { sh 'mvn package' }); 4. post:封装 stage/pipeline 执行后的步骤(如 post { success { echo '执行成功' } }
Directive(指令) 负责完成特定功能的语句或代码块 提供 Pipeline 的核心配置能力(如环境变量、工具依赖、触发条件等),支撑流程控制 1. environment:定义环境变量(如 environment { JAVA_HOME = '/usr/lib/jvm/java-11' }); 2. tools:指定工具及版本(如 tools { maven 'maven-3.8' }); 3. triggers:配置触发方式(如 triggers { cron('H 2 * * *') }); 4. input:设置人工确认步骤(如 input message: '是否继续部署?'); 5. when:定义 stage 执行条件(如 when { branch 'master' }
Steps(步骤) 标识特定 区块 的名称,内部可包含任何合法的 DSL 语句,是 Pipeline 的执行单元 承载具体业务操作,是 Pipeline 的核心执行逻辑(如代码拉取、编译、部署等) 1. git:拉取代码(git url: 'xxx.git', branch: 'master'); 2. sh/bat:执行 Shell/Batch 命令(sh 'docker build -t app:v1 .'); 3. echo:输出日志(echo '开始编译'); 4. junit:解析测试报告(junit 'target/surefire-reports/*.xml'

Pipeline支持的指令

Jenkins Pipeline支持指令主要有如下这些

指令名称 核心功能 使用范围 关键特性 实操示例(精简语法)
environment 定义环境变量,支持 credentials () 引用 Jenkins 凭证 pipeline/stage 级别 全局 / 阶段复用,凭证安全无明文 全局:APP_NAME='my-app'、DB_PWD=credentials ('db-cred');阶段:environment { BUILD_PATH='target' }
tools 指定 Maven/JDK/Git 等工具,自动加入 PATH 可直接调用 pipeline/stage 级别 需提前在 Jenkins "全局工具配置" 定义,支持阶段差异化版本 全局:tools {jdk 'jdk-11'};阶段:tools { maven '3.8.8' },步骤直接用:java -version
parameters 触发时用户输入参数(字符串 / 选择 / 布尔等) 仅 pipeline 级别 动态传参,支持多参数类型 parameters { choice('DEPLOY_ENV', ['dev','test','prod']);string('VERSION', 'v1.0') }
options 流水线全局配置(重试 / 超时 / 日志等),支持插件扩展 仅 pipeline 级别 控制运行规则,需对应插件(如 timestamps) options { retry(2);timeout(45, 'MINUTES');timestamps();disableConcurrentBuilds() }
triggers 自动触发(定时 / 代码轮询),与 Webhook 互补 仅 pipeline 级别 无需手动触发,Webhook 场景可省略 triggers { cron('H 3 * * *');pollSCM('H/30 * * * *') }
libraries 导入共享库,复用通用逻辑(部署脚本 / 工具类) 仅 pipeline 级别 需提前配置共享库,支持指定分支 / 版本 开头:@Library ('common-lib@main') _,步骤调用:deployToEnv (app:'my-app', env:'prod')
stage 封装功能阶段,包含 steps 及 when/input 等指令 仅 stages 段内 核心功能单元,支持嵌套 stage stage('Build&Test') { stages { stage('Compile') { sh 'mvn compile' } } }
input 暂停流水线,需用户输入 / 确认后继续(stage 专用) 仅 stage 级别 人工干预(如生产审核),支持超时配置 input (message:' 确认部署生产?', timeout:5, 'MINUTES'),之后执行:sh 'prod-deploy.sh'
when 设定 stage 运行条件,满足才执行(stage 专用) 仅 stage 级别 支持分支 / 参数等条件,可多条件组合 when { allOf { branch 'master';params.DEPLOY_ENV == 'prod' } }

Section: agent

参数类型 核心说明 使用场景 实操示例(精简语法)
any 匹配任意可用节点,是最常用的默认配置 无需指定特定节点,流水线可在任意节点执行 pipeline { agent any; stages { ... } }(pipeline 顶端默认配置)
none pipeline 顶端使用时,不定义默认 agent; 需为每个 stage 单独指定 agent 不同 stage 需在完全不同节点执行(无通用节点) pipeline { agent none; stages { stage('Build') { agent { label 'build' } ... } } }
label 匹配带有指定标签的节点(节点标签需提前在 Jenkins 配置) 需固定节点环境(如 "编译节点""部署节点") pipeline { agent { label 'build-node-01' }; ... }(全局)或 stage { agent { label 'deploy-node' } }(stage 单独指定)
node 功能同 label,额外支持指定自定义工作目录(customWorkspace) 需固定节点 + 自定义工作目录(如单独存储构建文件) agent { node { label 'test-node'; customWorkspace '/data/jenkins/workspace' } }
docker 在动态创建的容器中执行,需指定镜像;可匹配 label 节点或预配置容器节点 容器化部署、环境一致性要求高(避免节点依赖冲突) agent { docker { image 'maven:3.8.8'; label 'docker-node'; args '-v /tmp:/tmp' } }
dockerfile 基于指定 Dockerfile 构建镜像,再在容器中执行;要求 Jenkinsfile 从 SCM 加载 需自定义容器镜像(非公共镜像),多分支流水线场景 agent { dockerfile { filename 'Dockerfile'; dir './docker'; label 'build-node' } }
kubernetes 在 K8s 集群的 Pod 中执行;要求 Jenkinsfile 从 SCM 加载,需指定 Pod 模板 K8s 环境部署、微服务集群场景 agent { kubernetes { yaml '''apiVersion: v1; kind: Pod; spec: { containers: [{ name: 'maven', image: 'maven:3.8.8' }] }''' } }

官方文档:https://www.jenkins.io/doc/book/pipeline/syntax/

Section: post

post section在stage或pipeline的尾部定义一些step,并根据其所在stage或pipeline的完成情况来判定是否运行这些step;

Condition(条件) 触发规则 使用场景 实操示例(精简语法)
always 无论 stage/pipeline 状态如何(成功 / 失败 / 中止),必运行 清理资源、通用通知 pipeline 级:post {always { sh 'rm -rf /tmp/build/*'} };stage 级:post { always { echo "Test 阶段结束" } }
changed 本次与前一次运行状态不同(如前败→今成、前成→今败) 状态变更告警 post {changed { echo "流水线状态变更!前次 vs 本次不同"} }
fixed 本次成功,前一次为失败(failed)或不稳定(unstable) 故障恢复通知 post {fixed { echo "构建恢复成功!前次失败已修复"} }
regression 前一次成功,本次为失败 / 不稳定 / 中止 构建回退告警 post {regression { echo "构建回退!前次成功,本次异常"} }
aborted 运行被手动中止(Web UI 点击 "中止") 中止记录、通知负责人 stage ('Deploy') { post { aborted { echo "部署被中止,负责人:${BUILD_USER}" } } }
failure 运行状态为失败(脚本报错、命令执行失败) 失败告警、上报故障 post {failure { sh'send-alert.sh "流水线失败,构建号:${BUILD_NUMBER}"' } }
success 运行状态为成功(无报错,步骤正常完成) 成功通知、成果归档 post { success { sh 'cp target/app.war /data/archive/' } }
unstable 测试失败 / 代码冲突,状态为 "不稳定"(Web UI 黄色标识) 不稳定告警、提醒修复 post {unstable { echo "流水线不稳定!存在测试失败 / 代码冲突"} }
unsuccessful 非成功状态(失败 + 不稳定 + 中止,等价于!success) 非成功统一处理(记录日志) post {unsuccessful { echo "构建异常,日志路径:${WORKSPACE}/logs" } }
cleanup 所有 post 条件执行完毕后,最后运行(无论任何状态) 最终清理(关容器、删临时文件) post {success { echo "构建成功"}; cleanup { sh 'docker stop test-container' } }

Section: stages和steps

核心概念 核心作用 关键说明
stages 封装 pipeline 主体和逻辑的所有 stage 定义,描述 pipeline 中绝大部分实际工作 至少需包含一个 stage 指令,用于定义 CD 过程的离散部分(如构建、测试、部署等)
steps 在 stage 中定义一到多个 DSL 语句,完成该 stage 特定功能,是 pipeline 最核心的组成部分 1. 除 script 外,几乎是不可拆分的原子操作; 2. 内置大量 step,参考https://www.jenkins.io/doc/pipeline/steps; 3. 部分插件可直接当作 step 使用; 4. script {} 可引入脚本(非必要),复杂脚本应组织为 Shared Libraries 并导入使用; 5. DSL 语句可与 environment 等其他语句分隔开

官方文档:https://www.jenkins.io/doc/pipeline/steps/

Pipeline内置的常用step

文件 / 目录相关

step 名称 核心作用 关键说明
deleteDir 删除当前目录 无额外参数,直接执行删除操作
dir("/path/to/dir") 切换到指定目录 参数为目标目录路径,用于指定后续操作的工作目录
fileExists ("/path/to/dir") 判断文件或目录是否存在 参数为目标文件 / 目录路径,返回布尔值结果
isUnix 判断当前系统是否为类 Unix 系统 无参数,返回布尔值结果
pwd 打印当前工作目录 无参数,输出当前所在的工作目录路径
writeFile 将指定内容写入目标文件 参数包括:file(文件路径,支持相对 / 绝对路径)、text(待写入内容)、encoding(目标文件编码,可选,空值为系统默认,支持 base64)
readFile 读取指定文件的内容 参数包括:file(文件路径,支持相对 / 绝对路径)、encoding(读取时使用的编码格式,可选)

消息或控制

step 名称 核心作用 关键说明
echo("message") 打印指定的消息 参数为待打印的消息内容
error("message") 主动报错,并中止当前 pipeline 参数为报错消息内容
retry(count){} 重复执行指定次数的代码块 参数 count 为执行次数,{} 内为待重复执行的代码块
sleep 让 pipeline 休眠一段时间 参数包括:time(整数值,休眠时长)、unit(时间单位,可选,支持 NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS)
timeout 限制代码块的超时时长 参数包括:time(整数值,超时时长)、unit(时间单位,可选,支持 NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS)、activity (optional)(布尔类型,true 时无日志活动才算超时)
waitUntil 等待指定条件满足后执行代码块 参数包括:initialRecurrencePeriod (optional)(初始重试周期,默认 250ms)、quiet (optional)(是否禁止条件测试日志,默认 false,记入日志),{} 内为待执行代码块

发送通知

step 名称 核心作用 关键说明
mail 向指定邮箱发送邮件 参数包括:subject(邮件标题)、body(邮件正文)、from (optional)(发件人地址列表,逗号分隔)、cc (optional)(抄送地址列表,逗号分隔)、bcc (optional)(密送地址列表,逗号分隔)、charset (optional)(编码格式)、mimeType (optional)(正文 MIME 类型,默认 text/plain)、replyTo (optional)(回件地址,默认 Jenkins 全局配置邮箱)

Pipline的常用步骤

step 名称 核心作用 关键说明
sh 运行 shell 脚本 参数包括: ◆ 如果有多个命令需要执行可以使用sh ''' 命令 ''' ◆ script {}:脚本代码块,支持指定脚本解释器(如 "#!/usr/bin/perl"),未指定则使用系统默认解释器,且默认启用 - xe 选项; ◆ encoding (optional):脚本输出日志的编码格式,未定义时使用系统默认; ◆ label (optional):Web UI 中显示的详细描述信息; ◆ returnStdout (optional):布尔型,true 时标准输出作为 step 返回值(不打印到日志),错误仍记入日志; ◆ returnStatus (optional):布尔型,true 时返回 step 执行结果而非状态码,命令执行失败也不返回非零状态码
bat 执行 Windows 的批处理脚本 无额外参数说明,直接运行批处理命令或脚本
node 在指定的节点上运行后续脚本 参数为目标节点标识,用于指定 Pipeline 后续操作的执行节点
ws 为 Pipeline 分配工作空间 无额外参数说明,用于指定或分配 Pipeline 执行的工作目录空间
powershell 运行指定的 PowerShell 脚本 支持 Microsoft PowerShell 3 及以上版本
pwsh 运行 PowerShell Core 脚本 专门用于执行 PowerShell Core 相关脚本

stages和stage

维度 具体内容
核心作用 Pipeline 中最重要的 section,描述绝大部分实际工作,定义 CD 过程的离散部分(如构建、测试、部署等)
运行顺序 按照内部定义的顺序自下而后执行各个 stage
嵌套规则 1. 单个 stage 内部可嵌套 stages {}:内部 stage 以串行(顺序) 方式运行; 2. 单个 stage 内部可嵌套 parallel {}:内部 stage 以并行方式运行
注意事项 1. stage 内部仅能定义 steps、stages、parallel 或 matrix 四者之一; 2. 多层嵌套仅能用于最后一个 stage; 3. 已嵌套在 parallel 或 matrix 内部的 stage,不可再嵌套 parallel 或 matrix; 4. 上述嵌套场景下的 stage,仍可使用 agent、tools、when 等指令,也可嵌套 stages {} 以顺序运行子 stage

Pipeline代码片段生成器

指令配置段生成器能够帮助用户生成配置指令,包括agent、stages等

Pipline应用

在Pipline Job上使用环境变量

核心维度 具体内容
变量分类 1. 内置变量(Jenkins 自带); 2. 用户自定义变量(用户手动定义)
定义指令 统一使用 environment 指令,定义位置决定作用域
作用域规则 1. 定义在 pipeline{} 顶部:作用域为整个 Pipeline,所有 stage 均可引用; 2. 定义在 stage{} 内部:作用域仅为当前 stage,其他 stage 无法引用; 3. Jenkins 全局环境变量:作用域为所有 Pipeline,默认以 env. 为前缀
全局变量引用格式 1. ${env.<ENV_VAR_NAME>}(推荐,最规范); 2. $env.<ENV_VAR_NAME>;3. ${ENV_VAR_NAME}(简洁,常用)
groovy 复制代码
pipeline {
	agent any
	environment {
		APP_NAME = 'my-app'
		BUILD_INFO = "构建编号: ${env.BUILD_NUMBER} | 分支: ${env.GIT_BRANCH}"
	}
	stages{
		stage('测试全局变量'){
			steps{
				echo "引用自定义全局变量: ${APP_NAME}"
				echo "引用内置全局变量(格式1): ${env.BUILD_URL}"
				echo "引用内置全局变量(格式2): $env.JOB_NAME"
				echo "拼接全局变量: ${BUILD_INFO}"
			}
		}
		stage('测试stage局部变量'){
			environment{
				STAGE_TAG = 'test-001'
			}
			steps {
                echo "引用当前stage局部变量:${STAGE_TAG}"
                echo "当前stage仍可引用全局变量:${APP_NAME}"
			}
		}
	}
}

执行结果

在Pipline Job 的step中使用认证凭据

凭据类型 关键参数 / 说明
Username with password(用户名 + 密码) 脚本式:credentialsId(凭据 ID)、usernameVariable(用户名变量)、passwordVariable(密码变量) 声明式:变量值自动格式化为 用户名:密码,直接用于需要组合认证的场景
SSH 密钥 1.脚本式:keyFileVariable(私钥文件路径)、passphraseVariable(密钥口令)、usernameVariable(SSH 用户名) sshagent():直接传入 credentialsId 列表,自动管理 SSH 代理(推荐)
Secret text(秘文) credentialsIdvariable(秘文变量名) 适用于 API 令牌、机器人 Token 等单一字符串秘钥
Secret file(秘文文件) credentialsIdvariable(秘文文件路径变量) 适用于证书、kubeconfig、配置文件等文件类秘钥

Git拉取Gitlab代码(SSH密钥)

git拉取操作官方文档https://www.jenkins.io/doc/pipeline/steps/git/#git-git

groovy 复制代码
pipeline {
	agent any  //任意Jenkins节点都能跑
	environment{
		//仓库地址
		GIT_URL = "git@gitlab.chenshiquan.xyz:root/docker-java-hello.git"
		//引用内置变量,构建号
		BUILD_INFO = "这次构建编号是: ${BUILD_NUMBER}"
	}
	stages{
		//步骤1.拉取代码
		stage('拉代码'){
			steps{
				echo "${BUILD_INFO}" //打印构建号
				git(
					url: "${GIT_URL}", //URL
					branch: "master", //拉取主分支代码
					credentialsId: "71751bf3-bc66-48b1-8d7a-7ff7c0ee5c71" //凭据ID
				)
				echo "代码拉取完成!"
			}
		}
		//步骤2: 简单构建
		stage('构建'){
			steps{
				echo "工作目录是: ${env.WORKSPACE}" //工作目录(内置变量)
				sh "echo 构建成功" //使用简单shell命令模拟构建成功
			}
		}
	}
}

Docker登陆Harbor仓库(用户名+密码)

将凭据绑定为变量文档www.jenkins.io/doc/pipeline/steps/credentials-binding/

场景:Docker 登录 Harbor 仓库

groovy 复制代码
pipeline {
	agent any
	environment{
		HARBOR_URL = 'harbor.chenshiquan.xyz'
	}
	stages{
		stage('登陆Harbor仓库'){
			steps{
				withCredentials([
					usernamePassword(
                        //将账号设置为变量
						usernameVariable: 'HARBOR_USER',
                        //将密码绑定为变量
						passwordVariable: 'HARBOR_PWD',
                        //凭证ID用于判断是哪个凭证
						credentialsId: 'harbor-key'
						)
					]){
					//密码自动隐藏,日志不会泄露
					sh "docker login -u ${HARBOR_USER} -p ${HARBOR_PWD} ${HARBOR_URL}"
				}
				echo "Harbor登陆成功"
			}
		}
	}
}

调用钉钉机器人发送通知(密钥文本)

https://jenkinsci.github.io/dingtalk-plugin/guide/pipeline.html

Secret text(密钥文本)

场景:调用钉钉机器人发送通知

groovy 复制代码
pipeline {
    agent any
    stages {
        stage('钉钉通知') {
            steps {
                withCredentials([string(
                    credentialsId: 'dingtalk-robot-token', //凭证ID
                    variable: 'DING_TOKEN' //凭证TOKEN
                )]) {
                    //调用API发送消息
                    sh '''
                        curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=${DING_TOKEN}" \
                        -H "Content-Type: application/json" \
                        -d '{"msgtype":"text","text":{"content":"hello!"}}'
                    '''
                    //content发送消息的内容
                }
                echo "钉钉通知已发送!"
            }
        }
    }
}

凭证优雅用法

场景:整合凭据到环境变量,全局复用

groovy 复制代码
pipeline {
    agent any
    // 直接在environment中定义凭据变量,全局可用
    environment {
        DING_TOKEN = credentials('dingtalk-robot-token') // Secret text类型
        HARBOR_CRED = credentials('harbor-key') // Username with password类型(变量为"用户名:密码"格式)
    }
    stages {
        stage('测试全局凭据变量') {
            steps {
                // 钉钉通知(直接使用环境变量)
                    sh '''
                        curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=${DING_TOKEN}" \
                        -H "Content-Type: application/json" \
                        -d '{"msgtype":"text","text":{"content":"hello!测试全局变量"}}'
                    '''
            }
        }
    }
    post {
        success {
            echo "凭据变量自动掩码:${DING_TOKEN}" // 输出时自动显示为****
        }
    }
}

在Pipline Job中构建和推送Docker image

事前准备(必须先搞定这 3 件事)

准备项 作用 简单要求(一看就懂)
Docker 环境 构建、推送镜像必须依赖 Jenkins 运行的机器(agent)上装了 Docker(能跑 docker --version
Docker 仓库(Harbor) 存放推送的镜像 有可用的仓库地址(比如 hub.magedu.com),且有登录账号密码
项目 Dockerfile 构建镜像的 "配方" 项目根目录下有 Dockerfile(不然没法 docker build

Pipeline核心步骤

阶段名 核心操作 说明
Source(拉代码) git 拉取项目代码 从 Git 仓库把代码拉到 Jenkins 工作目录
Build(编译) mvn 打包 Java 项目 生成可运行的 jar 包(适配 Spring Boot 项目)
Test(测试) mvn test 执行测试 可选(不想测试可以删掉这个阶段)
Docker Build docker build 构建镜像 用 Dockerfile 把项目打包成镜像,打上标签
Docker Push 登录 Harbor + 推送镜像 先登录仓库,再把镜像推上去(需要账号密码凭据)

案例

  • 替换 GitRepo 为你的项目 Git 地址
  • 替换 HarborServer 为你的 Harbor 地址(比如 你的仓库地址.com
  • 替换 credentialsId: 'harbor-user-cred' 为你 Jenkins 里的 Harbor 账号密码凭据 ID
groovy 复制代码
pipeline {
    agent any // Jenkins 任意节点都能跑
    //自定义变量
    environment {
        GitRepo = "git@gitlab.chenshiquan.xyz:root/docker-java-hello.git" //GitLab地址
        HarborServer = "harbor.chenshiquan.xyz" // Harbor地址
        ImageName = "library/docker-java-hello" // 镜像名称
        DING_TOKEN = credentials('dingtalk-robot-token')
        ImageTag = "v1.0"
    }
    stages {
        // 步骤1:拉取代码
		stage('拉代码'){
			steps{
				deleteDir()  //清理工作空间缓存
				git(
					url: "${GitRepo}", //URL
					branch: "master", //拉取主分支代码
					credentialsId: "71751bf3-bc66-48b1-8d7a-7ff7c0ee5c71" //凭据ID
				)
				echo "代码拉取完成!"
			}
		}

        // 步骤2:构建 Docker 镜像
        stage('构建镜像') {
            steps {
                // 构建镜像并打标签:仓库地址/镜像名:标签
                sh "docker build -t ${HarborServer}/${ImageName}:${ImageTag} ."
            }
        }

        // 步骤4:推送镜像到 Harbor
        stage('推送镜像') {
            steps {
                // 用凭据登录 Harbor
                withCredentials([
                    usernamePassword(
                        credentialsId: 'harbor-key', //harbor凭据ID
                        usernameVariable: 'HARBOR_USER', // 用户名变量
                        passwordVariable: 'HARBOR_PASS'  // 密码变量
                    )
                ]) {
                    // 1. 登录 Harbor(日志里密码会显示成 ****,安全)
                    sh "docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} ${HarborServer}"
                    // 2. 推送镜像
                    sh "docker push ${HarborServer}/${ImageName}:${ImageTag}"
                    // 3. 登出
                    sh "docker logout ${HarborServer}"
                }
            }
        }
    }
    post {
        success {
            // 构建成功通知:包含项目、Git标签、镜像地址
            sh '''
                curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=${DING_TOKEN}" \
                -H "Content-Type: application/json" \
                -d '{
                    "msgtype":"text",
                    "text":{
                        "content":"hello! 构建成功!\n项目名称:${JOB_NAME}\n镜像地址:'${HarborServer}'/'${ImageName}':'${ImageTag}'\n构建编号:'${BUILD_NUMBER}'"
                    }
                }'
            '''
            echo "构建成功!钉钉通知已发送"
        }
        failure {
            // 构建失败通知:提示检查日志
            sh '''
                curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=${DING_TOKEN}" \
                -H "Content-Type: application/json" \
                -d '{
                    "msgtype":"text",
                    "text":{
                        "content":"hello! 构建失败!\n项目名称:${JOB_NAME}\n构建编号:'${BUILD_NUMBER}'\n请登录Jenkins检查日志"
                    }
                }'
            '''
            echo "构建失败!钉钉通知已发送"
        }
    }
}

配套的简单 Dockerfile(项目根目录下新建)

dockerfile 复制代码
FROM harbor.chenshiquan.xyz/library/maven:3-eclipse-temurin-11-alpine AS build
ENV CODE_DIR=/app/code
ARG NAME=test
ENV pord_NAME=${NAME}
WORKDIR ${CODE_DIR}
COPY settings.xml /usr/share/maven/conf/
COPY  docker-java-hello  ./
RUN  mvn clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
  -Dsonar.projectKey=${pord_NAME} \
  -Dsonar.projectName=${pord_NAME} \
  -Dsonar.host.url=http://10.0.0.123:9000 \
  -Dsonar.sources=. \
  -Dsonar.token=squ_6ec3aa500b872c85e62e5705d5fd87e0bea32e4d

FROM harbor.chenshiquan.xyz/library/tomcat:9.0-jdk8 
LABEL author=csq destcation="tomcat war包"
COPY --from=build /app/code/target/*.war webapps/ROOT.war
EXPOSE 8080
CMD ["catalina.sh","run"]

参数化Pipeline

参数类型 用途(我能用它干嘛) 语法模板(直接抄) 关键说明
string(字符串) 传单行文本(比如环境名、版本号) parameters { string(name: '参数名', defaultValue: '默认值', description: '说明') } 最常用,比如传 v1.0.0dev 这类短文本
text(多行文本) 传多行内容(比如配置文件、脚本) parameters { text(name: '参数名', defaultValue: '第一行\n第二行', description: '说明') } 支持换行,比如传多行的 nginx.conf 配置
booleanParam(布尔) 选 "是 / 否"(比如是否发布、是否调试) parameters { booleanParam(name: '参数名', defaultValue: true, description: '说明') } 只有两个选项:true(是)/false(否),引用时是字符串类型
choice(下拉选项) 从固定选项里选(比如分支、环境) parameters { choice(name: '参数名', choices: ['选项1', '选项2'], description: '说明') } 避免手动输错,比如固定可选分支 main/test
password(密码) 传敏感密码 / 令牌(隐藏显示) parameters { password(name: '参数名', defaultValue: '默认密码', description: '说明') } 输入和日志中都会隐藏(显示 ****),安全

使用变量拉取master分支v1.0标签

groovy 复制代码
pipeline {
    agent any
    // 变量集中定义,修改直接改这里
    environment {
        GIT_URL = "git@gitlab.chenshiquan.xyz:root/docker-java-hello.git" // SSH 仓库地址
        GIT_CRED_ID = "71751bf3-bc66-48b1-8d7a-7ff7c0ee5c71" // SSH 私钥凭据ID(替换为你的实际ID)
        TARGET_BRANCH = "master" // 标签所属分支
        TARGET_TAG = "v1.0" // 要拉取的标签
    }
    stages {
        stage('拉取 master 分支的 v1.0 标签代码(含子模块)') {
            steps {
                echo "拉取配置:仓库=${GIT_URL},分支=${TARGET_BRANCH},标签=${TARGET_TAG}"
                checkout scmGit(
                    branches: [[name: "refs/tags/${TARGET_TAG}"]], // 标签完整引用(必选)
                    extensions: [
                        // 仅保留子模块核心配置(递归拉取 + 复用主仓库SSH凭据)
                        submodule(
                            parentCredentials: true, // 子模块复用主仓库SSH凭据
                            recursiveSubmodules: true, // 递归拉取所有子模块
                            reference: ''
                        )
                    ],
                    userRemoteConfigs: [[
                        url: "${GIT_URL}",
                        credentialsId: "${GIT_CRED_ID}", // 必须是 SSH 私钥类型凭据
                        // SSH 协议无需额外配置,Jenkins 会自动用凭据中的私钥认证
                    ]]
                )

                // 可选验证:确保标签属于 master 分支(避免拉错同名标签)
                script {
                    sh "git branch -r --contains refs/tags/${TARGET_TAG} | grep -w origin/${TARGET_BRANCH} || error '标签 ${TARGET_TAG} 不属于 ${TARGET_BRANCH} 分支!'"
                }

                echo "代码拉取完成!当前版本:${TARGET_TAG}(含所有子模块)"
            }
        }
    }
}

单个参数示例

=示例 1:string(字符串参数)------ 传递部署环境

groovy 复制代码
pipeline {
    agent any
    // 定义参数:部署环境(默认 dev)
    parameters {
        string(
            name: 'DEPLOY_ENV', // 参数名(引用时用这个)
            defaultValue: 'dev', // 默认值(不填时自动用这个)
            description: '请输入部署环境(比如 dev/test/prod)' // 提示用户怎么填
        )
    }
    stages {
        stage('打印参数') {
            steps {
                // 引用参数:用 ${params.参数名}(必写 params.)
                echo "本次部署的环境是:${params.DEPLOY_ENV}"
            }
        }
    }
}

//运行效果
//构建时会弹出输入框,默认显示 dev,可改成 test,构建日志会打印 本次部署的环境是:test

示例 2:choice(下拉选项)------ 选择构建分支

groovy 复制代码
pipeline {
    agent any
    // 定义参数:下拉选择分支
    parameters {
        choice(
            name: 'BUILD_BRANCH',
            choices: ['master', 'test', 'dev'], // 固定选项(用户只能选,不能输)
            description: '请选择要构建的分支'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "正在拉取分支:${params.BUILD_BRANCH}"
                git branch: "${params.BUILD_BRANCH}", url: "git@gitlab.chenshiquan.xyz:root/docker-java-hello.git"
            }
        }
    }
}

示例 3:booleanParam(布尔参数)------ 是否发布版本

groovy 复制代码
pipeline {
    agent any
    // 定义参数:是否发布(默认不发布)
    parameters {
        booleanParam(
            name: 'IS_PUBLISH',
            defaultValue: false, // 默认 false(不发布)
            description: '是否将构建结果发布到生产环境(是选true,否选false)'
        )
    }
    stages {
        stage('判断是否发布') {
            steps {
                echo "是否发布:${params.IS_PUBLISH}" // 输出 true 或 false(字符串类型)
                // 条件判断:如果是 true,执行发布命令
                script {
                    if (params.IS_PUBLISH == 'true') { // 注意:布尔参数引用后是字符串!
                        echo "开始发布到生产环境..."
                        // sh "发布脚本" // 实际发布命令写这里
                    } else {
                        echo "仅构建,不发布"
                    }
                }
            }
        }
    }
}

示例 4:password(密码参数)------ 传递 Harbor 密码

groovy 复制代码
pipeline {
    agent any
    // 定义参数:Harbor 密码(隐藏显示)
    parameters {
        password(
            name: 'HARBOR_PASS',
            defaultValue: '', // 密码默认空,让用户手动输
            description: '请输入 Harbor 仓库的登录密码(输入时隐藏)'
        )
    }
    stages {
        stage('登录 Harbor') {
            steps {
                withCredentials([
                    usernamePassword(
                        credentialsId: 'harbor-user', // Jenkins 里的用户名凭据(只存用户名)
                        usernameVariable: 'HARBOR_USER'
                    )
                ]) {
                    // 密码用参数 ${params.HARBOR_PASS},日志中会显示 ****
                    sh "docker login -u ${HARBOR_USER} -p ${params.HARBOR_PASS} hub.magedu.com"
                }
            }
        }
    }
}

//运行效果:
//输入密码时显示圆点 / 星号,日志中密码会被隐藏,避免泄露

示例 5:text(多行文本)------ 传递配置文件内容

groovy 复制代码
pipeline {
    agent any
    // 定义参数:多行配置(比如 nginx 配置)
    parameters {
        text(
            name: 'NGINX_CONFIG',
            defaultValue: 'server {\n  listen 80;\n  server_name localhost;\n}', // 换行用 \n
            description: '请粘贴 nginx 配置文件内容(支持多行)'
        )
    }
    stages {
        stage('生成配置文件') {
            steps {
                // 将多行参数内容写入文件
                sh "echo '${params.NGINX_CONFIG}' > /etc/nginx/conf.d/default.conf"
                echo "已生成 nginx 配置文件"
            }
        }
    }
}

//运行效果:
//用户可以粘贴多行配置,Pipeline 会自动生成对应的配置文件。

多参数综合示例

下面的 Pipeline 包含 4 种参数,模拟 "选择分支→输入版本号→是否调试→输入密码" 的完整流程:

groovy 复制代码
pipeline {
    agent any
    // 多个参数一起定义(顺序不影响)
    parameters {
        // 1. 下拉选择分支
        choice(
            name: 'BRANCH',
            choices: ['master', 'test', 'dev'],
            description: '请选择构建分支'
        )
        // 2. 输入版本号(字符串)
        string(
            name: 'VERSION',
            defaultValue: 'v1.0',
            description: '请输入版本号'
        )
        // 3. 是否开启调试(布尔)
        booleanParam(
            name: 'DEBUG',
            defaultValue: false,
            description: '是否开启调试模式(true=开启,false=关闭)'
        )
        // 4. 输入数据库密码(密码类型)
    }
    stages {
        stage('构建前准备') {
            steps {
                echo "===== 构建参数汇总 ====="
                echo "分支:${params.BRANCH}"
                echo "版本号:${params.VERSION}"
                echo "调试模式:${params.DEBUG}"
            }
        }
        stage('拉取代码') {
            steps {
                git branch: "${params.BRANCH}", url: "git@gitlab.chenshiquan.xyz:root/java-hello.git"
            }
        }
        stage('构建打包') {
            steps {
                // 根据调试模式决定是否加调试参数
                script {
                    if (params.DEBUG == 'true') {
                        sh "mvn clean package -Ddebug" // 开启调试
                    } else {
                        sh "mvn clean package" // 正常构建
                    }
                }
            }
        }
        stage('部署') {
            steps {
                echo "部署 ${params.VERSION} 版本到 ${params.BRANCH} 分支对应的环境..."
            }
        }
    }
}


//运行效果:
//构建时会依次显示参数输入界面(下拉框 + 输入框 + 勾选框 + 密码框),用户填写后,Pipeline 会根据参数执行不同逻辑。

实现交互式功能

核心功能 支持的输入参数类型 变量作用域 关键特点 / 语法说明
运行时暂停,等待用户手动输入 string(字符串)、choice(下拉)、booleanParam(布尔)等(同 parameters 指令) 1. 局部:仅当前 Stage 2. 全局:所有 Stage 1. 需包裹在 script{} 中(声明式 Pipeline 要求) 2. 用变量接收输入结果,格式为 Map(键 = 参数名,值 = 用户输入)
限定审批人 / 审批组 (属于 input 自身配置) submitter: 'admin,dev组'(仅指定用户 / 组可审批)
记录审批人账号 (属于 input 自身配置) 同输入参数 submitterParameter: 'APPROVER'(自动存储审批人账号到 Map 中)
优雅全局复用 所有支持类型 所有 Stage 变量声明在 environment{} 中,或 Pipeline 外部 def 变量

示例 1:局部作用域(单参数)------ 仅当前 Stage 可用

场景:构建过程中暂停,让用户输入备注,仅当前 Stage 能引用该输入。

groovy 复制代码
pipeline {
    agent any
    stages {
        stage('用户输入参数') {
            steps {
                script {
                    def userInput = input(
                        message: '请填写构建相关参数',
                        parameters: [
                            string(name: 'REMARK', defaultValue: '常规构建', description: '构建备注'),
                            string(name: 'VERSION_DESC', defaultValue: 'V1.0.0', description: '版本说明')
                        ]
                    )
                    
                    // 多参数存入环境变量
                    env.BUILD_REMARK = userInput.REMARK
                    env.VERSION_DESC = userInput.VERSION_DESC
                    
                    echo "构建备注:${env.BUILD_REMARK}"
                    echo "版本说明:${env.VERSION_DESC}"
                }
            }
        }
        
        stage('跨Stage复用参数') {
            steps {
                echo "跨Stage引用 - 备注:${env.BUILD_REMARK}"
                echo "跨Stage引用 - 版本:${env.VERSION_DESC}"
            }
        }
    }
}

//运行效果:
//1. 构建到 "用户输入备注" Stage 时,会暂停,弹出输入框;
//2. 用户输入 "修复支付 bug",点击 "确定" 后,Pipeline 继续运行;
//3. 日志打印 "本次构建备注:修复支付 bug",下一个 Stage 无法引用该变量。

示例 2:全局作用域(多参数)------ 跨 Stage 可用

场景:部署前让用户选择目标环境、输入自定义参数,后续所有 Stage 都能引用这些输入(用 Pipeline 外部的 def 变量存储)。

groovy 复制代码
// 关键:在pipeline{}外部声明变量(全局作用域,所有Stage可引用)
def approvalInfo

pipeline {
    agent any
    stages {
        stage('审批与输入参数') {
            steps {
                script {
                    // input接收多参数,结果存入全局变量approvalInfo
                    approvalInfo = input(
                        message: '请审批并填写部署参数', // 弹窗标题
                        ok: '确认提交', // 弹窗确认按钮文字
                        submitter: 'admin,deploy组', // 仅指定用户/组能审批(可选)
                        submitterParameter: 'APPROVER', // 自动记录审批人账号,参数名=APPROVER
                        parameters: [
                            // 第一个参数:下拉选择目标环境
                            choice(
                                name: 'DEPLOY_ENV',
                                choices: ['dev(开发)', 'test(测试)', 'prod(生产)'],
                                description: '请选择部署环境(生产环境需谨慎)'
                            ),
                            // 第二个参数:字符串输入自定义备注
                            string(
                                name: 'CUSTOM_PARAM',
                                defaultValue: '',
                                description: '可选:输入额外部署参数(比如版本号、配置项)'
                            )
                        ]
                    )
                }
            }
        }
        stage('执行部署') {
            steps {
                echo "===== 部署信息汇总 ====="
                echo "审批人:${approvalInfo['APPROVER']}" // 引用审批人(自动记录)
                echo "目标环境:${approvalInfo['DEPLOY_ENV']}" // 引用下拉选择的环境
                echo "自定义参数:${approvalInfo['CUSTOM_PARAM']}" // 引用输入的字符串
                echo "开始部署到 ${approvalInfo['DEPLOY_ENV']} 环境..."
                // sh "部署脚本 --env ${approvalInfo['DEPLOY_ENV']}" // 实际部署命令
            }
        }
    }
}


//运行效果:
//1. 构建到 "审批与输入参数" Stage 时暂停,弹窗显示下拉框 + 输入框;
//2. 只有 admin 或 "deploy 组" 用户能看到 "确认提交" 按钮,普通用户无法操作;
//3. 用户选择 "test(测试)",输入 "v2.0.1",提交后,"执行部署" Stage 能正常引用所有参数。

示例 3:优雅方式(用环境变量存储 input 参数)

场景:把 input 输入的参数存入 environment 变量,全局可用,语法更简洁(无需在 Pipeline 外部声明 def)。

groovy 复制代码
pipeline {
    agent any
    // 环境变量:存储input参数(全局可用)
    environment {
        // 先声明环境变量,初始值为空
        DEPLOY_INFO = ''
    }
    stages {
        stage('获取部署参数') {
            steps {
                script {
                    // input结果存入环境变量DEPLOY_INFO(Map类型)
                    DEPLOY_INFO = input(
                        message: '填写部署参数',
                        parameters: [
                            booleanParam(
                                name: 'IS_ROLLBACK',
                                defaultValue: false,
                                description: '是否回滚(true=回滚,false=正常部署)'
                            ),
                            string(
                                name: 'ROLLBACK_VERSION',
                                defaultValue: 'v1.0.0',
                                description: '若回滚,输入回滚版本'
                            )
                        ]
                    )
                }
            }
        }
        stage('判断部署类型') {
            steps {
                script {
                    // 引用环境变量中的input参数(env.可省略)
                    if (DEPLOY_INFO['IS_ROLLBACK'] == 'true') {
                        echo "执行回滚,目标版本:${DEPLOY_INFO['ROLLBACK_VERSION']}"
                        // sh "回滚脚本 --version ${DEPLOY_INFO['ROLLBACK_VERSION']}"
                    } else {
                        echo "执行正常部署,不回滚"
                        // sh "部署脚本"
                    }
                }
            }
        }
    }
}

//运行效果
//1. 暂停时用户勾选 "是否回滚" 为 true,输入回滚版本 "v1.9.8";
//2. 后续 Stage 通过 `DEPLOY_INFO['参数名']` 引用,无需关心变量声明位置,语法更统一。

输入等待超时

input步骤与timeout步骤协同使用,可实现超时自动中止pipeline,以避免无限等待

参数名称 参数说明(新手友好) 必填性 取值示例
time 超时等待时长(整数) 1(分)、30(秒)、2(时)
unit 时间单位(默认分钟) SECONDS(秒)、MINUTES(分)、HOURS(时)、DAYS(天)
activity 按 "无活动" 计时(默认绝对时长) true(用户没操作才计时)、false(到点就超时)

示例 1:基础版(简单确认 + 1 分钟超时)

场景:暂停等待用户点击确认,1 分钟没操作自动失败。

groovy 复制代码
pipeline {
    agent any
    stages {
        stage('等待确认') {
            steps {
                echo "1分钟内未点击确认,Pipeline将自动失败"
                // 超时配置:1分钟(unit默认MINUTES,可省略)
                timeout(time: 1, unit: 'MINUTES') {
                    // 简单输入:仅确认按钮
                    input message: '是否继续执行下一步?'
                }
                echo "用户已确认,继续构建~"
            }
        }
    }
}

//运行效果
//1 分钟内点 "确认":打印后续日志,正常执行;
//1 分钟未操作:Pipeline 失败,日志提示 "Timeout waiting for input"。

示例 2:带参数版(选择环境 + 3 分钟超时)

场景:部署前让用户选环境,3 分钟超时自动中止,适合需要输入参数的场景。

groovy 复制代码
pipeline {
    agent any
    stages {
        stage('部署审批') {
            steps {
                script {
                    echo "请3分钟内选择部署环境并确认"
                    // 超时配置:明确单位为分钟(更清晰,避免歧义)
                    timeout(time: 3, unit: 'MINUTES') {
                        def deployEnv = input(
                            message: '选择部署环境(3分钟超时)',
                            parameters: [
                                choice(
                                    name: 'ENV', // 参数名(仅用于输入框标识,返回值不是Map)
                                    choices: ['dev', 'test', 'prod'],
                                    description: '只能选这3个环境'
                                )
                            ],
                            ok: '确认部署'
                        )
                        // 直接使用 deployEnv(字符串值),无需加 .ENV
                        echo "已选择部署环境:${deployEnv}"
                        
                        // 可选:存入环境变量,支持后续Stage引用
                        env.DEPLOY_ENV = deployEnv
                    }
                }
            }
        }
        
        // 可选:测试跨Stage引用部署环境
        stage('执行部署') {
            steps {
                echo "开始部署到 ${env.DEPLOY_ENV} 环境..."
                // 后续部署命令(如部署脚本、容器启动等)
                // sh "./deploy.sh ${env.DEPLOY_ENV}"
            }
        }
    }
}

//运行效果:

//3 分钟内选环境 + 确认:打印环境名称,继续部署;
//3 分钟未操作:直接失败,不执行部署。

示例 3:自定义超时行为(2 分钟超时不失败,跳过步骤)

场景:非关键步骤,2 分钟超时后不失败,仅跳过该步骤。

groovy 复制代码
pipeline {
    agent any
    stages {
        stage('可选测试步骤') {
            steps {
                script {
                    echo "2分钟内未确认,将跳过测试步骤"
                    // catchError:捕获超时错误,不中断整体构建
                    catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                        timeout(time: 2, unit: 'MINUTES') {
                            input message: '是否执行额外测试?'
                            echo "执行额外测试..."
                            sh "echo 测试脚本运行中"
                        }
                    }
                }
            }
        }
        stage('核心构建') {
            steps {
                echo "核心构建步骤不受超时影响,继续执行~"
            }
        }
    }
}

//运行效果:
//2 分钟内确认:执行测试步骤,核心构建正常;
//2 分钟超时:测试步骤标记为 "不稳定",直接执行核心构建,整体构建状态为成功。
相关推荐
代码不停2 小时前
BFS解决拓扑排序和FloodFill问题
java·算法·宽度优先
Chengbei112 小时前
CVE-2025-24813 Tomcat 最新 RCE 分析复现
java·安全·web安全·网络安全·tomcat·系统安全·网络攻击模型
AAA简单玩转程序设计2 小时前
救命!Java 进阶居然还在考这些“小儿科”?
java·前端
总是学不会.2 小时前
【JUC编程】多线程学习大纲
java·后端·开发
MediaTea2 小时前
思考与练习(第十章 文件与数据格式化)
java·linux·服务器·前端·javascript
7澄12 小时前
Maven 项目拆分与聚合实战:分层架构下的多模块开发
java·架构·maven·service·dao·pojo·数据库连接
一起养小猫2 小时前
LeetCode100天Day4-盛最多水的容器与两数之和II
java·数据结构·算法·leetcode
ZBritney2 小时前
JAVA中的多线程
java