Java 使用 GitLab4J 实现 WebHook

1、SpringBoot 接收 GitLab WebHook(核心)

GitLab 会 POST 请求你的服务接口,把事件 JSON 推过来;gitlab4j 提供GitLabWebHook工具类,直接解析各种事件对象,不用手写 JSON 解析。

GitLab WebHook 请求头:X-GitLab-Token,用于校验请求来源合法性。

2、代码实现

引入依赖

github地址:https://github.com/gitlab4j/gitlab4j-api

复制代码
<dependency>
    <groupId>org.gitlab4j</groupId>
    <artifactId>gitlab4j-api</artifactId>
    <version>${gitlab4j-api.version}</version>
</dependency>

配置信息

application.yaml配置

复制代码
audit:
  gitlab:
    base-url: http://127.0.0.1:21031 # gitlab 服务器地址
    token: 'aaa-bbb-ccc-' # gitlab 访问 token
    webhook-secret: 'fffffffffffffffffffwa1901111' # webhook 密钥

配置类

复制代码
package com.ybw.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "audit")
public record AuditProperties(
        GitLab gitlab
) {
    public record GitLab(String baseUrl, String token, String webhookSecret) {
    }
}

package com.ybw.config;

import org.gitlab4j.api.GitLabApi;
import org.gitlab4j.api.WebHookManager;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * GitLabApi Bean 配置
 * 使用 gitlab4j-api 框架替代原有的 RestClient 手工 HTTP 调用
 */
@Configuration
@EnableConfigurationProperties(AuditProperties.class)
public class GitLabApiConfig {

    /**
     * 连接超时(毫秒)
     */
    private static final int CONNECT_TIMEOUT_MS = 10_000;

    /**
     * 读取超时(毫秒),对于返回大 diff 的 MR changes 接口,需要较长超时
     */
    private static final int READ_TIMEOUT_MS = 120_000;

    @Bean
    public GitLabApi gitLabApi(AuditProperties auditProperties) {
        GitLabApi gitLabApi = new GitLabApi(auditProperties.gitlab().baseUrl(),
                auditProperties.gitlab().token());
        gitLabApi.withRequestTimeout(CONNECT_TIMEOUT_MS, READ_TIMEOUT_MS);
        return gitLabApi;
    }

    /**
     * WebHookManager 用于解析 GitLab webhook 请求
     * 自动根据 X-Gitlab-Event 头将 JSON 反序列化为对应的事件对象
     *
     * @param auditProperties 审计配置(含 webhook secret token)
     */
    @Bean
    public WebHookManager webHookManager(AuditProperties auditProperties) {
        String secret = auditProperties.gitlab().webhookSecret();
        if (secret != null && !secret.isEmpty()) {
            return new WebHookManager(secret);
        }
        return new WebHookManager();
    }
}

Controller 接收接口

复制代码
package com.ybw.controller;

import com.alibaba.fastjson2.JSON;
import com.ybw.service.AuditService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.gitlab4j.api.WebHookManager;
import org.gitlab4j.api.webhook.Event;
import org.gitlab4j.api.webhook.MergeRequestEvent;
import org.gitlab4j.api.webhook.PushEvent;
import org.gitlab4j.api.webhook.NoteEvent;
import org.gitlab4j.api.webhook.IssueEvent;
import org.gitlab4j.api.webhook.TagPushEvent;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * GitLab Webhook 事件处理
 * <p>
 * 使用 gitlab4j 的 WebHookManager 自动根据 X-Gitlab-Event 请求头
 * 将 JSON 反序列化为对应的事件对象(MergeRequestEvent、PushEvent 等),
 * 不同事件类型对应不同的实体类。
 *
 * @author ybw
 * @version V1.0
 * @className WebhookController
 * @date 2026/8/7
 **/
@Slf4j
@RestController
@RequestMapping("/api/webhook")
@RequiredArgsConstructor
public class WebhookController {

    private final AuditService auditService;
    private final WebHookManager webHookManager;

