以下是一个基于 Java Spring Boot 的云原生 TodoList Demo 项目,涵盖 容器化、Kubernetes 编排、CI/CD、可观测性、弹性扩缩容 等核心云原生特性,代码简洁且附详细操作指南,适合入门学习。

项目概览
- 目标:实现一个支持增删改查(CRUD)的 TodoList 后端服务,通过云原生技术栈部署,展示完整的云原生实践流程。
- 技术栈 :
- 后端:Java 17 + Spring Boot 3.2(轻量、模块化)
- 数据库:PostgreSQL(云原生持久化存储)
- 容器化:Docker
- 编排:Kubernetes(K8s)
- CI/CD:GitHub Actions
- 可观测性:Prometheus(监控) + Grafana(可视化) + Loki(日志)
- 配置管理:Spring Cloud Config(可选,或直接使用 K8s ConfigMap)
一、项目结构
todolist-cloudnative/
├── src/ # 核心业务代码
│ ├── main/
│ │ ├── java/com/example/todo/
│ │ │ ├── controller/ # REST 接口
│ │ │ ├── model/ # 数据库实体
│ │ │ ├── repository/ # 数据访问层
│ │ │ ├── service/ # 业务逻辑层
│ │ │ └── TodoApplication.java # 启动类
│ │ └── resources/
│ │ ├── application.yml # 主配置
│ │ └── logback-spring.xml # 日志配置(Loki 集成)
│ └── test/ # 单元测试
├── docker/ # Docker 相关配置
│ └── Dockerfile # 容器化构建脚本(多阶段构建)
├── k8s/ # K8s 部署配置
│ ├── todo-deployment.yaml # 应用 Deployment
│ ├── todo-service.yaml # 应用 Service
│ ├── postgres-statefulset.yaml # PostgreSQL StatefulSet
│ ├── postgres-service.yaml # PostgreSQL Service
│ ├── configmap.yaml # 非敏感配置(K8s 注入)
│ └── secret.yaml # 敏感配置(数据库密码,K8s Secret)
├── prometheus/ # Prometheus 监控配置
│ └── todolist.yml # 抓取规则
├── .github/ # GitHub Actions 工作流
│ └── workflows/
│ └── deploy.yml # CI/CD 自动化流程
└── README.md # 操作指南

