Jenkins DataBoundConstructor DataBoundSetter

@DataBoundConstructor

  这个注解的作用是用于参数注入(反射),jenkins将界面上获取的值,传递给这个构造函数。这样插件就能获取到在界面上配置的值了。

探讨 DataBoundConstructor DataBoundSetter 对流水线中的使用方法的影响

第一种情况

构造方法 有参数的,加上 注解的,每个 set 方法上也加上 注解的, config.jelly 文件没有配置 域的。

1
2
3
4
5
6
@DataBoundConstructor
public ReadIniStep(String file, String section, String option)


@DataBoundSetter 加到setter 方法上

然后流水线 可以这么来用, 可以看到 加上括号,和 不加 括号 都可以的。

1
2
def value =  readIni file:"./env.cfg", section:"build", option:"BUILD_COMBINATION"
println value
1
2
3
def value =  readIni(file:"./env.cfg", section:"build", option:"BUILD_COMBINATION")
println value

如果不加 关键字 file, section, option 这就会报错

1
2
3
4
5
def value =  readIni("./env.cfg", "build", "BUILD_COMBINATION")

java.lang.IllegalArgumentException: Expected named arguments but got [./env.cfg, build, BUILD_COMBINATION]


第 2 种情况

构造方法 没有参数的同时加上注解的,每个 set 方法上也加上注解的, config.jelly 文件没有配置 域的。 这个和上面 情况一样

第 3 种情况

添加 config.jelly 文件 的。 这个和上面 情况一样,这个 jelly 文件 就是让 片段生成器 那个页面 可以填写参数,然后生成 流水线代码。
这个对 怎么使用 流水线 没影响。

探讨 DataBoundConstructor DataBoundSetter 代码片段生成 的影响

readJSON 举例

readJSON 需要 3个 参数的, file, text, returnPojo 这3个,

在默认情况下 点击 生成流水线脚本 出来的代码片段是

1
readJSON file: '', text: ''   

为什么没有带上 returnPojo 这个参数呢, 当只有 勾选了 returnPojo 才会有

1
2
3

readJSON file: '', returnPojo: true, text: ''

readFile 举例

默认情况下:

1
readFile ''

把参数 file 和 encoding 都填上如下:

1
2
readFile encoding: 'utf-8', file: '/home/mamh/.gitconfig'

touch 举例

这个 touch 的 config.jelly、或者是 config.groovy 配置如下,配置了2个输入文本框,这样在代码片段生成器那里就会有2个input的。

1
2
3
4
5
6
7
f.entry(field: 'file', title: _('File')) {
f.textbox()
}

f.entry(field: 'timestamp', title: _('Timestamp')) {
f.textbox()
}
1
2
3
4
5
6
7
8
9
touch file: '1.txt', timestamp: 1000    和 
touch(file: '1.txt', timestamp: 1000) 等效

touch(file: '1.txt') 和
touch('1.txt') 和 如果构造函数里面没有参数的化,这里就会失败了,报 Expected named arguments but got
touch '1.txt' 是等效的

touch() 也可以执行,但是会抛出异常,因为构造方法里面判断了。 groovy中没有参数的情况是必须要带上 小括号的。

这里判断 只有一个参数的情况,调用 parseArgs 去解析, 否则的化 调用另外一个 parseArgs 方法去解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static NamedArgsAndClosure parseArgs(Object arg, StepDescriptor d) {
boolean singleArgumentOnly = false;
try {
DescribableModel<?> stepModel = DescribableModel.of(d.clazz);
singleArgumentOnly = stepModel.hasSingleRequiredParameter() && stepModel.getParameters().size() == 1;
if (singleArgumentOnly) { // Can fetch the one argument we need
DescribableParameter dp = stepModel.getSoleRequiredParameter();
String paramName = (dp != null) ? dp.getName() : null;
return parseArgs(arg, d.takesImplicitBlockArgument(), paramName, singleArgumentOnly, new HashSet<>());
}
} catch (NoStaplerConstructorException e) {
// Ignore steps without databound constructors and treat them as normal.
}
return parseArgs(arg,d.takesImplicitBlockArgument(), loadSoleArgumentKey(d), singleArgumentOnly, new HashSet<>());
}

如果这个 model 中只有一个,并且是必填项,就返回这个对象。这个对象和 null 比较,不是null的情况就说明是 只有一个参数的情况。

1
2
3
4
public boolean hasSingleRequiredParameter() {
return getSoleRequiredParameter()!=null;
}

