pring Boot + MyBatis + Redis 项目分层架构详解 —— 各层职责、关系与工作流程

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/1id = 1
@RequestBody 请求体 JSON {"cityName":"杭州"}City 对象
@RequestParam URL 查询参数 /api/city?id=1id = 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 项目开发的基石。 无论项目规模多大,核心思路都是一致的:让每一层专注于自己的职责,通过接口协作,实现高内聚、低耦合的系统设计。

相关推荐
上海安当技术1 小时前
老系统零改造接入:旁路代理统一认证实战
java·servlet·架构·kubernetes·jenkins
qq_454245032 小时前
.clinerules 系统提示词价值评判
人工智能·架构
qq_454245032 小时前
离散与连续的认知切割:变量与实例的公理化思辨
架构
豆瓣鸡3 小时前
微服务项目生产环境全链路架构:从前端访问到网关鉴权、服务调用、中间件与网络安全
微服务·架构
闲余生3 小时前
从三条路径到一个 AnalysisSession:一次代码诊断架构的合流实录
人工智能·架构
张忠琳3 小时前
【NVIDIA】k8s-device-plugin v0.19.3 辅助命令模块深度分析之七
云原生·容器·架构·kubernetes·nvidia
熊猫钓鱼>_>3 小时前
ArkTS 基础入门:从零搭建第一个交互页面
华为·架构·交互·ts·harmonyos·arkts·鸿蒙
QN1幻化引擎3 小时前
把 意识评测做成了一场"非侵入实验":不碰生产代码,分数反而更真了
人工智能·算法·架构
带娃的IT创业者3 小时前
单文件架构的极致美学:深入解析 Bento 的 HTML 幻灯片技术实现
前端·架构·html·web开发·bento·单文件架构