Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Jenkins插件开发

2
Aug
2019

Jenkins插件开发

By Alex
/ in Java
0 Comments
插件开发环境(Maven)

通常我们基于Maven来开发Jenkins插件。

添加Jenkins仓库

修改Maven配置文件,添加:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<profile>
    <id>jenkins</id>
    <repositories>
        <repository>
            <id>repo.jenkins-ci.org</id>
            <url>http://repo.jenkins-ci.org/public/</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>repo.jenkins-ci.org</id>
            <url>http://repo.jenkins-ci.org/public/</url>
        </pluginRepository>
    </pluginRepositories>
</profile>
父POM

插件项目的父POM通常设置为:

XML
1
2
3
4
5
6
  <parent>
    <groupId>org.jenkins-ci.plugins</groupId>
    <artifactId>plugin</artifactId>
    <version>3.43</version>
    <relativePath />
  </parent>

此POM为构件插件提供了合理的缺省配置, 修改属性以定制配置项:

XML
1
2
3
4
  <properties>
    <jenkins.version>2.60.1</jenkins.version>
    <java.level>8</java.level>
  </properties>
maven-hpi-plugin

这是一个Maven插件,用于构建Jenkins插件。

hpi:create

此目标用于创建一个新的插件项目骨架。目前已经废弃,应当考虑基于原型生成插件项目骨架

hpi:hpi

目标完整名称:org.jenkins-ci.tools:maven-hpi-plugin:3.7:hpi。构建Jenkins插件的WAR包。

此目标的必需参数:

参数 说明

minimumJavaVersion 

运行此插件需要的最低Java版本

此目标主要的可选参数:

参数 说明
compatibleSinceVersion 当前插件版本,在配置上兼容的最低插件版本号
maskClasses

空白符分隔的Java包前缀,插件不希望使用Jenkins Core提供的这些类

每个包前缀应当以 . 结尾

globalMaskClasses

类似于maskClasses,但是作用于Jenkins Core和所有插件的边界

主要供哪些提供JavaEE API的插件使用,例如提供JPA API的database插件。其它依赖于database插件的插件,亦可通过container类加载器看到JPA API

hpiName 生成的HPI的名称
pluginFirstClassLoader 设置为true,改变类加载器优先级,让插件自带的类优先于插件依赖的其它插件的类加载
hpi:hpl

类似hpi:hpi,但是打包为Debug布局。

hpi:run

目标完整名称:org.jenkins-ci.tools:maven-hpi-plugin:3.7:run。使用当前插件来运行Jenkins。

此目标的必需参数:

参数 说明

minimumJavaVersion 

运行此插件需要的最低Java版本

此目标主要的可选参数:

参数 说明
consoleForceReload 如果设置为true,在输入控制台上输入新行,则Web上下文自动重启
defaultPort 默认HTTP端口
hudsonHome
jenkinsHome

$JENKINS_HOME的位置,启动的Jenkins服务器以此目录为工作空间

jenkinsCoreId Jenkins Core 包的groupId:artifactId
reload 可选值automatic | manual,设置为automatic则文件改变后自动reload,否则输入控制台上输入新行后reload
maskClasses 参考HPI
pluginFirstClassLoader 参考HPI 
调试插件

可以通过mvnDebug进行Jenkins插件的单步跟踪。

Shell
1
2
3
4
5
mvnDebug hpi:run
 
# 或者
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n"
mvn hpi:run

使用系统属性来调整Jenkins的HTTP端口,以及上下文路径:

Shell
1
mvn hpi:run -Djetty.port=8090  -Dhpi.prefix=/jenkins
插件开发环境(Gradle)

现在可以通过Gradle JPI plugin,基于Gradle开发Jenkins插件。

Gradle配置
说明
Groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
plugins {
  id 'org.jenkins-ci.jpi' version '0.33.0'
}
 
group = 'org.jenkins-ci.plugins'
version = '1.2.0-SNAPSHOT'
description = 'A description of your plugin'
 
