Initial commit: Jenkins CI/CD Demo Project with Spring Boot 3

This commit is contained in:
Tianqi Wang 2025-06-23 16:50:28 +08:00
commit 877335f73f
21 changed files with 2527 additions and 0 deletions

140
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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);
}
}

View File

@ -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应用程序"
);
}
}

View 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()
));
}
}

View 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 + '\'' +
'}';
}
}

View 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));
}
}

View 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

View File

@ -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是否能正确创建和注入
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View 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());
}
}

View File

@ -0,0 +1,4 @@
# Test Configuration
spring.profiles.active=test
logging.level.com.jenkins.demo=DEBUG
server.port=0