依次循环 class TouchStep 中的 参数, 在 touch 这个例子中 有2个,file 和 timestamp。如果这个 model 中只有一个,并且是必填项,就返回这个对象。

1
2
3
4
5
6
7
8
9
10
public @CheckForNull DescribableParameter getSoleRequiredParameter() {
DescribableParameter rp = null;
for (DescribableParameter p : getParameters()) {
if (p.isRequired()) {
if (rp!=null) return null;
rp = p;
}
}
return rp;
}

在流水线中使用的时候,如果此参数是必须,表明isRequired返回的 是True。
什么情况决定是否是必须的呢?
通过DataBoundSetter设置的参数被认为是可选的。
通过DataBoundConstructor设置的所有参数都被认为是强制性的。 在 touch 这个例子中 timestamp 就配置了 setter方法,所以这个参数是可选的。

1
2
3
public boolean isRequired() {
return setter==null;
}

执行 touch(file: ‘1.txt’) 调用 parseArgs 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    static NamedArgsAndClosure parseArgs(Object arg, boolean expectsBlock, String soleArgumentKey, boolean singleRequiredArg, @NonNull Set<String> interpolatedStrings) {

返回 一个 NamedArgsAndClosure 对象,里面的 namedArgs 如下:
namedArgs = {LinkedHashMap@26830} size = 1
"file" -> "1.txt"

再往下执行,就会到这里
if (Util.isOverridden(StepDescriptor.class, d.getClass(), "newInstance", Map.class)) {
s = d.newInstance(ps.namedArgs);
} else {
DescribableModel<? extends Step> stepModel = DescribableModel.of(d.clazz);
s = stepModel.instantiate(ps.namedArgs, listener);
}

之后 这里的 s 就是 TouchStep 实例对象了。

生成流水线脚本 generateSnippet

点击这个按钮 是 发送个请求到 下面 地址上的

POST
http://jenkins.xxxxx.com/pipeline-syntax/generateSnippet

带的参数是 个json字符串,转成json对象就是 下面这个样子, 这里用的 readJSON 这个的

1
2
3
4
5
6
7
8
9
10

{
"":"text",
"file":"",
"text":"",
"returnPojo":true,
"stapler-class":"org.jenkinsci.plugins.pipeline.utility.steps.json.ReadJSONStep",
"$class":"org.jenkinsci.plugins.pipeline.utility.steps.json.ReadJSONStep"
}

通过搜索 发现这个代码 在 workflow-cps-plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/Snippetizer.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

@Restricted(DoNotUse.class) // accessed via REST API
public HttpResponse doGenerateSnippet(StaplerRequest req, @QueryParameter String json) throws Exception {
// TODO is there not an easier way to do this? Maybe Descriptor.newInstancesFromHeteroList on a one-element JSONArray?
JSONObject jsonO = JSONObject.fromObject(json);
Jenkins j = Jenkins.get();
Class<?> c = j.getPluginManager().uberClassLoader.loadClass(jsonO.getString("stapler-class"));
Descriptor descriptor = j.getDescriptor(c.asSubclass(Describable.class));
if (descriptor == null) {
return HttpResponses.text("<could not find " + c.getName() + ">");
}
Object o;
try {
o = descriptor.newInstance(req, jsonO);
} catch (RuntimeException x) { // e.g. IllegalArgumentException
return HttpResponses.text(Functions.printThrowable(x));
}
try {
Step step = null;
if (o instanceof Step) {
step = (Step) o;
} else {
// Look for a metastep which could take this as its delegate.
for (StepDescriptor d : StepDescriptor.allMeta()) {
if (d.getMetaStepArgumentType().isInstance(o)) {
DescribableModel<?> m = DescribableModel.of(d.clazz);
DescribableParameter soleRequiredParameter = m.getSoleRequiredParameter();
if (soleRequiredParameter != null) {
step = d.newInstance(Collections.singletonMap(soleRequiredParameter.getName(), o));
break;
}
}
}
}
if (step == null) {
return HttpResponses.text("Cannot find a step corresponding to " + o.getClass().getName());
}
String groovy = step2Groovy(step);
if (descriptor instanceof StepDescriptor && ((StepDescriptor) descriptor).isAdvanced()) {
String warning = Messages.Snippetizer_this_step_should_not_normally_be_used_in();
groovy = "// " + warning + "\n" + groovy;
}
return HttpResponses.text(groovy);
} catch (UnsupportedOperationException x) {
Logger.getLogger(CpsFlowExecution.class.getName()).log(Level.WARNING, "failed to render " + json, x);
return HttpResponses.text(x.getMessage());
}
}