问题

SpringMVC三层架构有什么好处?

答案

1. 核心概念

三层架构(Three-Tier Architecture)是一种经典的软件架构模式,将应用程序划分为三个逻辑层:

+-------------------+
|   Controller层    |  表现层(Presentation Layer)
+-------------------+
         ↓
+-------------------+
|    Service层      |  业务逻辑层(Business Logic Layer)
+-------------------+
         ↓
+-------------------+
|      Dao层        |  数据访问层(Data Access Layer)
+-------------------+
         ↓
+-------------------+
|     Database      |  数据库
+-------------------+

层次职责

  • Controller层:接收HTTP请求,参数校验,调用Service,返回响应
  • Service层:业务逻辑处理,事务控制,调用Dao
  • Dao层:数据库访问,CRUD操作,SQL执行

2. 三层架构的详细职责

2.1 Controller层(表现层)

核心职责

  • 接收客户端请求(HTTP参数解析)
  • 参数校验与数据绑定
  • 调用Service层获取业务数据
  • 封装响应数据并返回(JSON/XML/视图)
  • 异常处理与统一响应

示例代码

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 创建用户
     */
    @PostMapping
    public Result<User> createUser(@RequestBody @Valid UserDTO userDTO) {
        // 1. 参数校验(通过@Valid自动校验)
        // 2. 调用Service层
        User user = userService.createUser(userDTO);
        // 3. 封装响应
        return Result.success(user);
    }

    /**
     * 分页查询用户
     */
    @GetMapping
    public Result<PageInfo<User>> listUsers(
            @RequestParam(defaultValue = "1") Integer pageNum,
            @RequestParam(defaultValue = "10") Integer pageSize) {
        PageInfo<User> pageInfo = userService.listUsers(pageNum, pageSize);
        return Result.success(pageInfo);
    }

    /**
     * 根据ID查询用户
     */
    @GetMapping("/{id}")
    public Result<User> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user == null) {
            return Result.error("用户不存在");
        }
        return Result.success(user);
    }

    /**
     * 更新用户
     */
    @PutMapping("/{id}")
    public Result<Void> updateUser(@PathVariable Long id,
                                    @RequestBody @Valid UserDTO userDTO) {
        userService.updateUser(id, userDTO);
        return Result.success();
    }

    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    public Result<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return Result.success();
    }
}

注意事项

  • 不应包含业务逻辑(如复杂计算、状态判断)
  • 不应直接访问Dao层(必须通过Service)
  • 保持轻量级,只做请求分发和响应封装

2.2 Service层(业务逻辑层)

核心职责

  • 核心业务逻辑处理
  • 事务管理(@Transactional)
  • 数据组装与转换
  • 调用多个Dao完成复杂业务
  • 业务异常处理