jenkinsPlugin {
    // 依赖的Jenkins核心版本
    coreVersion = '1.420'
 
    // 插件的标识符,默认为项目名去掉-plugin后缀
    shortName = 'hello-world'
 
    // 人类可读的名称
    displayName = 'Hello World plugin built with Gradle'
 
    // 在Jenkins Wiki上的插件页面
    url = 'http://wiki.jenkins-ci.org/display/JENKINS/SomePluginPage'
 
    // 可选的插件源码库地址
    gitHubUrl = 'https://github.com/jenkinsci/some-plugin'
 
    // 是否让插件Classloader优先于核心Classloader加载类
    pluginFirstClassLoader = true
 
    // 不希望看到的核心的类前缀
    maskClasses = 'groovy.grape org.apache.commons.codec'
 
    // 当前版本的配置和什么版本之后的兼容
    compatibleSinceVersion = '1.1.0'
 
    // 开发服务器的工作目录
    workDir = file('/tmp/jenkins')
 
    // 部署此插件到什么地方
    repoUrl = 'https://repo.jenkins-ci.org/releases'
 
    // 部署此插件的快照版本到什么地方调试插件
    snapshotRepoUrl = 'https://repo.jenkins-ci.org/snapshots'
 
    // 是否禁用测试的依赖注入,用于检查Jelly语法等
    disabledTestInjection = false
 
    // 本地化任务输出目录
    localizerOutputDir = "${project.buildDir}/generated-src/localizer"
 
    // 是否配置中心仓库、本地仓库、Jenkins中心仓库
    configureRepositories = false
 
    // 是否配置部署仓库
    configurePublishing = false
 
    // 插件扩展名,hpi或者jpi
    fileExtension = 'hpi'
 
    // 开发者列表
    developers {
        developer {
            id 'abayer'
            name 'Andrew Bayer'
            email 'andrew.bayer@gmail.com'
        }
    }
 
    // License信息
    licenses {
        license {
            name 'Apache License, Version 2.0'
            url 'https://www.apache.org/licenses/LICENSE-2.0.txt'
            distribution 'repo'
            comments 'A business-friendly OSS license'
        }
    }
}
 
dependencies {
    // 依赖其它插件,目标插件的类在编译期可见
    jenkinsPlugins 'org.jenkinsci.plugins:git:1.1.15'
    // 可选依赖
    optionalJenkinsPlugins 'org.jenkins-ci.plugins:ant:1.2'
    // 测试依赖
    jenkinsTest 'org.jenkins-ci.main:maven-plugin:1.480'
    // 为server任务安装额外的插件
    jenkinsServer 'org.jenkins-ci.plugins:ant:1.2'
}
实例
Groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
plugins {
    id 'groovy'
    id "org.jenkins-ci.jpi" version "0.33.0"
}
 
sourceCompatibility = 1.8
 
group 'io.jenkins.plugins'
version '1.0-SNAPSHOT'
description = "This plugin provides pipeline steps for ALCM."
 
repositories {
    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
    maven { url 'https://repo.jenkins-ci.org/public/' }
    mavenLocal()
}
 
jenkinsPlugin {
    coreVersion = "2.164.1"
    displayName = "PA ALCM Steps"
    url = "http://wiki.jenkins-ci.org/display/JENKINS/ALCMSteps"
    shortName = "alcm-steps"
    workDir = file('/tmp/jenkins')
    fileExtension = 'jpi'
    pluginFirstClassLoader = true
}
 
dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.3.11'
    compile 'cc.gmem.yun.alcm:client:1.0-SNAPSHOT'
    compile 'org.jenkins-ci.plugins:structs:1.19'
    testCompile group: 'junit', name: 'junit', version: '4.12'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-step-api:2.19'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-cps:2.39'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-job:2.11.2'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-basic-steps:2.6'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-durable-task-step:2.13'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-api:2.30'
    jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-support:3.3'
    jenkinsTest 'org.jenkins-ci.plugins:script-security:1.39'
    jenkinsTest 'org.jenkins-ci.plugins:scm-api:2.2.6'
} 
Gradle任务
任务 说明
gradle jpi 构建出插件
gradle publishToMavenLocal 构建并安装到本地Maven仓库
gradle publish 部署到Jenkins的Maven仓库,以便在Update Center可见
gradle server 启动本地Jenkins实例便于调试: -Djenkins.httpPort=8082 
调试插件
Shell
1
2
3
4
5
6
7
8
# 调试
./gradlew server -Dorg.gradle.debug=true
 
# 定制端口
/gradlew server -Dorg.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
 
# 禁用Jelly缓存
./gradlew -Dstapler.jelly.noCache=false server
插件代码骨架

Jenkins提供了一系列插件的原型,我们可以根据基于这些原型生成自己的插件的骨架代码:

Shell
1
2
3
4
5
6
7
8
mvn -U -P jenkins archetype:generate -Dfilter="io.jenkins.archetypes:"
 
