Jira的集成有两种方式

第一种:直接使用现有的轮子,也就是封装好的Jira API客户端代码

第二种:通过Http调用Jira的Restful接口

 

两者相比第一种功能更丰富一些,但是要掌握Jira的API学习成本比较高,第二种上手更快一些,但是需要自己编程处理各种Response。

 

我的应用场景和需求:

需要在Jira上配置两个webhook:

第一个WebHook:当主任务的某个环节提交时自动创建子任务,而且子任务的部分属性需要从主任务中来获取。

第二个WebHook:当任务提交时按照我自己的邮件模板向jira中的指定人发送和抄送html格式的邮件。

拆分下具体的功能点:

1 与Jira的鉴权认证

2 根据条件查询Jira任务

3 创建Jira任务

4 html邮件模板

5 发送邮件

 

下面我们来一一分析下具体的代码实现

Jira鉴权认证:

对认证信息进行加密:

@Service
public class JiraAuthServiceImpl implements JiraAuthService{

	@Override
	public String getBasicAuth(String userName, String password) {
		 String basic_auth = "Basic ";
	     basic_auth += Base64.getEncoder().encodeToString((userName + ":" + password).getBytes());
	     return basic_auth;
	}

}

在发送httl请求时将加密后的认证信息放在请求头:

HttpPost httpPost = new HttpPost(restUrl);
httpPost.setHeader("Authorization", authInfo);

查询Jira任务

根据自己Jira属性定制实例对象:

public class JiraEntity {
	
	private String id;
	
	private String key;
	
	private String reporter;
	
	private String summary;

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}

	public String getReporter() {
		return reporter;
	}

	public void setReporter(String reporter) {
		this.reporter = reporter;
	}

	public String getSummary() {
		return summary;
	}

	public void setSummary(String summary) {
		this.summary = summary;
	}
	
	
}
public class JiraReleaseEntity extends JiraEntity{
	
	private String gitProject;
	
	private String onlineGitTag;
	
	private String rollbackGitTag;
	
	private String detail;
	
	private String[] notifyEmail;
	
	private String operator;
	
	private String tester;

	public String getGitProject() {
		return gitProject;
	}

	public void setGitProject(String gitProject) {
		this.gitProject = gitProject;
	}

	public String getOnlineGitTag() {
		return onlineGitTag;
	}

	public void setOnlineGitTag(String onlineGitTag) {
		this.onlineGitTag = onlineGitTag;
	}

	public String getRollbackGitTag() {
		return rollbackGitTag;
	}

	public void setRollbackGitTag(String rollbackGitTag) {
		this.rollbackGitTag = rollbackGitTag;
	}

	public String getDetail() {
		return detail;
	}

	public void setDetail(String detail) {
		this.detail = detail;
	}

	public String[] getNotifyEmail() {
		return notifyEmail;
	}

	public void setNotifyEmail(String[] notifyEmail) {
		this.notifyEmail = notifyEmail;
	}

	public String getOperator() {
		return operator;
	}

	public void setOperator(String operator) {
		this.operator = operator;
	}

	public String getTester() {
		return tester;
	}

	public void setTester(String tester) {
		this.tester = tester;
	}
}

 查询Jira并解析response消息体,封装为任务实例对象:

