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 任务事件
相关推荐
极小狐2 天前
极狐GitLab万人并发架构实践,大规模组织的扩容路线图
架构·gitlab·极狐gitlab
Lvan的前端笔记3 天前
GitLab CI 环境变量整理
ci/cd·gitlab
李迟4 天前
CICD实例:解决GitLab Runner无法执行CICD问题
gitlab
hanjq_code6 天前
gitlab本地文件 跨分支 忽略改动 不提交
gitlab
极小狐6 天前
极狐GitLab集群高可用方案,多节点架构与故障自愈实践
架构·gitlab·高可用·极狐gitlab
极小狐7 天前
极狐GitLab主备同步延迟治理,复制滞后监控与优化方法
gitlab·极狐gitlab·geo容灾
极小狐7 天前
跨项目流水线报 404 Not Found?CI_JOB_TOKEN 允许名单 9 步排查
ci/cd·gitlab·devops·权限管理·流水线·极狐gitlab·作业令牌
极小狐8 天前
极狐GitLab双机热备搭建指南,主机备机部署与切换实操
gitlab·高可用·极狐gitlab
有同事要进步14 天前
如何在github或者是gitlab上创建空文件
gitlab·github
GoodStudyAndDayDayUp15 天前
gitlab私有化跑CICD
gitlab·gitlab私有化跑cicd