    /**
     * 接收 GitLab webhook 请求,自动按事件类型分发
     */
    @PostMapping("/gitlab")
    public ResponseEntity<String> handleGitlabWebhook(HttpServletRequest request) {
        // WebHookManager 自动做两件事:
        // 1. 校验 X-Gitlab-Token(如果配置了 secretToken)
        // 2. 根据 X-Gitlab-Event 头将 body 反序列化为对应的事件子类
        Event event;
        try {
            event = webHookManager.handleRequest(request);
        } catch (Exception e) {
            log.error("Webhook 请求处理异常", e);
            return ResponseEntity.badRequest().body("Invalid webhook request: " + e.getMessage());
        }

        if (event == null) {
            log.warn("WebHookManager 返回了 null 事件");
            return ResponseEntity.badRequest().body("Unable to parse webhook event");
        }

        log.info("接收到 Webhook 事件: eventType={}", event.getClass().getSimpleName());

        // 按事件类型分发,不同类型使用不同对象
        if (event instanceof MergeRequestEvent mrEvent) {
            return handleMergeRequest(mrEvent);
        } else if (event instanceof PushEvent) {
            log.debug("Push 事件暂不处理");
            return ResponseEntity.ok("Push event received (not processed)");
        } else if (event instanceof NoteEvent) {
            log.debug("Note 事件暂不处理");
            return ResponseEntity.ok("Note event received (not processed)");
        } else if (event instanceof IssueEvent) {
            log.debug("Issue 事件暂不处理");
            return ResponseEntity.ok("Issue event received (not processed)");
        } else if (event instanceof TagPushEvent) {
            log.debug("Tag Push 事件暂不处理");
            return ResponseEntity.ok("Tag Push event received (not processed)");
        } else {
            log.debug("忽略不支持的事件类型: {}", event.getClass().getSimpleName());
            return ResponseEntity.ok("Ignored: unsupported event type");
        }
    }

    /**
     * 处理 Merge Request 事件
     */
    private ResponseEntity<String> handleMergeRequest(MergeRequestEvent event) {
        log.info("handleMergeRequest event:{}", JSON.toJSONString(event));
        var attrs = event.getObjectAttributes();
        log.info("Merge Request 事件: projectId={}, mrIid={}, action={}",
                event.getProject() != null ? event.getProject().getId() : null,
                attrs != null ? attrs.getIid() : null,
                attrs != null ? attrs.getAction() : "unknown");

        String action = attrs != null ? attrs.getAction() : "unknown";
        if (!"open".equals(action) && !"reopen".equals(action)) {
            log.debug("忽略非 open/reopen 的 MR 事件: action={}", action);
            return ResponseEntity.ok("Ignored MR action: " + action);
        }

        auditService.processMrWebhook(event);
        return ResponseEntity.ok("OK");
    }
}

合并打印日志