# 非交互式
mvn archetype:generate  -P jenkins -B -DarchetypeGroupId=io.jenkins.archetypes \
    -DarchetypeArtifactId=hello-world-plugin \
    -DarchetypeVersion=1.5 \
    -DgroupId=cc.gmem.yun.alcm -DartifactId=alcm-steps -Dversion=1.0-SNAPSHOT \
    -Dpackage=cc.gmem.yun.alcm.pipeline.jenkins.steps

从列表中选择一个合适的原型,然后根据提示填写各项参数即可。

原型列表
原型 说明
io.jenkins.archetypes:empty-plugin 仅仅包含POM和空白的源码树
io.jenkins.archetypes:global-configuration-plugin 全局配置插件的示例
io.jenkins.archetypes:hello-world-plugin 包含POM和一个示例的Build Step

本章后续内容均针对基于hello-world-plugin生成的代码骨架。

项目结构

从原型生成的插件目录结构如下:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
├── alcm-steps.iml
├── pom.xml
├── src
│   ├── main
│   │   ├── java      # 插件源代码
│   │   │   └── cc
│   │   │       └── gmem
│   │   │           └── yun
│   │   │               └── alcm
│   │   │                   └── pipeline
│   │   │                       └── jenkins
│   │   │                           └── steps
│   │   │                               └── HelloWorldBuilder.java # 插件类
│   │   ├── resources  # Jelly/Groovy视图文件
│   │   │   ├── index.jelly  # 插件说明信息
│   │   │   ├── cc
│   │   │   │   └── gmem
│   │   │   │       └── yun
│   │   │   │           └── alcm
│   │   │   │               └── pipeline
│   │   │   │                   └── jenkins
│   │   │   │                       └── steps
│   │   │   │                           ├── HelloWorldBuilder
│   │   │   │                           │   ├── config.jelly # 插件配置页面模板
│   │   │   │                           │   ├── config.properties # 模板变量值
│   │   │   │                           │   ├── config_zh_CN.properties # 模板变量值(国际化)
│   │   │   │                           │   ├── help-name.html # 字段name的帮助信息
│   │   │   │                           │   ├── help-name_zh_CN.html
│   │   │   │                           │   ├── help-useFrench.html
│   │   │   │                           │   └── help-useFrench_zh_CN.html
│   │   └── webapp  # 插件的静态资源
配置界面

该插件会为Jenkins Job提供一个Step,此Step的配置界面如下:hello-world-plugin-config

Name字段的名称为name,点击后面的问号,显示的内容是help-name.html中的。 

代码分析
HelloWorldBuilder

此类是插件的实现。它继承Builder,实现了SimpleBuildStep。

SimpleBuildStep是诸如Builder、Publisher之类的Step,可以在构建过程的任何时机调用多次。这些Step应当遵循:

  1. 不去实现BuildStep.prebuild方法,因为此方法假设了一种特定的执行顺序
  2. 不去实现BuildStep.getProjectActions方法,因为如果此Step不是项目的静态配置的一部分,则它可能永不会被调用
  3. 实现BuildStep.getRequiredMonitorService,且返回BuildStepMonitor.NONE,因为只对仅调用一次的Step有意义
  4. 不去实现DependencyDeclarer
  5. 不假设Executor.currentExecutor为非空,不使用Computer.currentComputer

从抽象类Builder继承的方法如下:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Builder extends BuildStepCompatibilityLayer implements Describable<Builder>, ExtensionPoint {
    
    public boolean prebuild(Build build, BuildListener listener) {
        return true;
    }
 
    // 由于Builder通常不依赖于它前面的步骤的结果,因此默认返回NONE
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }
 
    public Descriptor<Builder> getDescriptor() {
        return Jenkins.getInstance().getDescriptorOrDie(getClass());
    }
 
    public static DescriptorExtensionList<Builder,Descriptor<Builder>> all() {
        return Jenkins.getInstance().<Builder,Descriptor<Builder>>getDescriptorList(Builder.class);
    }
}

HelloWorldBuilder是插件的实现,实现插件不需要继承plugin类(这种方式已经废弃),推荐的方式是实现扩展点,并通过@hudson.Extension注解来注册(以便Jenkins自动发现)。

