前面一篇,我已经介绍了如何在钉钉提醒这个开源插件的一次比较少的代码改动,实现了我们自定义打包后下载的url跳转功能。今天,我们来再次做一个自定义二次开发,主要的需求就是,在钉钉提醒中,加入一个打包环境的字段显示。文字描述不清楚,没有关系,我们来看看需求前后的图片对比就应该很明白。


1.具体需求

需求前的效果


需求后的效果

其中 pre-online这个地方是一个变量的值,每次的值取决于打包人员选择的值,这个值来自job的参数化构建


2. 解决思路和遇到问题

       贴上这几张图,我们就很快明白这个需求是要实现什么样的功能了。因为前面一篇,我们实现了用户输入一个打包后下载地址,然后能够得到这个地址。所以,我们第一个反应就是在jetty配置文件,新加一个文本输入框,用来保存用户输入的打包类型的字段。如下图

       根据我们前面一篇介绍获取打包下载路径的方式,我们能够很快做出上面这个效果。但是这个难点就是,如果打包类型文本输入框输入的是一个变量,这个怎么解决呢。

我根据上一篇的知识,写的代码,效果如下

       本篇的重点就是如何解决这个地方显示变量名称,而不显示变量的值的问题。这个问题blocked了我好长时间,都快放弃了,之后网上搜索文章,涉及到这个知识点,但是原文作者写的不详细,就给出了他认为的关键两行代码。这期间,我通过看xcdoe打包和android gradle打包开源插件的代码,主要是观察类似文本输入框能接收变量输入的字段是如何写的,结果可能由于代码能力不够,还是看不明白,解决不了问题。最后,在testhome这个网站,找到发这篇文章作者,QQ截图给我看,通过这个截图,我找到了解决问题的思路,最后终于解决。下面就是解决问题的完整代码,希望给有类似需求的人给一些帮助和解决问题的思路。


3.代码实现

这里直接贴代码,具体项目结构,请看上一篇,或者钉钉提醒这个插件的github地址。

3.1 jelly配置文件,控制前端UI显示效果

config.jelly

<?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">
  <!--
    This jelly script is used for per-project configuration.

    See global.jelly for a general discussion about jelly script.
  -->

  <f:entry title="jenkins URL" field="jenkinsURL">
    <f:textbox default="${descriptor.getDefaultURL()}" />
  </f:entry>

  <f:entry title="打包后下载路径" field="buildDownloadURL">
      <f:textbox  />
  </f:entry>

  <f:entry title="打包类型" field="envType" description="自动获取参数化构建,表示打包环境.">
      <f:textbox  />
  </f:entry>

  <f:entry title="钉钉access token" field="accessToken">
    <f:textbox  />
  </f:entry>
  <f:entry title="在启动构建时通知">
      <f:checkbox name="onStart" value="true" checked="${instance.isOnStart()}"/>
  </f:entry>
  <f:entry title="构建成功时通知">
      <f:checkbox name="onSuccess" value="true" checked="${instance.isOnSuccess()}"/>
  </f:entry>
  <f:entry title="构建失败时通知">
      <f:checkbox name="onFailed" value="true" checked="${instance.isOnFailed()}"/>
  </f:entry>
</j:jelly>
注意field="envType"这个字段,待会我们需要在代码里用这个字段,两个地方需要保存一样的变量名称。

3.2 DingdingNotifier.java

package com.ztbsuper.dingding;


import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;

import java.io.IOException;

/**
 * Created by Marvin on 16/8/25.
 */
public class DingdingNotifier extends Notifier {

    private EnvVars env;
    private String accessToken;
    private boolean onStart;
    private boolean onSuccess;
    private boolean onFailed;
    private String jenkinsURL;
    private String buildDownloadURL;
    public String envType;



    public String getEnvType() {
        return env.expand(envType);
    }

    public String getJenkinsURL() {
        return jenkinsURL;
    }

    public String getBuildDownloadURL () {
        return buildDownloadURL;
    }

    public boolean isOnStart() {
        return onStart;
    }

    public boolean isOnSuccess() {
        return onSuccess;
    }

    public boolean isOnFailed() {
        return onFailed;
    }

