Jira API的踩坑记

我本来是想写篇文章,吐槽一下jira的api的,但是发现最终jira api,很多地方又让我学到了一些新知识。有些方面真的是没见过这么标准使用的。可能是我之前孤陋寡闻啦,所以本文的内容不仅仅是讲jira的坑, 还有一些是jira本身优良的品性,不仅让我学到了一些知识,也让我对规范有了新的理解。 本文的内容算是对我最近这段时间以来对接jira API的经验总结,希望能对各位有所帮助。

没有中文

这个是对我来说最大的困难,本身我的英文水平不好之前,阅读文档或者说是一些文章都是直接一键翻译,但是碰到jira的API文档就有点蒙逼了。 本来我以为在国内有很多公司都在用jira, 这里面少不了的API接口进行功能性的封装,肯定会有中文的文档结果经过几次尝试搜索之后,我终于确认jira API是没有中文文档的。

我使用的一键翻译软件是浏览器自带的尝试过一些,他们总是会把接口请求路径中的英文单词也翻译成汉字,这简直就是不能看。 虽然如此,我还是需要中文翻译和英文原文对照着看,因为有些地方翻译成中文之后语序不是那么通顺。总体来说,没有中文文档对App的接入还是有挺大影响的,因为需要不断地去对照着英文原文和理解翻译之后的结果。

在我搜索中文文档的过程中,我看到网上有很多人对API的实现进行了分享,对我来说还是有点大帮助的。 但内容比较少,仅限于两三个特别常用的API。 没有人完整的翻译过jira API的文档,然后我发现了一个巨坑的事情: jira App文档分嗯多个版本,基本上每一个版本的基拉就对应一个版本的API文档,我没有仔细去看这里面的区别,但是我觉得一个版本一个文档,着实有点坑了。 大家如果有机会对接jira API文档,到时候一定要首先确认jira的版本。

HTTPcode

在jira API文档中,http协议响应状态码有很多使用。在我之前的工作经历中,很少注意到http响应状态码这个数据。 因为大多数情况都是成功的话,返回200,不成功的话也是返回200(通过业务状态码来区分不同原因), 只有在接口请求失败,或者说服务器故障的时候会处理一下400和500系列的响应状态码。 在对接Jira API文档的过程中,我遇到了很多种之前没有接触过的200系列的http协议响应状态码。Jira API 是通过http,响应状态码来表示业务处理状态,他并没有使用业务状态码。 所以,在对接的过程中,需要单独处理每个接口的http响应状态码。

在POST和PUT全球方法的接口, 很少能看到200的状态响应码。 下面分享一下,我常见到的201和204状态响应码的标准规范。

201 Created

请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,且其 URI 已经随Location 头信息返回。假如需要的资源无法及时建立的话,应当返回 '202 Accepted'。

204 No Content

服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。

响应不统一

在之前文章一起吐槽接口文档中, 我吐槽了一下,接口文档最坑的就是响应不统一,没想到在对接Jira文档的时候就出现了特别多这样的实践机会。我曾经一度怀疑Jira文档是不是故意这么做的,因为各个接口的响应结果。均为json形式,但是最外层的json响应结构。有点1000个接口有1000个响应的即视感。

我之前写项目测试框架的时候,都会对响应结果进行统一的json格式处理,但是对于Jira的api就没有办法使用统一的格式处理,每一个接口都需要进行单独的处理。这无疑也增加了工作量。下面分享我遇到的几种响应结构。

{
    "issues": [
        {
            "id": "10000",
            "key": "TST-24",
            "self": "http://www.example.com/jira/rest/api/2/issue/10000"
        },
        {
            "id": "10001",
            "key": "TST-25",
            "self": "http://www.example.com/jira/rest/api/2/issue/10001"
        }
    ],
    "errors": []
}
{
    "id": "10000",
    "key": "TST-24",
    "self": "http://www.example.com/jira/rest/api/2/issue/10000"
}

下面这种就属于比较霸王级别的难看。一个响应结构体去。表示编辑之后的issues的状态。结果没想到在JSON对象中包了这么多层。为了让文章能缩短一下,我把里数组重复的内容给删除了,但是还是有这么复杂的响应结构体,简直就是丧心病狂!

