共享库
共享库(Shared Libraries)是 Jenkins Pipeline 的代码复用机制,它允许将通用的 Pipeline 逻辑封装成可复用的模块,供多个项目使用。通过共享库,团队可以遵循 DRY(Don't Repeat Yourself)原则,减少代码重复,提高构建脚本的可维护性。
为什么需要共享库?
在没有共享库之前,每个项目都需要维护自己的 Jenkinsfile。当构建流程发生变化时,需要逐一更新所有项目的 Jenkinsfile,这不仅耗时,而且容易出错。
共享库解决了这些问题:
代码复用:将通用的构建逻辑封装成函数或类,多个项目共享同一份实现。
集中维护:当构建流程需要修改时,只需更新共享库,所有使用该库的项目自动获得更新。
标准化:团队可以定义标准的构建流程模板,确保所有项目遵循统一的规范。
版本控制:共享库代码存储在独立的 Git 仓库中,支持版本管理和发布。
共享库目录结构
一个标准的共享库仓库包含以下目录结构:
shared-library/
├── src/ # Groovy 源代码目录
│ └── org/
│ └── company/
│ └── Utils.groovy # 工具类
│ └── Constants.groovy # 常量定义
│
├── vars/ # 全局变量目录
│ ├── buildJava.groovy # 自定义步骤
│ ├── buildJava.txt # 步骤文档
│ ├── deploy.groovy
│ ├── deploy.txt
│ └── notify.groovy
│
├── resources/ # 资源文件目录
│ └── org/
│ └── company/
│ └── templates/
│ └── deployment.yaml
│
└── README.md
src 目录
src 目录存放 Groovy 类文件,遵循标准的 Java 包结构。这些类可以被 Pipeline 直接导入和使用。
// src/org/company/Utils.groovy
package org.company
class Utils implements Serializable {
def steps
Utils(steps) {
this.steps = steps
}
def mvn(String args) {
steps.sh "mvn ${args}"
}
def notifySlack(String message, String color = 'good') {
steps.slackSend(
color: color,
message: message
)
}
static def getVersion() {
return new Date().format('yyyyMMdd-HHmmss')
}
}
注意:存储状态的类必须实现 Serializable 接口,因为 Pipeline 可能会在 Agent 之间传递对象。
vars 目录
vars 目录存放全局变量定义,每个 .groovy 文件对应一个全局变量,可以在 Pipeline 中直接调用。这是最常用的共享库使用方式。
// vars/buildJava.groovy
def call(Map config) {
// 默认配置
def defaults = [
jdkVersion: '11',
mavenVersion: '3.8',
goals: 'clean package',
skipTests: false
]
// 合并配置
config = defaults << config
pipeline {
agent any
tools {
jdk "JDK-${config.jdkVersion}"
maven "Maven-${config.mavenVersion}"
}
stages {
stage('Build') {
steps {
sh "mvn ${config.goals} ${config.skipTests ? '-DskipTests' : ''}"
}
}
}
}
}
resources 目录
resources 目录存放非 Groovy 资源文件,如模板文件、配置文件等。可以通过 libraryResource 步骤加载。
// 在 Pipeline 中加载资源
def template = libraryResource 'org/company/templates/deployment.yaml'
配置共享库
全局共享库配置
在 Jenkins 系统配置中添加全局共享库:
- 导航到 Manage Jenkins → System
- 找到 Global Trusted Pipeline Libraries 部分
- 点击 Add 添加新库
配置项:
Name: my-shared-library # 库名称,Pipeline 中引用
Source Code Management:
Git:
Project Repository: https://github.com/org/shared-library.git
Credentials: github-credentials
Default Version: main # 默认版本(分支、标签或 commit)
Load implicitly: false # 是否自动加载(不推荐)
Allow default version override: true # 是否允许 Pipeline 指定版本
Include @Library changes in build recent changes: true
Trusted: false # 是否为可信库
文件夹级别共享库
可以为特定文件夹配置共享库,仅在该文件夹内的 Pipeline 中可用:
- 进入文件夹配置页面
- 找到 Pipeline Libraries 部分
- 添加库配置
可信库与非可信库
可信库(Trusted):可以执行任意 Groovy 代码,包括系统操作、文件 I/O 等。只有具有 Overall/RunScripts 权限的用户才能配置可信库。
非可信库:在 Groovy 沙箱中运行,只能使用 Pipeline 允许的操作。适合允许开发者自行维护的库。
使用共享库
@Library 注解
使用 @Library 注解在 Pipeline 开头加载共享库:
// 加载最新版本
@Library('my-shared-library') _
// 加载指定版本
@Library('[email protected]') _
// 加载特定分支
@Library('my-shared-library@feature/new-feature') _
// 加载多个库
@Library(['my-shared-library', 'another-library@main']) _
// 加载并导入类
@Library('my-shared-library')
import org.company.Utils
注意:@Library 注解必须放在 Pipeline 脚本的开头,在 pipeline 块之前。
library 步骤
使用 library 步骤在构建过程中动态加载共享库:
pipeline {
agent any
stages {
stage('Load Library') {
steps {
script {
// 动态加载库
library 'my-shared-library@main'
// 或根据参数加载不同版本
library "my-shared-library@${params.LIB_VERSION}"
}
}
}
}
}
隐式加载
配置共享库时勾选 Load implicitly,库会自动加载到所有 Pipeline 中。这种方式虽然方便,但不推荐使用,因为:
- 增加了所有 Pipeline 的启动时间
- 可能在不知情的情况下使用了库中的功能
- 难以追踪 Pipeline 对库的依赖关系
定义全局变量
全局变量是共享库最常用的功能,它可以让 Pipeline 更加简洁。
简单的全局变量
// vars/log.groovy
def info(String message) {
echo "INFO: ${message}"
}
def warning(String message) {
echo "WARNING: ${message}"
}
def error(String message) {
echo "ERROR: ${message}"
}
在 Pipeline 中使用:
@Library('my-shared-library') _
pipeline {
agent any
stages {
stage('Example') {
steps {
script {
log.info 'Starting build'
log.warning 'This is a warning'
log.error 'This is an error'
}
}
}
}
}
自定义步骤
定义类似 sh、git 的自定义步骤,使用 call 方法:
// vars/dockerBuild.groovy
def call(String imageName, String tag = 'latest') {
sh "docker build -t ${imageName}:${tag} ."
sh "docker push ${imageName}:${tag}"
}
// 带代码块的自定义步骤
def call(Closure body) {
node('docker') {
try {
body()
} finally {
sh 'docker system prune -f'
}
}
}
使用方式:
// 简单调用
dockerBuild 'my-app', 'v1.0'
// 带代码块调用
dockerBuild {
sh 'docker build -t my-app .'
sh 'docker push my-app'
}
带配置的自定义步骤
// vars/buildService.groovy
def call(Map config) {
// 验证必需参数
if (!config.serviceName) {
error "serviceName is required"
}
// 设置默认值
config.imageTag = config.imageTag ?: env.BUILD_NUMBER
config.registry = config.registry ?: 'registry.example.com'
config.dockerfile = config.dockerfile ?: 'Dockerfile'
// 执行构建流程
pipeline {
agent any
environment {
IMAGE = "${config.registry}/${config.serviceName}:${config.imageTag}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Image') {
steps {
sh "docker build -f ${config.dockerfile} -t ${IMAGE} ."
}
}
stage('Push Image') {
steps {
sh "docker push ${IMAGE}"
}
}
stage('Deploy') {
when {
expression { config.deploy == true }
}
steps {
sh "kubectl set image deployment/${config.serviceName} ${config.serviceName}=${IMAGE}"
}
}
}
post {
always {
cleanWs()
}
}
}
}
使用方式:
@Library('my-shared-library') _
buildService(
serviceName: 'user-service',
imageTag: 'v2.0.0',
registry: 'registry.example.com',
dockerfile: 'Dockerfile.prod',
deploy: true
)
定义类
对于更复杂的功能,可以使用 Groovy 类来组织代码。
工具类
// src/org/company/BuildUtils.groovy
package org.company
class BuildUtils implements Serializable {
def steps
BuildUtils(steps) {
this.steps = steps
}
def buildMaven(Map config) {
steps.sh "mvn clean ${config.skipTests ? '-DskipTests' : ''} package"
}
def buildDocker(String imageName, String tag) {
steps.sh "docker build -t ${imageName}:${tag} ."
}
def runTests(String type = 'unit') {
switch(type) {
case 'unit':
steps.sh 'mvn test'
break
case 'integration':
steps.sh 'mvn verify -Pintegration-test'
break
default:
steps.error "Unknown test type: ${type}"
}
}
static def generateVersion(String prefix = '') {
def timestamp = new Date().format('yyyyMMdd-HHmmss')
return prefix ? "${prefix}-${timestamp}" : timestamp
}
}
在 Pipeline 中使用:
@Library('my-shared-library')
import org.company.BuildUtils
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def utils = new BuildUtils(this)
utils.buildMaven(skipTests: false)
utils.buildDocker('my-app', BuildUtils.generateVersion())
}
}
}
}
}
常量类
// src/org/company/Constants.groovy
package org.company
class Constants {
static final String REGISTRY = 'registry.example.com'
static final String PROD_NAMESPACE = 'production'
static final String STAGING_NAMESPACE = 'staging'
static final Map DEFAULT_JAVA_BUILD = [
jdk: '11',
maven: '3.8',
goals: 'clean package'
]
static final List NOTIFY_RECIPIENTS = ['[email protected]', '[email protected]']
}
使用资源文件
共享库的 resources 目录可以存储模板文件、配置文件等资源。
加载资源文件
// vars/deployToK8s.groovy
def call(String serviceName, String namespace) {
// 加载 Kubernetes 部署模板
def template = libraryResource 'org/company/templates/deployment.yaml'
// 替换变量
def deployment = template
.replace('${SERVICE_NAME}', serviceName)
.replace('${NAMESPACE}', namespace)
.replace('${IMAGE_TAG}', env.BUILD_NUMBER)
// 写入临时文件
writeFile file: 'deployment.yaml', text: deployment
// 应用配置
sh "kubectl apply -f deployment.yaml -n ${namespace}"
}
使用 Groovy 模板引擎
// vars/renderTemplate.groovy
def call(String templatePath, Map bindings) {
def template = libraryResource templatePath
def engine = new groovy.text.SimpleTemplateEngine()
def rendered = engine.createTemplate(template).make(bindings).toString()
return rendered
}
使用示例:
def config = renderTemplate('org/company/templates/config.json', [
appName: 'my-service',
version: env.BUILD_NUMBER,
environment: params.ENV
])
使用第三方库
可信库(Trusted Libraries)可以使用 @Grab 注解引入 Maven Central 上的第三方 Java 库。这为共享库提供了强大的扩展能力。
基本用法
// src/org/company/MathUtils.groovy
package org.company
@Grab('org.apache.commons:commons-math3:3.4.1')
import org.apache.commons.math3.primes.Primes
class MathUtils implements Serializable {
def steps
MathUtils(steps) {
this.steps = steps
}
def validatePrimeNumber(int number) {
if (!Primes.isPrime(number)) {
steps.error "${number} 不是质数"
}
steps.echo "${number} 是有效的质数"
}
static List getFirstNPrimes(int n) {
return Primes.firstNPrimes(n)
}
}
在 Pipeline 中使用
@Library('utils')
import org.company.MathUtils
pipeline {
agent any
stages {
stage('Validate') {
steps {
script {
def math = new MathUtils(this)
math.validatePrimeNumber(17)
def primes = MathUtils.getFirstNPrimes(10)
echo "前10个质数: ${primes}"
}
}
}
}
}
注意事项
- 第三方库默认缓存在 Jenkins 控制器的
~/.groovy/grapes/目录下 - 只有可信库才能使用
@Grab,非可信库无法访问第三方库 - 建议在库文档中明确标注依赖的第三方库及其版本
推荐的库:
| 库 | 用途 |
|---|---|
org.apache.commons:commons-lang3 | 字符串处理、对象操作 |
org.apache.commons:commons-math3 | 数学运算 |
com.google.guava:guava | 集合工具、缓存 |
org.yaml:snakeyaml | YAML 解析 |
定义声明式 Pipeline
从 Declarative Pipeline 1.2 版本开始,可以在共享库中定义完整的声明式 Pipeline。
// vars/javaPipeline.groovy
def call(Map config) {
pipeline {
agent any
tools {
jdk "JDK-${config.jdk ?: '11'}"
maven "Maven-${config.maven ?: '3.8'}"
}
environment {
APP_NAME = config.appName
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh "mvn clean ${config.skipTests ? '-DskipTests' : ''} package"
}
}
stage('Test') {
when {
expression { !config.skipTests }
}
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('SonarQube') {
when {
expression { config.sonarQube }
}
steps {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
stage('Docker Build') {
when {
expression { config.dockerBuild }
}
steps {
script {
def image = docker.build("${config.imageName}:${env.BUILD_NUMBER}")
if (config.pushImage) {
docker.withRegistry('', config.registryCredential) {
image.push()
}
}
}
}
}
}
post {
always {
cleanWs()
}
failure {
mail to: config.notifyEmail,
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check console output: ${env.BUILD_URL}"
}
}
}
}
Jenkinsfile 变得非常简洁:
@Library('my-shared-library') _
javaPipeline(
appName: 'user-service',
jdk: '17',
skipTests: false,
sonarQube: true,
dockerBuild: true,
imageName: 'registry.example.com/user-service',
pushImage: true,
registryCredential: 'docker-credentials',
notifyEmail: '[email protected]'
)
测试共享库
共享库的测试是确保质量的关键。可以使用 Jenkins Pipeline Unit 框架进行单元测试。
添加测试依赖
// build.gradle
dependencies {
testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.18'
}
编写测试
// src/test/groovy/org/company/BuildUtilsTest.groovy
package org.company
import com.lesfurets.jenkins.unit.BasePipelineTest
import org.junit.Before
import org.junit.Test
class BuildUtilsTest extends BasePipelineTest {
@Override
@Before
void setUp() {
super.setUp()
helper.registerAllowedMethod('sh', [String.class], null)
helper.registerAllowedMethod('error', [String.class], { msg ->
throw new Exception(msg)
})
}
@Test
void testBuildMavenWithTests() {
def utils = new BuildUtils(helper.getBinding())
utils.buildMaven(skipTests: false)
assert helper.callStack.any { call ->
call.methodName == 'sh' &&
call.argsToString().contains('mvn clean package')
}
}
@Test
void testBuildMavenSkipTests() {
def utils = new BuildUtils(helper.getBinding())
utils.buildMaven(skipTests: true)
assert helper.callStack.any { call ->
call.methodName == 'sh' &&
call.argsToString().contains('-DskipTests')
}
}
}
版本管理
语义化版本
为共享库使用语义化版本号(SemVer):MAJOR.MINOR.PATCH
- MAJOR:不兼容的 API 变更
- MINOR:向后兼容的功能新增
- PATCH:向后兼容的问题修复
使用标签
// 使用稳定版本
@Library('[email protected]') _
// 使用最新的主版本
@Library('my-shared-library@v1') _
// 使用开发版本(不推荐用于生产)
@Library('my-shared-library@develop') _
测试 PR 变更
在 Jenkinsfile 中测试共享库的 PR 变更:
@Library('my-shared-library@pull/123/head') _
// 正常的 Pipeline 定义...
这种方式的格式是 @Library('库名@pull/PR编号/head'),Jenkins 会从指定的 PR 分支加载库代码进行测试。
示例场景:假设你正在为共享库开发一个新功能,并开启了编号为 123 的 Pull Request。要测试这个 PR 的变更,可以在测试项目的 Jenkinsfile 中添加:
@Library('pipeline-library@pull/123/head') _
buildPlugin(
useContainerAgent: true,
configurations: [
[platform: 'linux', jdk: 21],
[platform: 'windows', jdk: 17],
]
)
使用 Replay 预测试库变更
Jenkins 的 Replay 功能是测试共享库变更的强大工具。当你发现构建使用的是不可信库时出现问题,可以直接通过 Replay 编辑库的源文件,验证修复是否有效。
使用步骤:
- 在构建历史中找到失败的构建
- 点击 Replay 链接
- 在编辑器中修改共享库的源文件
- 点击 Run 重新执行构建
- 验证修改是否解决了问题
重要说明:
- 即使请求的库版本是分支而非固定版本(如标签),Replay 构建仍会使用与原始构建完全相同的版本,不会重新检出库源码
- Replay 目前不支持可信库(Trusted Libraries)
- Replay 不支持修改资源文件
确认修复后:
当对修改满意后,可以从构建状态页面点击 diff 链接,查看变更差异,然后将差异应用到库仓库并提交:
# 查看差异后,将修改提交到库仓库
git add .
git commit -m "修复构建问题"
git push origin feature-branch
共享库最佳实践
保持简单
全局变量应该简洁明了,避免过度复杂的逻辑。如果一个函数变得复杂,考虑将其拆分为多个小函数或使用类来组织。
文档化
为每个全局变量提供文档文件(.txt 文件):
// vars/buildJava.txt
构建 Java 项目
参数:
- jdkVersion: JDK 版本 (默认: 11)
- mavenVersion: Maven 版本 (默认: 3.8)
- goals: Maven 目标 (默认: clean package)
- skipTests: 是否跳过测试 (默认: false)
示例:
buildJava(jdkVersion: '17', skipTests: true)
避免状态
全局变量应该是无状态的。不要在全局变量中存储可变状态,因为 Pipeline 可能会在 Jenkins 重启后恢复执行,存储的状态会丢失。
错误处理
提供清晰的错误信息:
def call(Map config) {
if (!config.appName) {
error "appName parameter is required"
}
try {
// 执行构建...
} catch (Exception e) {
echo "Build failed: ${e.message}"
throw e
}
}
渐进式更新
当需要更新共享库时:
- 创建新版本分支或标签
- 在测试项目中验证新版本
- 逐步将生产项目迁移到新版本
- 保留旧版本一段时间,确保回退能力
小结
共享库是 Jenkins Pipeline 代码复用的核心机制:
- 目录结构:
src/存放类,vars/存放全局变量,resources/存放资源文件 - 使用方式:
@Library注解或library步骤加载共享库 - 全局变量:定义可复用的自定义步骤和函数
- 类定义:组织复杂的业务逻辑
- 声明式 Pipeline:在共享库中定义完整的 Pipeline 模板
- 版本管理:使用语义化版本和 Git 标签管理发布
下一步
- 安全配置 - 共享库的安全考量
- 多分支 Pipeline - 在多分支项目中使用共享库
- 分布式构建 - 在共享库中使用不同的 Agent