二、核心功能实现(Spring Boot 后端)
1. 初始化项目(Spring Initializr)
- 访问 https://start.spring.io/,选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.0
- Dependencies:
- Spring Web(REST 接口)
- Spring Data JPA(数据库操作)
- PostgreSQL Driver(数据库驱动)
- Micrometer Tracing(可观测性)
- Actuator(健康检查、指标暴露)
2. 数据库模型(src/main/java/com/example/todo/model/TodoItem.java
)
java
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "todo_items")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TodoItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private String description;
@Column(columnDefinition = "BOOLEAN DEFAULT FALSE")
private boolean completed;
}
3. 数据访问层(src/main/java/com/example/todo/repository/TodoRepository.java
)
java
import com.example.todo.model.TodoItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TodoRepository extends JpaRepository<TodoItem, Long> {
}
4. 业务逻辑层(src/main/java/com/example/todo/service/TodoService.java
)
java
import com.example.todo.model.TodoItem;
import com.example.todo.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
public TodoItem createTodo(TodoItem todoItem) {
return todoRepository.save(todoItem);
}
public List<TodoItem> getAllTodos() {
return todoRepository.findAll();
}
public TodoItem getTodoById(Long id) {
return todoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Todo not found with id: " + id));
}
public TodoItem updateTodo(Long id, TodoItem updatedTodo) {
TodoItem existingTodo = getTodoById(id);
existingTodo.setTitle(updatedTodo.getTitle());
existingTodo.setDescription(updatedTodo.getDescription());
existingTodo.setCompleted(updatedTodo.isCompleted());
return todoRepository.save(existingTodo);
}
public void deleteTodo(Long id) {
todoRepository.deleteById(id);
}
}
5. REST 接口(src/main/java/com/example/todo/controller/TodoController.java
)
java
import com.example.todo.model.TodoItem;
import com.example.todo.service.TodoService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/todos")
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public TodoItem createTodo(@RequestBody TodoItem todoItem) {
return todoService.createTodo(todoItem);
}
@GetMapping
public List<TodoItem> getAllTodos() {
return todoService.getAllTodos();
}
@GetMapping("/{id}")
public TodoItem getTodoById(@PathVariable Long id) {
return todoService.getTodoById(id);
}
@PutMapping("/{id}")
public TodoItem updateTodo(@PathVariable Long id, @RequestBody TodoItem updatedTodo) {
return todoService.updateTodo(id, updatedTodo);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteTodo(@PathVariable Long id) {
todoService.deleteTodo(id);
}
}
6. 配置外部化(src/main/resources/application.yml
)
yaml
spring:
datasource:
url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
username: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update # 自动创建表(生产环境建议用 flyway)
show-sql: true
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus # 暴露监控端点
endpoint:
health:
show-details: always
三、容器化(Docker)
docker/Dockerfile
(多阶段构建,减小镜像体积)
dockerfile
# 阶段 1:构建 Maven 项目
FROM maven:3.9.6-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
# 缓存依赖(仅当 pom.xml 变化时重新下载)
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 阶段 2:运行 Spring Boot 应用
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 从构建阶段复制 JAR 包
COPY --from=build /app/target/todo-cloudnative-*.jar ./todo.jar
# 暴露 Spring Boot 端口(8080)
EXPOSE 8080
# 启动命令(禁用 Tomcat 优雅关闭,适配 K8s 探针)
CMD ["java", "-jar", "todo.jar", "--server.port=8080"]

四、Kubernetes 编排(云原生核心)
1. 配置管理(k8s/configmap.yaml
和 k8s/secret.yaml
)
非敏感配置(ConfigMap,注入数据库名、主机等):
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: todo-config
data:
POSTGRES_DB: "tododb"
POSTGRES_HOST: "postgres-service" # K8s Service 名称(服务发现)
敏感配置(Secret,注入数据库用户、密码,需 base64 编码):
bash
# 生成 base64 编码(实际生产建议用外部密钥管理系统如 HashiCorp Vault)
echo -n "admin" | base64 # 用户名
echo -n "mypassword" | base64 # 密码
echo -n "5432" | base64 # 端口
yaml
apiVersion: v1
kind: Secret
metadata:
name: todo-secret
type: Opaque
data:
POSTGRES_USER: "YWRtaW4=" # "admin" 的 base64
POSTGRES_PASSWORD: "bXlwYXNzd29yZA==" # "mypassword" 的 base64
POSTGRES_PORT: "NTQzNg==" # "5432" 的 base64
2. PostgreSQL 部署(k8s/postgres-statefulset.yaml
)
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-service
replicas: 1 # 生产环境建议 3 副本 + 主从复制(Patroni)
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: todo-secret
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: todo-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: todo-config
key: POSTGRES_DB
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # 持久化存储(云原生存储卷,如 AWS EBS、阿里云盘)
- metadata:
name: postgres-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
3. Todo 服务部署(k8s/todo-deployment.yaml
)
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-deployment
spec:
replicas: 2 # 初始副本数(弹性扩缩容基础)
selector:
matchLabels:
app: todo
template:
metadata:
labels:
app: todo
spec:
containers:
- name: todo
image: your-docker-username/todolist:v1 # 替换为实际镜像地址
ports:
- containerPort: 8080
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: todo-secret
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: todo-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_HOST
valueFrom:
configMapKeyRef:
name: todo-config
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: todo-secret
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: todo-config
key: POSTGRES_DB
livenessProbe: # 存活探针(自动重启异常实例)
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe: # 就绪探针(流量路由前检查)
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources: # 资源限制(K8s 调度依据)
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
4. 服务暴露(k8s/todo-service.yaml
和 k8s/todo-ingress.yaml
)
ClusterIP Service(内部访问):
yaml
apiVersion: v1
kind: Service
metadata:
name: todo-service
spec:
type: ClusterIP
selector:
app: todo
ports:
- protocol: TCP
port: 80
targetPort: 8080
Ingress(外部访问,需安装 NGINX Ingress Controller):
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: todo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /todos
pathType: Prefix
backend:
service:
name: todo-service
port:
number: 80

