Стандартный REST API Jira мощный, но иногда нужны кастомные эндпоинты для специфичных задач: интеграции с внутренними системами, агрегация данных, кастомная бизнес-логика. ScriptRunner позволяет создавать REST API эндпоинты через Groovy-скрипты без разработки плагинов. В этой статье разберу практические примеры создания кастомных API.
Зачем нужны кастомные REST API
Кастомные REST API полезны для:
- Интеграций с внешними системами, которым нужны специфичные данные
- Агрегации данных из нескольких источников
- Выполнения сложных операций, которые не покрывает стандартный API
- Создания упрощённых интерфейсов для мобильных приложений или дашбордов
- Автоматизации процессов через внешние скрипты
ScriptRunner REST эндпоинты создаются через Script Console и настраиваются через веб-интерфейс, что делает их создание быстрее, чем разработка плагинов.
Создание простого GET эндпоинта
Начнём с простого эндпоинта, который возвращает информацию о задаче. Создайте REST эндпоинт через: Administration → ScriptRunner → REST → Scripted REST Endpoints → Create endpoint.
Укажите путь (например, /api/scriptrunner/issue-info/{issueKey}) и метод GET.
Затем добавьте скрипт:
import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonBuilder
def issueManager = ComponentAccessor.getIssueManager()
def issueKey = httpMethod.getPathParameter("issueKey").toString()
def issue = issueManager.getIssueObject(issueKey)
if (!issue) {
return Response.status(404).entity([error: "Issue not found"]).build()
}
def result = [
key: issue.key,
summary: issue.summary,
status: issue.status.name,
assignee: issue.assignee?.displayName,
reporter: issue.reporter?.displayName,
created: issue.created.toString()
]
def json = new JsonBuilder(result).toPrettyString()
return Response.ok(json).type("application/json").build()
Этот эндпоинт можно вызвать через: GET /rest/scriptrunner/latest/custom/issue-info/PROJ-123
Работа с параметрами запроса
Можно получать параметры из query string. Например, эндпоинт для поиска задач с фильтрацией:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import groovy.json.JsonBuilder
def searchService = ComponentAccessor.getComponent(SearchService)
def userManager = ComponentAccessor.getUserManager()
// Получаем параметры запроса
def project = httpMethod.getQueryParameter("project")?.toString()
def status = httpMethod.getQueryParameter("status")?.toString()
if (!project) {
return Response.status(400).entity([error: "project parameter is required"]).build()
}
// Строим JQL запрос
def jql = "project = ${project}"
if (status) {
jql += " AND status = '${status}'"
}
def user = userManager.getUserByKey("admin")
def query = searchService.parseQuery(user, jql)
def results = searchService.search(user, query.query, PagerFilter.getUnlimitedFilter())
def issues = results.results.collect { issue ->
[
key: issue.key,
summary: issue.summary,
status: issue.status.name
]
}
def result = [
total: issues.size(),
issues: issues
]
def json = new JsonBuilder(result).toPrettyString()
return Response.ok(json).type("application/json").build()
POST эндпоинт для создания задач
Можно создавать задачи через POST запрос. Пример эндпоинта для создания задачи из JSON:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueFactory
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.project.ProjectManager
import groovy.json.JsonSlurper
def issueManager = ComponentAccessor.getIssueManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def projectManager = ComponentAccessor.getProjectManager()
def userManager = ComponentAccessor.getUserManager()
// Парсим тело запроса
def requestBody = httpMethod.getRequestBodyAsString()
def json = new JsonSlurper().parseText(requestBody)
// Валидация
if (!json.project || !json.summary || !json.issueType) {
return Response.status(400).entity([error: "Missing required fields"]).build()
}
// Получаем проект
def project = projectManager.getProjectObjByKey(json.project)
if (!project) {
return Response.status(404).entity([error: "Project not found"]).build()
}
// Создаём задачу
def issue = issueFactory.getIssue()
issue.setProjectObject(project)
issue.setIssueTypeId(json.issueType)
issue.setSummary(json.summary)
issue.setDescription(json.description ?: "")
if (json.assignee) {
def assignee = userManager.getUserByKey(json.assignee)
issue.setAssignee(assignee)
}
def currentUser = userManager.getUserByKey("admin")
def createdIssue = issueManager.createIssueObject(currentUser, issue)
def result = [
key: createdIssue.key,
summary: createdIssue.summary,
status: createdIssue.status.name
]
def responseJson = new JsonBuilder(result).toPrettyString()
return Response.status(201).entity(responseJson).type("application/json").build()
Аутентификация и авторизация
По умолчанию REST эндпоинты используют аутентификацию Jira. Можно проверить текущего пользователя:
import com.atlassian.jira.component.ComponentAccessor
def authContext = ComponentAccessor.getJiraAuthenticationContext()
def user = authContext.getLoggedInUser()
if (!user) {
return Response.status(401).entity([error: "Unauthorized"]).build()
}
// Проверка прав
def permissionManager = ComponentAccessor.getPermissionManager()
def project = ComponentAccessor.getProjectManager().getProjectObjByKey("PROJ")
if (!permissionManager.hasPermission(com.atlassian.jira.security.Permissions.CREATE_ISSUES, project, user)) {
return Response.status(403).entity([error: "Forbidden"]).build()
}
// Продолжаем выполнение...
Обработка ошибок
Важно правильно обрабатывать ошибки и возвращать понятные сообщения:
try {
// Основная логика
def result = performOperation()
return Response.ok(new JsonBuilder(result).toPrettyString()).type("application/json").build()
} catch (Exception e) {
log.error("Error in REST endpoint: ${e.message}", e)
def errorResponse = [
error: "Internal server error",
message: e.message
]
return Response.status(500)
.entity(new JsonBuilder(errorResponse).toPrettyString())
.type("application/json")
.build()
}
Агрегация данных
REST API полезны для агрегации данных из разных источников. Например, эндпоинт для статистики проекта:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import groovy.json.JsonBuilder
def searchService = ComponentAccessor.getComponent(SearchService)
def issueManager = ComponentAccessor.getIssueManager()
def userManager = ComponentAccessor.getUserManager()
def projectKey = httpMethod.getPathParameter("projectKey").toString()
def user = userManager.getUserByKey("admin")
// Статистика по статусам
def statuses = ["Open", "In Progress", "Done"]
def statusStats = [:]
statuses.each { status ->
def jql = "project = ${projectKey} AND status = '${status}'"
def query = searchService.parseQuery(user, jql)
def results = searchService.search(user, query.query, PagerFilter.getUnlimitedFilter())
statusStats[status] = results.total
}
// Статистика по типам задач
def issueTypes = ["Bug", "Story", "Task"]
def typeStats = [:]
issueTypes.each { type ->
def jql = "project = ${projectKey} AND type = '${type}'"
def query = searchService.parseQuery(user, jql)
def results = searchService.search(user, query.query, PagerFilter.getUnlimitedFilter())
typeStats[type] = results.total
}
def result = [
project: projectKey,
statistics: [
byStatus: statusStats,
byType: typeStats
]
]
def json = new JsonBuilder(result).toPrettyString()
return Response.ok(json).type("application/json").build()
Интеграция с внешними системами
REST эндпоинты могут быть прокси для внешних систем. Например, получение данных из другой системы и объединение с данными Jira:
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI
def issueKey = httpMethod.getPathParameter("issueKey").toString()
// Получаем данные из Jira
def issueManager = ComponentAccessor.getIssueManager()
def issue = issueManager.getIssueObject(issueKey)
// Получаем данные из внешней системы
def externalApiUrl = "https://external-api.company.com/issues/${issueKey}"
def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
.uri(URI.create(externalApiUrl))
.GET()
.build()
def externalData = [:]
try {
def response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
externalData = new JsonSlurper().parseText(response.body())
}
} catch (Exception e) {
log.warn("Failed to fetch external data: ${e.message}")
}
// Объединяем данные
def result = [
jira: [
key: issue.key,
summary: issue.summary,
status: issue.status.name
],
external: externalData
]
def json = new JsonBuilder(result).toPrettyString()
return Response.ok(json).type("application/json").build()
Кэширование ответов
Для тяжёлых операций можно использовать кэширование:
import com.atlassian.cache.Cache
import com.atlassian.cache.CacheManager
import com.atlassian.jira.component.ComponentAccessor
def cacheManager = ComponentAccessor.getComponent(CacheManager)
def cache = cacheManager.getCache("rest-api-cache", String.class, String.class)
def cacheKey = "stats-${projectKey}-${new Date().clearTime()}"
def cachedResult = cache.get(cacheKey)
if (cachedResult) {
return Response.ok(cachedResult).type("application/json").build()
}
// Выполняем тяжёлые вычисления
def result = computeStatistics()
def json = new JsonBuilder(result).toPrettyString()
// Кэшируем на 1 час
cache.put(cacheKey, json)
return Response.ok(json).type("application/json").build()
Типичные ошибки
Вот ошибки, которые часто допускают при создании REST API:
Ошибка 1: Отсутствие валидации входных данных
Всегда валидируйте входные данные. Не доверяйте данным от клиента — проверяйте все параметры и тело запроса.
Ошибка 2: Неправильная обработка ошибок
Возвращайте понятные сообщения об ошибках с правильными HTTP статус-кодами. Не возвращайте стектрейсы в production.
Ошибка 3: Игнорирование производительности
REST эндпоинты должны выполняться быстро. Для тяжёлых операций используйте кэширование или асинхронную обработку.
Выводы
ScriptRunner REST эндпоинты — мощный инструмент для создания кастомных API без разработки плагинов. Они полезны для интеграций, агрегации данных, упрощённых интерфейсов.
Всегда валидируйте входные данные, правильно обрабатывайте ошибки, учитывайте производительность. Используйте кэширование для тяжёлых операций, проверяйте права доступа пользователей.
Документируйте API для других разработчиков. Используйте стандартные HTTP методы и статус-коды, возвращайте JSON в понятном формате.
Если нужна помощь с созданием REST API — свяжитесь со мной.