Spring Boot + MyBatis + Redis 项目分层架构详解 ------ 各层职责、关系与工作流程
本文对项目中的每一层架构进行深度剖析,包括 Domain(实体层)、DAO(数据访问层)、Service(业务层)、Controller(接口层)、Common(公共层)和 Config(配置层),并说明它们之间的协作关系与完整工作流程。
一、项目分层总览
┌─────────────────────────────────────────────────────────┐
│ 请求入口层 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Controller(REST 接口层) │ │
│ │ CityRestController.java │ │
│ └──────────────────────┬────────────────────────────┘ │
└─────────────────────────┼───────────────────────────────┘
│ 调用
┌─────────────────────────┼───────────────────────────────┐
│ 业务逻辑层 │
│ ──────────────────────▼────────────────────────────┐ │
│ │ Service(业务层) │ │
│ │ CityService.java / CityServiceImpl.java │ │
│ └──────────────────────┬────────────────────────────┘ │
─────────────────────────┼───────────────────────────────┘
│ 调用
┌─────────────────────────┼───────────────────────────────
│ 数据访问层 │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ DAO(数据访问层) │ │
│ │ CityDao.java + CityMapper.xml │ │
│ └──────────────────────┬────────────────────────────┘ │
─────────────────────────┼───────────────────────────────┘
│ 执行 SQL
┌─────────────────────────┼───────────────────────────────┐
│ 数据存储层 │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ MySQL 数据库 / Redis 缓存 │ │
│ └───────────────────────────────────────────────────┘ │
─────────────────────────────────────────────────────────┘
横向贯穿各层:
┌─────────────────────────────────────────────────────────┐
│ Domain(实体层):City.java ------ 数据在各层之间传递的载体 │
│ Common(公共层):Result.java ------ 统一响应封装 │
│ Config(配置层):Application.java / RedisConfig.java │
─────────────────────────────────────────────────────────┘
二、Domain 层(实体层)
文件:City.java
java
public class City implements Serializable {
private static final long serialVersionUID = -1L;
private Long id; // 城市编号
private Long provinceId; // 省份编号
private String cityName; // 城市名称
private String description; // 描述
}
职责
Domain 层(也叫 Model 层、Entity 层)是整个项目的数据载体,负责:
| 职责 | 说明 |
|---|---|
| 定义数据结构 | 描述数据库表对应的字段 |
| 在各层之间传递数据 | Controller → Service → DAO 之间传递的都是 Domain 对象 |
| 支持序列化 | 实现 Serializable 接口,以便存入 Redis 缓存 |
为什么需要 Domain 层?
数据库表 city Java 对象 City
┌─────────────────┐ ┌─────────────────────┐
│ id: 1 │ ──映射──▶ │ id = 1 │
│ province_id: 3 │ │ provinceId = 3 │
│ city_name: 杭州 │ │ cityName = "杭州" │
│ description: ...│ │ description = "..." │
└─────────────────┘ └─────────────────────┘
Domain 对象是数据库记录和 Java 代码之间的桥梁:
- DAO 层从数据库读取数据 → 封装为
City对象 - Service 层接收
City对象 → 处理业务逻辑 - Controller 层将
City对象 → 序列化为 JSON 返回给前端
关键细节:为什么实现 Serializable?
java
public class City implements Serializable {
private static final long serialVersionUID = -1L;
因为 City 对象需要存入 Redis 缓存 ,Redis 客户端在存储对象时需要将其序列化。如果不实现 Serializable 接口,会抛出 NotSerializableException 异常。
serialVersionUID用于版本控制,确保序列化/反序列化时类版本一致。
三、DAO 层(数据访问层)
文件:CityDao.java + CityMapper.xml
DAO 接口
java
public interface CityDao {
List<City> findAllCity();
City findById(@Param("id") Long id);
Long saveCity(City city);
Long updateCity(City city);
Long deleteCity(Long id);
}
MyBatis SQL 映射
xml
<mapper namespace="org.spring.springboot.dao.CityDao">
<resultMap id="BaseResultMap" type="org.spring.springboot.domain.City">
<result column="id" property="id" />
<result column="province_id" property="provinceId" />
<result column="city_name" property="cityName" />
<result column="description" property="description" />
</resultMap>
<select id="findById" resultMap="BaseResultMap" parameterType="java.lang.Long">
SELECT id, province_id, city_name, description FROM city WHERE id = #{id}
</select>
<insert id="saveCity" parameterType="City" useGeneratedKeys="true" keyProperty="id">
INSERT INTO city(id, province_id, city_name, description)
VALUES(#{id}, #{provinceId}, #{cityName}, #{description})
</insert>
<update id="updateCity" parameterType="City">
UPDATE city SET
<if test="provinceId!=null">province_id = #{provinceId},</if>
<if test="cityName!=null">city_name = #{cityName},</if>
<if test="description!=null">description = #{description}</if>
WHERE id = #{id}
</update>
<delete id="deleteCity" parameterType="java.lang.Long">
DELETE FROM city WHERE id = #{id}
</delete>
</mapper>
职责
| 职责 | 说明 |
|---|---|
| 定义数据库操作接口 | 声明增删改查方法 |
| SQL 语句映射 | 通过 XML 文件将 Java 方法映射为 SQL |
| 结果集映射 | 将数据库查询结果映射为 Java 对象(City) |
DAO 层的工作流程
Service 调用 cityDao.findById(1L)
│
▼
MyBatis 框架拦截接口调用
│
▼
根据 namespace + 方法名 找到对应的 SQL
namespace: org.spring.springboot.dao.CityDao
方法名: findById
→ 定位到 CityMapper.xml 中 id="findById" 的 <select>
│
▼
执行 SQL: SELECT id, province_id, city_name, description FROM city WHERE id = 1
│
▼
将查询结果通过 resultMap 映射为 City 对象
column "id" → property "id"
column "province_id" → property "provinceId"
column "city_name" → property "cityName"
column "description" → property "description"
│
▼
返回 City 对象给 Service 层
关键细节
1. @Param 注解的作用
java
City findById(@Param("id") Long id);
@Param("id") 将方法参数命名为 id,这样在 XML 中可以用 #{id} 引用:
xml
WHERE id = #{id}
2. 动态 SQL(<if> 标签)
xml
<update id="updateCity">
UPDATE city SET
<if test="provinceId!=null">province_id = #{provinceId},</if>
<if test="cityName!=null">city_name = #{cityName},</if>
<if test="description!=null">description = #{description}</if>
WHERE id = #{id}
</update>
只更新传入的非空字段,避免将未传入的字段误置为 NULL。
3. useGeneratedKeys 自动生成主键
xml
<insert id="saveCity" useGeneratedKeys="true" keyProperty="id">
插入数据后,数据库自动生成的主键会回填到 City 对象的 id 属性中。
四、Service 层(业务层)
文件:CityService.java + CityServiceImpl.java
业务接口
java
public interface CityService {
City findCityById(Long id);
Long saveCity(City city);
Long updateCity(City city);
Long deleteCity(Long id);
}
业务实现(含 Redis 缓存逻辑)
java
@Service
public class CityServiceImpl implements CityService {
@Autowired
private CityDao cityDao;
@Autowired
private RedisTemplate redisTemplate;
public City findCityById(Long id) {
// 先查缓存 → 未命中查 DB → 写缓存
}
public Long updateCity(City city) {
// 更新 DB → 删除缓存
}
public Long deleteCity(Long id) {
// 删除 DB → 删除缓存
}
}
职责
| 职责 | 说明 |
|---|---|
| 核心业务逻辑 | 缓存策略、数据校验、事务管理等 |
| 协调多层操作 | 组合调用 DAO、Redis 等多个数据源 |
| 接口与实现分离 | 接口定义契约,实现类负责具体逻辑 |
接口与实现分离的意义
java
// 接口:定义"做什么"
public interface CityService {
City findCityById(Long id);
}
// 实现:定义"怎么做"
@Service
public class CityServiceImpl implements CityService {
public City findCityById(Long id) {
// 具体的缓存 + 数据库逻辑
}
}
| 好处 | 说明 |
|---|---|
| 解耦 | Controller 依赖的是接口,不关心具体实现 |
| 可替换 | 可以轻松切换不同的实现(如换一种缓存策略) |
| 可测试 | 测试时可以用 Mock 实现替换真实实现 |
| 符合开闭原则 | 对扩展开放,对修改关闭 |
Service 层的缓存策略详解
查询操作(findCityById):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 查缓存 │───▶│ 命中? │───▶│ 返回数据 │
└────────── └────┬─────┘ └──────────┘
│ 未命中
▼
┌──────────┐ ┌──────────┐
│ 查数据库 │───▶│ 写缓存 │───▶ 返回数据
└──────────┘ └──────────┘
更新操作(updateCity):
┌──────────┐ ──────────┐
│ 更新 DB │───▶│ 删缓存 │ (下次查询自动加载新数据)
──────────┘ └──────────┘
删除操作(deleteCity):
┌──────────┐ ┌──────────┐
│ 删除 DB │───▶│ 删缓存 │ (防止脏数据)
└──────────┘ └──────────┘
五、Controller 层(REST 接口层)
文件:CityRestController.java
java
@RestController
public class CityRestController {
@Autowired
private CityService cityService;
@RequestMapping(value = "/api/city/{id}", method = RequestMethod.GET)
public Result<Object> findOneCity(@PathVariable("id") Long id) {
City city = cityService.findCityById(id);
if (city == null) {
return Result.success("没有查询到数据");
}
return Result.success(city);
}
@RequestMapping(value = "/api/city", method = RequestMethod.POST)
public Result<City> createCity(@RequestBody City city) {
cityService.saveCity(city);
return Result.success(city);
}
@RequestMapping(value = "/api/city", method = RequestMethod.PUT)
public Result<City> modifyCity(@RequestBody City city) {
cityService.updateCity(city);
return Result.success(city);
}
@RequestMapping(value = "/api/city/{id}", method = RequestMethod.DELETE)
public Result<Long> deleteCity(@PathVariable("id") Long id) {
cityService.deleteCity(id);
return Result.success(id);
}
}
职责
| 职责 | 说明 |
|---|---|
| 定义 RESTful API | 通过 HTTP 方法(GET/POST/PUT/DELETE)暴露接口 |
| 参数解析 | 从 URL 路径、请求体中提取参数 |
| 调用 Service | 将请求转发给业务层处理 |
| 统一响应封装 | 将结果包装为 Result<T> 格式返回 |
参数解析方式
| 注解 | 来源 | 示例 |
|---|---|---|
@PathVariable |
URL 路径 | /api/city/1 → id = 1 |
@RequestBody |
请求体 JSON | {"cityName":"杭州"} → City 对象 |
@RequestParam |
URL 查询参数 | /api/city?id=1 → id = 1 |
@RequestHeader |
请求头 | Authorization: Bearer xxx |
六、Common 层(公共层)
文件:Result.java
java
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> error(int code, String message) {
return new Result<>(code, message, null);
}
}
职责
| 职责 | 说明 |
|---|---|
| 统一响应格式 | 所有接口返回相同的 JSON 结构 |
| 简化代码 | 通过静态方法快速构建响应对象 |
| 前后端约定 | 前端根据 code 判断成功或失败 |
响应格式示例
成功响应(有数据):
json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"provinceId": 1,
"cityName": "杭州",
"description": "人间天堂"
}
}
成功响应(无数据):
json
{
"code": 200,
"message": "success",
"data": "没有查询到数据"
}
失败响应:
json
{
"code": 500,
"message": "服务器内部错误",
"data": null
}
为什么需要统一响应?
java
// ❌ 没有统一响应:每个接口返回格式不一致
@GetMapping("/city/{id}")
public City getCity(@PathVariable Long id) { ... } // 返回 City 对象
@GetMapping("/city/list")
public List<City> getCityList() { ... } // 返回 List
@PostMapping("/city")
public String createCity(@RequestBody City city) { ... } // 返回字符串
java
// ✅ 统一响应:所有接口返回格式一致
@GetMapping("/city/{id}")
public Result<City> getCity(@PathVariable Long id) { ... }
@GetMapping("/city/list")
public Result<List<City>> getCityList() { ... }
@PostMapping("/city")
public Result<City> createCity(@RequestBody City city) { ... }
前端只需要一套解析逻辑:
javascript
// 前端统一处理
fetch('/api/city/1')
.then(res => res.json())
.then(result => {
if (result.code === 200) {
// 成功:使用 result.data
} else {
// 失败:显示 result.message
}
});
七、Config 层(配置层)
文件 1:Application.java(启动类)
java
@SpringBootApplication
@MapperScan("org.spring.springboot.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
| 注解/方法 | 作用 |
|---|---|
@SpringBootApplication |
标记为 Spring Boot 应用,包含自动配置 + 组件扫描 |
@MapperScan |
扫描 DAO 包,自动注册 MyBatis Mapper 接口为 Spring Bean |
SpringApplication.run() |
启动嵌入式 Tomcat,初始化 Spring 容器 |
文件 2:RedisConfig.java(Redis 配置)
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
| 配置项 | 作用 |
|---|---|
@Configuration |
标记为配置类,Spring 启动时自动加载 |
@Bean |
将方法返回值注册为 Spring 容器中的 Bean |
StringRedisSerializer |
Key 使用字符串序列化(city_1 清晰可读) |
Jackson2JsonRedisSerializer |
Value 使用 JSON 序列化(对象转为可读 JSON) |
为什么需要 RedisConfig?
Spring Data Redis 默认使用 JDK 序列化,存入 Redis 的数据是二进制格式:
# 默认序列化(乱码)
127.0.0.1:6379> GET city_1
"\xac\xed\x00\x05sr\x00\x1eorg.spring.springboot.domain.City..."
配置 JSON 序列化后:
# JSON 序列化(可读)
127.0.0.1:6379> GET city_1
"{\"id\":1,\"provinceId\":1,\"cityName\":\"杭州\",\"description\":\"人间天堂\"}"
八、配置文件(application.properties)
properties
## 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdb
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## MyBatis 配置
mybatis.typeAliasesPackage=org.spring.springboot.domain
mybatis.mapperLocations=classpath:mapper/*.xml
## Redis 配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
配置与代码的对应关系
application.properties Java 代码
───────────────────── ─────────
spring.datasource.* ──▶ DataSource(自动装配)
mybatis.typeAliasesPackage ──▶ MyBatis 实体类扫描
mybatis.mapperLocations ──▶ MyBatis XML 映射文件加载
spring.redis.host/port ──▶ RedisConnectionFactory(自动装配)
九、完整工作流程 ------ 以"查询城市"为例
场景:客户端请求 GET /api/city/1
第 1 步:HTTP 请求到达
─────────────────────────────────────────
客户端发送:GET http://localhost:8080/api/city/1
第 2 步:Controller 层接收请求
─────────────────────────────────────────
CityRestController.findOneCity(id=1)
│
├─ @PathVariable 从 URL 提取 id = 1
└─ 调用 cityService.findCityById(1)
第 3 步:Service 层处理业务
─────────────────────────────────────────
CityServiceImpl.findCityById(1)
│
├─ 构造缓存 key = "city_1"
├─ 查询 Redis:redisTemplate.hasKey("city_1")
│
├─ 【情况 A:缓存命中】
│ └─ 从 Redis 获取数据 → 直接返回 City 对象
│
└─ 【情况 B:缓存未命中】
├─ 调用 cityDao.findById(1)
│ └─ 第 4 步:DAO 层查询数据库
├─ 将结果写入 Redis(TTL 500 秒)
└─ 返回 City 对象
第 4 步:DAO 层查询数据库
─────────────────────────────────────────
CityDao.findById(1)
│
├─ MyBatis 定位到 CityMapper.xml 中 id="findById" 的 SQL
├─ 执行:SELECT id, province_id, city_name, description FROM city WHERE id = 1
├─ 通过 resultMap 将结果映射为 City 对象
└─ 返回 City 对象给 Service 层
第 5 步:Controller 层封装响应
─────────────────────────────────────────
CityRestController 收到 Service 返回的 City 对象
│
├─ 调用 Result.success(city)
─ 返回 JSON:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"provinceId": 1,
"cityName": "杭州",
"description": "人间天堂"
}
}
第 6 步:HTTP 响应返回客户端
─────────────────────────────────────────
客户端收到 JSON 响应,解析并展示数据
十、完整工作流程 ------ 以"更新城市"为例
场景:客户端请求 PUT /api/city + JSON Body
json
{"id": 1, "cityName": "新杭州", "description": "更新后的描述"}
第 1 步:HTTP 请求到达
─────────────────────────────────────────
客户端发送:PUT http://localhost:8080/api/city
请求体:{"id": 1, "cityName": "新杭州", "description": "更新后的描述"}
第 2 步:Controller 层接收请求
─────────────────────────────────────────
CityRestController.modifyCity(city)
│
├─ @RequestBody 将 JSON 解析为 City 对象
│ city.id = 1
│ city.cityName = "新杭州"
│ city.description = "更新后的描述"
└─ 调用 cityService.updateCity(city)
第 3 步:Service 层处理业务
─────────────────────────────────────────
CityServiceImpl.updateCity(city)
│
├─ 调用 cityDao.updateCity(city)
│ └─ 第 4 步:DAO 层更新数据库
│
├─ 构造缓存 key = "city_1"
├─ 检查 Redis 中是否存在该缓存
└─ 如果存在 → 删除缓存(保证下次查询走数据库获取最新数据)
第 4 步:DAO 层更新数据库
─────────────────────────────────────────
CityDao.updateCity(city)
│
├─ MyBatis 定位到 CityMapper.xml 中 id="updateCity" 的 SQL
├─ 动态 SQL 生成(只更新非空字段):
│ UPDATE city SET
│ city_name = '新杭州',
│ description = '更新后的描述'
│ WHERE id = 1
├─ 执行 SQL,更新数据库
└─ 返回影响行数
第 5 步:Controller 层封装响应
─────────────────────────────────────────
返回 JSON:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"cityName": "新杭州",
"description": "更新后的描述"
}
}
十一、各层之间的依赖关系图
──────────────────────────────────────────────────────────────┐
│ Spring 容器 │
│ │
│ ┌──────────────┐ @Autowired ┌──────────────────────┐ │
│ │ Controller │ ───────────────▶ │ Service │ │
│ │ (Bean) │ │ (Bean) │ │
│ └────────────── └──────────┬───────────┘ │
│ │ │
│ @Autowired │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ DAO │ │
│ │ (MyBatis Proxy) │ │
│ └──────────┬───────────┘ │
│ │ │
│ @Autowired │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ RedisTemplate │ │
│ │ (Spring Bean) │ │
│ └──────────────────────┘ │
│ │
──────────────────────────────────────────────────────────────┘
依赖注入方式:
Controller 依赖 Service → @Autowired private CityService cityService;
Service 依赖 DAO → @Autowired private CityDao cityDao;
Service 依赖 Redis → @Autowired private RedisTemplate redisTemplate;
十二、各层文件清单与职责总结
| 层 | 文件 | 职责 | 关键注解 |
|---|---|---|---|
| 启动配置 | Application.java |
应用入口,扫描 Mapper | @SpringBootApplication, @MapperScan |
| Redis 配置 | RedisConfig.java |
配置 JSON 序列化 | @Configuration, @Bean |
| 实体层 | City.java |
数据载体,跨层传递 | Serializable |
| DAO 层 | CityDao.java |
定义数据库操作接口 | MyBatis 接口 |
| SQL 映射 | CityMapper.xml |
SQL 语句与结果映射 | MyBatis XML |
| 业务接口 | CityService.java |
定义业务契约 | 普通接口 |
| 业务实现 | CityServiceImpl.java |
核心业务逻辑 + 缓存 | @Service |
| 接口层 | CityRestController.java |
RESTful API 暴露 | @RestController |
| 公共层 | Result.java |
统一响应封装 | 普通类 |
| 配置文件 | application.properties |
数据源/MyBatis/Redis 配置 | - |
十三、分层设计原则
| 原则 | 说明 | 本项目体现 |
|---|---|---|
| 单一职责 | 每层只做自己该做的事 | Controller 不写业务,Service 不处理 HTTP |
| 依赖倒置 | 上层依赖下层接口,不依赖实现 | Controller 依赖 CityService 接口 |
| 单向依赖 | 只能向下调用,不能反向 | Controller → Service → DAO |
| 接口隔离 | 接口定义契约,实现类负责细节 | CityService 接口 + CityServiceImpl 实现 |
| 开闭原则 | 对扩展开放,对修改关闭 | 新增业务只需添加 Service 方法,不改 Controller |
十四、总结
本项目虽然代码量不大,但完整展示了 Spring Boot 分层架构 的核心思想:
请求 → Controller(接收请求、参数解析)
↓
Service(业务逻辑、缓存策略)
↓
DAO(数据库操作、SQL 执行)
↓
MySQL / Redis(数据存储)
Domain 对象在各层之间传递数据
Result 对象统一封装响应格式
Config 配置层负责框架集成
掌握这套分层架构,就掌握了 Spring Boot 项目开发的基石。 无论项目规模多大,核心思路都是一致的:让每一层专注于自己的职责,通过接口协作,实现高内聚、低耦合的系统设计。