Linux环境下完整搭建GitLab私有代码仓库的详细流程
作者:知远漫谈
在现代软件开发中,代码版本控制是团队协作的基石。虽然市面上有众多代码托管平台,但出于安全性、定制化和成本控制等多方面考虑,越来越多的企业选择搭建自己的私有代码仓库。GitLab 作为一款功能强大、开源免费的 DevOps 平台,无疑是私有代码仓库的最佳选择之一。
本文将带你从零开始,在 Linux 环境下完整搭建 GitLab 私有代码仓库,并通过实际 Java 项目演示如何使用这一平台进行日常开发工作。无论你是系统管理员、开发工程师还是技术负责人,都能从中获得实用的知识和技能。
环境准备与系统要求
在开始安装 GitLab 之前,我们需要确保服务器环境满足基本要求。GitLab 对硬件资源有一定要求,特别是在用户量较大或项目较多的情况下。
硬件要求
- CPU: 至少 2 核心(推荐 4 核心以上)
- 内存: 至少 4GB(推荐 8GB 以上)
- 存储: 至少 20GB 可用空间(根据项目数量和大小调整)
- 操作系统: Ubuntu 20.04/22.04, CentOS 7/8, RHEL 7/8 等主流 Linux 发行版
# 检查当前系统信息 uname -a cat /etc/os-release free -h df -h
软件依赖
GitLab 需要以下基础软件支持:
# 更新系统包 sudo apt update && sudo apt upgrade -y # Ubuntu/Debian # 或 sudo yum update -y # CentOS/RHEL # 安装必要依赖 sudo apt install -y curl openssh-server ca-certificates tzdata perl # 或 sudo yum install -y curl openssh-server ca-certificates tzdata perl
防火墙配置
确保必要的端口开放:
# 开放 HTTP/HTTPS 和 SSH 端口 sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw allow 22/tcp # SSH sudo ufw enable # 或使用 firewalld (CentOS/RHEL) sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --permanent --add-service=ssh sudo firewall-cmd --reload
GitLab 安装与配置
现在我们开始正式安装 GitLab。GitLab 提供了多种安装方式,这里我们采用最简单直接的 Omnibus 包安装方法。
添加 GitLab 仓库
首先添加 GitLab 的官方仓库:
# 下载并安装 GitLab 仓库配置 curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash # 或对于 RPM 系统 curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash
安装 GitLab CE
执行安装命令:
# Ubuntu/Debian sudo EXTERNAL_URL="http://your-domain.com" apt install gitlab-ce # CentOS/RHEL sudo EXTERNAL_URL="http://your-domain.com" yum install gitlab-ce
注意:将 your-domain.com 替换为你的实际域名或服务器 IP 地址
基础配置
安装完成后,编辑 GitLab 配置文件:
sudo vim /etc/gitlab/gitlab.rb
主要配置项:
# 基础 URL 配置 external_url 'http://gitlab.yourcompany.com' # 邮件配置(可选但推荐) gitlab_rails['gitlab_email_enabled'] = true gitlab_rails['gitlab_email_from'] = 'gitlab@yourcompany.com' gitlab_rails['gitlab_email_display_name'] = 'GitLab' gitlab_rails['gitlab_email_reply_to'] = 'noreply@yourcompany.com' # SMTP 设置示例 gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = "smtp.yourcompany.com" gitlab_rails['smtp_port'] = 587 gitlab_rails['smtp_user_name'] = "gitlab@yourcompany.com" gitlab_rails['smtp_password'] = "your_password" gitlab_rails['smtp_domain'] = "yourcompany.com" gitlab_rails['smtp_authentication'] = "login" gitlab_rails['smtp_enable_starttls_auto'] = true gitlab_rails['smtp_tls'] = false
应用配置并启动
# 重新配置 GitLab(这会重启服务) sudo gitlab-ctl reconfigure # 检查服务状态 sudo gitlab-ctl status # 查看日志 sudo gitlab-ctl tail
首次访问时,系统会要求设置 root 用户密码。请妥善保存这个密码!
用户管理与权限控制
GitLab 提供了完善的用户管理和权限控制系统,让我们能够精细地控制谁可以访问哪些资源。
创建用户
# 通过命令行创建用户(可选) sudo gitlab-rails console # 在 Rails 控制台中执行: user = User.create!(email: 'developer@yourcompany.com', password: 'securepassword', username: 'developer', name: 'Developer Name') user.skip_confirmation! user.save!
或者通过 Web 界面:
- 使用 root 用户登录
- 进入 Admin Area → Users
- 点击 “New user” 按钮
- 填写用户信息并保存
用户组与项目权限
GitLab 的权限体系分为多个层级:

