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


4、常见事件类型
| 事件类 | 说明 |
|---|---|
| PushEvent | 代码 push 提交 |
| MergeRequestEvent | 合并请求 MR |
| TagPushEvent | Tag 推送 |
| IssueEvent | Issue 事件 |
| NoteEvent | 评论事件 |
| PipelineEvent | CI 流水线事件 |
| JobEvent | CI Job 任务事件 |