复制代码
{
    "object_kind": "merge_request",
    "changes": {},
    "eventType": "merge_request",
    "labels": [],
    "objectAttributes": {
        "action": "update",
        "assigneeIds": [],
        "authorId": 4,
        "blockingDiscussionsResolved": true,
        "createdAt": "2026-08-08 11:29:37",
        "description": "",
        "detailedMergeStatus": "unchecked",
        "firstContribution": false,
        "headPipelineId": 200,
        "id": 4301,
        "iid": 2,
        "labels": [],
        "lastCommit": {
            "author": {
                "email": "test@aaa.com",
                "name": "test"
            },
            "id": "daca0e0c40ab04c62a15d15315389b8a7dc12ef5",
            "message": "edit\n",
            "timestamp": "2026-08-08 11:42:16",
            "title": "edit",
            "url": "http://gitlab.example.com/test/git-audit/-/commit/daca0e0c40ab04c62a15d15315389b8a7dc12ef5"
        },
        "mergeParams": {
            "force_remove_source_branch": "1"
        },
        "mergeStatus": "unchecked",
        "mergeWhenPipelineSucceeds": false,
        "oldrev": "b1d98ac6079962de9760b009a21d3965827223f0",
        "reviewerIds": [],
        "source": {
            "defaultBranch": "main",
            "gitHttpUrl": "http://gitlab.example.com/test/git-audit.git",
            "gitSshUrl": "git@gitlab.example.com:test/git-audit.git",
            "homepage": "http://gitlab.example.com/test/git-audit",
            "httpUrl": "http://gitlab.example.com/test/git-audit.git",
            "id": 505,
            "name": "git-audit",
            "namespace": "test",
            "pathWithNamespace": "test/git-audit",
            "sshUrl": "git@gitlab.example.com:test/git-audit.git",
            "url": "git@gitlab.example.com:test/git-audit.git",
            "visibilityLevel": 0,
            "webUrl": "http://gitlab.example.com/test/git-audit"
        },
        "sourceBranch": "dev",
        "sourceProjectId": 505,
        "state": "opened",
        "stateId": 1,
        "target": {
            "defaultBranch": "main",
            "gitHttpUrl": "http://gitlab.example.com/test/git-audit.git",
            "gitSshUrl": "git@gitlab.example.com:test/git-audit.git",
            "homepage": "http://gitlab.example.com/test/git-audit",
            "httpUrl": "http://gitlab.example.com/test/git-audit.git",
            "id": 505,
            "name": "git-audit",
            "namespace": "test",
            "pathWithNamespace": "test/git-audit",
            "sshUrl": "git@gitlab.example.com:test/git-audit.git",
            "url": "git@gitlab.example.com:test/git-audit.git",
            "visibilityLevel": 0,
            "webUrl": "http://gitlab.example.com/test/git-audit"
        },
        "targetBranch": "main",
        "targetProjectId": 505,
        "timeChange": 0,
        "timeEstimate": 0,
        "title": "Dev",
        "totalTimeSpent": 0,
        "updatedAt": "2026-08-08 11:41:33",
        "url": "http://gitlab.example.com/test/git-audit/-/merge_requests/2",
        "workInProgress": false
    },
    "objectKind": "merge_request",
    "project": {
        "defaultBranch": "main",
        "gitHttpUrl": "http://gitlab.example.com/test/git-audit.git",
        "gitSshUrl": "git@gitlab.example.com:test/git-audit.git",
        "homepage": "http://gitlab.example.com/test/git-audit",
        "httpUrl": "http://gitlab.example.com/test/git-audit.git",
        "id": 505,
        "name": "git-audit",
        "namespace": "test",
        "pathWithNamespace": "test/git-audit",
        "sshUrl": "git@gitlab.example.com:test/git-audit.git",
        "url": "git@gitlab.example.com:test/git-audit.git",
        "visibilityLevel": 0,
        "webUrl": "http://gitlab.example.com/test/git-audit"
    },
    "repository": {
        "homepage": "http://gitlab.example.com/test/git-audit",
        "name": "git-audit",
        "url": "git@gitlab.example.com:test/git-audit.git"
    },
    "user": {
        "avatarUrl": "https://www.gravatar.com/avatar/077105529dfd283d0b36419f639754f51bc953a1a6af6f7119a2818edbd046b6?s=80&d=identicon",
        "email": "[REDACTED]",
        "id": 4,
        "name": "audit-test",
        "username": "audit-test"
    }
}

3、gitlab webhooks配置

  1. gitlab->设置->Webhooks->添加新的webhook。
  2. Secret令牌和配置信息中的webhook-secret相同。
  3. 勾选需要触发的事件:合并请求事件等
  4. 添加 webhook,点击【测试】发送测试请求验证接口。

4、常见事件类型

事件类 说明
PushEvent 代码 push 提交
MergeRequestEvent 合并请求 MR
TagPushEvent Tag 推送
IssueEvent Issue 事件
NoteEvent 评论事件
PipelineEvent CI 流水线事件
JobEvent CI Job 任务事件
相关推荐
snow@li4 天前
服务器运维:K3S 下 Jenkins ↔ GitLab 的 CI/CD 闭环 / 访问成功
运维·gitlab·jenkins
旧梦星轨6 天前
Git Flow 工作流实践
git·gitee·gitlab·github
旧梦星轨6 天前
Github Flow 与 Gitlab Flow工作流实践解读
gitlab·github
极小狐9 天前
干掉流水线里的长期密钥:用 OIDC 让 CI/CD 与云以临时凭证对接
microsoft·ci/cd·gitlab·devops·ci·cd·oidc
snow@li9 天前
服务器运维:阿里云安装软件gitlab/智能体协助安装/智能体协同运维
运维·gitlab
极小狐10 天前
CI 流水线提速实战:缓存设计与依赖代理的工程实践
缓存·ci/cd·gitlab·devops
DevOps老兵11 天前
AI Infra实战11:模型部署Pipeline——CI/CD自动化
人工智能·ci/cd·ai·自动化·gitlab
极小狐11 天前
从复制粘贴到可复用组件:极狐GitLab CI/CD 组件目录实战
java·ci/cd·gitlab·devops·组件化
极小狐11 天前
用 Flow 编排 Agent 智能体:极狐GitLab Duo 自定义工作流实战
ai·gitlab·agent·devops·flow·duo
陈皮糖..11 天前
基于 Kubernetes 与 GitLab CI/CD 的云原生自动化交付平台
运维·ci/cd·云原生·架构·kubernetes·自动化·gitlab