示例代码

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 创建用户(包含事务控制)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public User createUser(UserDTO userDTO) {
        // 1. 业务校验
        if (userDao.existsByUsername(userDTO.getUsername())) {
            throw new BusinessException("用户名已存在");
        }

        // 2. 数据转换与加密
        User user = new User();
        BeanUtils.copyProperties(userDTO, user);
        user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        user.setCreateTime(new Date());

        // 3. 保存用户
        userDao.insert(user);

        // 4. 分配默认角色
        Role defaultRole = roleDao.selectByName("ROLE_USER");
        userDao.insertUserRole(user.getId(), defaultRole.getId());

        // 5. 发送欢迎邮件(异步)
        emailService.sendWelcomeEmail(user.getEmail());

        return user;
    }

    /**
     * 分页查询用户
     */
    @Override
    public PageInfo<User> listUsers(Integer pageNum, Integer pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userDao.selectAll();
        return new PageInfo<>(users);
    }

    /**
     * 更新用户信息
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateUser(Long id, UserDTO userDTO) {
        // 1. 检查用户是否存在
        User user = userDao.selectById(id);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }

        // 2. 检查用户名是否冲突
        if (!user.getUsername().equals(userDTO.getUsername())) {
            if (userDao.existsByUsername(userDTO.getUsername())) {
                throw new BusinessException("用户名已被占用");
            }
        }

        // 3. 更新数据
        BeanUtils.copyProperties(userDTO, user);
        user.setUpdateTime(new Date());
        userDao.update(user);
    }

    /**
     * 删除用户(逻辑删除)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteUser(Long id) {
        User user = userDao.selectById(id);
        if (user == null) {
            throw new BusinessException("用户不存在");
        }

        // 逻辑删除
        user.setDeleted(true);
        user.setDeleteTime(new Date());
        userDao.update(user);

        // 删除用户角色关联
        userDao.deleteUserRoles(id);
    }
}

注意事项

  • 事务控制在Service层(@Transactional)
  • 可以调用多个Dao(如创建用户时操作user表和user_role表)
  • 封装复杂的业务逻辑(如状态机、流程控制)
  • 不应包含HTTP相关代码(如HttpServletRequest)

2.3 Dao层(数据访问层)

核心职责

  • 数据库CRUD操作
  • SQL语句封装
  • ORM映射(MyBatis、JPA)
  • 单表或简单关联查询

示例代码

// MyBatis方式
@Mapper
public interface UserDao {

    /**
     * 插入用户
     */
    @Insert("INSERT INTO user(username, password, email, create_time) " +
            "VALUES(#{username}, #{password}, #{email}, #{createTime})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    /**
     * 根据ID查询
     */
    @Select("SELECT * FROM user WHERE id = #{id} AND deleted = false")
    User selectById(Long id);

    /**
     * 查询所有用户
     */
    @Select("SELECT * FROM user WHERE deleted = false ORDER BY create_time DESC")
    List<User> selectAll();

    /**
     * 更新用户
     */
    @Update("UPDATE user SET username = #{username}, email = #{email}, " +
            "update_time = #{updateTime} WHERE id = #{id}")
    int update(User user);

    /**
     * 检查用户名是否存在
     */
    @Select("SELECT COUNT(1) FROM user WHERE username = #{username} AND deleted = false")
    boolean existsByUsername(String username);

    /**
     * 插入用户角色关联
     */
    @Insert("INSERT INTO user_role(user_id, role_id) VALUES(#{userId}, #{roleId})")
    int insertUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId);

    /**
     * 删除用户角色关联
     */
    @Delete("DELETE FROM user_role WHERE user_id = #{userId}")
    int deleteUserRoles(Long userId);
}

JPA方式

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 自定义查询方法
    User findByUsername(String username);

    boolean existsByUsername(String username);

    @Query("SELECT u FROM User u WHERE u.email = :email AND u.deleted = false")
    User findByEmail(@Param("email") String email);

    @Modifying
    @Query("UPDATE User u SET u.deleted = true WHERE u.id = :id")
    int logicDeleteById(@Param("id") Long id);
}

注意事项

  • 只负责数据访问,不包含业务逻辑
  • 单一职责,一个Dao对应一个实体或表
  • 不应调用其他Dao(复杂关联查询可通过SQL JOIN实现)
  • 不应包含事务控制(事务在Service层)

3. 三层架构的核心优势

3.1 职责分离,降低耦合

好处

  • 每层专注自己的职责,代码清晰易懂
  • 修改某层不影响其他层(如更换数据库只改Dao层)
  • 团队协作高效(前端对接Controller,后端专注Service和Dao)

示例

// 从MySQL迁移到MongoDB,只需修改Dao层
// Service层和Controller层无需改动

// 原MySQL实现
@Mapper
public interface UserDao {
    User selectById(Long id);
}

// 新MongoDB实现
@Repository
public class UserDaoImpl implements UserDao {
    @Autowired
    private MongoTemplate mongoTemplate;

    public User selectById(Long id) {
        return mongoTemplate.findById(id, User.class);
    }
}

3.2 复用性强

好处

  • Service层可被多个Controller调用
  • Dao层可被多个Service调用
  • 减少重复代码

示例

// UserService被多个Controller复用
@RestController
@RequestMapping("/admin/users")
public class AdminUserController {
    @Autowired
    private UserService userService;  // 复用UserService

    @GetMapping
    public Result<List<User>> listUsers() {
        return Result.success(userService.listUsers(1, 100).getList());
    }
}

@RestController
@RequestMapping("/api/users")
public class ApiUserController {
    @Autowired
    private UserService userService;  // 复用UserService

    @GetMapping("/profile")
    public Result<User> getProfile() {
        Long userId = getCurrentUserId();
        return Result.success(userService.getUserById(userId));
    }
}

3.3 易于测试

好处

  • 每层可独立单元测试
  • Mock依赖,隔离测试环境
  • 提高测试覆盖率

示例

