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部署到Linux
linux·运维·gitlab
InfinitePlus2 天前
Gitlab docker版本安装
docker·容器·gitlab
进击切图仔2 天前
GitLab 新用户创建与项目授权操作指南
gitlab
技术小结-李爽3 天前
【工具】git与gitee和gitlab和github等等有什么区别?
git·gitee·gitlab
xiaoxiangsiyan4 天前
GitLab CI/CD 自托管(EE 企业版)+ Kubernetes Runner 集群 + ArgoCD(GitOps 部署)
运维·网络·ci/cd·容器·kubernetes·gitlab·argocd
csdn2015_11 天前
vscode从gitlab拉项目到本地
vscode·gitlab
AOwhisky14 天前
AI审AI:GitLab上线AI代码审查,开发者可以松一口气了吗?
人工智能·gitlab
杨了个杨898215 天前
GitLab的简介及安装
gitlab
AOwhisky16 天前
云原生 DevOps 工具链从入门到实战(第一期)——DevOps概述与GitLab部署——从理念到工具落地
运维·ci/cd·云原生·gitlab·开发·devops