Linux環(huán)境下完整搭建GitLab私有代碼倉庫的詳細流程
在現(xiàn)代軟件開發(fā)中,代碼版本控制是團隊協(xié)作的基石。雖然市面上有眾多代碼托管平臺,但出于安全性、定制化和成本控制等多方面考慮,越來越多的企業(yè)選擇搭建自己的私有代碼倉庫。GitLab 作為一款功能強大、開源免費的 DevOps 平臺,無疑是私有代碼倉庫的最佳選擇之一。
本文將帶你從零開始,在 Linux 環(huán)境下完整搭建 GitLab 私有代碼倉庫,并通過實際 Java 項目演示如何使用這一平臺進行日常開發(fā)工作。無論你是系統(tǒng)管理員、開發(fā)工程師還是技術負責人,都能從中獲得實用的知識和技能。
環(huán)境準備與系統(tǒng)要求
在開始安裝 GitLab 之前,我們需要確保服務器環(huán)境滿足基本要求。GitLab 對硬件資源有一定要求,特別是在用戶量較大或項目較多的情況下。
硬件要求
- CPU: 至少 2 核心(推薦 4 核心以上)
- 內(nèi)存: 至少 4GB(推薦 8GB 以上)
- 存儲: 至少 20GB 可用空間(根據(jù)項目數(shù)量和大小調(diào)整)
- 操作系統(tǒng): Ubuntu 20.04/22.04, CentOS 7/8, RHEL 7/8 等主流 Linux 發(fā)行版
# 檢查當前系統(tǒng)信息 uname -a cat /etc/os-release free -h df -h
軟件依賴
GitLab 需要以下基礎軟件支持:
# 更新系統(tǒng)包 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 安裝與配置
現(xiàn)在我們開始正式安裝 GitLab。GitLab 提供了多種安裝方式,這里我們采用最簡單直接的 Omnibus 包安裝方法。
添加 GitLab 倉庫
首先添加 GitLab 的官方倉庫:
# 下載并安裝 GitLab 倉庫配置 curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash # 或對于 RPM 系統(tǒng) curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash
安裝 GitLab CE
執(zhí)行安裝命令:
# 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 # 檢查服務狀態(tài) sudo gitlab-ctl status # 查看日志 sudo gitlab-ctl tail
首次訪問時,系統(tǒng)會要求設置 root 用戶密碼。請妥善保存這個密碼!
用戶管理與權限控制
GitLab 提供了完善的用戶管理和權限控制系統(tǒng),讓我們能夠精細地控制誰可以訪問哪些資源。
創(chuàng)建用戶
# 通過命令行創(chuàng)建用戶(可選) sudo gitlab-rails console # 在 Rails 控制臺中執(zhí)行: 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: 可以推送代碼、創(chuàng)建合并請求、管理議題等
- Reporter: 可以創(chuàng)建議題、評論、查看代碼等
- Guest: 基本只讀權限
創(chuàng)建用戶組
# 通過 Web 界面創(chuàng)建群組 # 1. 點擊右上角頭像 → Groups → Create group # 2. 填寫群組名稱、路徑、描述 # 3. 設置可見性級別(Private/Internal/Public) # 4. 點擊 "Create group"
添加成員到群組
// 雖然這不是真正的 Java 代碼,但我們可以模擬一個用戶管理的 API 調(diào)用
public class GitLabUserManager {
public static void main(String[] args) {
GitLabClient client = new GitLabClient("https://gitlab.yourcompany.com", "your-access-token");
try {
// 創(chuàng)建新用戶
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());
}
}
}創(chuàng)建和管理 Java 項目
現(xiàn)在讓我們創(chuàng)建一個實際的 Java 項目,演示如何在 GitLab 上進行日常開發(fā)工作。
創(chuàng)建新項目
- 登錄 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 項目
# 創(chuàng)建項目目錄 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
創(chuàng)建 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>創(chuàng)建 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 命令行參數(shù)
*/
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.");
}
/**
* 處理數(shù)據(jù)的示例方法
* @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();
}
/**
* 計算兩個數(shù)的和
* @param a 第一個數(shù)
* @param b 第二個數(shù)
* @return 兩數(shù)之和
*/
public int addNumbers(int a, int b) {
logger.trace("Adding numbers: {} + {}", a, b);
return a + b;
}
}創(chuàng)建測試類
// 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() {
// 假設我們只想在開發(fā)環(huán)境中運行某些測試
boolean isProduction = System.getProperty("env") != null &&
System.getProperty("env").equals("production");
assumeTrue(!isProduction, "Skipping test in production environment");
// 實際測試邏輯
assertTrue(true);
}
}創(chuàng)建日志配置
<!-- 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
分支策略與工作流
在團隊協(xié)作中,合理的分支策略至關重要。GitLab 支持多種工作流,我們推薦使用 Git Flow 或類似的分支管理策略。
Git Flow 工作流

