Initial commit: Jenkins CI/CD Demo Project with Spring Boot 3
This commit is contained in:
commit
877335f73f
140
.gitignore
vendored
Normal file
140
.gitignore
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Eclipse
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated_test/
|
||||
|
||||
# Scala IDE specific (Scala & Java IDE for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# NetBeans
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# SonarQube
|
||||
.sonar/
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Application specific
|
||||
application-local.yml
|
||||
application-local.properties
|
||||
|
||||
# Test coverage
|
||||
*.exec
|
||||
jacoco.exec
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@ -0,0 +1,54 @@
|
||||
# 使用多阶段构建优化镜像大小
|
||||
FROM openjdk:17-jdk-slim as builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制Maven配置文件
|
||||
COPY pom.xml .
|
||||
COPY .mvn .mvn
|
||||
COPY mvnw .
|
||||
|
||||
# 下载依赖(利用Docker缓存层)
|
||||
RUN ./mvnw dependency:go-offline -B
|
||||
|
||||
# 复制源代码
|
||||
COPY src ./src
|
||||
|
||||
# 构建应用
|
||||
RUN ./mvnw clean package -DskipTests
|
||||
|
||||
# 运行时镜像
|
||||
FROM openjdk:17-jre-slim
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建非root用户
|
||||
RUN groupadd -r spring && useradd -r -g spring spring
|
||||
|
||||
# 创建应用目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制jar文件
|
||||
COPY --from=builder /app/target/*.jar app.jar
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /app/logs && chown -R spring:spring /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER spring
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/api/health || exit 1
|
||||
|
||||
# JVM调优参数
|
||||
ENV JAVA_OPTS="-server -Xms256m -Xmx512m -XX:+UseG1GC -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar app.jar"]
|
320
Jenkinsfile
vendored
Normal file
320
Jenkinsfile
vendored
Normal file
@ -0,0 +1,320 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
timeout(time: 30, unit: 'MINUTES')
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
JAVA_HOME = '/usr/lib/jvm/java-17-openjdk'
|
||||
MAVEN_HOME = '/opt/maven'
|
||||
PATH = "${MAVEN_HOME}/bin:${JAVA_HOME}/bin:${env.PATH}"
|
||||
|
||||
// Docker相关环境变量
|
||||
DOCKER_REGISTRY = 'your-registry.com'
|
||||
IMAGE_NAME = 'jenkins-demo'
|
||||
IMAGE_TAG = "${BUILD_NUMBER}"
|
||||
|
||||
// SonarQube配置
|
||||
SONAR_HOST_URL = 'http://your-sonar-server:9000'
|
||||
SONAR_PROJECT_KEY = 'jenkins-demo'
|
||||
}
|
||||
|
||||
tools {
|
||||
maven 'Maven-3.9.3'
|
||||
jdk 'JDK-17'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
echo '🔄 开始检出代码...'
|
||||
checkout scm
|
||||
|
||||
script {
|
||||
env.GIT_COMMIT_SHORT = sh(
|
||||
script: "git rev-parse --short HEAD",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
}
|
||||
|
||||
echo "📋 Git提交ID: ${env.GIT_COMMIT_SHORT}"
|
||||
}
|
||||
}
|
||||
|
||||
stage('环境检查') {
|
||||
steps {
|
||||
echo '🔍 检查构建环境...'
|
||||
sh '''
|
||||
echo "Java版本:"
|
||||
java -version
|
||||
echo "Maven版本:"
|
||||
mvn -version
|
||||
echo "Git版本:"
|
||||
git --version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('编译') {
|
||||
steps {
|
||||
echo '🔨 开始编译项目...'
|
||||
sh 'mvn clean compile -DskipTests=true'
|
||||
}
|
||||
}
|
||||
|
||||
stage('单元测试') {
|
||||
steps {
|
||||
echo '🧪 运行单元测试...'
|
||||
sh 'mvn test'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
// 发布测试结果
|
||||
publishTestResults testResultsPattern: 'target/surefire-reports/*.xml'
|
||||
|
||||
// 发布代码覆盖率报告
|
||||
step([$class: 'JacocoPublisher',
|
||||
execPattern: 'target/jacoco.exec',
|
||||
classPattern: 'target/classes',
|
||||
sourcePattern: 'src/main/java',
|
||||
exclusionPattern: '**/*Test*.class'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('代码质量扫描') {
|
||||
steps {
|
||||
echo '🔍 运行SonarQube代码扫描...'
|
||||
script {
|
||||
try {
|
||||
withSonarQubeEnv('SonarQube') {
|
||||
sh '''
|
||||
mvn sonar:sonar \
|
||||
-Dsonar.projectKey=${SONAR_PROJECT_KEY} \
|
||||
-Dsonar.host.url=${SONAR_HOST_URL} \
|
||||
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
|
||||
'''
|
||||
}
|
||||
|
||||
// 等待质量门检查结果
|
||||
timeout(time: 5, unit: 'MINUTES') {
|
||||
def qg = waitForQualityGate()
|
||||
if (qg.status != 'OK') {
|
||||
error "质量门检查失败: ${qg.status}"
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
echo "⚠️ SonarQube扫描失败,继续构建流程: ${e.getMessage()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('打包') {
|
||||
steps {
|
||||
echo '📦 开始打包应用程序...'
|
||||
sh 'mvn package -DskipTests=true'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
// 归档构建产物
|
||||
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('构建Docker镜像') {
|
||||
steps {
|
||||
echo '🐳 构建Docker镜像...'
|
||||
script {
|
||||
def image = docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
|
||||
|
||||
// 也创建latest标签
|
||||
sh "docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest"
|
||||
|
||||
echo "✅ Docker镜像构建完成: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('推送Docker镜像') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'master'
|
||||
branch 'develop'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
echo '📤 推送Docker镜像到仓库...'
|
||||
script {
|
||||
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
|
||||
def image = docker.image("${IMAGE_NAME}:${IMAGE_TAG}")
|
||||
image.push()
|
||||
image.push("latest")
|
||||
}
|
||||
echo "✅ Docker镜像推送完成"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('部署到测试环境') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'develop'
|
||||
branch 'feature/*'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
echo '🚀 部署到测试环境...'
|
||||
script {
|
||||
// 部署到测试服务器
|
||||
sshagent(['test-server-ssh']) {
|
||||
sh '''
|
||||
ssh -o StrictHostKeyChecking=no user@test-server << EOF
|
||||
# 停止现有容器
|
||||
docker stop jenkins-demo-test || true
|
||||
docker rm jenkins-demo-test || true
|
||||
|
||||
# 运行新容器
|
||||
docker run -d --name jenkins-demo-test \\
|
||||
-p 8080:8080 \\
|
||||
--restart unless-stopped \\
|
||||
${IMAGE_NAME}:${IMAGE_TAG}
|
||||
|
||||
echo "✅ 测试环境部署完成"
|
||||
EOF
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('部署到生产环境') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'master'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
echo '🎯 部署到生产环境...'
|
||||
script {
|
||||
// 需要手动确认
|
||||
input message: '确认部署到生产环境?', ok: '部署',
|
||||
parameters: [choice(name: 'DEPLOY_ENV', choices: ['prod'], description: '选择部署环境')]
|
||||
|
||||
// 部署到生产服务器
|
||||
sshagent(['prod-server-ssh']) {
|
||||
sh '''
|
||||
ssh -o StrictHostKeyChecking=no user@prod-server << EOF
|
||||
# 备份当前版本
|
||||
docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:backup-$(date +%Y%m%d-%H%M%S) || true
|
||||
|
||||
# 停止现有容器
|
||||
docker stop jenkins-demo-prod || true
|
||||
docker rm jenkins-demo-prod || true
|
||||
|
||||
# 运行新容器
|
||||
docker run -d --name jenkins-demo-prod \\
|
||||
-p 80:8080 \\
|
||||
--restart unless-stopped \\
|
||||
-e SPRING_PROFILES_ACTIVE=prod \\
|
||||
${IMAGE_NAME}:${IMAGE_TAG}
|
||||
|
||||
echo "✅ 生产环境部署完成"
|
||||
EOF
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('健康检查') {
|
||||
steps {
|
||||
echo '🏥 执行应用健康检查...'
|
||||
script {
|
||||
// 等待应用启动
|
||||
sleep(time: 30, unit: 'SECONDS')
|
||||
|
||||
// 检查应用健康状态
|
||||
def healthCheckUrl = "http://localhost:8080/api/health"
|
||||
def response = sh(
|
||||
script: "curl -s -o /dev/null -w '%{http_code}' ${healthCheckUrl}",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
|
||||
if (response == "200") {
|
||||
echo "✅ 应用健康检查通过"
|
||||
} else {
|
||||
error "❌ 应用健康检查失败,HTTP状态码: ${response}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
echo '🧹 清理工作空间...'
|
||||
// 清理Docker镜像
|
||||
sh '''
|
||||
docker image prune -f
|
||||
docker system prune -f
|
||||
'''
|
||||
}
|
||||
|
||||
success {
|
||||
echo '✅ 流水线执行成功!'
|
||||
// 发送成功通知
|
||||
script {
|
||||
def message = """
|
||||
🎉 Jenkins构建成功!
|
||||
|
||||
📋 项目: ${env.JOB_NAME}
|
||||
🔢 构建号: ${env.BUILD_NUMBER}
|
||||
🌿 分支: ${env.BRANCH_NAME}
|
||||
📝 提交: ${env.GIT_COMMIT_SHORT}
|
||||
⏱️ 持续时间: ${currentBuild.durationString}
|
||||
🔗 构建链接: ${env.BUILD_URL}
|
||||
"""
|
||||
|
||||
// 可以集成钉钉、企业微信、邮件等通知
|
||||
echo message
|
||||
}
|
||||
}
|
||||
|
||||
failure {
|
||||
echo '❌ 流水线执行失败!'
|
||||
// 发送失败通知
|
||||
script {
|
||||
def message = """
|
||||
💥 Jenkins构建失败!
|
||||
|
||||
📋 项目: ${env.JOB_NAME}
|
||||
🔢 构建号: ${env.BUILD_NUMBER}
|
||||
🌿 分支: ${env.BRANCH_NAME}
|
||||
📝 提交: ${env.GIT_COMMIT_SHORT}
|
||||
⏱️ 持续时间: ${currentBuild.durationString}
|
||||
🔗 构建链接: ${env.BUILD_URL}
|
||||
📄 查看日志: ${env.BUILD_URL}console
|
||||
"""
|
||||
|
||||
echo message
|
||||
}
|
||||
}
|
||||
|
||||
unstable {
|
||||
echo '⚠️ 构建不稳定,可能存在测试失败'
|
||||
}
|
||||
|
||||
cleanup {
|
||||
// 清理工作空间
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
239
README.md
Normal file
239
README.md
Normal file
@ -0,0 +1,239 @@
|
||||
# Jenkins Demo - Spring Boot 3 CI/CD 实践项目
|
||||
|
||||
这是一个用于实践Jenkins CI/CD流程的Spring Boot 3示例项目。项目包含完整的代码拉取、编译构建、单元测试、代码扫描、Docker打包和部署流程。
|
||||
|
||||
## 🚀 项目特性
|
||||
|
||||
- **Spring Boot 3.2.6** + **JDK 17**
|
||||
- **RESTful API** - 提供用户管理相关接口
|
||||
- **单元测试** - 完整的JUnit 5测试覆盖
|
||||
- **代码质量** - 集成SonarQube代码扫描
|
||||
- **Docker支持** - 多阶段构建优化
|
||||
- **Jenkins Pipeline** - 完整的CI/CD流程
|
||||
- **健康检查** - 应用程序监控端点
|
||||
|
||||
## 📋 API接口
|
||||
|
||||
### 健康检查
|
||||
- `GET /api/health` - 应用健康状态
|
||||
- `GET /api/info` - 应用信息
|
||||
- `GET /api/welcome` - 欢迎信息
|
||||
|
||||
### 用户管理
|
||||
- `GET /api/users` - 获取所有用户
|
||||
- `GET /api/users/{id}` - 获取指定用户
|
||||
- `POST /api/users` - 创建新用户
|
||||
- `PUT /api/users/{id}` - 更新用户信息
|
||||
- `DELETE /api/users/{id}` - 删除用户
|
||||
- `POST /api/users/{id}/activate` - 激活用户
|
||||
- `POST /api/users/{id}/deactivate` - 停用用户
|
||||
- `GET /api/users/stats` - 用户统计信息
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **后端框架**: Spring Boot 3.2.6
|
||||
- **Java版本**: JDK 17
|
||||
- **构建工具**: Maven 3.9+
|
||||
- **测试框架**: JUnit 5 + MockMvc
|
||||
- **代码覆盖率**: JaCoCo
|
||||
- **容器化**: Docker + Docker Compose
|
||||
- **CI/CD**: Jenkins Pipeline
|
||||
|
||||
## 🏃♂️ 快速开始
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd jenkins-demo
|
||||
```
|
||||
|
||||
2. **编译项目**
|
||||
```bash
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
3. **运行测试**
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
4. **启动应用**
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
5. **访问应用**
|
||||
- 应用地址: http://localhost:8080
|
||||
- 健康检查: http://localhost:8080/api/health
|
||||
- 用户列表: http://localhost:8080/api/users
|
||||
|
||||
### Docker运行
|
||||
|
||||
1. **构建镜像**
|
||||
```bash
|
||||
docker build -t jenkins-demo:latest .
|
||||
```
|
||||
|
||||
2. **运行容器**
|
||||
```bash
|
||||
docker run -d --name jenkins-demo -p 8080:8080 jenkins-demo:latest
|
||||
```
|
||||
|
||||
3. **使用Docker Compose**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🔧 Maven命令
|
||||
|
||||
```bash
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 生成代码覆盖率报告
|
||||
mvn test jacoco:report
|
||||
|
||||
# 打包应用
|
||||
mvn clean package
|
||||
|
||||
# 跳过测试打包
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 代码质量扫描(需要配置SonarQube)
|
||||
mvn sonar:sonar
|
||||
|
||||
# 启动应用
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## 🔍 代码质量
|
||||
|
||||
项目集成了以下代码质量工具:
|
||||
|
||||
- **JaCoCo** - 代码覆盖率分析
|
||||
- **SonarQube** - 静态代码分析
|
||||
- **Maven Surefire** - 单元测试报告
|
||||
|
||||
生成测试报告:
|
||||
```bash
|
||||
mvn clean test jacoco:report
|
||||
```
|
||||
|
||||
报告位置:`target/site/jacoco/index.html`
|
||||
|
||||
## 🚀 Jenkins CI/CD流程
|
||||
|
||||
### Pipeline阶段
|
||||
|
||||
1. **代码检出** - 从Git仓库拉取代码
|
||||
2. **环境检查** - 验证Java、Maven等工具版本
|
||||
3. **编译** - 编译Java源代码
|
||||
4. **单元测试** - 运行JUnit测试并生成报告
|
||||
5. **代码扫描** - SonarQube静态代码分析
|
||||
6. **打包** - 生成可执行JAR文件
|
||||
7. **Docker构建** - 构建Docker镜像
|
||||
8. **镜像推送** - 推送到Docker Registry
|
||||
9. **自动部署** - 部署到目标环境
|
||||
10. **健康检查** - 验证应用启动状态
|
||||
|
||||
### 环境要求
|
||||
|
||||
Jenkins服务器需要安装:
|
||||
- JDK 17
|
||||
- Maven 3.9+
|
||||
- Docker
|
||||
- Git
|
||||
- SonarQube Scanner(可选)
|
||||
|
||||
### 部署配置
|
||||
|
||||
修改`Jenkinsfile`中的以下配置:
|
||||
|
||||
```groovy
|
||||
environment {
|
||||
DOCKER_REGISTRY = 'your-registry.com' // Docker仓库地址
|
||||
SONAR_HOST_URL = 'http://your-sonar-server:9000' // SonarQube服务器
|
||||
// ... 其他配置
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 项目结构
|
||||
|
||||
```
|
||||
jenkins-demo/
|
||||
├── src/
|
||||
│ ├── main/
|
||||
│ │ ├── java/com/jenkins/demo/
|
||||
│ │ │ ├── JenkinsDemoApplication.java # 应用主类
|
||||
│ │ │ ├── controller/ # REST控制器
|
||||
│ │ │ ├── service/ # 业务服务
|
||||
│ │ │ └── model/ # 数据模型
|
||||
│ │ └── resources/
|
||||
│ │ └── application.yml # 应用配置
|
||||
│ └── test/ # 测试代码
|
||||
├── Dockerfile # Docker镜像构建
|
||||
├── Jenkinsfile # Jenkins Pipeline
|
||||
├── docker-compose.yml # Docker Compose配置
|
||||
├── pom.xml # Maven配置
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 🔗 API示例
|
||||
|
||||
### 创建用户
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"name": "测试用户"
|
||||
}'
|
||||
```
|
||||
|
||||
### 获取所有用户
|
||||
```bash
|
||||
curl http://localhost:8080/api/users
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
## 📊 监控端点
|
||||
|
||||
Spring Boot Actuator提供了以下监控端点:
|
||||
|
||||
- `/actuator/health` - 健康状态
|
||||
- `/actuator/info` - 应用信息
|
||||
- `/actuator/metrics` - 应用指标
|
||||
- `/actuator/prometheus` - Prometheus指标(如果启用)
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork项目
|
||||
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
|
||||
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||
5. 开启Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目使用MIT许可证。详情请参见[LICENSE](LICENSE)文件。
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
|
||||
- 项目Issues: [GitHub Issues](https://github.com/your-username/jenkins-demo/issues)
|
||||
- 邮箱: your-email@example.com
|
||||
|
||||
---
|
||||
|
||||
🎉 **祝您使用愉快!** 如果这个项目对您有帮助,请给个⭐Star支持一下!
|
285
deploy.sh
Normal file
285
deploy.sh
Normal file
@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jenkins Demo 部署脚本
|
||||
# 用于生产环境部署
|
||||
|
||||
set -e
|
||||
|
||||
# 配置变量
|
||||
APP_NAME="jenkins-demo"
|
||||
APP_VERSION="1.0.0"
|
||||
DOCKER_IMAGE="${APP_NAME}:${APP_VERSION}"
|
||||
CONTAINER_NAME="${APP_NAME}-prod"
|
||||
APP_PORT="80"
|
||||
CONTAINER_PORT="8080"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查Docker是否安装
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker未安装,请先安装Docker"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Docker检查通过"
|
||||
}
|
||||
|
||||
# 停止并移除现有容器
|
||||
stop_existing_container() {
|
||||
if docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
log_info "停止现有容器: ${CONTAINER_NAME}"
|
||||
docker stop ${CONTAINER_NAME} || true
|
||||
docker rm ${CONTAINER_NAME} || true
|
||||
log_success "现有容器已停止并移除"
|
||||
else
|
||||
log_info "未发现现有容器: ${CONTAINER_NAME}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 备份现有镜像
|
||||
backup_current_image() {
|
||||
if docker images | grep -q "${APP_NAME}.*latest"; then
|
||||
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
|
||||
log_info "备份当前镜像为: ${APP_NAME}:${BACKUP_TAG}"
|
||||
docker tag ${APP_NAME}:latest ${APP_NAME}:${BACKUP_TAG}
|
||||
log_success "镜像备份完成"
|
||||
fi
|
||||
}
|
||||
|
||||
# 拉取最新镜像
|
||||
pull_latest_image() {
|
||||
log_info "拉取最新镜像: ${DOCKER_IMAGE}"
|
||||
if docker pull ${DOCKER_IMAGE}; then
|
||||
docker tag ${DOCKER_IMAGE} ${APP_NAME}:latest
|
||||
log_success "镜像拉取完成"
|
||||
else
|
||||
log_error "镜像拉取失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动新容器
|
||||
start_new_container() {
|
||||
log_info "启动新容器: ${CONTAINER_NAME}"
|
||||
|
||||
docker run -d \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--restart unless-stopped \
|
||||
-p ${APP_PORT}:${CONTAINER_PORT} \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-e JAVA_OPTS="-Xms512m -Xmx1024m" \
|
||||
-v /var/log/${APP_NAME}:/app/logs \
|
||||
--health-cmd="curl -f http://localhost:${CONTAINER_PORT}/api/health || exit 1" \
|
||||
--health-interval=30s \
|
||||
--health-timeout=10s \
|
||||
--health-retries=3 \
|
||||
--health-start-period=60s \
|
||||
${APP_NAME}:latest
|
||||
|
||||
log_success "容器启动完成"
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
health_check() {
|
||||
log_info "执行健康检查..."
|
||||
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -f -s http://localhost:${APP_PORT}/api/health > /dev/null; then
|
||||
log_success "应用程序健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "健康检查失败,重试 (${attempt}/${max_attempts})"
|
||||
sleep 10
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
log_error "健康检查失败,应用程序可能未正常启动"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 回滚函数
|
||||
rollback() {
|
||||
log_warning "开始回滚操作..."
|
||||
|
||||
# 停止失败的容器
|
||||
docker stop ${CONTAINER_NAME} || true
|
||||
docker rm ${CONTAINER_NAME} || true
|
||||
|
||||
# 查找最新的备份镜像
|
||||
BACKUP_IMAGE=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep "${APP_NAME}:backup-" | head -1)
|
||||
|
||||
if [ -n "$BACKUP_IMAGE" ]; then
|
||||
log_info "使用备份镜像回滚: ${BACKUP_IMAGE}"
|
||||
docker tag ${BACKUP_IMAGE} ${APP_NAME}:latest
|
||||
start_new_container
|
||||
|
||||
if health_check; then
|
||||
log_success "回滚成功"
|
||||
else
|
||||
log_error "回滚后健康检查仍然失败"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_error "未找到备份镜像,无法回滚"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理旧镜像
|
||||
cleanup_old_images() {
|
||||
log_info "清理旧的Docker镜像..."
|
||||
|
||||
# 保留最近的5个备份镜像
|
||||
OLD_BACKUPS=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep "${APP_NAME}:backup-" | tail -n +6)
|
||||
|
||||
if [ -n "$OLD_BACKUPS" ]; then
|
||||
echo "$OLD_BACKUPS" | while read -r image; do
|
||||
log_info "删除旧备份镜像: $image"
|
||||
docker rmi "$image" || true
|
||||
done
|
||||
fi
|
||||
|
||||
# 清理悬挂镜像
|
||||
docker image prune -f
|
||||
|
||||
log_success "镜像清理完成"
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
show_deployment_info() {
|
||||
echo
|
||||
log_success "=== 部署信息 ==="
|
||||
echo "应用名称: ${APP_NAME}"
|
||||
echo "应用版本: ${APP_VERSION}"
|
||||
echo "容器名称: ${CONTAINER_NAME}"
|
||||
echo "访问地址: http://localhost:${APP_PORT}"
|
||||
echo "健康检查: http://localhost:${APP_PORT}/api/health"
|
||||
echo "API文档: http://localhost:${APP_PORT}/api/users"
|
||||
echo
|
||||
log_success "=== 部署完成 ==="
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
log_info "开始部署 ${APP_NAME} v${APP_VERSION}"
|
||||
|
||||
# 检查环境
|
||||
check_docker
|
||||
|
||||
# 备份现有镜像
|
||||
backup_current_image
|
||||
|
||||
# 停止现有容器
|
||||
stop_existing_container
|
||||
|
||||
# 拉取新镜像
|
||||
pull_latest_image
|
||||
|
||||
# 启动新容器
|
||||
start_new_container
|
||||
|
||||
# 健康检查
|
||||
if health_check; then
|
||||
# 清理旧镜像
|
||||
cleanup_old_images
|
||||
|
||||
# 显示部署信息
|
||||
show_deployment_info
|
||||
else
|
||||
# 健康检查失败,执行回滚
|
||||
rollback
|
||||
fi
|
||||
}
|
||||
|
||||
# 脚本使用说明
|
||||
usage() {
|
||||
echo "用法: $0 [选项]"
|
||||
echo
|
||||
echo "选项:"
|
||||
echo " deploy 执行部署(默认)"
|
||||
echo " rollback 回滚到上一个版本"
|
||||
echo " status 查看应用状态"
|
||||
echo " logs 查看应用日志"
|
||||
echo " stop 停止应用"
|
||||
echo " help 显示帮助信息"
|
||||
echo
|
||||
}
|
||||
|
||||
# 查看应用状态
|
||||
status() {
|
||||
echo "=== 容器状态 ==="
|
||||
docker ps -a --filter name=${CONTAINER_NAME}
|
||||
echo
|
||||
echo "=== 镜像列表 ==="
|
||||
docker images | grep ${APP_NAME}
|
||||
}
|
||||
|
||||
# 查看应用日志
|
||||
logs() {
|
||||
if docker ps --filter name=${CONTAINER_NAME} --format '{{.Names}}' | grep -q ${CONTAINER_NAME}; then
|
||||
docker logs -f ${CONTAINER_NAME}
|
||||
else
|
||||
log_error "容器 ${CONTAINER_NAME} 未运行"
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止应用
|
||||
stop() {
|
||||
log_info "停止应用..."
|
||||
docker stop ${CONTAINER_NAME} || true
|
||||
docker rm ${CONTAINER_NAME} || true
|
||||
log_success "应用已停止"
|
||||
}
|
||||
|
||||
# 根据参数执行不同操作
|
||||
case "${1:-deploy}" in
|
||||
deploy)
|
||||
main
|
||||
;;
|
||||
rollback)
|
||||
rollback
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
logs)
|
||||
logs
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "未知参数: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@ -0,0 +1,36 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
jenkins-demo:
|
||||
build: .
|
||||
container_name: jenkins-demo-app
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SPRING_PROFILES_ACTIVE=docker
|
||||
- JAVA_OPTS=-Xms256m -Xmx512m
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# 可选:添加数据库服务(如果需要)
|
||||
# postgres:
|
||||
# image: postgres:15-alpine
|
||||
# container_name: jenkins-demo-db
|
||||
# environment:
|
||||
# POSTGRES_DB: jenkins_demo
|
||||
# POSTGRES_USER: jenkins_user
|
||||
# POSTGRES_PASSWORD: jenkins_password
|
||||
# volumes:
|
||||
# - postgres_data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
|
||||
# volumes:
|
||||
# postgres_data:
|
233
mvnw
vendored
Normal file
233
mvnw
vendored
Normal file
@ -0,0 +1,233 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir"; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname $0)")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
205
mvnw.cmd
vendored
Normal file
205
mvnw.cmd
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
135
pom.xml
Normal file
135
pom.xml
Normal file
@ -0,0 +1,135 @@
|
||||
<?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
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.6</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.jenkins.demo</groupId>
|
||||
<artifactId>jenkins-demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>jenkins-demo</name>
|
||||
<description>Jenkins CI/CD Demo Project with Spring Boot 3</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<sonar.projectKey>jenkins-demo</sonar.projectKey>
|
||||
<sonar.projectName>Jenkins Demo</sonar.projectName>
|
||||
<sonar.projectVersion>1.0.0</sonar.projectVersion>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Spring Boot Maven Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<!-- Maven Compiler Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Maven Surefire Plugin for Unit Tests -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
<include>**/*Tests.java</include>
|
||||
</includes>
|
||||
<reportFormat>xml</reportFormat>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- JaCoCo Plugin for Code Coverage -->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.10</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>test</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- SonarQube Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.sonarsource.scanner.maven</groupId>
|
||||
<artifactId>sonar-maven-plugin</artifactId>
|
||||
<version>3.9.1.2184</version>
|
||||
</plugin>
|
||||
|
||||
<!-- Docker Maven Plugin -->
|
||||
<plugin>
|
||||
<groupId>com.spotify</groupId>
|
||||
<artifactId>dockerfile-maven-plugin</artifactId>
|
||||
<version>1.4.13</version>
|
||||
<configuration>
|
||||
<repository>jenkins-demo</repository>
|
||||
<tag>${project.version}</tag>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
18
src/main/java/com/jenkins/demo/JenkinsDemoApplication.java
Normal file
18
src/main/java/com/jenkins/demo/JenkinsDemoApplication.java
Normal file
@ -0,0 +1,18 @@
|
||||
package com.jenkins.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Jenkins Demo Application Main Class
|
||||
*
|
||||
* @author Jenkins Demo Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class JenkinsDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(JenkinsDemoApplication.class, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.jenkins.demo.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Health Check Controller
|
||||
* 提供应用程序健康检查接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class HealthController {
|
||||
|
||||
/**
|
||||
* 健康检查接口
|
||||
* GET /api/health
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public Map<String, Object> health() {
|
||||
return Map.of(
|
||||
"status", "UP",
|
||||
"timestamp", LocalDateTime.now().toString(),
|
||||
"application", "Jenkins Demo Application",
|
||||
"version", "1.0.0"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用信息接口
|
||||
* GET /api/info
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public Map<String, Object> info() {
|
||||
return Map.of(
|
||||
"name", "Jenkins Demo Application",
|
||||
"description", "Spring Boot 3 应用程序用于Jenkins CI/CD流程演示",
|
||||
"version", "1.0.0",
|
||||
"java.version", System.getProperty("java.version"),
|
||||
"spring.profiles.active", System.getProperty("spring.profiles.active", "default")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的欢迎接口
|
||||
* GET /api/welcome
|
||||
*/
|
||||
@GetMapping("/welcome")
|
||||
public Map<String, String> welcome() {
|
||||
return Map.of(
|
||||
"message", "欢迎使用Jenkins Demo应用程序!",
|
||||
"description", "这是一个用于演示Jenkins CI/CD流程的Spring Boot应用程序"
|
||||
);
|
||||
}
|
||||
}
|
132
src/main/java/com/jenkins/demo/controller/UserController.java
Normal file
132
src/main/java/com/jenkins/demo/controller/UserController.java
Normal file
@ -0,0 +1,132 @@
|
||||
package com.jenkins.demo.controller;
|
||||
|
||||
import com.jenkins.demo.model.User;
|
||||
import com.jenkins.demo.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* User REST Controller
|
||||
* 提供用户管理的REST API接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户
|
||||
* GET /api/users
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<User>> getAllUsers() {
|
||||
List<User> users = userService.getAllUsers();
|
||||
return ResponseEntity.ok(users);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
* GET /api/users/{id}
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
Optional<User> user = userService.getUserById(id);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* POST /api/users
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createUser(@Valid @RequestBody User user) {
|
||||
try {
|
||||
User createdUser = userService.createUser(user);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* PUT /api/users/{id}
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
|
||||
try {
|
||||
Optional<User> updatedUser = userService.updateUser(id, user);
|
||||
return updatedUser.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* DELETE /api/users/{id}
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
boolean deleted = userService.deleteUser(id);
|
||||
return deleted ? ResponseEntity.noContent().build()
|
||||
: ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活用户
|
||||
* POST /api/users/{id}/activate
|
||||
*/
|
||||
@PostMapping("/{id}/activate")
|
||||
public ResponseEntity<Map<String, String>> activateUser(@PathVariable Long id) {
|
||||
boolean activated = userService.activateUser(id);
|
||||
if (activated) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户已激活"));
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用用户
|
||||
* POST /api/users/{id}/deactivate
|
||||
*/
|
||||
@PostMapping("/{id}/deactivate")
|
||||
public ResponseEntity<Map<String, String>> deactivateUser(@PathVariable Long id) {
|
||||
boolean deactivated = userService.deactivateUser(id);
|
||||
if (deactivated) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户已停用"));
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计信息
|
||||
* GET /api/users/stats
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<Map<String, Object>> getUserStats() {
|
||||
long totalUsers = userService.getUserCount();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"totalUsers", totalUsers,
|
||||
"timestamp", System.currentTimeMillis()
|
||||
));
|
||||
}
|
||||
}
|
91
src/main/java/com/jenkins/demo/model/User.java
Normal file
91
src/main/java/com/jenkins/demo/model/User.java
Normal file
@ -0,0 +1,91 @@
|
||||
package com.jenkins.demo.model;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* User Entity Model
|
||||
*/
|
||||
public class User {
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 2, max = 50, message = "用户名长度必须在2-50个字符之间")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
@Size(min = 1, max = 100, message = "姓名长度必须在1-100个字符之间")
|
||||
private String name;
|
||||
|
||||
private String status;
|
||||
|
||||
// 默认构造函数
|
||||
public User() {}
|
||||
|
||||
// 带参构造函数
|
||||
public User(Long id, String username, String email, String name, String status) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.name = name;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
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 String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
133
src/main/java/com/jenkins/demo/service/UserService.java
Normal file
133
src/main/java/com/jenkins/demo/service/UserService.java
Normal file
@ -0,0 +1,133 @@
|
||||
package com.jenkins.demo.service;
|
||||
|
||||
import com.jenkins.demo.model.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* User Service Implementation
|
||||
* 提供用户管理的业务逻辑
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final Map<Long, User> users = new ConcurrentHashMap<>();
|
||||
private final AtomicLong idGenerator = new AtomicLong(1);
|
||||
|
||||
public UserService() {
|
||||
// 初始化一些测试数据
|
||||
initializeTestData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户
|
||||
*/
|
||||
public List<User> getAllUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
*/
|
||||
public Optional<User> getUserById(Long id) {
|
||||
return Optional.ofNullable(users.get(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
*/
|
||||
public Optional<User> getUserByUsername(String username) {
|
||||
return users.values().stream()
|
||||
.filter(user -> user.getUsername().equals(username))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
public User createUser(User user) {
|
||||
if (getUserByUsername(user.getUsername()).isPresent()) {
|
||||
throw new IllegalArgumentException("用户名已存在: " + user.getUsername());
|
||||
}
|
||||
|
||||
Long id = idGenerator.getAndIncrement();
|
||||
user.setId(id);
|
||||
user.setStatus("ACTIVE");
|
||||
users.put(id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
public Optional<User> updateUser(Long id, User userUpdates) {
|
||||
User existingUser = users.get(id);
|
||||
if (existingUser == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// 检查用户名是否被其他用户使用
|
||||
if (!existingUser.getUsername().equals(userUpdates.getUsername())) {
|
||||
Optional<User> userWithSameUsername = getUserByUsername(userUpdates.getUsername());
|
||||
if (userWithSameUsername.isPresent() && !userWithSameUsername.get().getId().equals(id)) {
|
||||
throw new IllegalArgumentException("用户名已存在: " + userUpdates.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
existingUser.setUsername(userUpdates.getUsername());
|
||||
existingUser.setEmail(userUpdates.getEmail());
|
||||
existingUser.setName(userUpdates.getName());
|
||||
|
||||
return Optional.of(existingUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
public boolean deleteUser(Long id) {
|
||||
return users.remove(id) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户总数
|
||||
*/
|
||||
public long getUserCount() {
|
||||
return users.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活用户
|
||||
*/
|
||||
public boolean activateUser(Long id) {
|
||||
User user = users.get(id);
|
||||
if (user != null) {
|
||||
user.setStatus("ACTIVE");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用用户
|
||||
*/
|
||||
public boolean deactivateUser(Long id) {
|
||||
User user = users.get(id);
|
||||
if (user != null) {
|
||||
user.setStatus("INACTIVE");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化测试数据
|
||||
*/
|
||||
private void initializeTestData() {
|
||||
createUser(new User(null, "admin", "admin@jenkins-demo.com", "系统管理员", null));
|
||||
createUser(new User(null, "user1", "user1@jenkins-demo.com", "张三", null));
|
||||
createUser(new User(null, "user2", "user2@jenkins-demo.com", "李四", null));
|
||||
}
|
||||
}
|
37
src/main/resources/application.yml
Normal file
37
src/main/resources/application.yml
Normal file
@ -0,0 +1,37 @@
|
||||
# Spring Boot Application Configuration
|
||||
spring:
|
||||
application:
|
||||
name: jenkins-demo
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
# Actuator Configuration
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
level:
|
||||
com.jenkins.demo: INFO
|
||||
org.springframework: WARN
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/jenkins-demo.log
|
||||
|
||||
# Custom Application Properties
|
||||
app:
|
||||
name: Jenkins Demo Application
|
||||
version: 1.0.0
|
||||
description: Spring Boot 3 application for Jenkins CI/CD pipeline demonstration
|
@ -0,0 +1,24 @@
|
||||
package com.jenkins.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* Jenkins Demo Application Tests
|
||||
*/
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:application-test.properties")
|
||||
class JenkinsDemoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// 测试Spring上下文是否能正常加载
|
||||
}
|
||||
|
||||
@Test
|
||||
void applicationStarts() {
|
||||
// 测试应用程序是否能正常启动
|
||||
// 这个测试会验证所有的Spring Bean是否能正确创建和注入
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.jenkins.demo.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Health Controller Tests
|
||||
*/
|
||||
@WebMvcTest(HealthController.class)
|
||||
class HealthControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/health - 应该返回健康状态")
|
||||
void shouldReturnHealthStatus() throws Exception {
|
||||
mockMvc.perform(get("/api/health"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.status").value("UP"))
|
||||
.andExpect(jsonPath("$.timestamp").exists())
|
||||
.andExpect(jsonPath("$.application").value("Jenkins Demo Application"))
|
||||
.andExpect(jsonPath("$.version").value("1.0.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/info - 应该返回应用信息")
|
||||
void shouldReturnApplicationInfo() throws Exception {
|
||||
mockMvc.perform(get("/api/info"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.name").value("Jenkins Demo Application"))
|
||||
.andExpect(jsonPath("$.version").value("1.0.0"))
|
||||
.andExpect(jsonPath("$.java.version").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/welcome - 应该返回欢迎信息")
|
||||
void shouldReturnWelcomeMessage() throws Exception {
|
||||
mockMvc.perform(get("/api/welcome"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.message").value("欢迎使用Jenkins Demo应用程序!"))
|
||||
.andExpect(jsonPath("$.description").exists());
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package com.jenkins.demo.controller;
|
||||
|
||||
import com.jenkins.demo.model.User;
|
||||
import com.jenkins.demo.service.UserService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* User Controller Integration Tests
|
||||
*/
|
||||
@WebMvcTest(UserController.class)
|
||||
class UserControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private User testUser;
|
||||
private List<User> testUsers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testUser = new User(1L, "testuser", "test@example.com", "测试用户", "ACTIVE");
|
||||
testUsers = Arrays.asList(
|
||||
testUser,
|
||||
new User(2L, "user2", "user2@example.com", "用户2", "ACTIVE")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/users - 应该返回所有用户")
|
||||
void shouldReturnAllUsers() throws Exception {
|
||||
when(userService.getAllUsers()).thenReturn(testUsers);
|
||||
|
||||
mockMvc.perform(get("/api/users"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.length()").value(2))
|
||||
.andExpect(jsonPath("$[0].username").value("testuser"))
|
||||
.andExpect(jsonPath("$[1].username").value("user2"));
|
||||
|
||||
verify(userService).getAllUsers();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/users/{id} - 应该返回指定用户")
|
||||
void shouldReturnUserById() throws Exception {
|
||||
when(userService.getUserById(1L)).thenReturn(Optional.of(testUser));
|
||||
|
||||
mockMvc.perform(get("/api/users/1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.username").value("testuser"))
|
||||
.andExpect(jsonPath("$.email").value("test@example.com"));
|
||||
|
||||
verify(userService).getUserById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/users/{id} - 用户不存在时应该返回404")
|
||||
void shouldReturn404WhenUserNotFound() throws Exception {
|
||||
when(userService.getUserById(999L)).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(get("/api/users/999"))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(userService).getUserById(999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/users - 应该创建新用户")
|
||||
void shouldCreateNewUser() throws Exception {
|
||||
User newUser = new User(null, "newuser", "new@example.com", "新用户", null);
|
||||
User createdUser = new User(3L, "newuser", "new@example.com", "新用户", "ACTIVE");
|
||||
|
||||
when(userService.createUser(any(User.class))).thenReturn(createdUser);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(newUser)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id").value(3))
|
||||
.andExpect(jsonPath("$.username").value("newuser"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
|
||||
verify(userService).createUser(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/users - 创建用户时验证失败应该返回400")
|
||||
void shouldReturn400ForInvalidUserData() throws Exception {
|
||||
User invalidUser = new User(null, "", "invalid-email", "", null);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(invalidUser)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/users/{id} - 应该更新用户")
|
||||
void shouldUpdateUser() throws Exception {
|
||||
User updateData = new User(null, "updateduser", "updated@example.com", "更新用户", null);
|
||||
User updatedUser = new User(1L, "updateduser", "updated@example.com", "更新用户", "ACTIVE");
|
||||
|
||||
when(userService.updateUser(eq(1L), any(User.class))).thenReturn(Optional.of(updatedUser));
|
||||
|
||||
mockMvc.perform(put("/api/users/1")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(updateData)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.username").value("updateduser"))
|
||||
.andExpect(jsonPath("$.email").value("updated@example.com"));
|
||||
|
||||
verify(userService).updateUser(eq(1L), any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DELETE /api/users/{id} - 应该删除用户")
|
||||
void shouldDeleteUser() throws Exception {
|
||||
when(userService.deleteUser(1L)).thenReturn(true);
|
||||
|
||||
mockMvc.perform(delete("/api/users/1"))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(userService).deleteUser(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/users/{id}/activate - 应该激活用户")
|
||||
void shouldActivateUser() throws Exception {
|
||||
when(userService.activateUser(1L)).thenReturn(true);
|
||||
|
||||
mockMvc.perform(post("/api/users/1/activate"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.message").value("用户已激活"));
|
||||
|
||||
verify(userService).activateUser(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/users/stats - 应该返回用户统计信息")
|
||||
void shouldReturnUserStats() throws Exception {
|
||||
when(userService.getUserCount()).thenReturn(5L);
|
||||
|
||||
mockMvc.perform(get("/api/users/stats"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.totalUsers").value(5))
|
||||
.andExpect(jsonPath("$.timestamp").exists());
|
||||
|
||||
verify(userService).getUserCount();
|
||||
}
|
||||
}
|
148
src/test/java/com/jenkins/demo/service/UserServiceTest.java
Normal file
148
src/test/java/com/jenkins/demo/service/UserServiceTest.java
Normal file
@ -0,0 +1,148 @@
|
||||
package com.jenkins.demo.service;
|
||||
|
||||
import com.jenkins.demo.model.User;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* User Service Unit Tests
|
||||
*/
|
||||
class UserServiceTest {
|
||||
|
||||
private UserService userService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userService = new UserService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够获取所有用户")
|
||||
void shouldGetAllUsers() {
|
||||
List<User> users = userService.getAllUsers();
|
||||
assertNotNull(users);
|
||||
assertEquals(3, users.size()); // 初始化数据包含3个用户
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够根据ID获取用户")
|
||||
void shouldGetUserById() {
|
||||
Optional<User> user = userService.getUserById(1L);
|
||||
assertTrue(user.isPresent());
|
||||
assertEquals("admin", user.get().getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取不存在的用户应该返回空")
|
||||
void shouldReturnEmptyForNonExistentUser() {
|
||||
Optional<User> user = userService.getUserById(999L);
|
||||
assertFalse(user.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够根据用户名查找用户")
|
||||
void shouldFindUserByUsername() {
|
||||
Optional<User> user = userService.getUserByUsername("admin");
|
||||
assertTrue(user.isPresent());
|
||||
assertEquals("admin", user.get().getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够创建新用户")
|
||||
void shouldCreateNewUser() {
|
||||
User newUser = new User(null, "testuser", "test@example.com", "测试用户", null);
|
||||
User createdUser = userService.createUser(newUser);
|
||||
|
||||
assertNotNull(createdUser.getId());
|
||||
assertEquals("testuser", createdUser.getUsername());
|
||||
assertEquals("test@example.com", createdUser.getEmail());
|
||||
assertEquals("测试用户", createdUser.getName());
|
||||
assertEquals("ACTIVE", createdUser.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建重复用户名应该抛出异常")
|
||||
void shouldThrowExceptionForDuplicateUsername() {
|
||||
User duplicateUser = new User(null, "admin", "admin2@example.com", "管理员2", null);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> userService.createUser(duplicateUser)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("用户名已存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够更新用户信息")
|
||||
void shouldUpdateUser() {
|
||||
User updateData = new User(null, "admin_updated", "admin_new@example.com", "更新的管理员", null);
|
||||
Optional<User> updatedUser = userService.updateUser(1L, updateData);
|
||||
|
||||
assertTrue(updatedUser.isPresent());
|
||||
assertEquals("admin_updated", updatedUser.get().getUsername());
|
||||
assertEquals("admin_new@example.com", updatedUser.get().getEmail());
|
||||
assertEquals("更新的管理员", updatedUser.get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新不存在的用户应该返回空")
|
||||
void shouldReturnEmptyWhenUpdatingNonExistentUser() {
|
||||
User updateData = new User(null, "nonexistent", "test@example.com", "不存在的用户", null);
|
||||
Optional<User> result = userService.updateUser(999L, updateData);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够删除用户")
|
||||
void shouldDeleteUser() {
|
||||
boolean deleted = userService.deleteUser(1L);
|
||||
assertTrue(deleted);
|
||||
|
||||
Optional<User> user = userService.getUserById(1L);
|
||||
assertFalse(user.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除不存在的用户应该返回false")
|
||||
void shouldReturnFalseWhenDeletingNonExistentUser() {
|
||||
boolean deleted = userService.deleteUser(999L);
|
||||
assertFalse(deleted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够获取用户总数")
|
||||
void shouldGetUserCount() {
|
||||
long count = userService.getUserCount();
|
||||
assertEquals(3, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够激活用户")
|
||||
void shouldActivateUser() {
|
||||
boolean activated = userService.activateUser(1L);
|
||||
assertTrue(activated);
|
||||
|
||||
Optional<User> user = userService.getUserById(1L);
|
||||
assertTrue(user.isPresent());
|
||||
assertEquals("ACTIVE", user.get().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该能够停用用户")
|
||||
void shouldDeactivateUser() {
|
||||
boolean deactivated = userService.deactivateUser(1L);
|
||||
assertTrue(deactivated);
|
||||
|
||||
Optional<User> user = userService.getUserById(1L);
|
||||
assertTrue(user.isPresent());
|
||||
assertEquals("INACTIVE", user.get().getStatus());
|
||||
}
|
||||
}
|
4
src/test/resources/application-test.properties
Normal file
4
src/test/resources/application-test.properties
Normal file
@ -0,0 +1,4 @@
|
||||
# Test Configuration
|
||||
spring.profiles.active=test
|
||||
logging.level.com.jenkins.demo=DEBUG
|
||||
server.port=0
|
Loading…
x
Reference in New Issue
Block a user