// Service层单元测试
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UserDao userDao;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Test
    public void testCreateUser_success() {
        // 1. 准备测试数据
        UserDTO userDTO = new UserDTO();
        userDTO.setUsername("testuser");
        userDTO.setPassword("123456");

        // 2. Mock Dao层行为
        when(userDao.existsByUsername(anyString())).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("encrypted");
        when(userDao.insert(any(User.class))).thenReturn(1);

        // 3. 执行测试
        User user = userService.createUser(userDTO);

        // 4. 验证结果
        assertNotNull(user);
        assertEquals("testuser", user.getUsername());
        verify(userDao, times(1)).insert(any(User.class));
    }

    @Test(expected = BusinessException.class)
    public void testCreateUser_duplicateUsername() {
        UserDTO userDTO = new UserDTO();
        userDTO.setUsername("existuser");

        // Mock用户名已存在
        when(userDao.existsByUsername("existuser")).thenReturn(true);

        // 执行测试,期望抛出异常
        userService.createUser(userDTO);
    }
}

3.4 便于维护和扩展

好处

  • 新增功能时只需扩展对应层
  • 修改业务逻辑不影响数据访问
  • 符合开闭原则(对扩展开放,对修改关闭)

示例

// 新增导出功能,扩展Controller层
@GetMapping("/export")
public void exportUsers(HttpServletResponse response) throws IOException {
    List<User> users = userService.listUsers(1, 1000).getList();

    // 导出为Excel
    response.setContentType("application/vnd.ms-excel");
    response.setHeader("Content-Disposition", "attachment; filename=users.xlsx");

    ExcelWriter writer = EasyExcel.write(response.getOutputStream(), User.class).build();
    writer.write(users);
    writer.finish();
}

3.5 事务管理清晰

好处

  • 事务边界明确(Service层)
  • 避免事务泄漏或嵌套事务问题
  • 便于控制事务粒度

示例

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private ProductDao productDao;

    @Autowired
    private InventoryService inventoryService;

    /**
     * 创建订单(事务控制)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Order createOrder(OrderDTO orderDTO) {
        // 1. 创建订单
        Order order = new Order();
        orderDao.insert(order);

        // 2. 扣减库存
        for (OrderItem item : orderDTO.getItems()) {
            inventoryService.deductStock(item.getProductId(), item.getQuantity());
        }

        // 3. 更新商品销量
        productDao.incrementSales(orderDTO.getItems());

        return order;
        // 事务在方法结束时自动提交,异常时自动回滚
    }
}

4. 三层架构最佳实践

4.1 DTO与实体分离

// DTO:用于接收前端参数
@Data
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度6-20位")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;
}

// Entity:对应数据库表
@Data
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;
    private String email;
    private Date createTime;
    private Date updateTime;
    private Boolean deleted;
}

// VO:返回给前端的数据(隐藏敏感字段)
@Data
public class UserVO {
    private Long id;
    private String username;
    private String email;
    // 不包含password字段
}

4.2 统一异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        return Result.error(e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(message);
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后重试");
    }
}

4.3 统一响应格式

@Data
public class Result<T> {
    private Integer 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(String message) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMessage(message);
        return result;
    }
}

5. 面试答题总结

标准回答模板

SpringMVC三层架构将应用划分为 Controller(表现层)Service(业务逻辑层)Dao(数据访问层),主要好处包括:

  1. 职责分离,降低耦合:每层专注自己的职责,修改某层不影响其他层
  2. 复用性强:Service层可被多个Controller调用,Dao层可被多个Service调用
  3. 易于测试:每层可独立单元测试,通过Mock隔离依赖
  4. 便于维护和扩展:新增功能只需扩展对应层,符合开闭原则
  5. 事务管理清晰:事务边界在Service层,避免事务泄漏

职责划分

  • Controller:接收请求、参数校验、调用Service、返回响应
  • Service:业务逻辑、事务控制、调用Dao、数据组装
  • Dao:数据库CRUD、SQL封装、ORM映射

最佳实践

  • DTO与实体分离(UserDTO接收参数,User对应数据库,UserVO返回前端)
  • 统一异常处理(@RestControllerAdvice)
  • 统一响应格式(Result

常见追问

  • Controller层可以直接调用Dao吗? → 不可以,必须通过Service层,否则事务控制和业务逻辑无法统一管理
  • 事务为什么放在Service层? → 业务逻辑可能调用多个Dao,事务边界应覆盖整个业务操作
  • 三层架构和MVC模式的区别? → MVC是表现层模式(Model-View-Controller),三层架构是整体架构(包含业务层和数据层)