这是一个最小可行性生产级" (Minimum Viable Production-Ready) 的 Java 项目模板。
它虽然功能简单(一个待办事项 Todo List API),但包含了上线所需的所有核心骨架:统一返回、全局异常、参数校验、日志规范、Docker 部署。
你可以直接复制这个结构,作为你未来所有项目的起点。
一.项目结构
bash
todo-api/
├── src/main/java/com/example/todo/
│ ├── common/ # 公共模块
│ │ ├── Result.java # 统一响应封装
│ │ └── BusinessException.java # 自定义业务异常
│ ├── config/ # 配置类
│ │ └── GlobalExceptionHandler.java # 全局异常处理
│ ├── controller/ # 控制层
│ │ ── TodoController.java
│ ├── service/ # 业务层
│ │ ├── TodoService.java
│ │ ── impl/TodoServiceImpl.java
│ ├── mapper/ # 数据访问层
│ │ └── TodoMapper.java
│ ├── entity/ # 数据库实体
│ │ └── Todo.java
│ ├── dto/ # 请求参数对象
│ │ └── CreateTodoDTO.java
│ ── TodoApplication.java # 启动类
── src/main/resources/
│ ├── application.yml # 配置文件
│ └── schema.sql # 建表语句
├── Dockerfile # 镜像构建文件
└── pom.xml # Maven 依赖
二. 核心代码实现
A. pom.xml (关键依赖)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>todo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
📂 src/main/java/com/example/todo/
Main.java(启动类)
java
package com.example.todo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.todo.mapper") // 扫描 Mapper 接口
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
B. common/Result.java (统一返回格式)
这是生产项目的门面,前端只认这个格式。
Result.java(统一响应封装)
java
package com.example.todo.common;
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
C. common/BusinessException.java (自定义异常)
BusinessException.java(自定义业务异常)
java
package com.example.todo.common;
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
D. config/GlobalExceptionHandler.java (全局异常捕获)
这是生产项目的安全网,确保用户永远看不到堆栈信息。
GlobalExceptionHandler.java(全局异常处理)
java
package com.example.todo.config;
import com.example.todo.common.BusinessException;
import com.example.todo.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.warn("Business Exception: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(400, errorMsg);
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("System Error: ", e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
E. entity/Todo.java
Todo.java(数据库实体)
java
package com.example.todo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("todos")
public class Todo {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private Boolean completed;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
F. service/impl/TodoServiceImpl.java (业务逻辑)
TodoServiceImpl.java(服务实现)
java
package com.example.todo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.todo.common.BusinessException;
import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;
import com.example.todo.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TodoServiceImpl implements TodoService {
@Autowired
private TodoMapper todoMapper;
@Override
public Todo createTodo(CreateTodoDTO dto) {
if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
throw new BusinessException(400, "标题不能为空");
}
if (dto.getTitle().length() > 100) {
throw new BusinessException(400, "标题过长(最大100字符)");
}
Todo todo = new Todo();
todo.setTitle(dto.getTitle());
todo.setCompleted(false);
todoMapper.insert(todo);
return todo;
}
@Override
public Todo updateTodo(Long id, UpdateTodoDTO dto) {
Todo todo = todoMapper.selectById(id);
if (todo == null) {
throw new BusinessException(404, "待办事项不存在");
}
if (dto.getTitle() != null && !dto.getTitle().trim().isEmpty()) {
if (dto.getTitle().length() > 100) {
throw new BusinessException(400, "标题过长");
}
todo.setTitle(dto.getTitle());
}
if (dto.getCompleted() != null) {
todo.setCompleted(dto.getCompleted());
}
todoMapper.updateById(todo);
return todo;
}
@Override
public void deleteTodo(Long id) {
Todo todo = todoMapper.selectById(id);
if (todo == null) {
throw new BusinessException(404, "待办事项不存在");
}
todoMapper.deleteById(id);
}
@Override
public Todo getTodoById(Long id) {
Todo todo = todoMapper.selectById(id);
if (todo == null) {
throw new BusinessException(404, "待办事项不存在");
}
return todo;
}
@Override
public List<Todo> getAllTodos() {
return todoMapper.selectList(new QueryWrapper<>());
}
@Override
public List<Todo> getCompletedTodos() {
return todoMapper.selectList(new QueryWrapper<Todo>().eq("completed", true));
}
@Override
public List<Todo> getPendingTodos() {
return todoMapper.selectList(new QueryWrapper<Todo>().eq("completed", false));
}
}
G. controller/TodoController.java
TodoController.java(控制器)
java
package com.example.todo.controller;
import com.example.todo.common.Result;
import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.service.TodoService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/todos")
public class TodoController {
@Autowired
private TodoService todoService;
@PostMapping
public Result<Todo> create(@Valid @RequestBody CreateTodoDTO dto) {
Todo todo = todoService.createTodo(dto);
return Result.success(todo);
}
@PutMapping("/{id}")
public Result<Todo> update(@PathVariable Long id, @Valid @RequestBody UpdateTodoDTO dto) {
Todo todo = todoService.updateTodo(id, dto);
return Result.success(todo);
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
todoService.deleteTodo(id);
return Result.success(null);
}
@GetMapping("/{id}")
public Result<Todo> getById(@PathVariable Long id) {
Todo todo = todoService.getTodoById(id);
return Result.success(todo);
}
@GetMapping
public Result<List<Todo>> getAll() {
List<Todo> todos = todoService.getAllTodos();
return Result.success(todos);
}
@GetMapping("/completed")
public Result<List<Todo>> getCompleted() {
List<Todo> todos = todoService.getCompletedTodos();
return Result.success(todos);
}
@GetMapping("/pending")
public Result<List<Todo>> getPending() {
List<Todo> todos = todoService.getPendingTodos();
return Result.success(todos);
}
}
📂 mapper/
TodoMapper.java(数据访问层)
java
package com.example.todo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.todo.entity.Todo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TodoMapper extends BaseMapper<Todo> {
// MyBatis-Plus 已提供常用 CRUD,无需额外方法
}
📂 service/
TodoService.java(服务接口)
java
package com.example.todo.service;
import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;
import java.util.List;
public interface TodoService {
Todo createTodo(CreateTodoDTO dto);
Todo updateTodo(Long id, UpdateTodoDTO dto);
void deleteTodo(Long id);
Todo getTodoById(Long id);
List<Todo> getAllTodos();
List<Todo> getCompletedTodos();
List<Todo> getPendingTodos();
}
CreateTodoDTO.java
java
package com.example.todo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class CreateTodoDTO {
@NotBlank(message = "标题不能为空")
private String title;
}
UpdateTodoDTO.java
java
package com.example.todo.dto;
import lombok.Data;
@Data
public class UpdateTodoDTO {
private String title;
private Boolean completed;
}
application.yml(配置文件)
java
spring:
datasource:
url: jdbc:mysql://localhost:3306/todo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
server:
port: 8080
schema.sql(建表语句 ------ 放在 resources 下)
sql
-- src/main/resources/schema.sql
CREATE DATABASE IF NOT EXISTS todo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE todo_db;
CREATE TABLE IF NOT EXISTS todos (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
title VARCHAR(255) NOT NULL COMMENT '待办标题',
completed BOOLEAN DEFAULT FALSE COMMENT '是否完成',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待办事项表';
三. 部署配置 (Docker)
A. Dockerfile
放在项目根目录。使用多阶段构建或简单的 JRE 镜像。
dockerfile
# 使用轻量级的 Eclipse Temurin (OpenJDK) 运行时
FROM eclipse-temurin:17-jre-alpine
# 设置工作目录
WORKDIR /app
# 复制打包好的 jar 包 (假设 mvn package 生成在 target/)
COPY target/todo-api-0.0.1-SNAPSHOT.jar app.jar
# 暴露端口
EXPOSE 8080
# 启动命令,指定生产环境 profile
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app/app.jar"]FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app/app.jar"]
B. docker-compose.yml (一键启动 DB + App)
yaml
version: '3.8'
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todo_db
volumes:
- ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/todo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: secret
depends_on:
db:
condition: service_healthy
restart: unless-stopped
四. 如何运行和上线?
本地开发:
- 修改: application.yml 连接本地 MySQL。
- 运行: mvn spring-boot:run。
- 测试:curl -XPOST http://localhost:8080/api/todos -H "Content-Type:
application/json" -d '{"title":"Learn Java"}'
Docker 一键部署:
bash
mvn clean package -DskipTests
docker-compose up --build
访问:http://localhost:8080/api/todos
生产上线:
- 打包:mvn clean package -DskipTests
- 构建镜像:docker build -t my-todo-app .
- 启动:docker-compose up -d
下一步建议 :
在这个基础上,你可以逐步添加:
Redis:缓存热点 Todo。
Spring Security + JWT:实现用户登录认证。
Actuator:添加 /actuator/health 健康检查接口。
GitHub Actions:实现代码提交后自动构建 Docker 镜像。
功能清单:
