一、概述

        三层架构是一种将应用程序的逻辑划分为三个核心职责层的设计模式,这三层分别是:控制层(Controller)、业务逻辑层(Service)和数据访问层(Data Access Object)。它的主要目标是实现“高内聚、低耦合”,使得代码结构清晰,便于维护、测试和团队协作。

        通过将应用程序的不同职责分离,三层架构使得每一层可以独立开发和修改,而不影响其他层。这种分层模式提高了软件的可扩展性、可维护性和可测试性,是软件开发中广泛采用的一种架构模式

二、三层架构的职责划分与核心价值

1.三层架构的各层职责是什么?

这三层架构分别是:

  1. 控制层(controller):接收前端发送的请求(如HTTP请求),对请求进行处理,并响应数据(如返回JSON数据)。
  2. 业务逻辑层(service):包含了应用程序的核心业务逻辑,处理来自控制层的请求,执行复杂的业务计算、验证和流程控制,并且协助数据访问层的工作。
  3. 数据访问层(Data Access object):负责与数据库进行交互,执行数据的增删改查操作。

2.为什么在软件开发过程中要使用三层架构的模式?

相信大家对三层架构应该有个了初步的了解了。我再举一个在生活中的例子,来让大家更好的理解为什么要使用三层架构模式来开发应用程序,使用三层架构模式到底能带来怎么样的好处。大家一定去过饭馆吃饭,我们就把开发一个软件现象成经营一家饭馆。

假如这家饭馆只有老板一个人,也就是只有一层(三层代码都写在一层中)。老板作为“全能员工”,那他需要负责的工作:

  • 他要去门口迎接客人。负责点菜,上菜,和打扫桌面(控制层的活)
  • 客人点完菜,他要跑去后厨炒菜(业务逻辑层的活)
  • 炒菜的时候发现没盐了,没油了,要去仓库取货。(数据访问层的活)

这个全能员工又要当服务员,又要当厨师,又要负责仓库货物的存取。午高峰到了,客人一个接一个。他刚接过客人油腻的盘子,又去炒菜,卫生堪忧(代码混乱,容易出错)。万一他炒菜的口味变了,可能会影响迎客的态度(修改一处,可能意外影响其他地方)。他一个人来回跑,客人点完菜要等很久才能吃上(系统性能差)。要是想推出新菜式,可能需要重新培训这个“全能超人”,风险极高(修改或扩展功能困难,牵一发而动全身)。如果这个“超人”生病请假了,整个餐厅就停摆了(代码只有一个人能看懂,难以维护)。

由此三层架构出现了。饭馆把工作分给了三个专业的角色:

  • 服务员——控制层:只负责微笑迎客、递菜单、点菜、上菜。他不需要知道菜怎么做的。Controller,只管接收请求和返回结果。
  • 厨师——业务逻辑层:只负责在后厨按照标准食谱做菜。他不需要知道客人是谁,也不需要自己去仓库拿菜。Service,包含核心业务规则,比如“下单要扣库存”、“计算折扣”。
  • 仓库管理员——数据访问层:只负责管理仓库里的食材。保证食材的进出库记录准确。他不需要知道这些食材是做什么菜的。Mapper/Dao,只负责执行SQL,跟数据库打交道。

那么好处显而易见了:

  1. 专业分工,效率高:三个人各司其职,并发工作。服务员可以同时接待几桌客人,厨师专注炒菜,仓库管理员保证物资供应。整个餐厅运作流畅(系统性能好,易于维护)。

  2. 容易修改和扩展

    • 你想换菜单(修改业务逻辑)?只需要告诉厨师改食谱就行了,不影响服务员和仓库管理员。

    • 你想把服务员制服从旗袍换成西装(改变前端界面)?换掉就行,完全不用动后厨和仓库。

    • 你想把仓库从本地搬去大型冷链中心(把MySQL数据库换成云数据库)?只需要告诉仓库管理员新地址和规矩,厨师和服务员的工作照旧。

    • 易于测试和排查问题

      • 菜咸了,肯定是厨师的问题(业务逻辑层Bug)。

      • 上错菜了,肯定是服务员记错了(表示层Bug)。

      • 菜卖完了系统还没提示,肯定是仓库管理员库存没同步好(数据访问层Bug)。

      • 问题很容易被定位和解决。

    • 便于团队协作:前端工程师就和服务员打交道(定义接口),Java工程师专心写业务逻辑(厨师),DBA专心优化数据库(仓库管理员)。大家可以并行开发。