{
    "id": "https://docs.atlassian.com/jira/REST/schema/issue-update#",
    "title": "Issue Update",
    "type": "object",
    "properties": {
        "transition": {
            "title": "Transition",
            "type": "object",
            "properties": {
                "id": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "to": {
                    "title": "Status",
                    "type": "object",
                    "properties": {
                        "statusColor": {
                            "type": "string"
                        },
                        "description": {
                            "type": "string"
                        },
                        "iconUrl": {
                            "type": "string"
                        },
                        "name": {
                            "type": "string"
                        },
                        "id": {
                            "type": "string"
                        },
                        "statusCategory": {
                            "title": "Status Category",
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "integer"
                                },
                                "key": {
                                    "type": "string"
                                },
                                "colorName": {
                                    "type": "string"
                                },
                                "name": {
                                    "type": "string"
                                }
                            },
                            "additionalProperties": false
                        }
                    },
                    "additionalProperties": false
                },
                "fields": {
                    "type": "object",
                    "patternProperties": {
                        ".+": {
                            "title": "Field Meta",
                            "type": "object",
                            "properties": {
                                "required": {
                                    "type": "boolean"
                                },
                                "schema": {
                                    "title": "Json Type",
                                    "type": "object",
                                    "properties": {
                                        "type": {
                                            "type": "string"
                                        },
                                        "items": {
                                            "type": "string"
                                        },
                                        "system": {
                                            "type": "string"
                                        },
                                        "custom": {
                                            "type": "string"
                                        },
                                        "customId": {
                                            "type": "integer"
                                        }
                                    },
                                    "additionalProperties": false
                                },
                                "name": {
                                    "type": "string"
                                },
                                "autoCompleteUrl": {
                                    "type": "string"
                                },
                                "hasDefaultValue": {
                                    "type": "boolean"
                                },
                                "operations": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    }
                                },
                                "allowedValues": {
                                    "type": "array",
                                    "items": {}
                                }
                            },
                            "additionalProperties": false,
                            "required": [
                                "required"
                            ]
                        }
                    },
                    "additionalProperties": false
                }
            },
            "additionalProperties": false
        },
        "fields": {
            "type": "object",
            "patternProperties": {
                ".+": {}
            },
            "additionalProperties": false
        },
        "update": {
            "type": "object",
            "patternProperties": {
                ".+": {
                    "type": "array",
                    "items": {
                        "title": "Field Operation",
                        "type": "object"
                    }
                }
            },
            "additionalProperties": false
        },
        "historyMetadata": {
            "title": "History Metadata",
            "type": "object",
            "properties": {
                "type": {
                    "type": "string"
                },
                "description": {
                    "type": "string"
                },
                "descriptionKey": {
                    "type": "string"
                },
                "activityDescription": {
                    "type": "string"
                },
                "activityDescriptionKey": {
                    "type": "string"
                },
                "emailDescription": {
                    "type": "string"
                },
                "emailDescriptionKey": {
                    "type": "string"
                },
                "actor": {
                    "$ref": "#/definitions/history-metadata-participant"
                },
                "generator": {
                    "$ref": "#/definitions/history-metadata-participant"
                },
                "cause": {
                    "$ref": "#/definitions/history-metadata-participant"
                },
                "extraData": {
                    "type": "object",
                    "patternProperties": {
                        ".+": {
                            "type": "string"
                        }
                    },
                    "additionalProperties": false
                }
            },
            "additionalProperties": false
        },
        "properties": {
            "type": "array",
            "items": {
                "title": "Entity Property",
                "type": "object",
                "properties": {
                    "key": {
                        "type": "string"
                    },
                    "value": {}
                },
                "additionalProperties": false
            }
        }
    },
    "definitions": {
        "history-metadata-participant": {
            "title": "History Metadata Participant",
            "type": "object",
            "properties": {
                "id": {
                    "type": "string"
                },
                "displayName": {
                    "type": "string"
                },
                "displayNameKey": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                },
                "avatarUrl": {
                    "type": "string"
                },
                "url": {
                    "type": "string"
                }
            },
            "additionalProperties": false
        }
    },
    "additionalProperties": false
}

包装过度

在通常所用到的接口文档中的接口参数,一般都是通过key value形式去传参。比较复杂的,可能会用到数组。但是在对接Jira文档的时候,我发现完全不能以之前的思维惯性去理解Jira API文档中的接口参数传递方式。如果说通常接口参数通过JSON包装一层的话,那么Jira文档的接口参数就是里三层外三层。下面我通过几个实例给大家真实的再现一下鸡爪文档中接口参数的复杂性。

{
    "update": {
        "worklog": [
            {
                "add": {
                    "timeSpent": "60m",
                    "started": "2011-07-05T11:05:00.000+0000"
                }
            }
        ]
    },
    "fields": {
        "project": {
            "id": "10000"
        },
        "summary": "something's wrong",
        "issuetype": {
            "id": "10000"
        },
        "assignee": {
            "name": "homer"
        },
        "reporter": {
            "name": "smithers"
        },
        "priority": {
            "id": "20000"
        },
        "labels": [
            "bugfix",
            "blitz_test"
        ],
        "timetracking": {
            "originalEstimate": "10",
            "remainingEstimate": "5"
        },
        "security": {
            "id": "10000"
        },
        "versions": [
            {
                "id": "10000"
            }
        ],
        "environment": "environment",
        "description": "description",
        "duedate": "2011-03-11",
        "fixVersions": [
            {
                "id": "10001"
            }
        ],
        "components": [
            {
                "id": "10000"
            }
        ],
        "customfield_30000": [
            "10000",
            "10002"
        ],
        "customfield_80000": {
            "value": "red"
        },
        "customfield_20000": "06/Jul/11 3:25 PM",
        "customfield_40000": "this is a text field",
        "customfield_70000": [
            "jira-administrators",
            "jira-software-users"
        ],
        "customfield_60000": "jira-software-users",
        "customfield_50000": "this is a text area. big text.",
        "customfield_10000": "09/Jun/81"
    }
}
{
    "startAt": 0,
    "maxResults": 1,
    "total": 1,
    "comments": [
        {
            "self": "http://www.example.com/jira/rest/api/2/issue/10010/comment/10000",
            "id": "10000",
            "author": {
                "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
                "name": "fred",
                "displayName": "Fred F. User",
                "active": false
            },
            "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget venenatis elit. Duis eu justo eget augue iaculis fermentum. Sed semper quam laoreet nisi egestas at posuere augue semper.",
            "updateAuthor": {
                "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
                "name": "fred",
                "displayName": "Fred F. User",
                "active": false
            },
            "created": "2016-09-27T09:43:02.795+0000",
            "updated": "2016-09-27T09:43:02.795+0000",
            "visibility": {
                "type": "role",
                "value": "Administrators"
            }
        }
    ]
}

