Gymterview
middle

Как интегрировать внешнее Spring-приложение с Jira?

Интеграция внешнего Spring Boot приложения с Jira — частая задача enterprise-разработки, включающая синхронизацию задач, автоматизацию workflow и агрегацию данных.

Архитектура интеграции

Пример
┌─────────────────┐         REST API          ┌──────────┐
│  Spring Boot    │ ──────────────────────────→│   Jira   │
│  Application    │ ←─────── Webhooks ─────── │  DC/Cloud│
│                 │                            │          │
│  - JiraClient   │         Events             │          │
│  - WebhookCtrl  │ ←──── (push model) ─────  │          │
│  - SyncService  │                            │          │
└─────────────────┘                            └──────────┘

Конфигурация (application.yml)

Пример
jira:
  base-url: https://jira.company.com
  api-version: 2
  auth:
    type: pat  # pat | api-token | oauth2
    token: ${JIRA_PAT}
  connection:
    connect-timeout: 5s
    read-timeout: 10s
    max-connections: 20
  retry:
    max-attempts: 3
    backoff: 1s

Jira Client с retry и error handling

Код JiraClientConfig и JiraClient
@Configuration
public class JiraClientConfig {

    @Bean
    public RestClient jiraRestClient(JiraProperties props) {
        return RestClient.builder()
                .baseUrl(props.getBaseUrl() + "/rest/api/" + props.getApiVersion())
                .defaultHeader("Authorization", "Bearer " + props.getAuth().getToken())
                .defaultHeader("Content-Type", "application/json")
                .defaultHeader("Accept", "application/json")
                .build();
    }
}

@Service
public class JiraClient {

    private static final Logger log = LoggerFactory.getLogger(JiraClient.class);
    private final RestClient restClient;
    private final RetryTemplate retryTemplate;

    public JiraClient(RestClient jiraRestClient) {
        this.restClient = jiraRestClient;
        this.retryTemplate = RetryTemplate.builder()
                .maxAttempts(3)
                .exponentialBackoff(1000, 2.0, 10000)
                .retryOn(RestClientException.class)
                .build();
    }

    public JiraIssue getIssue(String issueKey) {
        return retryTemplate.execute(ctx -> {
            log.debug("Запрос задачи {}, попытка {}", issueKey, ctx.getRetryCount() + 1);
            return restClient.get()
                    .uri("/issue/{key}", issueKey)
                    .retrieve()
                    .body(JiraIssue.class);
        });
    }

    public List<JiraIssue> searchByJql(String jql) {
        List<JiraIssue> allIssues = new ArrayList<>();
        int startAt = 0;
        int maxResults = 50;
        int total;

        do {
            SearchRequest request = new SearchRequest(jql, startAt, maxResults,
                    List.of("summary", "status", "assignee"));

            SearchResult result = retryTemplate.execute(ctx ->
                    restClient.post()
                            .uri("/search")
                            .body(request)
                            .retrieve()
                            .body(SearchResult.class));

            allIssues.addAll(result.getIssues());
            total = result.getTotal();
            startAt += maxResults;
        } while (startAt < total);

        return allIssues;
    }
}

Синхронизация данных

Код JiraSyncService
@Service
public class JiraSyncService {

    private final JiraClient jiraClient;
    private final TaskRepository taskRepository;

    @Scheduled(fixedDelay = 300_000) // каждые 5 минут
    public void syncRecentlyUpdated() {
        String jql = "project = PROJ AND updated >= -10m ORDER BY updated DESC";
        List<JiraIssue> issues = jiraClient.searchByJql(jql);

        for (JiraIssue issue : issues) {
            taskRepository.upsert(mapToTask(issue));
        }
    }

    private Task mapToTask(JiraIssue issue) {
        return Task.builder()
                .jiraKey(issue.getKey())
                .summary(issue.getFields().getSummary())
                .status(issue.getFields().getStatus().getName())
                .assignee(issue.getFields().getAssignee() != null
                        ? issue.getFields().getAssignee().getDisplayName() : null)
                .lastSynced(Instant.now())
                .build();
    }
}

Частые ошибки

  • Хранение токенов/паролей в application.yml — используйте переменные окружения или Vault
  • Отсутствие retry-логики — Jira может временно возвращать 5xx
  • Игнорирование rate limiting в Cloud — нужно обрабатывать HTTP 429 и ждать Retry-After
  • Синхронные вызовы Jira API в обработчике HTTP-запроса — вызывает задержки для пользователя

Как используется в 2026

  • Spring Boot 3.x + RestClient — стандартный стек для интеграций
  • JRJC (Jira REST Java Client) устарел и не обновляется — для новых проектов лучше собственный клиент
  • Популярны интеграции через Spring Cloud Stream / Kafka: webhook -> Kafka topic -> обработчик
  • Для Cloud-интеграций OAuth 2.0 (3LO) стал обязательным для user-context операций

На собеседовании: покажите понимание двух моделей интеграции: polling (scheduled sync) и push (webhooks). Комбинируйте оба подхода: webhooks для real-time, polling для catch-up. Обязательно упомяните retry, пагинацию и безопасное хранение токенов.