权限说明:
- Owner: 拥有群组或项目的完全控制权
- Maintainer: 可以管理项目设置、保护分支、添加成员等
- Developer: 可以推送代码、创建合并请求、管理议题等
- Reporter: 可以创建议题、评论、查看代码等
- Guest: 基本只读权限
创建用户组
# 通过 Web 界面创建群组 # 1. 点击右上角头像 → Groups → Create group # 2. 填写群组名称、路径、描述 # 3. 设置可见性级别(Private/Internal/Public) # 4. 点击 "Create group"
添加成员到群组
// 虽然这不是真正的 Java 代码,但我们可以模拟一个用户管理的 API 调用
public class GitLabUserManager {
public static void main(String[] args) {
GitLabClient client = new GitLabClient("https://gitlab.yourcompany.com", "your-access-token");
try {
// 创建新用户
User newUser = client.createUser("john.doe@company.com", "John Doe", "johndoe");
System.out.println("User created: " + newUser.getId());
// 将用户添加到群组
GroupMembership membership = client.addUserToGroup(123, newUser.getId(), AccessLevel.DEVELOPER);
System.out.println("User added to group with access level: " + membership.getAccessLevel());
// 获取群组成员列表
List<GroupMembership> members = client.getGroupMembers(123);
for (GroupMembership member : members) {
System.out.println("Member: " + member.getUser().getName() +
" - Access Level: " + member.getAccessLevel());
}
} catch (GitLabApiException e) {
System.err.println("Error managing users: " + e.getMessage());
}
}
}创建和管理 Java 项目
现在让我们创建一个实际的 Java 项目,演示如何在 GitLab 上进行日常开发工作。
创建新项目
- 登录 GitLab
- 点击 “New project” 按钮
- 选择 “Create blank project”
- 填写项目信息:
- Project name:
java-sample-app - Project description:
A sample Java application demonstrating GitLab features - Visibility Level: Private
- Project name:
- 点击 “Create project”
初始化本地 Java 项目
# 创建项目目录 mkdir java-sample-app cd java-sample-app # 初始化 Git 仓库 git init # 添加远程仓库(替换为你的实际 GitLab 项目地址) git remote add origin http://gitlab.yourcompany.com/your-username/java-sample-app.git
创建 Maven 项目结构
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yourcompany</groupId>
<artifactId>java-sample-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Java Sample Application</name>
<description>A sample Java application for GitLab demonstration</description>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>创建 Java 类
// src/main/java/com/yourcompany/App.java
package com.yourcompany;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 主应用程序类
*/
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
/**
* 主方法
* @param args 命令行参数
*/
public static void main(String[] args) {
logger.info("Starting Java Sample Application...");
App app = new App();
String result = app.processData("Hello GitLab!");
System.out.println(result);
logger.info("Application finished successfully.");
}
/**
* 处理数据的示例方法
* @param input 输入字符串
* @return 处理后的结果
*/
public String processData(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("Input cannot be null or empty");
}
logger.debug("Processing input: {}", input);
return "Processed: " + input.toUpperCase();
}
/**
* 计算两个数的和
* @param a 第一个数
* @param b 第二个数
* @return 两数之和
*/
public int addNumbers(int a, int b) {
logger.trace("Adding numbers: {} + {}", a, b);
return a + b;
}
}创建测试类
// src/test/java/com/yourcompany/AppTest.java
package com.yourcompany;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* App 类的单元测试
*/
@DisplayName("App Class Tests")
class AppTest {
private App app;
@BeforeEach
void setUp() {
app = new App();
}
@Test
@DisplayName("should process string correctly")
void testProcessData() {
// 测试正常情况
String result = app.processData("test input");
assertEquals("Processed: TEST INPUT", result);
// 测试边界情况
result = app.processData(" spaces ");
assertEquals("Processed: SPACES ", result);
}
@Test
@DisplayName("should throw exception for null input")
void testProcessDataNull() {
assertThrows(IllegalArgumentException.class, () -> {
app.processData(null);
});
}
@Test
@DisplayName("should throw exception for empty input")
void testProcessDataEmpty() {
assertThrows(IllegalArgumentException.class, () -> {
app.processData("");
});
}
@Nested
@DisplayName("Add Numbers Tests")
class AddNumbersTests {
@Test
@DisplayName("should add positive numbers correctly")
void testAddPositiveNumbers() {
int result = app.addNumbers(5, 3);
assertEquals(8, result);
}
@Test
@DisplayName("should add negative numbers correctly")
void testAddNegativeNumbers() {
int result = app.addNumbers(-5, -3);
assertEquals(-8, result);
}
@Test
@DisplayName("should handle zero correctly")
void testAddWithZero() {
int result = app.addNumbers(5, 0);
assertEquals(5, result);
}
@Test
@DisplayName("should handle large numbers")
void testAddLargeNumbers() {
int result = app.addNumbers(Integer.MAX_VALUE, 1);
assertEquals(Integer.MIN_VALUE, result); // 溢出测试
}
}
@Test
@DisplayName("should skip test in production environment")
void testConditionalExecution() {
// 假设我们只想在开发环境中运行某些测试
boolean isProduction = System.getProperty("env") != null &&
System.getProperty("env").equals("production");
assumeTrue(!isProduction, "Skipping test in production environment");
// 实际测试逻辑
assertTrue(true);
}
}创建日志配置
<!-- src/main/resources/logback.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.yourcompany" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>提交代码到 GitLab
# 添加所有文件 git add . # 提交更改 git commit -m "Initial commit: Setup basic Java project structure" # 推送到 GitLab git push -u origin master
分支策略与工作流
在团队协作中,合理的分支策略至关重要。GitLab 支持多种工作流,我们推荐使用 Git Flow 或类似的分支管理策略。
Git Flow 工作流