創(chuàng)建和管理分支
# 創(chuàng)建新分支 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);
/**
* 執(zhí)行四則運算
* @param a 第一個操作數(shù)
* @param b 第二個操作數(shù)
* @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 要計算平方根的數(shù)
* @return 平方根結果
* @throws IllegalArgumentException 當輸入為負數(shù)時
*/
public double squareRoot(double number) {
if (number < 0) {
throw new IllegalArgumentException("Cannot calculate square root of negative number");
}
return Math.sqrt(number);
}
/**
* 計算冪運算
* @param base 底數(shù)
* @param exponent 指數(shù)
* @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);
}
}創(chuàng)建合并請求
在 GitLab 中,我們通常通過合并請求(Merge Request)來審查和合并代碼:
# 推送功能分支 git push origin feature/calculator-enhancement # 在 GitLab Web 界面創(chuàng)建合并請求: # 1. 進入項目頁面 # 2. 點擊 "Merge Requests" → "New merge request" # 3. 選擇源分支和目標分支 # 4. 填寫標題和描述 # 5. 指定評審人員 # 6. 點擊 "Create merge request"
CI/CD 流水線配置
GitLab 內(nèi)置了強大的 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
# 代碼質(zhì)量檢查
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
# 開發(fā)環(huán)境部署
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
# 預生產(chǎn)環(huán)境部署
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
# 生產(chǎn)環(huán)境部署
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 實踐
讓我們?yōu)?Java 項目創(chuàng)建一些實用的腳本:
# 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 # 創(chuàng)建部署目錄 DEPLOY_DIR="/opt/java-apps/$CI_PROJECT_NAME" mkdir -p $DEPLOY_DIR # 復制 jar 文件 cp target/*.jar $DEPLOY_DIR/app.jar # 創(chuàng)建或更新 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 {
/**
* 獲取應用健康狀態(tài)
* @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<>();
// 數(shù)據(jù)庫連接檢查(模擬)
checks.put("database", checkDatabaseConnection() ? "UP" : "DOWN");
// 磁盤空間檢查(模擬)
checks.put("diskSpace", checkDiskSpace() ? "UP" : "DOWN");
// 內(nèi)存使用檢查(模擬)
checks.put("memory", checkMemoryUsage() ? "UP" : "DOWN");
return checks;
}
private boolean checkDatabaseConnection() {
// 模擬數(shù)據(jù)庫連接檢查
try {
Thread.sleep(100); // 模擬網(wǎng)絡延遲
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() {
// 模擬內(nèi)存使用檢查
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"(基于我們的模擬實現(xiàn))
assertEquals("UP", checks.get("database"));
assertEquals("UP", checks.get("diskSpace"));
assertEquals("UP", checks.get("memory"));
}
}代碼質(zhì)量與安全分析
GitLab 不僅提供版本控制,還內(nèi)置了代碼質(zhì)量分析和安全掃描功能。
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 代碼質(zhì)量實踐
// 高質(zhì)量 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;
/**
* 用戶服務類
* 提供用戶相關的業(yè)務邏輯
*/
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
private final ExecutorService executorService;
/**
* 構造函數(shù)
* @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);
}
/**
* 創(chuàng)建新用戶
* @param userData 用戶數(shù)據(jù)
* @return 創(chuàng)建的用戶
* @throws ValidationException 當用戶數(shù)據(jù)無效時
*/
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 更新的用戶數(shù)據(jù)
* @return 更新后的用戶
* @throws UserNotFoundException 當用戶不存在時
* @throws ValidationException 當用戶數(shù)據(jù)無效時
*/
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);
}
}
/**
* 驗證用戶數(shù)據(jù)
* @param userData 用戶數(shù)據(jù)
* @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; }
}
/**
* 用戶數(shù)據(jù)傳輸對象
*/
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);
}
}團隊協(xié)作與代碼評審
GitLab 提供了完善的團隊協(xié)作功能,包括議題跟蹤、代碼評審、討論等。
創(chuàng)建和管理議題
// 我們可以創(chuàng)建一個簡單的議題管理系統(tǒng)來演示協(xié)作功能
package com.yourcompany.issue;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 簡單的議題管理系統(tǒng)
*/
public class IssueManager {
private final Map<Long, Issue> issues;
private long nextId;
public IssueManager() {
this.issues = new ConcurrentHashMap<>();
this.nextId = 1;
}
/**
* 創(chuàng)建新議題
* @param title 標題
* @param description 描述
* @param author 作者
* @param labels 標簽
* @return 創(chuàng)建的議題
*/
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 新狀態(tài)(可選)
* @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 評論內(nèi)容
* @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());
}
/**
* 根據(jù)狀態(tài)獲取議題
* @param status 狀態(tài)
* @return 指定狀態(tài)的議題列表
*/
public List<Issue> getIssuesByStatus(IssueStatus status) {
return issues.values().stream()
.filter(issue -> issue.getStatus() == status)
.collect(Collectors.toList());
}
/**
* 根據(jù)標簽獲取議題
* @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());
}
/**
* 根據(jù)作者獲取議題
* @param author 作者
* @return 指定作者創(chuàng)建的議題列表
*/
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);
}
/**
* 獲取議題總數(shù)
* @return 議題總數(shù)
*/
public int getTotalIssueCount() {
return issues.size();
}
/**
* 獲取開放議題數(shù)
* @return 開放議題數(shù)
*/
public int getOpenIssueCount() {
return (int) issues.values().stream()
.filter(issue -> issue.getStatus() == IssueStatus.OPEN)
.count();
}
/**
* 獲取關閉議題數(shù)
* @return 關閉議題數(shù)
*/
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環(huán)境下完整搭建GitLab私有代碼倉庫的詳細流程的詳細內(nèi)容,更多關于Linux搭建GitLab私有代碼倉庫的資料請關注腳本之家其它相關文章!
相關文章
Linux CentOs7 ping網(wǎng)址未知的名稱或服務問題
文章介紹了如何在Linux虛擬機中解決IP地址配置問題的步驟,包括檢查IP、查看網(wǎng)絡配置、編輯網(wǎng)絡配置文件以及重啟網(wǎng)絡服務,最終成功獲取IP地址并能ping通2024-12-12
linux環(huán)境搭建圖數(shù)據(jù)庫neo4j的講解
今天小編就為大家分享一篇關于linux環(huán)境搭建圖數(shù)據(jù)庫neo4j的講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-04-04
Linux不丟失數(shù)據(jù)無損擴容分區(qū)操作命令實例
這篇文章主要介紹了Linux不丟失數(shù)據(jù)無損擴容分區(qū)操作命令實例,在實際操作前,請確保備份重要數(shù)據(jù),并確認分區(qū)和文件系統(tǒng)的類型,因為不同的文件系統(tǒng)擴展方法會不同,如果是在線擴容,請確保沒有掛載使用該分區(qū)或者文件系統(tǒng)2024-06-06
Linux系統(tǒng)設置PATH環(huán)境變量(3種方法)
這篇文章主要介紹了Linux系統(tǒng)設置PATH環(huán)境變量(3種方法),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11
在ubuntu16.04上創(chuàng)建matlab的快捷方式(實現(xiàn)方法)
下面小編就為大家分享一篇在ubuntu16.04上創(chuàng)建matlab的快捷方式實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助2017-12-12
MySQL/MariaDB/Percona數(shù)據(jù)庫升級腳本
這篇文章主要介紹了MySQL/MariaDB/Percona數(shù)據(jù)庫升級腳本的相關資料,需要的朋友可以參考下2016-12-12