关于这种扩展的方式,需要注意:

  1. 一般来说,你的插件应该继承已有的扩展点(实现ExtensionPoint的类),并在静态内部类中继承对应的描述符类,这些描述符类是hudson.model.Descriptor的子类
  2. 注解@Extension必须放在上述内部类之上,这样Jenkins才能发现扩展
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package cc.gmem.yun.alcm.pipeline.jenkins.steps;
 
import hudson.Launcher;
import hudson.Extension;
import hudson.FilePath;
import hudson.util.FormValidation;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
 
import javax.servlet.ServletException;
import java.io.IOException;
import jenkins.tasks.SimpleBuildStep;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundSetter;
 
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
 
    private final String name;
    private boolean useFrench;
 
    // 绑定对象时使用此构造器
    @DataBoundConstructor
    public HelloWorldBuilder(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public boolean isUseFrench() {
        return useFrench;
    }
 
    @DataBoundSetter
    public void setUseFrench(boolean useFrench) {
        this.useFrench = useFrench;
    }
 
    /**
     * SimpleBuildStep的核心方法,执行此Step
     * @param run 此Step所属的Build
     * @param workspace 此Build的工作区,用于文件系统操作
     * @param launcher 用于启动进程
     * @param listener 用于发送输出
     * @throws InterruptedException 如果此Step被中断
     * @throws IOException 出现其它错误,更“礼貌”的错误是AbortException
     */
    public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
        if (useFrench) {
            listener.getLogger().println("Bonjour, " + name + "!");
        } else {
            listener.getLogger().println("Hello, " + name + "!");
        }
    }
 
    // 此注解为Jenkins扩展定义唯一的标识符,驼峰式大小写,尽量简短。在流水线脚本中引用此Step时即使用此标识符
    // 标识符不需要全局唯一,只需要在一个扩展点内是唯一的即可
    @Symbol("greet")
    // 标记字段、方法或类,以便Huson能自动发现定位到ExtensionPoint的实现
    @Extension
    // Build / Publisher的描述符
    // Descriptor是可配置实例(Describable)的元数据,也作为Describable的工厂
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
 
        // 校验Project配置表单中的字段
        // 下面的方法检查name字段,参数value注入name的值,可以用@QueryParameter注入其它参数
        // 如果要校验useFrench字段,则需要实现doCheckUsdeFrench方法
        public FormValidation doCheckName(@QueryParameter String value, @QueryParameter boolean useFrench)
                throws IOException, ServletException {
            if (value.length() == 0)
                // 引用资源束
                return FormValidation.error(Messages.HelloWorldBuilder_DescriptorImpl_errors_missingName());
            if (value.length() < 4)
                return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_tooShort());
            if (!useFrench && value.matches(".*[éáàç].*")) {
                return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_reallyFrench());
            }
            return FormValidation.ok();
        }
 
        // 判断当前Step是否能用于目标类型的项目
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }
 
        // 此Step的显示名称
        @Override
        public String getDisplayName() {
            return Messages.HelloWorldBuilder_DescriptorImpl_DisplayName();
        }
 
    }
 
}
config.jelly