“一层架构”就像是让一个人包揽所有杂活,短期内看着省事,但项目稍微复杂一点,就会变成一场灾难。而三层架构是软件开发实践中总结出的最佳“管理智慧”。

三、架构分层解析

        我将基于一个简单的用户注册功能为例,首先展示如何将所有代码写在一层(比如Controller层)中,然后逐步重构为三层架构。以此来让大家更好理解三层架构的代码实现。不过大家要注意拆分的三层架构代码只是为了大家更好理解三层架构每层的各自职责。其实在实际开发过程中,我所展示的代码仍然是不足的,还有需要优化的地方。例如在Controller层,我们一般会将返回给前端的数据(如状态码、提示信息、业务数据)封装到一个统一的Result类中。Controller的方法直接返回这个Result类的实例对象,这样不仅可以保证响应格式统一,还能避免代码复用性低的问题。大家在往后的学习中,就会一步步明白了的。

@RestController
public class UserController {

    // 直接注入数据库操作工具(如JdbcTemplate或MyBatis的SqlSession)
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @GetMapping("/user/{id}")
    public Map<String, Object> getUserById(@PathVariable Long id) {

        // 编写SQL查询
        String sql = "SELECT id, username, email, create_time FROM user WHERE id = ?";
        
        // 执行数据库查询
        Map<String, Object> userMap;
        try {
            userMap = jdbcTemplate.queryForMap(sql, id);
        } catch (Exception e) {
            // 数据库异常处理
            Map<String, Object> error = new HashMap<>();
            error.put("code", 500);
            error.put("message", "数据库查询失败");
            return error;
        }
        
        
        // 业务规则:如果用户不存在,返回特定错误
        if (userMap == null || userMap.isEmpty()) {
            Map<String, Object> error = new HashMap<>();
            error.put("code", 404);
            error.put("message", "用户不存在");
            return error;
        }
        
        // 业务规则:敏感信息脱敏(如邮箱)
        String email = (String) userMap.get("email");
        if (email != null && email.contains("@")) {
            String[] parts = email.split("@");
            if (parts[0].length() > 2) {
                String maskedEmail = parts[0].substring(0, 2) + "***@" + parts[1];
                userMap.put("email", maskedEmail);
            }
        }
        
        // 业务规则:格式化创建时间
        Timestamp createTime = (Timestamp) userMap.get("create_time");
        if (createTime != null) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            userMap.put("create_time", sdf.format(createTime));
        }
        
        // === 响应数据
        Map<String, Object> response = new HashMap<>();
        response.put("code", 200);
        response.put("message", "成功");
        response.put("data", userMap);
        
        return response;
    }
}

1.控制层(controller)

