在Java后端开发中,搭建一个规范的RESTful API服务是每个开发者必须掌握的技能。本文将以Spring Boot 3.x为核心,整合MyBatis和MySQL,从零开始构建一个完整的用户管理系统API。无论你是刚入门的新手还是想梳理最佳实践的进阶开发者,都能从这篇文章中获得实用的参考。
📖 目录
1. 项目初始化与工程结构
推荐使用 Spring Initializr 来初始化项目。选择以下依赖:
- Spring Web:提供RESTful接口支持
- Spring Boot Starter JDBC:数据库连接基础
- MyBatis Spring Boot Starter:ORM框架
- MySQL Connector:MySQL驱动
- Lombok:简化实体类代码
- Spring Boot Starter Validation:参数校验
项目创建后的目录结构如下:
src/main/java/com/example/userapi/
├── UserApiApplication.java // 启动类
├── controller/
│ └── UserController.java // 控制器层
├── service/
│ ├── UserService.java // 接口
│ └── impl/
│ └── UserServiceImpl.java // 实现类
├── repository/
│ └── UserRepository.java // 数据访问层
├── entity/
│ └── User.java // 数据库实体
├── dto/
│ ├── CreateUserRequest.java // 创建用户请求DTO
│ └── UpdateUserRequest.java // 更新用户请求DTO
├── vo/
│ ├── UserVO.java // 用户响应VO
│ └── PageResult.java // 分页结果封装
├── common/
│ ├── Result.java // 统一响应封装
│ └── GlobalExceptionHandler.java // 全局异常处理
└── config/
└── SwaggerConfig.java // Swagger配置
pom.xml 中的核心依赖配置如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL -->
<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>
<!-- Knife4j (Swagger增强) -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
</dependencies>
在 application.yml 中配置数据源和MyBatis:
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_api?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.userapi.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8080
2. 数据库设计与MyBatis集成
以用户管理为例,设计一张基础的用户表:
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
对应的实体类 User.java:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String email;
private String phone;
private String avatar;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
MyBatis的Mapper XML文件 UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.userapi.repository.UserRepository">
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="email" property="email" />
<result column="phone" property="phone" />
<result column="avatar" property="avatar" />
<result column="status" property="status" />
<result column="created_at" property="createdAt" />
<result column="updated_at" property="updatedAt" />
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, email, phone, avatar, status)
VALUES (#{username}, #{email}, #{phone}, #{avatar}, #{status})
</insert>
<select id="findById" resultMap="BaseResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="findPage" resultMap="BaseResultMap">
SELECT * FROM user
ORDER BY created_at DESC
LIMIT #{offset}, #{limit}
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM user
</select>
<update id="update">
UPDATE user
SET username = #{username},
email = #{email},
phone = #{phone},
avatar = #{avatar},
status = #{status}
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
对应的 UserRepository.java 接口:
@Mapper
public interface UserRepository {
int insert(User user);
User findById(Long id);
List<User> findPage(@Param("offset") int offset, @Param("limit") int limit);
long count();
int update(User user);
int deleteById(Long id);
}
3. Controller-Service-Repository三层架构
三层架构是Spring Boot项目中最核心的组织方式,每一层各有分工:
| 层次 | 职责 | 注解 | 注意事项 |
|---|---|---|---|
| Controller | 接收请求、参数校验、调用Service、返回响应 | @RestController / @RequestMapping | 只做路由和校验,不做业务逻辑 |
| Service | 业务逻辑编排、事务管理 | @Service / @Transactional | 接口+实现类分离,便于测试 |
| Repository | 数据库CRUD操作 | @Mapper | 只做数据访问,不做业务判断 |
UserController.java 完整示例:
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户CRUD相关接口")
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "创建用户")
public Result<UserVO> create(@Valid @RequestBody CreateUserRequest request) {
UserVO userVO = userService.createUser(request);
return Result.success(userVO);
}
@GetMapping("/{id}")
@Operation(summary = "查询用户详情")
public Result<UserVO> getById(@PathVariable Long id) {
UserVO userVO = userService.getUserById(id);
return Result.success(userVO);
}
@GetMapping
@Operation(summary = "分页查询用户列表")
public Result<PageResult<UserVO>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
PageResult<UserVO> pageResult = userService.listUsers(page, size);
return Result.success(pageResult);
}
@PutMapping("/{id}")
@Operation(summary = "更新用户信息")
public Result<UserVO> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserVO userVO = userService.updateUser(id, request);
return Result.success(userVO);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
public Result<Void> delete(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success();
}
}
UserServiceImpl.java 业务逻辑实现:
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
@Transactional
public UserVO createUser(CreateUserRequest request) {
// 1. 校验用户是否已存在
User existingUser = userRepository.findByUsername(request.getUsername());
if (existingUser != null) {
throw new BusinessException("用户名已存在");
}
// 2. 创建实体并保存
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setStatus(1);
userRepository.insert(user);
// 3. 转换为VO返回
return UserVO.fromEntity(user);
}
@Override
public UserVO getUserById(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new ResourceNotFoundException("用户不存在");
}
return UserVO.fromEntity(user);
}
@Override
public PageResult<UserVO> listUsers(int page, int size) {
int offset = (page - 1) * size;
List<User> users = userRepository.findPage(offset, size);
long total = userRepository.count();
List<UserVO> voList = users.stream()
.map(UserVO::fromEntity)
.collect(Collectors.toList());
return new PageResult<>(voList, total, page, size);
}
@Override
@Transactional
public UserVO updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id);
if (user == null) {
throw new ResourceNotFoundException("用户不存在");
}
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
userRepository.update(user);
return UserVO.fromEntity(user);
}
@Override
@Transactional
public void deleteUser(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new ResourceNotFoundException("用户不存在");
}
userRepository.deleteById(id);
}
}
4. DTO与VO设计模式
在实际项目中,直接将实体类 User 暴露给前端是非常危险的做法。正确的做法是使用DTO(数据传输对象)和VO(视图对象)进行隔离:
- DTO:接收前端传入的请求参数,通常配合 javax.validation 注解做参数校验
- VO:返回给前端的响应数据,只包含前端需要看到的字段
CreateUserRequest.java(请求DTO):
@Data
@Schema(description = "创建用户请求")
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度3-50个字符")
@Schema(description = "用户名", example = "zhangsan")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", example = "13800138000")
private String phone;
}
UserVO.java(响应VO):
@Data
@Schema(description = "用户信息")
public class UserVO {
@Schema(description = "用户ID")
private Long id;
@Schema(description = "用户名")
private String username;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "状态:0-禁用 1-启用")
private Integer status;
@Schema(description = "创建时间")
private String createdAt;
public static UserVO fromEntity(User user) {
if (user == null) return null;
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setEmail(user.getEmail());
vo.setPhone(user.getPhone());
vo.setAvatar(user.getAvatar());
vo.setStatus(user.getStatus());
vo.setCreatedAt(user.getCreatedAt()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return vo;
}
}
设计原则:永远不要直接将 Entity 返回给前端。通过 VO 做一层转换,你可以安全地隐藏敏感字段(如密码)、格式化时间、拼接展示字段,而不影响数据库模型。
5. 统一异常处理
没有统一异常处理的API就像没有安全网的高空作业。使用 @RestControllerAdvice 可以集中处理所有异常:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/** 参数校验失败 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.error(400, msg);
}
/** 业务异常 */
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException ex) {
return Result.error(400, ex.getMessage());
}
/** 资源不存在 */
@ExceptionHandler(ResourceNotFoundException.class)
public Result<Void> handleNotFound(ResourceNotFoundException ex) {
return Result.error(404, ex.getMessage());
}
/** 未预期的异常 */
@ExceptionHandler(Exception.class)
public Result<Void> handleUnknown(Exception ex) {
log.error("未预期异常", ex);
return Result.error(500, "服务器内部错误");
}
}
统一响应封装类 Result.java:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> success() {
return new Result<>(200, "success", null);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}
这种设计保证了所有API响应都遵循统一的JSON格式,前端可以根据 code 字段做全局拦截处理:
// 成功响应
{ "code": 200, "message": "success", "data": { ... } }
// 失败响应
{ "code": 400, "message": "用户名不能为空", "data": null }
// 未找到
{ "code": 404, "message": "用户不存在", "data": null }
6. Swagger / Knife4j接口文档
没有文档的API很难被他人使用。Knife4j 是 Swagger 的增强版,提供更友好的UI界面。配置方式如下:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户管理系统 API")
.version("1.0.0")
.description("基于Spring Boot + MyBatis的用户管理RESTful API"));
}
}
启动项目后访问 http://localhost:8080/doc.html 即可看到可视化的接口文档页面。Knife4j的优势包括:
- 在线调试:直接在文档页面填写参数并发送请求
- 全局参数:支持统一添加Token等认证参数
- 分组展示:按Controller自动分组,结构清晰
- 导出功能:支持导出 Markdown / OpenAPI 格式
配合 @Operation、@Schema 注解,可以让生成的文档更加完善。这些注解已经在前面Controller和DTO的代码中使用过。
完整的API端点一览:
| HTTP方法 | 路径 | 描述 | 请求体 | 响应 |
|---|---|---|---|---|
| POST | /api/users | 创建用户 | CreateUserRequest | UserVO |
| GET | /api/users/{id} | 查询用户详情 | - | UserVO |
| GET | /api/users | 分页查询用户列表 | page, size (query) | PageResult<UserVO> |
| PUT | /api/users/{id} | 更新用户信息 | UpdateUserRequest | UserVO |
| DELETE | /api/users/{id} | 删除用户 | - | - |
7. 部署与发布
完成开发后,常用的部署方式有两种:
7.1 Jar包部署(传统方式)
# 构建项目
mvn clean package -DskipTests
# 启动服务
java -jar target/user-api-1.0.0.jar \
--spring.profiles.active=prod \
--server.port=8080
7.2 Docker容器化部署(推荐方式)
# Dockerfile
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/user-api-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: user_api
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/user_api?useUnicode=true&characterEncoding=utf-8
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root123
depends_on:
- mysql
volumes:
mysql_data:
7.3 生产环境优化建议
- 配置外置化:将数据库配置、日志级别等通过环境变量或配置中心管理
- 健康检查:引入
spring-boot-starter-actuator添加 /actuator/health 端点 - 日志收集:使用Logback + ELK或Loki集中管理日志
- 限流熔断:引入 Sentinel 或 Resilience4j 做流量防护
- CI/CD:配置GitHub Actions或Jenkins自动化构建与部署
从项目初始化到完整部署,一个标准的RESTful API服务需要关注的点确实不少。但只要遵循本文介绍的架构模式 -- 清晰的分层设计、严谨的DTO/VO隔离、统一的异常处理和规范的接口文档,你就能构建出易于维护、扩展和协作的后端服务。这套模式在淘宝、京东等大型互联网公司中被广泛采用,也是面试中考察后端架构能力的高频考点。
建议你在实际项目中反复实践这些模式,逐渐形成自己的架构风格。代码的优雅不在于用了多复杂的技术,而在于每个细节都经得起推敲。