    public String getAccessToken() {
        return accessToken;
    }

    @DataBoundConstructor
    public DingdingNotifier(String envType, String accessToken, boolean onStart, boolean onSuccess, boolean onFailed, String jenkinsURL, String buildDownloadURL) {
        super();
        this.accessToken = accessToken;
        this.onStart = onStart;
        this.onSuccess = onSuccess;
        this.onFailed = onFailed;
        this.jenkinsURL = jenkinsURL;
        this.buildDownloadURL = buildDownloadURL;
        this.envType = envType;

    }

    public DingdingService newDingdingService(AbstractBuild build, TaskListener listener) {
        return new DingdingServiceImpl(envType,jenkinsURL, buildDownloadURL, accessToken, onStart, onSuccess, onFailed, listener, build);
    }

    @Override
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }

    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
        env = build.getEnvironment(listener);
        env.overrideAll(build.getBuildVariables());
        return true;
    }


    @Override
    public DingdingNotifierDescriptor getDescriptor() {
        return (DingdingNotifierDescriptor) super.getDescriptor();
    }

    @Extension
    public static class DingdingNotifierDescriptor extends BuildStepDescriptor<Publisher> {


        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }

        @Override
        public String getDisplayName() {
            return "钉钉通知器配置";
        }

        public String getDefaultURL() {
            Jenkins instance = Jenkins.getInstance();
            assert instance != null;
            if(instance.getRootUrl() != null){
                return instance.getRootUrl();
            }else{
                return "";
            }
        }

    }
}
这个地方的envType可以控制,在jelly前端打包类型文本输入框显示我们的变量的值,核心代码有


3.3 DingdingServiceImpl.java代码

package com.ztbsuper.dingding;

import com.alibaba.fastjson.JSONObject;
import hudson.EnvVars;
import hudson.ProxyConfiguration;
import hudson.model.AbstractBuild;
import hudson.model.TaskListener;
import jenkins.model.Jenkins;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 * Created by Marvin on 16/10/8.
 */
public class DingdingServiceImpl implements DingdingService {

    private Logger logger = LoggerFactory.getLogger(DingdingService.class);

    private String jenkinsURL;

    private String envType;

    private String buildDownloadRUL;

    private boolean onStart;

    private boolean onSuccess;

    private boolean onFailed;

    private TaskListener listener;

    private AbstractBuild build;

    private static final String apiUrl = "https://oapi.dingtalk.com/robot/send?access_token=";

    private String api;
    private EnvVars env;

    public String getEnvType() throws IOException, InterruptedException {
        env = build.getEnvironment(listener);
        return env.expand(envType);
    }


    public DingdingServiceImpl(String envType, String jenkinsURL, String buildDownloadURL, String token, boolean onStart, boolean onSuccess, boolean onFailed, TaskListener listener, AbstractBuild build) {
        this.jenkinsURL = jenkinsURL;
        this.envType = envType;
        this.buildDownloadRUL = buildDownloadURL;
        this.onStart = onStart;
        this.onSuccess = onSuccess;
        this.onFailed = onFailed;
        this.listener = listener;
        this.build = build;
        this.api = apiUrl + token;
    }

    @Override
    public void start() {
        String pic = "http://icon-park.com/imagefiles/loading7_gray.gif";
        String title = String.format("%s%s开始构建", build.getProject().getDisplayName(), build.getDisplayName());
        String content = String.format("项目[%s%s]开始构建", build.getProject().getDisplayName(), build.getDisplayName());

        String link = getBuildUrl();
        if (onStart) {
            logger.info("send link msg from " + listener.toString());
            sendLinkMessage(link, content, title, pic);
        }

    }

    private String getBuildUrl() {
        if (jenkinsURL.endsWith("/")) {
            return jenkinsURL + build.getUrl();
        } else {
            return jenkinsURL + "/" + build.getUrl();
        }
    }

    private String getBuildDownloadRUL() {
        return buildDownloadRUL;
    }