创建和管理分支
# 创建新分支 git checkout -b feature/new-feature # 推送分支到远程 git push origin feature/new-feature # 切换回主分支 git checkout main # 合并分支(在本地) git merge feature/new-feature # 删除本地分支 git branch -d feature/new-feature # 删除远程分支 git push origin --delete feature/new-feature
Java 项目中的分支实践
让我们在之前的 Java 项目中添加一个新功能:
// 在 feature/calculator-enhancement 分支上工作
// src/main/java/com/yourcompany/Calculator.java
package com.yourcompany;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 增强型计算器类
*/
public class Calculator {
private static final Logger logger = LoggerFactory.getLogger(Calculator.class);
/**
* 执行四则运算
* @param a 第一个操作数
* @param b 第二个操作数
* @param operation 运算符 (+, -, *, /)
* @return 计算结果
* @throws IllegalArgumentException 当运算符不支持时
* @throws ArithmeticException 当除零时
*/
public double calculate(double a, double b, char operation) {
logger.info("Calculating: {} {} {}", a, operation, b);
switch (operation) {
case '+':
return add(a, b);
case '-':
return subtract(a, b);
case '*':
return multiply(a, b);
case '/':
return divide(a, b);
default:
throw new IllegalArgumentException("Unsupported operation: " + operation);
}
}
private double add(double a, double b) {
return a + b;
}
private double subtract(double a, double b) {
return a - b;
}
private double multiply(double a, double b) {
return a * b;
}
private double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
}
/**
* 计算平方根
* @param number 要计算平方根的数
* @return 平方根结果
* @throws IllegalArgumentException 当输入为负数时
*/
public double squareRoot(double number) {
if (number < 0) {
throw new IllegalArgumentException("Cannot calculate square root of negative number");
}
return Math.sqrt(number);
}
/**
* 计算幂运算
* @param base 底数
* @param exponent 指数
* @return 幂运算结果
*/
public double power(double base, double exponent) {
return Math.pow(base, exponent);
}
}对应的测试类:
// src/test/java/com/yourcompany/CalculatorTest.java
package com.yourcompany;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@ParameterizedTest
@CsvSource({
"5.0, 3.0, +, 8.0",
"5.0, 3.0, -, 2.0",
"5.0, 3.0, *, 15.0",
"6.0, 3.0, /, 2.0"
})
@DisplayName("should perform basic arithmetic operations correctly")
void testBasicOperations(double a, double b, char operation, double expected) {
double result = calculator.calculate(a, b, operation);
assertEquals(expected, result, 0.001);
}
@Test
@DisplayName("should throw exception for unsupported operation")
void testUnsupportedOperation() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculate(5, 3, '%');
});
}
@Test
@DisplayName("should throw exception for division by zero")
void testDivisionByZero() {
assertThrows(ArithmeticException.class, () -> {
calculator.calculate(5, 0, '/');
});
}
@ParameterizedTest
@ValueSource(doubles = {0.0, 1.0, 4.0, 9.0, 16.0})
@DisplayName("should calculate square root correctly")
void testSquareRoot(double number) {
double result = calculator.squareRoot(number);
assertEquals(Math.sqrt(number), result, 0.001);
}
@Test
@DisplayName("should throw exception for negative square root")
void testNegativeSquareRoot() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.squareRoot(-1);
});
}
@ParameterizedTest
@CsvSource({
"2.0, 3.0, 8.0",
"5.0, 2.0, 25.0",
"10.0, 0.0, 1.0",
"2.0, -1.0, 0.5"
})
@DisplayName("should calculate power correctly")
void testPower(double base, double exponent, double expected) {
double result = calculator.power(base, exponent);
assertEquals(expected, result, 0.001);
}
}创建合并请求
在 GitLab 中,我们通常通过合并请求(Merge Request)来审查和合并代码:
# 推送功能分支 git push origin feature/calculator-enhancement # 在 GitLab Web 界面创建合并请求: # 1. 进入项目页面 # 2. 点击 "Merge Requests" → "New merge request" # 3. 选择源分支和目标分支 # 4. 填写标题和描述 # 5. 指定评审人员 # 6. 点击 "Create merge request"
CI/CD 流水线配置
GitLab 内置了强大的 CI/CD 功能,让我们可以通过 .gitlab-ci.yml 文件定义自动化构建、测试和部署流程。
基础 CI/CD 配置
# .gitlab-ci.yml
image: maven:3.8.6-openjdk-11
stages:
- build
- test
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
cache:
paths:
- .m2/repository/
build:
stage: build
script:
- mvn $MAVEN_CLI_OPTS compile
artifacts:
paths:
- target/
expire_in: 1 week
test:
stage: test
script:
- mvn $MAVEN_CLI_OPTS test
coverage: '/Total.*?([0-9]{1,3})%/'
deploy-dev:
stage: deploy
script:
- echo "Deploying to development environment..."
- mvn $MAVEN_CLI_OPTS package
- cp target/java-sample-app-*.jar ./app.jar
- echo "Deployment completed!"
environment:
name: development
url: http://dev.yourcompany.com
only:
- main
deploy-prod:
stage: deploy
script:
- echo "Deploying to production environment..."
- mvn $MAVEN_CLI_OPTS package
- scp target/java-sample-app-*.jar production-server:/opt/app/
- ssh production-server "systemctl restart java-app"
environment:
name: production
url: https://yourcompany.com
when: manual
only:
- main高级 CI/CD 配置
# 更复杂的 .gitlab-ci.yml 示例
image: maven:3.8.6-openjdk-11
stages:
- lint
- build
- test
- security
- deploy
- notify
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
SONAR_HOST_URL: "http://sonarqube.yourcompany.com"
SONAR_LOGIN: "$SONAR_TOKEN"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .m2/repository/
before_script:
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
- java -version
# 代码质量检查
lint:
stage: lint
script:
- mvn $MAVEN_CLI_OPTS checkstyle:check
- mvn $MAVEN_CLI_OPTS spotbugs:check
allow_failure: true
# 构建阶段
build:
stage: build
script:
- mvn $MAVEN_CLI_OPTS clean compile
- mvn $MAVEN_CLI_OPTS package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 week
except:
- tags
# 单元测试
unit-test:
stage: test
script:
- mvn $MAVEN_CLI_OPTS test
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
paths:
- target/surefire-reports/
reports:
junit: target/surefire-reports/TEST-*.xml
# 集成测试
integration-test:
stage: test
script:
- mvn $MAVEN_CLI_OPTS verify -P integration-test
when: on_success
dependencies:
- build
# 代码安全扫描
security-scan:
stage: security
script:
- mvn $MAVEN_CLI_OPTS dependency-check:check
- mvn $MAVEN_CLI_OPTS sonar:sonar
allow_failure: true
# 开发环境部署
deploy-dev:
stage: deploy
script:
- echo "Deploying version ${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} to dev environment"
- ./scripts/deploy-dev.sh
environment:
name: development
url: http://dev.yourcompany.com
only:
- main
- develop
# 预生产环境部署
deploy-staging:
stage: deploy
script:
- echo "Deploying version ${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} to staging environment"
- ./scripts/deploy-staging.sh
environment:
name: staging
url: https://staging.yourcompany.com
when: manual
only:
- main
# 生产环境部署
deploy-production:
stage: deploy
script:
- echo "Deploying version ${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} to production environment"
- ./scripts/deploy-production.sh
environment:
name: production
url: https://yourcompany.com
when: manual
only:
- /^release\/.*/
- tags
# 通知阶段
notify-slack:
stage: notify
script:
- |
if [ "$CI_JOB_STATUS" = "success" ]; then
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"🚀 Build succeeded: '"$CI_PROJECT_NAME"' - '"$CI_COMMIT_REF_NAME"'"}' \
$SLACK_WEBHOOK_URL
else
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"❌ Build failed: '"$CI_PROJECT_NAME"' - '"$CI_COMMIT_REF_NAME"'"}' \
$SLACK_WEBHOOK_URL
fi
when: always
only:
- main
- tagsJava 项目中的 CI/CD 实践
让我们为 Java 项目创建一些实用的脚本:
# scripts/deploy-dev.sh #!/bin/bash set -e echo "Starting deployment to development environment..." echo "Project: $CI_PROJECT_NAME" echo "Branch: $CI_COMMIT_REF_NAME" echo "Commit: $CI_COMMIT_SHORT_SHA" # 构建应用 mvn clean package # 创建部署目录 DEPLOY_DIR="/opt/java-apps/$CI_PROJECT_NAME" mkdir -p $DEPLOY_DIR # 复制 jar 文件 cp target/*.jar $DEPLOY_DIR/app.jar # 创建或更新 systemd 服务文件 cat > /etc/systemd/system/java-app.service << EOF [Unit] Description=Java Sample Application After=network.target [Service] Type=simple User=appuser WorkingDirectory=$DEPLOY_DIR ExecStart=/usr/bin/java -jar $DEPLOY_DIR/app.jar Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF # 重新加载 systemd 配置 systemctl daemon-reload # 重启服务 systemctl restart java-app echo "Deployment completed successfully!"
// 为了支持 CI/CD,我们可以添加一个健康检查端点
// src/main/java/com/yourcompany/HealthCheckController.java
package com.yourcompany;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 健康检查控制器
*/
public class HealthCheckController {
/**
* 获取应用健康状态
* @return 包含健康信息的 Map
*/
public Map<String, Object> getHealthStatus() {
Map<String, Object> healthInfo = new HashMap<>();
healthInfo.put("status", "UP");
healthInfo.put("application", "java-sample-app");
healthInfo.put("version", "1.0.0");
healthInfo.put("timestamp", LocalDateTime.now().toString());
healthInfo.put("checks", getHealthChecks());
return healthInfo;
}
private Map<String, String> getHealthChecks() {
Map<String, String> checks = new HashMap<>();
// 数据库连接检查(模拟)
checks.put("database", checkDatabaseConnection() ? "UP" : "DOWN");
// 磁盘空间检查(模拟)
checks.put("diskSpace", checkDiskSpace() ? "UP" : "DOWN");
// 内存使用检查(模拟)
checks.put("memory", checkMemoryUsage() ? "UP" : "DOWN");
return checks;
}
private boolean checkDatabaseConnection() {
// 模拟数据库连接检查
try {
Thread.sleep(100); // 模拟网络延迟
return true; // 假设连接成功
} catch (InterruptedException e) {
return false;
}
}
private boolean checkDiskSpace() {
// 模拟磁盘空间检查
long freeSpace = Runtime.getRuntime().freeMemory();
long totalSpace = Runtime.getRuntime().totalMemory();
double usagePercentage = ((double) (totalSpace - freeSpace) / totalSpace) * 100;
return usagePercentage < 90; // 如果使用率低于90%,则认为正常
}
private boolean checkMemoryUsage() {
// 模拟内存使用检查
long maxMemory = Runtime.getRuntime().maxMemory();
long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
double usagePercentage = ((double) usedMemory / maxMemory) * 100;
return usagePercentage < 85; // 如果使用率低于85%,则认为正常
}
}对应的测试:
// src/test/java/com/yourcompany/HealthCheckControllerTest.java
package com.yourcompany;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class HealthCheckControllerTest {
private HealthCheckController controller;
@BeforeEach
void setUp() {
controller = new HealthCheckController();
}
@Test
void testGetHealthStatus() {
Map<String, Object> healthStatus = controller.getHealthStatus();
assertNotNull(healthStatus);
assertEquals("UP", healthStatus.get("status"));
assertEquals("java-sample-app", healthStatus.get("application"));
assertEquals("1.0.0", healthStatus.get("version"));
@SuppressWarnings("unchecked")
Map<String, String> checks = (Map<String, String>) healthStatus.get("checks");
assertNotNull(checks);
assertTrue(checks.containsKey("database"));
assertTrue(checks.containsKey("diskSpace"));
assertTrue(checks.containsKey("memory"));
}
@Test
void testHealthChecks() {
@SuppressWarnings("unchecked")
Map<String, String> checks = (Map<String, String>) controller.getHealthStatus().get("checks");
// 所有检查都应该返回 "UP"(基于我们的模拟实现)
assertEquals("UP", checks.get("database"));
assertEquals("UP", checks.get("diskSpace"));
assertEquals("UP", checks.get("memory"));
}
}代码质量与安全分析
GitLab 不仅提供版本控制,还内置了代码质量分析和安全扫描功能。
SonarQube 集成
# 在 .gitlab-ci.yml 中添加 SonarQube 分析
sonarqube-analysis:
stage: test
script:
- mvn $MAVEN_CLI_OPTS sonar:sonar \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.login=$SONAR_LOGIN \
-Dsonar.projectKey=$CI_PROJECT_NAME \
-Dsonar.projectName="$CI_PROJECT_TITLE" \
-Dsonar.projectVersion=$CI_COMMIT_TAG \
-Dsonar.branch.name=$CI_COMMIT_REF_NAME
allow_failure: true
only:
- main
- develop依赖安全扫描
# 依赖漏洞扫描
dependency-scanning:
stage: security
script:
- mvn $MAVEN_CLI_OPTS dependency-check:check
artifacts:
reports:
dependency_scanning: target/dependency-check-report.json
allow_failure: trueJava 代码质量实践
// 高质量 Java 代码示例
package com.yourcompany.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
/**
* 用户服务类
* 提供用户相关的业务逻辑
*/
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
private final ExecutorService executorService;
/**
* 构造函数
* @param userRepository 用户仓库
*/
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.executorService = Executors.newFixedThreadPool(5);
}
/**
* 异步获取用户信息
* @param userId 用户ID
* @return CompletableFuture 包含用户信息
*/
public CompletableFuture<Optional<User>> getUserAsync(Long userId) {
logger.info("Fetching user asynchronously: {}", userId);
return CompletableFuture.supplyAsync(() -> {
try {
logger.debug("Starting async user fetch for ID: {}", userId);
Optional<User> user = userRepository.findById(userId);
logger.debug("Completed async user fetch for ID: {}", userId);
return user;
} catch (Exception e) {
logger.error("Error fetching user with ID: {}", userId, e);
throw new UserServiceException("Failed to fetch user", e);
}
}, executorService);
}
/**
* 创建新用户
* @param userData 用户数据
* @return 创建的用户
* @throws ValidationException 当用户数据无效时
*/
public User createUser(UserData userData) throws ValidationException {
validateUserData(userData);
logger.info("Creating new user: {}", userData.getEmail());
User user = new User();
user.setEmail(userData.getEmail());
user.setName(userData.getName());
user.setCreatedAt(java.time.LocalDateTime.now());
user.setUpdatedAt(user.getCreatedAt());
try {
User savedUser = userRepository.save(user);
logger.info("User created successfully: ID {}", savedUser.getId());
return savedUser;
} catch (Exception e) {
logger.error("Failed to create user: {}", userData.getEmail(), e);
throw new UserServiceException("Failed to create user", e);
}
}
/**
* 更新用户信息
* @param userId 用户ID
* @param userData 更新的用户数据
* @return 更新后的用户
* @throws UserNotFoundException 当用户不存在时
* @throws ValidationException 当用户数据无效时
*/
public User updateUser(Long userId, UserData userData)
throws UserNotFoundException, ValidationException {
validateUserData(userData);
logger.info("Updating user: {}", userId);
Optional<User> existingUser = userRepository.findById(userId);
if (!existingUser.isPresent()) {
logger.warn("User not found for update: {}", userId);
throw new UserNotFoundException("User not found: " + userId);
}
User user = existingUser.get();
user.setEmail(userData.getEmail());
user.setName(userData.getName());
user.setUpdatedAt(java.time.LocalDateTime.now());
try {
User updatedUser = userRepository.save(user);
logger.info("User updated successfully: ID {}", updatedUser.getId());
return updatedUser;
} catch (Exception e) {
logger.error("Failed to update user: {}", userId, e);
throw new UserServiceException("Failed to update user", e);
}
}
/**
* 删除用户
* @param userId 用户ID
* @throws UserNotFoundException 当用户不存在时
*/
public void deleteUser(Long userId) throws UserNotFoundException {
logger.info("Deleting user: {}", userId);
Optional<User> existingUser = userRepository.findById(userId);
if (!existingUser.isPresent()) {
logger.warn("User not found for deletion: {}", userId);
throw new UserNotFoundException("User not found: " + userId);
}
try {
userRepository.deleteById(userId);
logger.info("User deleted successfully: {}", userId);
} catch (Exception e) {
logger.error("Failed to delete user: {}", userId, e);
throw new UserServiceException("Failed to delete user", e);
}
}
/**
* 验证用户数据
* @param userData 用户数据
* @throws ValidationException 当验证失败时
*/
private void validateUserData(UserData userData) throws ValidationException {
if (userData == null) {
throw new ValidationException("User data cannot be null");
}
if (userData.getEmail() == null || userData.getEmail().trim().isEmpty()) {
throw new ValidationException("Email cannot be empty");
}
if (!isValidEmail(userData.getEmail())) {
throw new ValidationException("Invalid email format: " + userData.getEmail());
}
if (userData.getName() == null || userData.getName().trim().isEmpty()) {
throw new ValidationException("Name cannot be empty");
}
if (userData.getName().length() < 2 || userData.getName().length() > 100) {
throw new ValidationException("Name must be between 2 and 100 characters");
}
}
/**
* 验证邮箱格式
* @param email 邮箱地址
* @return 是否有效
*/
private boolean isValidEmail(String email) {
if (email == null) return false;
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return email.matches(emailRegex);
}
/**
* 关闭服务,释放资源
*/
public void shutdown() {
logger.info("Shutting down UserService");
executorService.shutdown();
}
}
/**
* 用户实体类
*/
class User {
private Long id;
private String email;
private String name;
private java.time.LocalDateTime createdAt;
private java.time.LocalDateTime updatedAt;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public java.time.LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(java.time.LocalDateTime createdAt) { this.createdAt = createdAt; }
public java.time.LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(java.time.LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
/**
* 用户数据传输对象
*/
class UserData {
private String email;
private String name;
public UserData() {}
public UserData(String email, String name) {
this.email = email;
this.name = name;
}
// Getters and setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
/**
* 用户仓库接口
*/
interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
void deleteById(Long id);
}
/**
* 自定义异常类
*/
class UserServiceException extends RuntimeException {
public UserServiceException(String message) {
super(message);
}
public UserServiceException(String message, Throwable cause) {
super(message, cause);
}
}
class ValidationException extends Exception {
public ValidationException(String message) {
super(message);
}
}
class UserNotFoundException extends Exception {
public UserNotFoundException(String message) {
super(message);
}
}对应的测试:
// src/test/java/com/yourcompany/service/UserServiceTest.java
package com.yourcompany.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class UserServiceTest {
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
}
@Test
@DisplayName("should create user with valid data")
void testCreateUserValidData() throws ValidationException {
UserData userData = new UserData("test@example.com", "Test User");
User savedUser = new User();
savedUser.setId(1L);
savedUser.setEmail(userData.getEmail());
savedUser.setName(userData.getName());
when(userRepository.save(any(User.class))).thenReturn(savedUser);
User result = userService.createUser(userData);
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("test@example.com", result.getEmail());
assertEquals("Test User", result.getName());
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should throw exception for null user data")
void testCreateUserNullData() {
assertThrows(ValidationException.class, () -> {
userService.createUser(null);
});
}
@Test
@DisplayName("should throw exception for invalid email")
void testCreateUserInvalidEmail() {
UserData userData = new UserData("invalid-email", "Test User");
assertThrows(ValidationException.class, () -> {
userService.createUser(userData);
});
}
@Test
@DisplayName("should update existing user")
void testUpdateUserExisting() throws UserNotFoundException, ValidationException {
Long userId = 1L;
UserData userData = new UserData("updated@example.com", "Updated User");
User existingUser = new User();
existingUser.setId(userId);
existingUser.setEmail("old@example.com");
existingUser.setName("Old Name");
User updatedUser = new User();
updatedUser.setId(userId);
updatedUser.setEmail(userData.getEmail());
updatedUser.setName(userData.getName());
when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser));
when(userRepository.save(any(User.class))).thenReturn(updatedUser);
User result = userService.updateUser(userId, userData);
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("updated@example.com", result.getEmail());
assertEquals("Updated User", result.getName());
verify(userRepository).findById(userId);
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should throw exception for non-existent user update")
void testUpdateUserNonExistent() {
Long userId = 999L;
UserData userData = new UserData("test@example.com", "Test User");
when(userRepository.findById(userId)).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> {
userService.updateUser(userId, userData);
});
}
@Test
@DisplayName("should delete existing user")
void testDeleteUserExisting() throws UserNotFoundException {
Long userId = 1L;
User existingUser = new User();
existingUser.setId(userId);
when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser));
assertDoesNotThrow(() -> {
userService.deleteUser(userId);
});
verify(userRepository).findById(userId);
verify(userRepository).deleteById(userId);
}
@Test
@DisplayName("should throw exception for non-existent user deletion")
void testDeleteUserNonExistent() {
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> {
userService.deleteUser(userId);
});
}
@Test
@DisplayName("should fetch user asynchronously")
void testGetUserAsync() throws ExecutionException, InterruptedException {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setEmail("test@example.com");
user.setName("Test User");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
CompletableFuture<Optional<User>> future = userService.getUserAsync(userId);
Optional<User> result = future.get();
assertTrue(result.isPresent());
assertEquals(userId, result.get().getId());
assertEquals("test@example.com", result.get().getEmail());
assertEquals("Test User", result.get().getName());
verify(userRepository).findById(userId);
}
}团队协作与代码评审
GitLab 提供了完善的团队协作功能,包括议题跟踪、代码评审、讨论等。
创建和管理议题
// 我们可以创建一个简单的议题管理系统来演示协作功能
package com.yourcompany.issue;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 简单的议题管理系统
*/
public class IssueManager {
private final Map<Long, Issue> issues;
private long nextId;
public IssueManager() {
this.issues = new ConcurrentHashMap<>();
this.nextId = 1;
}
/**
* 创建新议题
* @param title 标题
* @param description 描述
* @param author 作者
* @param labels 标签
* @return 创建的议题
*/
public Issue createIssue(String title, String description, String author, Set<String> labels) {
if (title == null || title.trim().isEmpty()) {
throw new IllegalArgumentException("Title cannot be empty");
}
Issue issue = new Issue();
issue.setId(nextId++);
issue.setTitle(title.trim());
issue.setDescription(description != null ? description.trim() : "");
issue.setAuthor(author != null ? author.trim() : "anonymous");
issue.setLabels(labels != null ? new HashSet<>(labels) : new HashSet<>());
issue.setStatus(IssueStatus.OPEN);
issue.setCreatedAt(LocalDateTime.now());
issue.setUpdatedAt(issue.getCreatedAt());
issues.put(issue.getId(), issue);
return issue;
}
/**
* 更新议题
* @param id 议题ID
* @param updater 更新者
* @param title 新标题(可选)
* @param description 新描述(可选)
* @param status 新状态(可选)
* @param labels 新标签(可选)
* @return 更新后的议题
* @throws IssueNotFoundException 当议题不存在时
*/
public Issue updateIssue(Long id, String updater, String title, String description,
IssueStatus status, Set<String> labels) throws IssueNotFoundException {
Issue issue = getIssue(id);
if (title != null && !title.trim().isEmpty()) {
issue.setTitle(title.trim());
}
if (description != null) {
issue.setDescription(description.trim());
}
if (status != null) {
issue.setStatus(status);
}
if (labels != null) {
issue.setLabels(new HashSet<>(labels));
}
issue.setUpdatedAt(LocalDateTime.now());
issue.addComment(new Comment(updater, "Issue updated", issue.getUpdatedAt()));
return issue;
}
/**
* 关闭议题
* @param id 议题ID
* @param closer 关闭者
* @return 关闭后的议题
* @throws IssueNotFoundException 当议题不存在时
*/
public Issue closeIssue(Long id, String closer) throws IssueNotFoundException {
Issue issue = getIssue(id);
issue.setStatus(IssueStatus.CLOSED);
issue.setUpdatedAt(LocalDateTime.now());
issue.addComment(new Comment(closer, "Issue closed", issue.getUpdatedAt()));
return issue;
}
/**
* 重新打开议题
* @param id 议题ID
* @param opener 重新打开者
* @return 重新打开后的议题
* @throws IssueNotFoundException 当议题不存在时
*/
public Issue reopenIssue(Long id, String opener) throws IssueNotFoundException {
Issue issue = getIssue(id);
issue.setStatus(IssueStatus.OPEN);
issue.setUpdatedAt(LocalDateTime.now());
issue.addComment(new Comment(opener, "Issue reopened", issue.getUpdatedAt()));
return issue;
}
/**
* 为议题添加评论
* @param id 议题ID
* @param author 评论作者
* @param content 评论内容
* @return 添加评论后的议题
* @throws IssueNotFoundException 当议题不存在时
*/
public Issue addComment(Long id, String author, String content) throws IssueNotFoundException {
if (content == null || content.trim().isEmpty()) {
throw new IllegalArgumentException("Comment content cannot be empty");
}
Issue issue = getIssue(id);
Comment comment = new Comment(author, content.trim(), LocalDateTime.now());
issue.addComment(comment);
issue.setUpdatedAt(LocalDateTime.now());
return issue;
}
/**
* 获取议题
* @param id 议题ID
* @return 议题
* @throws IssueNotFoundException 当议题不存在时
*/
public Issue getIssue(Long id) throws IssueNotFoundException {
Issue issue = issues.get(id);
if (issue == null) {
throw new IssueNotFoundException("Issue not found: " + id);
}
return issue;
}
/**
* 获取所有议题
* @return 所有议题的列表
*/
public List<Issue> getAllIssues() {
return new ArrayList<>(issues.values());
}
/**
* 根据状态获取议题
* @param status 状态
* @return 指定状态的议题列表
*/
public List<Issue> getIssuesByStatus(IssueStatus status) {
return issues.values().stream()
.filter(issue -> issue.getStatus() == status)
.collect(Collectors.toList());
}
/**
* 根据标签获取议题
* @param label 标签
* @return 包含指定标签的议题列表
*/
public List<Issue> getIssuesByLabel(String label) {
if (label == null) return new ArrayList<>();
return issues.values().stream()
.filter(issue -> issue.getLabels().contains(label))
.collect(Collectors.toList());
}
/**
* 根据作者获取议题
* @param author 作者
* @return 指定作者创建的议题列表
*/
public List<Issue> getIssuesByAuthor(String author) {
if (author == null) return new ArrayList<>();
return issues.values().stream()
.filter(issue -> author.equals(issue.getAuthor()))
.collect(Collectors.toList());
}
/**
* 删除议题
* @param id 议题ID
* @throws IssueNotFoundException 当议题不存在时
*/
public void deleteIssue(Long id) throws IssueNotFoundException {
Issue issue = getIssue(id);
issues.remove(id);
}
/**
* 获取议题总数
* @return 议题总数
*/
public int getTotalIssueCount() {
return issues.size();
}
/**
* 获取开放议题数
* @return 开放议题数
*/
public int getOpenIssueCount() {
return (int) issues.values().stream()
.filter(issue -> issue.getStatus() == IssueStatus.OPEN)
.count();
}
/**
* 获取关闭议题数
* @return 关闭议题数
*/
public int getClosedIssueCount() {
return (int) issues.values().stream()
.filter(issue -> issue.getStatus() == IssueStatus.CLOSED)
.count();
}
}
/**
* 议题类
*/
class Issue {
private Long id;
private String title;
private String description;
private String author;
private IssueStatus status;
private Set<String> labels;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private List<Comment> comments;
public Issue() {
this.comments = new ArrayList<>();
this.labels = new HashSet<>();
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public以上就是Linux环境下完整搭建GitLab私有代码仓库的详细流程的详细内容,更多关于Linux搭建GitLab私有代码仓库的资料请关注脚本之家其它相关文章!