Controller只应该处理HTTP相关的事情。

  • 移除所有业务逻辑和数据库操作

  • 只保留请求解析、响应封装和异常处理

  • 通过依赖注入调用Service层

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<Map<String, Object>> getUserById(@PathVariable Long id) {
        // 统一响应格式的DTO
        Map<String, Object> response = new HashMap<>();
        
        try {
            // 调用Service层处理业务逻辑
            User user = userService.getUserById(id);
            
            // 控制层只负责组装响应
            response.put("code", 200);
            response.put("message", "成功");
            response.put("data", user);
            
            return ResponseEntity.ok(response);
            
        } catch (RuntimeException e) {
            // 捕获业务异常
            response.put("code", 404);
            response.put("message", e.getMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
            
        } catch (Exception e) {
            // 捕获系统异常
            response.put("code", 500);
            response.put("message", "系统错误");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }
}

2.业务逻辑层(service)

所有业务规则和数据处理逻辑都应该独立出来。

  • 创建UserService接口定义业务方法

  • 实现业务逻辑,注入Dao依赖

  • 将业务规则(用户检查、脱敏等)从Controller移到Service

// Service接口
public interface UserService {
    /**
     * 根据ID查询用户信息(包含业务处理)
     */
    User getUserById(Long id);
}

// Service实现
@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserDao userDao;
    
    @Override
    public User getUserById(Long id) {
        // 调用Dao层获取原始数据
        User user = userDao.selectById(id);
        
        // 业务规则:用户不存在检查
        if (user == null) {
            throw new RuntimeException("用户不存在");
        }
        
        // 业务规则:敏感信息脱敏
        maskSensitiveInfo(user);
        
        // 业务规则:时间格式化等可以在这一层处理
        // 但通常更推荐在DTO转换时处理
        
        return user;
    }
    
    /**
     * 脱敏处理:私有方法,封装脱敏逻辑
     */
    private void maskSensitiveInfo(User user) {
        if (user.getEmail() != null && user.getEmail().contains("@")) {
            String[] parts = user.getEmail().split("@");
            if (parts[0].length() > 2) {
                user.setEmail(parts[0].substring(0, 2) + "***@" + parts[1]);
            }
        }
    }
}

3.数据持久层(dao)

所有直接操作数据库的代码都应该独立出来。

  • 创建实体类User来替代Map

  • 创建UserDao接口定义数据操作方法

  • 将SQL查询逻辑移动到Dao层

// 首先定义一个实体类(在实际开发过程中,实体类写到pojo包下)
@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private Date createTime;
}

// Dao接口
@Mapper
public interface UserDao {
    User selectById(Long id);
}

// Dao实现(MyBatis的XML映射文件)
// UserDao.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.dao.UserDao">
    <select id="selectById" resultType="User">
        SELECT id, username, email, create_time as createTime 
        FROM user 
        WHERE id = #{id}
    </select>
</mapper>

四、三层架构的交互流程

1、核心交互原则

要理解三层交互协同的过程,我们必须先理解三层交互协同遵循的两个核心原则:

        1.1单向依赖原则。具体而言就是上层可以调用下层,下层绝不能调用上层。控制层(上层)可调用业务逻辑层(下层)的订单处理接口,但业务逻辑层不能调用控制层。调用顺序是Controller → Service → Dao。

// 交互顺序
@RestController
public class UserController {
    @Autowired
    private UserService userService;  // Controller依赖Service
    
    public void getUser() {
        userService.findUser(1L);  // 上层调用下层
    }
}

@Service 
public class UserService {
    @Autowired
    private UserDao userDao;  // Service依赖Dao
    
    public User findUser(Long id) {
        return userDao.selectById(id);  // 上层调用下层
    }
}

@Mapper
public interface UserDao {
    User selectById(Long id);  // Dao是最底层,不依赖任何人
}

1.2数据流不可逆原则请求流:用户请求 → Controller → Service → Dao → 数据库。响应流:数据库 → Dao → Service → Controller → 用户响应。即数据在各层间传递时,只能按照这个顺序流动,不能跳跃或逆向。

// 请求流(不可逆)
用户点击"查询用户" 
    → Controller的getUser()方法被调用
    → Controller调用userService.findUser(1L)
    → Service调用userDao.selectById(1L)
    → Dao执行SQL: SELECT * FROM user WHERE id = 1

// 响应流(不可逆)  
数据库返回用户数据
    → Dao返回User对象给Service
    → Service返回User对象给Controller  
    → Controller将User转为JSON返回给用户

在明确分层原则后,我们具体看各层间的协作实现

2、Controller 如何调用 Service

Controller向Service传递DTO对象,此时Controller线程同步阻塞,直到Service返回响应。

// Controller层
@RestController
public class UserController {
    
    @Autowired  // 关键交互点1:注入Service
    private UserService userService;  // 持有Service的引用
    