此文件是Project配置页面的模板:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
    <!-- 文本表单字段,${%引用资源束中的条目 -->
    <f:entry title="${%Name}" field="name">
        <f:textbox />
    </f:entry>
    <f:advanced>
        <f:entry title="${%French}" field="useFrench"
                 description="${%FrenchDescr}">
            <f:checkbox />
        </f:entry>
    </f:advanced>
</j:jelly>

模板使用的资源束为config.properties。 

HelloWorldBuilderTest

这个类包含了单元测试:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package cc.gmem.yun.alcm.pipeline.jenkins.steps;
 
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Label;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
 
public class HelloWorldBuilderTest {
 
    // 标记此字段为TestRule
    // JUnit的TestRule决定一个或一组测试方法如何运行、如何报告
    @Rule
    public JenkinsRule jenkins = new JenkinsRule();
 
    final String name = "Bobby";
 
    @Test
    public void testConfigRoundtrip() throws Exception {
        // 启动一个临时Jenkins服务器,并创建自由项目
        FreeStyleProject project = jenkins.createFreeStyleProject();
        // 添加一个构建步骤
        project.getBuildersList().add(new HelloWorldBuilder(name));
        // 加载配置页面,不经修改的提交
        project = jenkins.configRoundtrip(project);
        // 判断相等
        jenkins.assertEqualDataBoundBeans(new HelloWorldBuilder(name), project.getBuildersList().get(0));
    }
 
    @Test
    public void testConfigRoundtripFrench() throws Exception {
        FreeStyleProject project = jenkins.createFreeStyleProject();
        HelloWorldBuilder builder = new HelloWorldBuilder(name);
        builder.setUseFrench(true);
        project.getBuildersList().add(builder);
        project = jenkins.configRoundtrip(project);
 
        HelloWorldBuilder lhs = new HelloWorldBuilder(name);
        lhs.setUseFrench(true);
        jenkins.assertEqualDataBoundBeans(lhs, project.getBuildersList().get(0));
    }
 
    @Test
    public void testBuild() throws Exception {
        FreeStyleProject project = jenkins.createFreeStyleProject();
        HelloWorldBuilder builder = new HelloWorldBuilder(name);
        project.getBuildersList().add(builder);
        // 执行构建,并断言构建成功
        FreeStyleBuild build = jenkins.buildAndAssertSuccess(project);
        // 断言本次构建的控制台输出包括指定字样
        jenkins.assertLogContains("Hello, " + name, build);
    }
 
    @Test
    public void testBuildFrench() throws Exception {
 
        FreeStyleProject project = jenkins.createFreeStyleProject();
        HelloWorldBuilder builder = new HelloWorldBuilder(name);
        builder.setUseFrench(true);
        project.getBuildersList().add(builder);
 
        FreeStyleBuild build = jenkins.buildAndAssertSuccess(project);
        jenkins.assertLogContains("Bonjour, " + name, build);
    }
 
    // 在流水线中使用此Step
    @Test
    public void testScriptedPipeline() throws Exception {
        String agentLabel = "my-agent";
        // 在本机创建一个新的Slave(Agent),等待其上线
        jenkins.createOnlineSlave(Label.get(agentLabel));
        // 定义一个流水线任务
        WorkflowJob job = jenkins.createProject(WorkflowJob.class, "test-scripted-pipeline");
        // 流水线定义,注意通过@Symbol指定的标识符来引用本插件定义的Step
        String pipelineScript
                = "node {\n"
                + "  greet '" + name + "'\n"
                + "}";
        // CPS,即continuation passing style,是运行Groovy脚本的一种方式
        // 允许应用程序被随时暂停、重启继续
        job.setDefinition(new CpsFlowDefinition(pipelineScript, true));
        WorkflowRun completedBuild = jenkins.assertBuildStatusSuccess(job.scheduleBuild2(0));
        String expectedString = "Hello, " + name + "!";
        jenkins.assertLogContains(expectedString, completedBuild);
    }
 
}
编写插件
依赖注入

Jenkins插件的依赖注入,可以基于Google Guice(一个轻量级的IoC框架)实现。下面是一个例子:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 需要被注入的接口及其实现
public interace MySvc {
    public void myServiceMethod()
}
 
public class MySvcImpl implements MySvc {
   public void myServiceMethod() {
       System.out.println("It works!");
   }
}
 
// IoC配置类
public class MyGuiceModule extends com.google.inject.AbstractModule {
    @Override
    public void configure() {
        bind(MySvc.class).to(MySvcImpl.class).in(com.google.inject.Singleton.class);
    }
}

下面的插件类会被注入MySvc:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.google.inject.Guice;
import com.google.inject.Inject;
public class MyPublisher extends Publisher {
    private transient MySvc mySvc;
 
    private String someJobConfig
    @DataBoundConstructor
    public MyPublisher(String someJobConfig) {
        this.someJobConfig = someJobConfig;
    }
    @Inject
    public void setMySvc(MySvc mySvc) {
        this.mySvc = mySvc;
    }
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) {
        if (mySvc == null) {
            // 进行注入
            Guice.createInjector(new MyModule()).injectMembers(this);
        }
        mySvc.myServiceMethod();
    }
}
启动进程

每当构建时, perform(Build, Launcher, BuildListener)方法会被调用,你可以利用Launcher来执行任意命令:

Java
1
2
3
4
5
6
7
8
9
launcher.launch(cmd, env, out, workDir)
 
// 启动进程
launcher.launch("dir", new String[0], listener.getLogger(), build.getProject().getWorkspace());
 
 
// 获取返回值
Proc proc = launcher.launch("dir", build.getEnvVars(), listener.getLogger(), build.getProject().getWorkspace());
int exitCode = proc.join();
分布式调用

Jenkins使用一种分布式机制来运行任务 —— 运行在Master节点上的线程,能够发送闭包到远程机器上,并且在闭包执行完毕后,取回结果。

支持分布式执行的闭包示例如下: 