    @Override
    public void success() throws IOException, InterruptedException {
        String pic = "http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-check-icon.png";
        String title = String.format("%s%s构建成功,本次打包环境:%s", build.getProject().getDisplayName(), build.getDisplayName(),getEnvType());
        String content = String.format("项目[%s%s]构建成功, summary:%s, duration:%s", build.getProject().getDisplayName(), build.getDisplayName(), build.getBuildStatusSummary().message, build.getDurationString());

        String link = getBuildDownloadRUL();
        logger.info(link);
        if (onSuccess) {
            logger.info("send link msg from " + listener.toString());
            sendLinkMessage(link, content, title, pic);
        }
    }

    @Override
    public void failed() throws IOException, InterruptedException {
        String pic = "http://www.iconsdb.com/icons/preview/soylent-red/x-mark-3-xxl.png";
        String title = String.format("%s%s构建失败,本次打包环境:%s", build.getProject().getDisplayName(), build.getDisplayName(), getEnvType());
        String content = String.format("项目[%s%s]构建失败, summary:%s, duration:%s", build.getProject().getDisplayName(), build.getDisplayName(), build.getBuildStatusSummary().message, build.getDurationString());

        String link = getBuildUrl();
        logger.info(link);
        if (onFailed) {
            logger.info("send link msg from " + listener.toString());
            sendLinkMessage(link, content, title, pic);
        }
    }

    private void sendTextMessage(String msg) {

    }

    private void sendLinkMessage(String link, String msg, String title, String pic) {
        HttpClient client = getHttpClient();
        PostMethod post = new PostMethod(api);

        JSONObject body = new JSONObject();
        body.put("msgtype", "link");


        JSONObject linkObject = new JSONObject();
        linkObject.put("text", msg);
        linkObject.put("title", title);
        linkObject.put("picUrl", pic);
        linkObject.put("messageUrl", link);

        body.put("link", linkObject);
        try {
            post.setRequestEntity(new StringRequestEntity(body.toJSONString(), "application/json", "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            logger.error("build request error", e);
        }
        try {
            client.executeMethod(post);
            logger.info(post.getResponseBodyAsString());
        } catch (IOException e) {
            logger.error("send msg error", e);
        }
        post.releaseConnection();
    }


    private HttpClient getHttpClient() {
        HttpClient client = new HttpClient();
        Jenkins jenkins = Jenkins.getInstance();
        if (jenkins != null && jenkins.proxy != null) {
            ProxyConfiguration proxy = jenkins.proxy;
            if (proxy != null && client.getHostConfiguration() != null) {
                client.getHostConfiguration().setProxy(proxy.name, proxy.port);
                String username = proxy.getUserName();
                String password = proxy.getPassword();
                // Consider it to be passed if username specified. Sufficient?
                if (username != null && !"".equals(username.trim())) {
                    logger.info("Using proxy authentication (user=" + username + ")");
                    client.getState().setProxyCredentials(AuthScope.ANY,
                            new UsernamePasswordCredentials(username, password));
                }
            }
        }
        return client;
    }
}
注意观察我在打包成功和打包失败方法,添加了本篇开头介绍的需求,重点是看getEnvType方法的写法。

    public String getEnvType() throws IOException, InterruptedException {
        env = build.getEnvironment(listener);
        return env.expand(envType);
    }
原来,在不同类中,我们都需要从全部变量中找envType这个变量的值,不然,在构造JSON字符串发送钉钉提醒,得到的就是${EnvType},而不是变量的具体值。

4.测试效果

前端打包完成后,你会发现当前保存的是${EnvType}上一次用户选择打包类型的值。


       但是这个结果不会影响到你下一次打包,因为当前文本输入框还是记住了你配置的时候填写的${EnvType},这个会随着每次打包选择不同类型而发生变化。本次二次开发举例就介绍完成,你可以试试,在钉钉消息提醒,加入一个字段,例如获取jenkins当前打包人员的账号,显示每次打包是那个人触发的构建。

       2017-12-20添加备注:上面每次构建后在钉钉插件UI打包类型文本输入框显示变量的值没有显示${EnvType},这个是一个bug,解决方法是这样的,找到DingdingNotifier.java下的getEnvType方法,return 语句改成return envType,这个时候就不需要去环境变量池去取出变量的值,因为我们在钉钉提醒插件前端UI打包类型文本输入框,还是建议一致保持变量的显示,这样可能避免疑惑。


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