    @PostMapping("/register")
    public ResponseEntity register(@RequestBody UserRegisterRequest request) {
        
        // 关键交互点2:调用Service方法
        UserDTO result = userService.register(request);
        // ↑↑↑ 这里发生了层间交互!
        // Controller把请求数据交给Service,等待Service处理完成
        
        return ResponseEntity.ok(result);
    }
}

3、Service如何调用DAO

一个Service方法可能调用多个Dao方法,通过@Transactional注解声明方法级事务,内部所有Dao操作共享同一事务上下文,Service决定什么时候调用哪个Dao

// Service层
@Transactional
@Service
public class UserServiceImpl implements UserService {
    
    @Autowired  // 关键交互点1:注入Dao
    private UserDao userDao;  // 持有Dao的引用
    
    @Override
    public UserDTO register(UserRegisterRequest request) {
        
        // 关键交互点2:调用Dao检查用户是否存在
        boolean exists = userDao.existsByUsername(request.getUsername());
        // ↑↑↑ Service向Dao发起第一次交互!
        
        if (exists) {
            throw new RuntimeException("用户名已存在");
        }
        
        // 关键交互点3:调用Dao保存用户
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(encodePassword(request.getPassword()));
        
        userDao.save(user);  // ↑↑↑ Service向Dao发起第二次交互!
        
        return convertToDTO(user);
    }
}

4、Dao 与数据库的交互

Dao层封装所有具体的数据库操作细节,并将查询到的数据库记录转换为对象。同时,它负责捕获底层的数据库异常,并将其抛给上层的Service层进行处理。

// Dao层
@Mapper
public interface UserDao {
    // 这些方法由MyBatis自动实现,与数据库交互
    boolean existsByUsername(String username);
    void save(User user);
    User findById(Long id);
}

// 对应的SQL映射
// <insert id="save">INSERT INTO users ...</insert>
// <select id="existsByUsername">SELECT COUNT(*) ...</select>

5、完整的交互链条

5.1请求处理流程(交互时序)

用户点击注册按钮
    ↓
HTTP请求到达 Controller.register()
    ↓
Controller 解析JSON数据 → 创建UserRegisterRequest对象
    ↓
Controller 调用 userService.register(request)  // 层间交互1
    ↓
Service 开始处理业务逻辑
    ↓
Service 调用 userDao.existsByUsername()  // 层间交互2
    ↓
Dao 执行SQL: SELECT COUNT(*) FROM users WHERE username = ?
    ↓
数据库返回结果 → Dao返回boolean给Service
    ↓
Service 判断用户名是否可用
    ↓
Service 调用 userDao.save(user)  // 层间交互3  
    ↓
Dao 执行SQL: INSERT INTO users ...
    ↓
数据库插入成功 → Dao返回void给Service
    ↓
Service 创建UserDTO对象 → 返回给Controller  // 层间交互4
    ↓
Controller 包装成HTTP响应 → 返回给用户

5.2方法调用链

Controller.method()
    ↓ 调用
Service.method()  
    ↓ 调用  
Dao.method()
    ↓ 执行
数据库操作

5.3数据传递链

HTTP请求数据 → Controller参数 → Service参数 → Dao参数 → SQL参数
    ↓
SQL结果集 → Dao返回值 → Service返回值 → Controller返回值 → HTTP响应

5.4异常传递链

数据库异常 → Dao异常 → Service异常 → Controller异常处理 → HTTP错误响应

五、总结

三层架构的本质是专业化分工,通过将软件系统划分为三个职责分明的层次,实现"高内聚、低耦合"的设计目标。就像一家高效运营的公司,每个部门专注于自己的核心业务,通过标准流程协同工作。

注:在示例代码中,我使用了多种注释方式来标注不同层次的代码。同时,代码中涉及到的依赖注入(Dependency Injection)和控制反转(Inversion of Control)等高级概念,在此不做深入讲解。如果这些概念对您来说还比较陌生,不必过于纠结,可以暂时跳过。此文重点是希望能够帮到大家理解三层架构的基本思想和各层之间的数据交互过程。

感谢您的阅读,如有表述不准确或错误,欢迎批评指正!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