Java
1
2
3
4
5
6
private static class GetSystemProperties extends MasterToSlaveCallable<Properties,RuntimeException> {
    public Properties call() {
        return System.getProperties();
    }
    private static final long serialVersionUID = 1L;
}

关键之处在于需要实现hudson.remoting.Callable,此接口声明了返回值、异常类型。

下面的调用将闭包发送给Slave执行:

Java
1
Properties systemProperties = channel.call(new GetSystemProperties());
隐式远程调用

Jenkins提供了一些关键的抽象,将其分布式执行的特性隐藏起来。

抽象 说明
hudson.FilePath 用于代替java.io.File,可以指向位于Master或任何Slave上的目录或文件
hudson.Launcher 类似于 java.lang.ProcessBuilder,但是可以在远程JVM上启动进程
视图开发
使用自定义图片

你需要把图片放在src/main/webapp下。在jelly或者Java页面上获取图片的方法是:

Java
1
2
3
4
public String getIconPath() {
  PluginWrapper wrapper = Hudson.getInstance().getPluginManager().getPlugin([YOUR-PLUGIN-MAIN-CLASS].class);
  return Hudson.getInstance().getRootUrl() + "plugin/"+ wrapper.getShortName()+"/";
}
插入空格

要在jelly页面添加空格,只需要:  <st:nbsp/>

插件实例

本章分析一些真实的Jenkins插件的代码。

Publisher + Recorder

Publisher可以在构建完成后,触发任意的动作。Recorder可以为每次构建记录一些统计信息。

Discard Old Build plugin是这种扩展的例子,用于在构建完毕后,检查并删除过期的构建历史。本节分析其代码:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package org.jenkinsci.plugins.discardbuild;
 
import hudson.Extension;
import hudson.Launcher;
import hudson.model.*;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.RunList;
import org.kohsuke.stapler.DataBoundConstructor;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class DiscardBuildPublisher extends Recorder {
    // 插件参数
    /**
     * If not -1, history is only kept up to this days.
     */
    private final int daysToKeep;
 
    // 从表单绑定插件参数
    @DataBoundConstructor
    public DiscardBuildPublisher( String daysToKeep) {
        this.daysToKeep = parse(daysToKeep);
    }
 
 
    private void deleteOldBuildsByDays(AbstractBuild<?, ?> build, BuildListener listener, int daysToKeep) {
        ArrayList<Run<?, ?>> list = updateBuildsList(build, listener);
        try {
            Calendar cal = getCurrentCalendar();
            cal.add(Calendar.DAY_OF_YEAR, -daysToKeep);
            for (Run<?, ?> r : list) {
                if (r.getTimestamp().before(cal)) {
                    discardBuild(r, "it is older than daysToKeep", listener); //$NON-NLS-1$
                }
            }
        } catch (IOException e) {
            e.printStackTrace(listener.error("")); //$NON-NLS-1$
        }
    }
 
 
    private ArrayList<Run<?, ?>> updateBuildsList(AbstractBuild<?, ?> build, BuildListener listener) {
        RunList<Run<?, ?>> builds = new RunList<Run<?, ?>>();
        ArrayList<Run<?, ?>> list = new ArrayList<Run<?, ?>>();
        // 获取本次Build对应的Job
        Job<?, ?> job = (Job<?, ?>) build.getParent();
        // 获取Job的所有构建
        builds = (RunList<Run<?, ?>>) job.getBuilds();
        list = discardLastBuilds(build, listener, builds);
        return list;
    }
 
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) {
        listener.getLogger().println("Discard old builds..."); //$NON-NLS-1$
        deleteOldBuildsByDays(build, listener, daysToKeep);
        return true;
    }
 
    private void discardBuild(Run<?, ?> history, String reason, BuildListener listener) throws IOException {
        listener.getLogger().printf("#%d is removed because %s\n", history.getNumber(), reason); //$NON-NLS-1$
        history.delete();
    }
 
 
    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }
 
    // 描述符
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
 
        @SuppressWarnings("rawtypes")
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }
 
        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            return Messages.DiscardHistoryBuilder_description();
        }
    }
 
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }
}
Trigger 

这个扩展点用于触发某个Job的构建。

Files Found Trigger是这种扩展的例子,能够轮询一个或多个目录,当发现特定的文件后,自动触发构建。本节分析其代码。

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package hudson.plugins.filesfoundtrigger;
 