@Override
	public JiraReleaseEntity findReleaseIssueById(String restUrl, String projectKey, String issueId, String authInfo) {
		
		String postBody = "{\"jql\":\"project = "+ projectKey
		+"&id="+issueId
		+ "\",\"startAt\":0,\"maxResults\":1,\"expand\":[\"renderedFields\"],\"fields\":[\"id\",\"key\",\"reporter\",\"customfield_10151\",\"customfield_10208\", \"customfield_10223\", \"customfield_10245\", \"customfield_10241\", \"summary\", \"customfield_10147\", \"customfield_10148\" ]}";
		
        ResponseHandler<String> handler = new BasicResponseHandler();
        
        HttpPost httpPost = new HttpPost(restUrl);
        httpPost.setHeader("Authorization", authInfo);
        httpPost.setHeader("Content-type", "application/json");
        StringEntity entity = new StringEntity(postBody, "UTF-8");
        entity.setContentType("application/json;charset=UTF-8");
        httpPost.setEntity(entity);
        String responseBody = null;
        HttpResponse response = null;
        try {
        	CloseableHttpClient httpclient = HttpClients.createDefault();
			response = httpclient.execute(httpPost);
		} catch (ClientProtocolException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
        try {
			responseBody = handler.handleResponse(response);
		} catch (ClientProtocolException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
        JiraReleaseEntity jiraEntity = null;
        try
        {
            if (responseBody != null)
            {
                int entityNum = new JSONObject(responseBody).getInt("total");
                if(entityNum > 0) {
                	String issuesString = new JSONObject(responseBody).getString("issues");
                	JSONArray jsonArray = JSONArray.fromObject(issuesString);
                	if (jsonArray != null && jsonArray.size() > 0) {
                		jiraEntity = new JiraReleaseEntity();
            			jiraEntity.setId(jsonArray.getJSONObject(0).getString("id"));
            			jiraEntity.setKey(jsonArray.getJSONObject(0).getString("key"));
            			jiraEntity.setSummary(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("summary"));
            			jiraEntity.setGitProject(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10208"));
            			jiraEntity.setOnlineGitTag(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10151"));
            			jiraEntity.setRollbackGitTag(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10223"));
            			jiraEntity.setReporter(new JSONObject(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("reporter")).getString("name"));
            			jiraEntity.setOperator(new JSONObject(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10147")).getString("name"));
            			jiraEntity.setTester(new JSONObject(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10148")).getString("name"));
            			jiraEntity.setDetail(new JSONObject(jsonArray.getJSONObject(0).getString("renderedFields")).getString("customfield_10241"));
            			String emailObject = new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10245");
            			if(emailObject!=null) {
            				JSONArray emailArray = JSONArray.fromObject(new JSONObject(jsonArray.getJSONObject(0).getString("fields")).getString("customfield_10245"));
                			if(emailArray!=null && emailArray.size()>0) {
                				String[] emails = new String[emailArray.size()];
                				for(int i=0; i<emailArray.size(); i++) {
                					emails[i]= emailArray.getJSONObject(i).getString("value");
                				}
                				jiraEntity.setNotifyEmail(emails);
                			}
            			}
         			
                	}
                }
            }
        } catch (JSONException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return jiraEntity;
    }

重点:

1 jql就像hsql、sql意义一样,发送查询条件的,startAt、maxResults做分页,fields指定返回属性,expand讲邮件时再说。

2 jira中自定义的字段在代码中是以customfield_XXXX这种形式唯一标识的

3 在对返回值做Json解析时要特别留意时间、人员、下拉列表框等各种复杂类型,并不像文本类型那么简单

4 如果请求报错bad request 404,基本就是jql或fields有问题,可以现在jira页面上尝试操作一次。例如字段在界面不存在但是你代码中又使用到了,就会bad reqeust。

创建Jira任务

构造post请求的报文:

@Override
	public String constructFindIssuePostBody(Map<String,String> map) {
		String postBody = "{\"fields\": { \"project\": { \"key\": \"" + map.get("projectKey") + "\" }, \"parent\":{\"key\": \""+ map.get("parentIssueKey") +"\"},"
				+ "\"reporter\": {\"name\": \""+ map.get("reporter") +"\"}, "
                + "\"summary\": \""+map.get("summary")+"\", "
                + "\"customfield_10151\": \""+ map.get("gitOnlineTag") +"\", "
                + "\"customfield_10208\": \""+ map.get("gitProject") +"\", "
                + "\"customfield_10223\": \""+ map.get("gitRollbackTag") +"\", "
                + "\"issuetype\": {\"id\": \""+ map.get("issuetype") +"\"}"
                + "}}";
		return postBody;
	}

将报文推送给Jira:

 

@Override
	public String createIssue(String restUrl, String postBody, String authInfo) {
        ResponseHandler<String> handler = new BasicResponseHandler();
        
        HttpPost httpPost = new HttpPost(restUrl);
        httpPost.setHeader("Authorization", authInfo);
        httpPost.setHeader("Content-type", "application/json");
        StringEntity entity = new StringEntity(postBody, "UTF-8");
        entity.setContentType("application/json;charset=UTF-8");
        httpPost.setEntity(entity);
        String responseBody = null;
        HttpResponse response = null;
        try {
        	CloseableHttpClient httpclient = HttpClients.createDefault();
			response = httpclient.execute(httpPost);
		} catch (ClientProtocolException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
        try {
			responseBody = handler.handleResponse(response);
		} catch (ClientProtocolException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
        String ticketNum = null;
        try
        {
            if (responseBody != null)
            {
                ticketNum = new JSONObject(responseBody).getString("key");
            }
        } catch (JSONException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("Ticket Number: " + ticketNum);
        return ticketNum;
    }

这里同样需要注意bad reqeust和自定义字段唯一标识的问题。 

Html邮件模板

Python常用的模板是Jinjia2,Jiava中我们使用VelocityEngine

private String mergeMessage(String velocity, Map<String, Object> params) {
		VelocityEngine ve = new VelocityEngine();
		ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
		ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
		ve.init();
		// 载入(获取)模板对象
		Template t = ve.getTemplate(velocity);
		VelocityContext ctx = new VelocityContext(params);
		StringWriter sw = new StringWriter();
		t.merge(ctx, sw);
		return sw.toString();
	}

 velocity文件release_html.vm

<div id="container" style="width:500px">

<div id="header" style="background-color:#FFFFFF;text-align:center;">
<h3 style="margin-bottom:0;">${title}</h3></div>

<div id="content" style="background-color:#FFFFFF;float:center;">
${detail}
<p>
项目地址:  ${git}
</p>
<p>
新版本:  ${version}
</p>
<p>
jira链接:<a href="http://jira.imgo.tv/browse/${key}">${key}</a>
</p>
</div>
<div id="footer" style="background-color:#FFFFFF;clear:both;text-align:right;">
<i>该邮件由上线助手自动生成</i></div>

</div>

pom引用:

<dependency>
     	<groupId>org.apache.velocity</groupId>
     	<artifactId>velocity-engine-core</artifactId>
     	<version>2.0</version>
</dependency>

 

发送邮件

采用最通用的spring boot email

@Override
	public void sendSimpleEmail(String eFrom, String[] eTo, String[] eCc, String title, String velocity, Map<String, Object> params) {
		SimpleMailMessage message = new SimpleMailMessage();
		message.setFrom(eFrom);
		message.setTo(eTo);
		if (!StringUtils.isEmpty(eCc)) {
			message.setCc(eCc);
		}
		message.setSubject(title);
		message.setText(mergeMessage(velocity, params));
		mailSender.send(message);

	}

@Override
	public void sendHtmlEmail(String eFrom, String[] eTo, String[] eCc, String title, String velocity,
			Map<String, Object> params) {
		MimeMessage message=mailSender.createMimeMessage();
		MimeMessageHelper helper;
		try {
			helper = new MimeMessageHelper(message,true);
			helper.setFrom(eFrom);
	        helper.setTo(eTo);
	        helper.setSubject(title);
	        if (!StringUtils.isEmpty(eCc)) {
	        	helper.setCc(eCc);
			}
	        helper.setText(mergeMessage(velocity, params),true);
	        mailSender.send(message);
		} catch (MessagingException e) {
			logger.error(e.getMessage());
			e.printStackTrace();
		}	
	}

前面留了一个坑:查询jira的请求中有一个expend设置了一个renderedFields属性,这个是干什么用的呢?Jira的复杂文本框属性(例如描述、common)支持自带的渲染器,可以对文字颜色、大小、段落等进行编辑和渲染,但是它使用的是Jira内置的格式并不是标准的html,所以需要借助renderedFields对需要渲染的内容进行一次html的转换。如果你只发送text的邮件内容可以把这里的expend去掉。

pom引用:

<dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

 

关于Jira集成的核心是要掌握Restful接口,常用的查询相关是baseUrl+"/rest/api/2/search"这种格式开头,任务相关是baseUrl+"/rest/api/2/issue"开头。

本文完成代码下载地址:https://github.com/yejingtao/ci-jiratool