以上两个是我用遇到的比较麻烦的两个接口的传参方式,大家看起来可能会比较复杂。也有点儿迷惑,但其实这两个接口都不算。最复杂的,因为他案例中这些参数的值大部分是可以不传的。Jira API文档中 最让我感觉到不爽的,还不是这种里山城外三成的包装方式, 而是同一个参数,可能会出现在多个包装结构中。 而且这些包装结构的作用范围并没有在文档中标识出来,导致我想去查一个参数,并不知道两个地方现在多个地方到底哪个地方有用只能去一各一各的尝试,虽然对接文档的工作已经完成了,但是对于文档中所标记的参数以及传参格式部分字段依然稀里糊涂。反正功能已经实现啦,就先不去管它啦。

Demo错误

接口文档中最难以让人忍受的就是接口文档中存在着硬性错误。本来对接接口文档已经是一个比较麻烦的事情了。如果文档中出现一些硬性的错误。会让我付出更多的时间和精力去纠正这些错误,如果再碰到非常复杂的包装格式,就更让人恼火了。回到刚才提到过的Jira api文档,有非常多个版本,如果文档出现错误,修复起来肯定也是比较多的。我一度认为他这个文档就是通过工具直接生成的。跟源码中的文档标记很相似。

下面分享一条文档中的错误,这是一个接口传参格式的Demo。乍一看其实没什么问题,但是这其实并不是JSON的标准格式。在我们阅读文档的时候首先就,首先就是要解析出这个中接口传参格式的JSON展示,我们才能知道具体在JSON好在传参的时候,在哪一层去传什么样的参数。

{"update":{"summary":[{"set":"Bug in business logic"}],"components":[{"set":""}],"timetracking":[{"edit":{"originalEstimate":"1w 1d","remainingEstimate":"4d"}}],"labels":[{"add":"triaged"},{"remove":"blocker"}]},"fields":{"summary":"This is a shorthand for a set operation on the summary field","customfield_10010":1,"customfield_10000":"This is a shorthand for a set operation on a text custom field"},"historyMetadata":{"type":"myplugin:type","description":"text description","descriptionKey":"plugin.changereason.i18.key","activityDescription":"text description","activityDescriptionKey":"plugin.activity.i18.key","actor":{"id":"tony","displayName":"Tony","type":"mysystem-user","avatarUrl":"http://mysystem/avatar/tony.jpg","url":"http://mysystem/users/tony"},"generator":{"id":"mysystem-1","type":"mysystem-application"},"cause":{"id":"myevent","type":"mysystem-event"},"extraData":{"keyvalue":"extra data","goes":"here"}},"properties":[{"key":"key1","value":'properties' : 'can be set at issue create or update time'},{"key":"key2","value":'and' : 'there can be multiple properties'}]}

其实问题出现在最后一个"properties":[{"key":"key1","value":'properties' : 'can be set at issue create or update time'},{"key":"key2","value":'and' : 'there can be multiple properties'}],后面的can be其实是对这个参数的描述,不知道怎么到了接口的传参Demo里面了。着实让人头大。

中英混排

Jira的api文档都是英文版的,我一度怀疑他并不重视中国区用户,但是当我看到某些接口的字段值的时候,我一下子震惊了,原来他的字段值还是中英混排的。一下子有点儿不知所措。如图所示:

jira上issue状态

issue状态中居然有中文。真是让我感动的痛哭涕零,这都什么玩意儿。

POST PUT

在我之前的工作当中,主要接触的还是get和post接口。对于其他HTTP请求方式并不十分了解,也不太清楚这其中的规范。一直以来的概念就是获取数据用get,修改数据用post。但是在接触Jira API文档的过程中,我仔细地看了看post和PUT的区别。总结如下:

  • 创建用POST,修改PUT
  • POST非幂等,PUT幂等

在传参格式上,post和put都一样。设置请求entity的方式也是一样的,在Java代码中是通用的,原因在于org.apache.http.client.methods.HttpPutorg.apache.http.client.methods.HttpPost都继承于抽象类org.apache.http.client.methods.HttpEntityEnclosingRequestBase。这个小知识点我又重新学习了一遍。

Have Fun ~ Tester !