import static hudson.Util.fixNull;
 
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.kohsuke.stapler.DataBoundConstructor;
 
import com.google.common.collect.ImmutableList;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.mapper.Mapper;
 
import antlr.ANTLRException;
import hudson.Extension;
import hudson.model.BuildableItem;
import hudson.model.Item;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.RobustReflectionConverter;
 
public final class FilesFoundTrigger extends Trigger<BuildableItem> {
 
  private static final Logger LOGGER = Logger.getLogger(FilesFoundTrigger.class.getName());
 
  private static final AtomicLong logCounter = new AtomicLong();
 
  // 从什么Slave节点上检查文件是否存在,如果为空则检查master节点
  private final String node;
 
  // 检查的目录
  private final String directory;
 
  // 期望存在的文件的pattern
  private final String files;
 
  // 忽略的文件的pattern
  private final String ignoredFiles;
 
  // 仅仅当发现文件数量大于此数字时才触发
  private final String triggerNumber;
 
  // 额外的文件pattern
  private final ArrayList<FilesFoundTriggerConfig> additionalConfigs;
 
  /**
   * 构造函数
   *
   * @param spec
   *          轮询文件系统的Cron表达式
   * @param configs
   *          文件模式列表
   * @throws ANTLRException
   *           无法解析Cron表达式抛出此异常
   */
  @DataBoundConstructor
  public FilesFoundTrigger(String spec, List<FilesFoundTriggerConfig> configs) throws ANTLRException {
    // Trigger天生基于Cron进行检查
    super(spec);
 
    ArrayList<FilesFoundTriggerConfig> configsCopy = new ArrayList<FilesFoundTriggerConfig>(fixNull(configs));
    FilesFoundTriggerConfig firstConfig;
    if (configsCopy.isEmpty()) {
      firstConfig = new FilesFoundTriggerConfig(null, "", "", "", "1");
    } else {
      firstConfig = configsCopy.remove(0);
    }
    this.node = firstConfig.getNode();
    this.directory = firstConfig.getDirectory();
    this.files = firstConfig.getFiles();
    this.ignoredFiles = firstConfig.getIgnoredFiles();
    this.triggerNumber = firstConfig.getTriggerNumber();
    if (configsCopy.isEmpty()) {
      configsCopy = null;
    }
    this.additionalConfigs = configsCopy;
  }
 
 
  public List<FilesFoundTriggerConfig> getConfigs() {
    ImmutableList.Builder<FilesFoundTriggerConfig> builder = ImmutableList.builder();
    builder.add(new FilesFoundTriggerConfig(node, directory, files, ignoredFiles, triggerNumber));
    if (additionalConfigs != null) {
      builder.addAll(additionalConfigs);
    }
    return builder.build();
  }
 
  // Cron每次触发的逻辑
  @Override
  public void run() {
    long counter = logCounter.incrementAndGet();
    for (FilesFoundTriggerConfig config : getConfigs()) {
      FilesFoundTriggerConfig expandedConfig = config.expand();
      try {
        FileSearch.Result result = FileSearch.perform(expandedConfig);
        int triggerNumber = Integer.parseInt(expandedConfig.getTriggerNumber());
        boolean triggerBuild = result.files.size() >= triggerNumber;
        if (triggerBuild) {
          // 触发构建
          job.scheduleBuild(0, new FilesFoundTriggerCause(expandedConfig));
          return;
        }
      } catch (NumberFormatException e) {
        LOGGER.log(Level.FINE, "{0} - Result: Invalid trigger number (build not triggered)", counter);
      } catch (InterruptedException e) {
        LOGGER.log(Level.FINE, "{0} - Result: Thread interrupted (build not triggered)", counter);
        Thread.currentThread().interrupt();
      }
    }
  }
 
 
  // 将当前插件注册为Trigger扩展
  @Extension
  public static final class DescriptorImpl extends TriggerDescriptor {
 
    @Override
    public boolean isApplicable(Item item) {
      return item instanceof BuildableItem;
    }
 
    @Override
    public String getDisplayName() {
      return Messages.DisplayName();
    }
  }
}
单元测试

Jenkins提供一系列JUnit周边工具来简化单元测试。支持的特性包括:

  1. 启动内嵌的Servlet容器,允许通过浏览器访问Jenkins页面,进行测试
  2. HtmlUnit用于简化UI测试
  3. 为每个测试用例准备隔离的Jenkins实例
  4. 测试代码可以直接访问Jenkins对象模型,并可直接执行一些操作,而不需要通过浏览器
  5. 基于注解的、声明式的测试用例环境说明
