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 任务事件
相关推荐
极小狐9 小时前
从 GitLab Flow 到 Pipeline:分支策略 + CI/CD + 运维一文打通
ci/cd·gitlab·devops·极狐gitlab
猫头虎1 天前
GitHub 入门教程:如何加入并为开源项目贡献代码
gitee·开源·gitlab·github·开放原子·开源协议·gitcode
悠然南风3 天前
【云原生学习】Prometheus监控、GitLab与Jenkins部署实战
kubernetes·gitlab
极小狐4 天前
极狐GitLab Runner 自托管安全加固指南
安全·ci/cd·gitlab·devops·runner·安全加固
钝挫力PROGRAMER5 天前
Windows Docker 环境搭建 GitLab CI/CD 流水线
windows·docker·gitlab·gitlab-runner
我一定会有钱11 天前
打造完美工作流:Git 配置”一次推送,三端同步”(Gitee/GitCode/GitHub)
开发语言·windows·vscode·搜索引擎·gitlab·github·visual studio code
LXY_BUAA11 天前
Git_在飞牛中部署 GitLab_20260817
git·gitlab
Jeanyia14 天前
GitLab 配套 Git 常用命令
git·gitlab
极小狐14 天前
极狐GitLab 补丁版本:19.2.1、19.1.3、19.0.5
网络·安全·gitlab·极狐gitlab·安全更新·补丁版本
rustfs15 天前
GitLab 如何与 RustFS 集成?
分布式·rust·gitlab