五、CI/CD 自动化(GitHub Actions)
.github/workflows/deploy.yml
yaml
name: Deploy to Kubernetes
on:
push:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Maven
run: mvn clean package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: your-docker-username/todolist:latest,your-docker-username/todolist:${{ github.sha }}
deploy-to-k8s:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Deploy to Kubernetes cluster
run: |
# 替换镜像标签为最新提交 SHA
sed -i "s|your-docker-username/todolist:v1|your-docker-username/todolist:${{ github.sha }}|g" k8s/todo-deployment.yaml
# 应用 K8s 配置
kubectl apply -f k8s/
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }} # 从 GitHub Secrets 读取集群配置

六、可观测性配置
1. Prometheus 监控(prometheus/todolist.yml
)
yaml
scrape_configs:
- job_name: "todo_service"
scrape_interval: 15s
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ["todo-service:80"] # K8s Service 域名(集群内部可解析)
2. 日志集成(Loki)
-
在
logback-spring.xml
中配置 Logstash 或直接输出 JSON 格式日志:xml<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender> <root level="info"> <appender-ref ref="CONSOLE"/> </root> </configuration>
-
使用 Promtail 收集日志并发送到 Loki(需额外部署 Promtail)。
3. Grafana 仪表盘(示例查询)
- 请求速率 :
rate(http_server_requests_seconds_count{job="todo_service"}[5m])
- 平均延迟 :
rate(http_server_requests_seconds_sum{job="todo_service"}[5m]) / rate(http_server_requests_seconds_count{job="todo_service"}[5m])
- JVM 内存使用 :
jvm_memory_used_bytes{area="heap"}

七、云原生特性验证
1. 容器化
- 本地构建镜像:
docker build -t todolist:v1 -f docker/Dockerfile .
- 运行测试:
docker run -p 8080:8080 todolist:v1
,访问http://localhost:8080/api/todos
验证接口。
2. Kubernetes 部署
- 本地搭建 K8s 集群(Minikube 或 Kind):
minikube start
- 应用配置:
kubectl apply -f k8s/
- 查看状态:
kubectl get pods,svc,ingress
。
3. 弹性扩缩容
-
手动扩缩容:
kubectl scale deployment/todo-deployment --replicas=3
-
自动扩缩容(HPA):
yamlapiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: todo-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: todo-deployment minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 # CPU 使用率超 70% 自动扩容
4. 故障自愈
- 手动删除 Pod:
kubectl delete pod <pod-name>
,观察 K8s 自动重建新 Pod(状态变为Running
)。
5. 持续交付
- 推送代码到 GitHub
main
分支,触发 GitHub Actions 自动构建、测试、部署。

八、总结
通过这个 TodoList Demo,你可以完整体验云原生应用的核心流程:
- 容器化:用 Docker 封装应用,确保环境一致性。
- Kubernetes 编排:通过 Deployment、Service 等资源实现自动化管理。
- CI/CD:GitHub Actions 实现代码提交到生产的全自动化。
- 可观测性:Prometheus + Grafana 监控性能,Loki 收集日志。
- 弹性扩缩容:HPA 根据负载自动调整资源,保障高可用。
后续可扩展方向:
- 微服务拆分(如用户服务、待办服务),用 Istio 实现服务网格。
- 引入数据库读写分离(主从复制)。
- 添加认证鉴权(OAuth2、JWT)。
- 使用 Helm 打包 K8s 配置,简化多环境部署。