HowTos
JenkinsRule

下面的例子示例了如何基于JenkinsRule进行单元测试:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.jvnet.hudson.test.JenkinsRule;
import org.apache.commons.io.FileUtils;
import hudson.model.*;
import hudson.tasks.Shell;
import org.junit.Test;
import org.junit.Rule;
public class AppTest {
  @Rule public JenkinsRule j = new JenkinsRule();
  @Test public void first() throws Exception {
    // 自由风格项目
    FreeStyleProject project = j.createFreeStyleProject();
    // 添加一个构建步骤
    project.getBuildersList().add(new Shell("echo hello"));
    // 得到某个构建步骤
    Shell shell = project.getBuildersList().get(Shell.class);
    // 调度构建并获得结果
    FreeStyleBuild build = project.scheduleBuild2(0).get();
    System.out.println(build.getDisplayName() + " completed");
    // 读取日志文件内容
    String s = FileUtils.readFileToString(build.getLogFile());
    assertThat(s, contains("+ echo hello"));
  }
}
Stubbing

某些情况下,你希望在不依赖于完整Jenkins实例的前提下进行快速测试,这时可以使用仿冒:

Java
1
2
AbstractBuild build = Mockito.mock(AbstractBuild.class);
Mockito.when(build.getResult()).thenReturn(Result.FAILURE);
HTML抓取

用于测试Jenkins生成的HTML页面:

Java
1
2
3
HtmlPage page = j.createWebClient().goTo("computer/test/");
HtmlElement navbar = page.getElementById("left-top-nav");
assertEquals(1,navbar.selectNodes(".//a").size());
提交表单 
Java
1
2
3
HtmlPage configPage = j.createWebClient().goTo("configure");
HtmlForm form = configPage.getFormByName("config");
form.submit((HtmlButton)last(form.getHtmlElementsByTagName("button")));
设置环境变量
Java
1
2
3
4
5
6
public void setEnvironmentVariables() throws IOException {
    EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty();
    EnvVars envVars = prop.getEnvVars();
    envVars.put("sampleEnvVarKey", "sampleEnvVarValue");
    j.jenkins.getGlobalNodeProperties().add(prop);
}
测试安全性为

下面的代码允许任何用户名登陆,只要输入的密码和用户名一致:

Java
1
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
自定义Builder

要实现一个一次性的Builder,可以选择继承TestBuilder:

Java
1
2
3
4
5
6
7
8
9
10
FreeStyleProject project = j.createFreeStyleProject();
project.getBuildersList().add(new TestBuilder() {
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
        // 向工作区中的文件中写入数据
        build.getWorkspace().child("abc.txt").write("hello","UTF-8");
        return true;
    }
});
project.scheduleBuild2(0);
线程协调

Jenkins中构建时异步发生的,如果需要在测试代码中进行同步,可以使用OneShotEvent:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final OneShotEvent buildStarted = new OneShotEvent();
FreeStyleProject project = j.createFreeStyleProject();
project.getBuildersList().add(new TestBuilder() {
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
        BuildListener listener) throws InterruptedException, IOException {
        // 构建真正开始后,发起信号
        buildStarted.signal();
        return true;
    }
});
project.scheduleBuild2(0);
// 阻塞直到信号到达
buildStarted.block(); 
常见问题
依赖其它插件

如果希望使用其它Jenkins插件提供的类,只需要将目标插件声明为Maven依赖即可。很多插件专门提供基础功能,供其它插件依赖,例如Git Client Plugin。

应当尽量依赖插件(而不是自己引入jar依赖),因为hpi打包时,依赖的插件(以及这些插件的传递性依赖)会自动排除,可以减小压缩包的大小。

类冲突问题
共享库引起的冲突

共享库没有ClassLoader隔离机制,Jenkins Core的同名类会优先使用,从而导致冲突。

解决办法是,将存在冲突的部分代码封装到插件中,然后共享库调用插件。

插件引起的冲突

插件具有类隔离机制,通过适当的配置即可避免和Jenkins Core的类冲突。但是,在基于JenkinsRule进行测试时,这种类隔离机制不生效,需要修改JenkinsRule的源码。

 

← 日出•印象
Kustomize学习笔记 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Java5新特性
  • ActiveMQ知识集锦
  • SOFAStack学习笔记
  • Groovy学习笔记
  • Bazel学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2