Spring MVC的三层架构使用及解析
作者:祈祷苍天赐我java之术
一、Spring MVC 三层架构概述
在传统的 Java Web 开发中(如 Servlet+JSP),代码往往混杂在一起:数据处理、页面交互、业务逻辑全部写在 Servlet 中,导致项目维护困难、扩展性差。
这种开发模式存在以下典型问题:
- 单个Servlet文件可能包含数百行代码,混合处理业务逻辑和视图渲染
- 数据库操作直接嵌入业务逻辑中,难以更换数据源
- 代码复用率低,相似功能需要重复实现
- 单元测试困难,各功能模块耦合度过高
Spring MVC 的三层架构正是为解决这些问题而生,通过职责拆分,将系统分为三个核心层级,每层专注于特定功能,既降低了耦合度,又提升了代码的可复用性和可维护性。
这种分层架构模式借鉴了企业级应用开发的成熟经验,并针对Web应用场景进行了优化。
1.1 三层架构的核心定义
Spring MVC 的三层架构并非独立存在,而是相互协作、自上而下的调用关系,具体包括:
表现层(Presentation Layer)
- 直接与用户交互,负责接收请求、返回响应
主要功能包括:
- 接收HTTP请求参数并进行基本校验
- 调用Service层处理业务逻辑
- 返回响应(页面渲染或JSON数据)
- 核心组件:Spring MVC的Controller
典型实现示例:
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
业务逻辑层(Business Logic Layer,简称Service层)
- 处理核心业务逻辑的中枢
主要职责包括:
- 复杂业务规则的实现
- 数据有效性校验
- 事务管理(通过@Transactional注解)
- 异常处理
- 组合多个数据操作完成业务功能
典型实现示例:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Transactional
@Override
public User createUser(UserDto userDto) {
// 业务校验
if(userMapper.existsByUsername(userDto.getUsername())) {
throw new BusinessException("用户名已存在");
}
// 数据转换
User user = new User();
BeanUtils.copyProperties(userDto, user);
// 持久化操作
userMapper.insert(user);
return user;
}
}
持久层(Persistence Layer)
- 专注于数据持久化操作
主要特点:
- 只关心"如何访问数据",不关心业务逻辑
- 提供标准的CRUD操作方法
- 支持多种持久化技术(JDBC、MyBatis、JPA等)
核心组件:
- DAO(Data Access Object)或Repository
- MyBatis的Mapper接口
典型实现示例:
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO users(username, password) VALUES(#{username}, #{password})")
void insert(User user);
}
1.2 三层架构的调用流程
请求接收阶段
- 用户通过浏览器访问
/users/123 - HTTP请求被Spring MVC的前端控制器DispatcherServlet拦截
- DispatcherServlet查找所有已注册的HandlerMapping
请求路由阶段
- HandlerMapping根据URL路径
/users/123匹配到UserController的getUser方法 - 将路径变量
123解析为方法参数id
业务处理阶段
- Controller调用UserService的getUserById方法
Service层可能执行以下操作:
- 参数校验(如ID有效性)
- 业务规则判断(如权限检查)
- 调用持久层获取数据
数据访问阶段
- Service调用UserMapper的selectById方法
- MyBatis执行SQL:
SELECT * FROM users WHERE id = 123 - 将查询结果映射为User对象返回
响应返回阶段
- 查询结果沿调用链返回:Mapper → Service → Controller
- Controller将User对象转换为JSON格式
- DispatcherServlet将响应写入HttpServletResponse
视图渲染阶段(可选)
如果返回的是视图(如JSP):
- DispatcherServlet将ModelAndView交给ViewResolver
- ViewResolver解析视图名称,定位具体的JSP文件
- 视图引擎渲染JSP,生成HTML响应
三层架构交互时序图示例:
Client → DispatcherServlet → Controller → Service → Mapper → DB
↑ |
| ↓
Client ← DispatcherServlet ← Controller ← Service ← Mapper这种分层架构使得系统各部分的职责更加清晰,便于团队协作开发、单元测试和后期维护。例如:
- 前端开发人员只需要关注Controller层的接口定义
- 业务分析师可以基于Service层的代码理解业务规则
- DBA可以优化Mapper层的SQL语句而不影响业务逻辑
二、各层详细拆解
2.1 表现层(Controller 层):用户交互的 "入口"
表现层是 Spring MVC 框架中与用户直接交互的层级,作为系统的"门面",负责处理 HTTP 请求和响应。它的核心组件是 Controller 类,主要职责包括:
- 接收客户端请求(解析请求参数、请求头等)
- 进行基础参数校验
- 调用 Service 层处理业务逻辑
- 组装响应数据并返回给客户端
- 处理异常情况并返回友好错误信息
2.1.1 核心组件与注解详解
@Controller
- 作用:标识一个类作为 Spring MVC 的控制器
- 实现原理:Spring 会在启动时扫描带有该注解的类,并将其注册为 Spring Bean
- 配套机制:配合组件扫描注解 @ComponentScan 使用
示例:
@Controller
public class HomeController {
// 控制器方法...
}
@RequestMapping
核心功能:
- 路径映射:将 HTTP 请求映射到控制器方法
- 请求方法限定:通过 method 属性指定处理的 HTTP 方法
- 参数匹配:通过 params 属性匹配特定请求参数
- 头部匹配:通过 headers 属性匹配特定请求头
高级用法:
- 通配符支持:如 "/user/*" 可匹配 "/user/123" 等路径
- 类级别与方法级别组合:类级别定义基础路径,方法级别定义具体路径
- 媒体类型限定:通过 produces/consumes 指定处理的内容类型
示例:
@RequestMapping(value = "/products", method = RequestMethod.GET)
public String listProducts() {...}
@GetMapping/@PostMapping
优点:
- 代码更简洁:相比 @RequestMapping(method = RequestMethod.GET) 更易读
- 语义更明确:直接表明处理的 HTTP 方法
- 支持继承:可以组合使用 @RequestMapping 的类级别注解
示例对比:
// 传统方式
@RequestMapping(value = "/user", method = RequestMethod.GET)
// 简化方式
@GetMapping("/user")
@RequestParam
主要参数:
- name/value:指定绑定的请求参数名称
- required:是否为必须参数(默认 true)
- defaultValue:参数默认值
使用场景:
- 处理查询参数:如 ?page=1&size=10
- 处理表单数据:如 application/x-www-form-urlencoded
示例:
@GetMapping("/search")
public String search(@RequestParam(name = "keyword", required = false, defaultValue = "") String keyword) {...}
@PathVariable
特点:
- 用于 RESTful 风格的 URL 参数获取
- 支持正则表达式匹配路径变量
- 可配合 @RequestMapping 的通配符使用
示例:
@GetMapping("/users/{userId}/orders/{orderId}")
public String getOrder(@PathVariable Long userId, @PathVariable String orderId) {...}
@ResponseBody
工作机制:
- 通过 HttpMessageConverter 将返回值转换为指定格式
- 常用转换器:MappingJackson2HttpMessageConverter(JSON)
- 可自定义转换器处理特殊格式
典型应用:
- 前后端分离架构中的 API 接口
- AJAX 请求响应
- 移动端接口开发
示例:
@ResponseBody
@GetMapping("/api/user/{id}")
public User getUser(@PathVariable Long id) {...}
@RestController
组合优势:
- 减少样板代码:无需在每个方法上添加 @ResponseBody
- 语义更清晰:明确表示该类是纯 API 控制器
- 自动配置:默认启用 JSON 序列化
实现原理:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {...}
2.1.2 示例:一个完整的 Controller 实现
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
import java.util.List;
@Controller
@RequestMapping("/user")
@Validated // 启用方法参数验证
public class UserController {
private final UserService userService;
// 推荐使用构造器注入
public UserController(UserService userService) {
this.userService = userService;
}
/**
* 用户列表页面
* @param page 页码(从1开始)
* @param size 每页条数
* @param model 视图模型
* @return 视图名称
*/
@GetMapping("/list")
public String listUsers(
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(defaultValue = "10") @Min(1) int size,
Model model) {
PageInfo<User> pageInfo = userService.getUsersByPage(page, size);
model.addAttribute("pageInfo", pageInfo);
return "user/list";
}
/**
* 获取用户详情(REST API)
* @param userId 用户ID
* @return 统一响应结果
*/
@GetMapping("/{userId}")
@ResponseBody
public Result<User> getUserDetail(@PathVariable @Min(1) Long userId) {
User user = userService.getUserById(userId);
return Result.success(user);
}
/**
* 创建新用户
* @param userDTO 用户数据
* @return 创建结果
*/
@PostMapping
@ResponseBody
public Result<Long> createUser(@RequestBody @Valid UserDTO userDTO) {
Long userId = userService.createUser(userDTO);
return Result.success(userId);
}
}
2.1.3 最佳实践与注意事项
职责分离原则
Controller 应保持"瘦身",仅处理:
- 请求/响应转换
- 基础参数验证
- 异常捕获
所有业务逻辑应委托给 Service 层
参数校验建议
使用 JSR-380 规范注解:
- @NotNull/@NotEmpty/@NotBlank
- @Size(min=, max=)
- @Pattern(regexp=)
- @Min/@Max
分组校验:通过 groups 属性实现不同场景的校验规则
自定义校验:实现 ConstraintValidator 接口
统一响应格式
推荐结构:
public class Result<T> {
private int code; // 状态码
private String msg; // 消息
private T data; // 数据
private long timestamp = System.currentTimeMillis();
// 构造方法、静态工厂方法等...
}
使用示例:
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
异常处理
推荐使用 @ControllerAdvice 统一处理异常
示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result<?> handleBusinessException(BusinessException e) {
return Result.fail(e.getErrorCode(), e.getMessage());
}
}
性能考虑
避免在 Controller 中进行:
- 复杂计算
- 数据库操作
- 耗时 I/O 操作
使用异步处理:@Async、DeferredResult 等
安全建议
- 对敏感参数进行过滤
- 重要操作添加权限校验
- 防止 CSRF 攻击
- 输入参数进行 XSS 防护
测试建议
- 使用 MockMvc 进行单元测试
测试用例应覆盖:
- 正常流程
- 参数校验失败情况
- 异常情况处理
- 边界条件
2.2 业务逻辑层(Service 层):系统的 "大脑"
Service 层是整个系统的核心业务处理中枢,负责实现业务规则、处理事务、协调多个持久层操作,是表现层与持久层之间的 "桥梁"。
它相当于应用系统的"大脑",负责处理复杂的业务逻辑,确保业务流程的正确性和数据一致性。
2.2.1 核心组件与注解
@Service 注解
- 作用:标记一个类为 Service 层组件,Spring 会自动扫描并注册为 Bean,供 Controller 层注入
- 使用场景:所有业务逻辑处理类都应该使用此注解
示例:
@Service
public class OrderServiceImpl implements OrderService {...}
@Transactional 注解
- 作用:声明事务管理,可用于类或方法上,指定事务的各种属性
常用参数:
propagation:事务传播行为(如REQUIRED, REQUIRES_NEW)isolation:事务隔离级别(如DEFAULT, READ_COMMITTED)rollbackFor:指定哪些异常需要回滚timeout:事务超时时间
示例:
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = 30,
rollbackFor = Exception.class)
接口与实现类设计
优势:
- 面向接口编程,便于后续扩展
- 方便进行单元测试(可mock接口)
- 实现多态特性
典型结构:
├── service │ ├── UserService.java // 接口 │ └── impl │ └── UserServiceImpl.java // 实现类
2.2.2 示例:Service 接口与实现类
1. Service接口设计
/**
* 用户服务接口
* 定义业务契约,不包含具体实现
*/
public interface UserService {
/**
* 查询所有用户
* @return 用户列表(可能为空列表)
*/
List<User> getAllUsers();
/**
* 根据ID查询用户
* @param userId 用户ID
* @return 用户实体
* @throws BusinessException 当ID无效时抛出
*/
User getUserById(Integer userId) throws BusinessException;
/**
* 新增用户
* @param user 用户实体
* @return 操作是否成功
* @throws BusinessException 当用户名已存在等业务异常时抛出
*/
boolean addUser(User user) throws BusinessException;
/**
* 批量导入用户
* @param users 用户列表
* @return 成功导入的数量
*/
@Transactional
int batchImportUsers(List<User> users);
}
2. Service实现类详解
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* 用户服务实现类
* 实现具体的业务逻辑
*/
@Service // 标记为Service组件
public class UserServiceImpl implements UserService {
// 使用@Resource或@Autowired注入持久层组件
@Resource
private UserMapper userMapper;
@Resource
private RoleService roleService;
@Resource
private LogService logService;
// 简单查询操作通常不需要事务
@Override
public List<User> getAllUsers() {
// 直接调用Mapper层方法获取数据
// 可添加缓存逻辑提升性能
return userMapper.selectAll();
}
@Override
public User getUserById(Integer userId) throws BusinessException {
// 业务校验
if (userId == null || userId <= 0) {
throw new BusinessException(ErrorCode.INVALID_USER_ID, "用户ID无效");
}
// 查询用户
User user = userMapper.selectById(userId);
// 业务处理:如果用户不存在
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND, "用户不存在");
}
return user;
}
// 需要事务管理的业务方法
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addUser(User user) throws BusinessException {
// 1. 参数校验
if (user == null || StringUtils.isEmpty(user.getUsername())) {
throw new BusinessException(ErrorCode.INVALID_PARAM, "用户信息不完整");
}
// 2. 业务校验(用户名唯一性检查)
User existingUser = userMapper.selectByUsername(user.getUsername());
if (existingUser != null) {
throw new BusinessException(ErrorCode.USERNAME_EXISTS, "用户名已存在");
}
// 3. 设置默认值等业务处理
user.setCreateTime(new Date());
user.setStatus(1); // 默认激活状态
// 4. 调用Mapper层保存数据
int rows = userMapper.insert(user);
// 5. 关联操作(如分配默认角色)
roleService.assignDefaultRole(user.getId());
// 6. 记录操作日志(异步处理)
logService.asyncRecordLog("USER_ADD", "新增用户:" + user.getUsername());
return rows > 0;
}
@Override
@Transactional
public int batchImportUsers(List<User> users) {
int successCount = 0;
for (User user : users) {
try {
if (addUser(user)) {
successCount++;
}
} catch (BusinessException e) {
// 记录导入失败的用户
log.warn("导入用户失败: {}", user.getUsername(), e);
}
}
return successCount;
}
}
2.2.3 开发注意事项
职责划分原则
- Service层应专注于业务逻辑处理
- 所有数据库操作必须通过Mapper/DAO层完成
- 避免在Service层直接编写SQL语句
事务管理最佳实践
- 事务注解应加在Service层而非Controller层
- 默认情况下只对RuntimeException回滚,建议明确指定
rollbackFor - 只读操作使用
@Transactional(readOnly = true)提升性能 - 避免在同一个类中自调用事务方法(因代理机制会失效)
异常处理规范
// 自定义业务异常示例
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// getter方法...
}
- 使用自定义业务异常替代RuntimeException
- 不同业务错误定义不同的错误码
- 在Controller层统一处理业务异常
性能优化建议
- 复杂查询考虑添加缓存
- 批量操作使用批量处理方法
- 耗时操作考虑异步处理
测试考量
- Service层应易于单元测试
- 使用Mock对象隔离依赖
- 测试应覆盖各种业务场景和异常分支
典型业务场景处理
- 分布式事务:对于跨服务调用,考虑使用Seata等分布式事务解决方案
- 幂等性处理:对于支付等关键业务,需要实现幂等性控制
- 业务流水号:重要业务操作应生成唯一业务流水号便于追踪
2.3 持久层(Mapper/DAO 层):数据的 "搬运工"
持久层作为应用程序与数据库之间的桥梁,专注于数据的持久化操作。其主要职责包括:
- 执行基本的 CRUD 操作(Create/Read/Update/Delete)
- 处理数据库事务
- 实现数据缓存(可选)
- 进行数据校验(基础级别)
与业务层不同,持久层不关心业务逻辑,只关注"数据如何存储和获取"。在现代 Java Web 开发中,MyBatis 已成为最流行的持久层框架之一,相比传统的 JDBC 和 DAO 模式,它具有以下优势:
- 简化了数据库操作
- 提供自动的对象关系映射(ORM)
- 支持动态SQL
- 具有更好的性能
2.3.1 核心组件与配置
1. Mapper 接口
Mapper 接口是 MyBatis 的核心概念,它替代了传统的 DAO 接口。与 DAO 不同:
- 不需要编写实现类
- MyBatis 通过动态代理技术自动生成实现
- 方法签名直接对应 SQL 操作
// 典型 Mapper 接口定义
@Mapper
public interface UserMapper {
// 查询方法
User selectById(Long id);
// 插入方法
int insert(User user);
// 更新方法
int update(User user);
// 删除方法
int deleteById(Long id);
}
2. 关键注解
@Mapper:标记接口为 MyBatis Mapper 接口@MapperScan:在 Spring Boot 启动类或配置类上使用,指定 Mapper 接口的扫描路径@Param:用于多参数方法,指定参数名称
3. XML 映射文件
XML 文件是 SQL 语句的主要存放位置,通常包含:
- 结果映射定义(
<resultMap>) - SQL 查询语句(
<select>) - 插入语句(
<insert>) - 更新语句(
<update>) - 删除语句(
<delete>)
2.3.2 示例详解
1. Mapper 接口增强版
@Mapper
public interface UserMapper {
// 基础CRUD操作
List<User> selectAll();
User selectById(@Param("id") Long id);
int insert(User user);
int update(User user);
int deleteById(@Param("id") Long id);
// 分页查询
List<User> selectByPage(@Param("offset") int offset,
@Param("pageSize") int pageSize);
// 条件查询
List<User> selectByCondition(@Param("condition") UserQueryCondition condition);
// 批量操作
int batchInsert(@Param("users") List<User> users);
int batchUpdate(@Param("users") List<User> users);
}
2. 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.mapper.UserMapper">
<!-- 高级结果映射 -->
<resultMap id="UserDetailResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="create_time" property="createTime"
jdbcType="TIMESTAMP"/>
<!-- 关联映射 -->
<association property="department" javaType="Department">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
</resultMap>
<!-- 动态SQL查询 -->
<select id="selectByCondition" resultMap="UserDetailResultMap">
SELECT u.*, d.id as dept_id, d.name as dept_name
FROM user u
LEFT JOIN department d ON u.dept_id = d.id
<where>
<if test="condition.username != null">
AND u.username LIKE CONCAT('%', #{condition.username}, '%')
</if>
<if test="condition.email != null">
AND u.email = #{condition.email}
</if>
<if test="condition.createTimeStart != null">
AND u.create_time >= #{condition.createTimeStart}
</if>
</where>
ORDER BY u.create_time DESC
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user (username, email, create_time)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.createTime})
</foreach>
</insert>
</mapper>
2.3.3 高级特性与最佳实践
动态SQL:
- 使用
<if>,<choose>,<when>,<otherwise>标签实现条件判断 <where>标签自动处理WHERE子句<set>标签用于UPDATE语句
关联查询:
- 一对一:
<association> - 一对多:
<collection> - 延迟加载:配置
lazyLoadingEnabled=true
性能优化:
- 使用二级缓存(需谨慎)
- 批量操作代替单条操作
- 合理使用延迟加载
事务管理:
- 在Service层使用
@Transactional注解 - 配置适当的事务传播行为
分页实现:
- 使用PageHelper插件
- 手动实现分页(limit offset)
2.3.4 常见问题解决方案
参数映射问题:
- 简单类型参数:直接使用
#{param} - 对象参数:使用
#{propertyName} - Map参数:使用
#{key} - 多参数:必须使用
@Param注解
结果映射问题:
- 字段名与属性名不一致时使用
<resultMap> - 复杂类型使用嵌套映射
- 使用
<constructor>进行构造函数映射
SQL注入防护:
- 永远使用
#{}而不是${}进行参数绑定 - 对用户输入进行严格校验
性能监控:
- 配置SQL日志输出
- 使用MyBatis-Plus的性能分析插件
- 监控慢SQL
通过以上规范和最佳实践,可以构建出高效、可维护的持久层,为应用程序提供可靠的数据访问支持。
三、基于三层架构搭建 Spring MVC 项目
3.1 项目结构(Maven)
一个标准的 Spring MVC 三层架构项目结构如下(以 IntelliJ IDEA 为例):
src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ ├── controller/ # 表现层(Controller) │ │ │ └── UserController.java │ │ ├── service/ # 业务逻辑层(Service) │ │ │ ├── UserService.java # 接口 │ │ │ └── impl/ │ │ │ └── UserServiceImpl.java # 实现类 │ │ ├── mapper/ # 持久层(Mapper) │ │ │ └── UserMapper.java │ │ ├── entity/ # 实体类(对应数据库表) │ │ │ └── User.java │ │ ├── exception/ # 自定义异常 │ │ │ └── BusinessException.java │ │ ├── config/ # Spring配置类 │ │ │ └── SpringMvcConfig.java │ │ └── util/ # 工具类 │ │ └── Result.java # 统一响应封装 │ ├── resources/ │ │ ├── mapper/ # Mapper XML文件 │ │ │ └── UserMapper.xml │ │ ├── application.properties # 全局配置(数据库、MyBatis等) │ │ └── static/ # 静态资源(CSS、JS、图片) │ └── webapp/ # Web资源 │ ├── WEB-INF/ │ │ ├── views/ # 视图页面(JSP/HTML) │ │ │ └── userList.jsp │ │ └── web.xml # Web配置(可选,Spring Boot可省略) └── pom.xml # Maven依赖
各层职责说明
- 表现层(Controller):接收HTTP请求,参数校验,调用Service并返回响应
- 业务逻辑层(Service):处理业务逻辑,事务控制,调用Mapper层
- 持久层(Mapper):数据库操作,与MyBatis框架交互
- 实体层(Entity):定义数据模型,与数据库表映射
- 配置层(Config):Spring相关配置,如MVC配置、组件扫描等
3.2 核心依赖(pom.xml)
<!-- Spring MVC核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.28</version>
</dependency>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- MyBatis整合Spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.2</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- 数据库连接池(HikariCP) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
<!-- JSP依赖(若使用JSP视图) -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- lombok(简化实体类代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<optional>true</optional>
</dependency>
<!-- JSON处理(Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
3.3 核心配置(application.properties)
# 数据库配置(HikariCP) spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/spring_mvc_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=123456 # HikariCP连接池配置 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=300000 # MyBatis配置 # Mapper XML文件路径 mybatis.mapper-locations=classpath:mapper/*.xml # 实体类别名扫描包(简化XML中的type配置) mybatis.type-aliases-package=com.example.entity # 开启MyBatis日志(便于调试SQL) mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # Spring MVC视图解析器配置(JSP) spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp # 静态资源访问配置(CSS/JS/图片) spring.mvc.static-path-pattern=/static/**
3.4 配置类(SpringMvcConfig.java)
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 标记为配置类
@EnableWebMvc // 开启Spring MVC功能
@ComponentScan("com.example") // 扫描组件(Controller/Service等)
@MapperScan("com.example.mapper") // 扫描MyBatis Mapper接口
public class SpringMvcConfig implements WebMvcConfigurer {
// 配置视图解析器(JSP)
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/views/", ".jsp");
}
// 配置静态资源访问(避免静态资源被DispatcherServlet拦截)
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
// 配置JSON消息转换器(自动注册Jackson)
// Spring会自动注册MappingJackson2HttpMessageConverter
}
3.5 功能测试:验证三层架构流程
3.5.1 数据库表准备(MySQL)
CREATE DATABASE IF NOT EXISTS spring_mvc_db;
USE spring_mvc_db;
CREATE TABLE IF NOT EXISTS `user` (
`user_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
`nickname` VARCHAR(50) DEFAULT '' COMMENT '昵称',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用户表';
-- 初始化测试数据
INSERT INTO `user`(username, nickname) VALUES
('admin', '管理员'),
('user1', '普通用户1'),
('user2', '普通用户2');
3.5.2 接口测试(Postman)
1. 查询所有用户(GET)
- 请求地址:
http://localhost:8080/spring-mvc-demo/user/list - 请求方式:GET
预期结果:
- 返回userList.jsp页面
- 页面中展示数据库中的用户列表
- 页面包含用户ID、用户名、昵称和创建时间信息
2. 根据ID查询用户(GET)
- 请求地址:
http://localhost:8080/spring-mvc-demo/user/1 - 请求方式:GET
- 请求头:
Accept: application/json
预期结果:
{
"code": 200,
"msg": "success",
"data": {
"userId": 1,
"username": "admin",
"nickname": "管理员",
"createTime": "2024-05-20 10:30:00"
}
}
3. 新增用户(POST)
- 请求地址:
http://localhost:8080/spring-mvc-demo/user/add - 请求方式:POST
- 请求头:
Content-Type: application/json
请求体:
{
"username": "newuser",
"nickname": "新用户"
}
预期结果:
{
"code": 200,
"msg": "添加成功",
"data": null
}
四、三层架构常见问题与解决方案
4.1 表现层常见问题
问题 1:Controller 无法接收请求(404 错误)
可能原因分析:
请求路径映射问题:
- 前端发送的请求路径与后端
@RequestMapping注解定义的路径不匹配 - 常见错误包括:大小写不一致(如
/userInfovs/userinfo)、缺少或多余斜杠(如/api/uservs/api/user/) - 特殊字符编码问题(如空格应编码为
%20)
Controller 配置问题:
- 类未添加
@Controller或@RestController注解
类未被Spring组件扫描到,可能原因:
@ComponentScan配置的包路径不正确- Controller类所在的包不在主启动类的同级或子级目录下
- 使用了错误的扫描注解(如
@ServletComponentScan)
DispatcherServlet 配置问题:
web.xml中url-pattern配置为/*会拦截所有请求,包括静态资源和JSP- 未正确配置静态资源处理器
- 缺少必要的Servlet映射配置
解决方案及实施步骤:
路径核对:
- 使用Postman等工具直接测试Controller接口
- 在Controller方法中添加日志输出,确认请求是否到达
- 检查是否有
@PathVariable参数但请求未提供
注解检查:
@RestController // 或 @Controller
@RequestMapping("/api/users")
public class UserController {
// 确保方法上有@RequestMapping或其派生注解
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// ...
}
}
DispatcherServlet 配置:
推荐配置:
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern> <!-- 而不是 /* -->
</servlet-mapping>
Spring Boot中配置静态资源:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
问题 2:参数绑定失败(400 错误)
可能原因深度分析:
类型不匹配:
- 前端传递字符串"123",但后端使用
Integer接收 - 集合类型参数未正确格式化(如
List<String>需要?names=aa&names=bb)
日期格式化:
常见日期格式冲突:
- 前端:
"2024-05-20" - 后端期望:
"2024/05/20"或时间戳
时区问题(如UTC与本地时区差异)
命名不一致:
- 驼峰命名与下划线命名转换问题
- 嵌套对象属性访问(如
user.address.city)
完整解决方案:
基础类型处理:
@GetMapping("/detail")
public Result detail(@RequestParam("user_id") Integer userId) {
// 明确指定参数名
}
日期处理最佳实践:
@PostMapping("/schedule")
public Result createSchedule(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestBody @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") Date endTime) {
// 分别处理URL参数和JSON体中的日期
}
复杂对象绑定:
// 实体类
public class User {
@JsonProperty("user_id") // 处理JSON字段名
private Long userId;
@RequestParam("user_name") // 处理URL参数名
private String userName;
}
// Controller
@PostMapping("/update")
public Result updateUser(@Valid User user) {
// 支持混合绑定方式
}
补充技巧:
全局日期格式配置(application.yml):
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
自定义参数解析器:实现HandlerMethodArgumentResolver处理特殊参数类型
4.2 业务逻辑层常见问题
问题 1:事务不生效(数据提交后未回滚)
详细原因分析:
注解位置问题:
@Transactional注解在private/protected方法上无效- 注解被同类中的非事务方法调用
异常处理问题:
- 捕获了异常但未重新抛出
- 抛出的异常类型不是
RuntimeException - 自定义异常未继承
RuntimeException
代理机制问题:
- 使用
this.method()调用导致绕过Spring代理 - 特殊场景:异步方法、synchronized方法
完整解决方案:
正确的事务配置:
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void createOrder(OrderDTO dto) throws BusinessException {
// 业务代码
}
}
解决自调用问题方案:
方案1:重构代码结构
方案2:通过AopContext获取代理对象
((OrderService) AopContext.currentProxy()).internalMethod();
方案3:使用@Autowired注入自身(需配合@Lazy)
事务调试技巧:
开启事务日志:
logging.level.org.springframework.transaction.interceptor=DEBUG logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
问题 2:Service 层循环依赖(BeanCreationException)
典型场景分析:
直接循环依赖:
@Service
class AService { @Autowired BService b; }
@Service
class BService { @Autowired AService a; }
间接循环依赖: A → B → C → A
构造器注入导致的不可解循环:
@Service
class AService {
private final BService b;
public AService(BService b) { this.b = b; }
}
进阶解决方案:
架构层面重构:
- 提取公共逻辑到新的
CommonService - 使用门面模式封装相关服务
技术解决方案:
// 方案1:使用setter注入 + @Lazy
@Service
class AService {
private BService b;
@Autowired
public void setB(@Lazy BService b) { this.b = b; }
}
// 方案2:使用ApplicationContext
@Service
class BService implements ApplicationContextAware {
private ApplicationContext context;
public void someMethod() {
AService a = context.getBean(AService.class);
}
}
Spring Boot 2.6+ 处理:
配置允许循环引用(不推荐):
spring.main.allow-circular-references=true
4.3 持久层常见问题
问题 1:Mapper 接口与 XML 不匹配(BindingException)
完整排查清单:
路径匹配问题:
- 检查XML文件是否在
resources目录的对应包路径下 - Maven项目注意
src/main/resources和src/main/java的目录结构一致性
ID匹配问题:
- 方法重载导致混淆
- 泛型方法特殊处理
配置问题:
- MyBatis配置文件中
<mappers>配置错误 - Spring Boot中
mybatis.mapper-locations配置不完整
详细解决方案:
项目结构规范:
src/main/java
└─com/example/mapper
UserMapper.java
src/main/resources
└─com/example/mapper
UserMapper.xmlXML配置示例:
<?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.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
Spring Boot配置:
# 确保扫描到Mapper接口 mybatis.mapper-locations=classpath*:mapper/**/*.xml # 开启MyBatis日志 logging.level.com.example.mapper=DEBUG
问题 2:SQL 执行报错(SQLSyntaxErrorException)
深度排查指南:
SQL语法问题:
- 数据库方言差异(MySQL vs Oracle)
- 保留关键字冲突(如使用
order作为表名) - 分页语法差异
参数处理问题:
#{}和${}混用导致的语法错误- 参数类型不匹配(如字符串参数未加引号)
数据库连接问题:
- 连接池配置不当
- 数据库版本不兼容
- SSL连接配置错误
专业解决方案:
SQL调试技巧:
# 打印完整执行的SQL(包括参数) mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
参数处理规范:
<!-- 正确用法 -->
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE username = #{name}
AND create_time > #{date,jdbcType=TIMESTAMP}
</select>
<!-- 动态表名用法(需确保安全) -->
<select id="selectByTable" resultType="map">
SELECT * FROM ${tableName}
WHERE id = #{id}
</select>
数据库连接配置:
# 完整连接配置示例 spring.datasource.url=jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
高级技巧:
使用MyBatis-Plus的SQL注入器防止SQL错误
配置SQL执行超时时间:
<select id="complexQuery" timeout="30">
<!-- 复杂查询 -->
</select>
五、三层架构优化建议
5.1 代码复用与解耦
统一异常处理
使用Spring提供的@ControllerAdvice+@ExceptionHandler实现全局异常处理机制,可以避免在各个Controller中重复编写try-catch块。这种集中式异常处理方式具有以下优势:
- 减少重复代码,提高代码整洁度
- 统一异常响应格式,便于前端处理
- 可灵活分类处理不同类型的异常
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
* @param e 业务异常对象
* @return 统一响应结果
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result<?> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage(), e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理系统异常
* @param e 异常对象
* @return 统一响应结果
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<?> handleException(Exception e) {
log.error("系统异常:", e);
return Result.fail(500, "系统繁忙,请稍后再试");
}
}
通用Service/DAO层设计
通过抽取通用CRUD操作方法到基类中,可以大幅减少重复代码。这种设计模式特别适合具有大量相似CRUD操作的系统:
1.定义通用Mapper接口
public interface BaseMapper<T> {
int insert(T entity); // 插入单条记录
int deleteById(@Param("id") Serializable id); // 根据主键删除
int updateById(@Param("entity") T entity); // 根据主键更新
T selectById(@Param("id") Serializable id); // 根据主键查询
List<T> selectList(@Param("entity") T entity); // 条件查询列表
Page<T> selectPage(Page<T> page, @Param("entity") T entity); // 分页查询
}
2.业务Mapper继承通用接口
public interface UserMapper extends BaseMapper<User> {
// 自定义方法
@Select("SELECT * FROM user WHERE username = #{username}")
User selectByUsername(@Param("username") String username);
// 复杂查询示例
@Select("SELECT u.* FROM user u JOIN department d ON u.dept_id = d.id WHERE d.name = #{deptName}")
List<User> selectByDepartmentName(@Param("deptName") String deptName);
}
3.通用Service实现
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
@Autowired
protected M baseMapper;
@Override
public boolean save(T entity) {
return baseMapper.insert(entity) > 0;
}
@Override
public boolean updateById(T entity) {
return baseMapper.updateById(entity) > 0;
}
// 其他通用方法实现...
}
5.2 性能优化
缓存设计
在Service层合理使用缓存可以显著提升系统性能,特别是对于读多写少的场景:
缓存使用场景:
- 高频访问的配置数据
- 用户基础信息
- 商品详情等静态数据
- 计算结果缓存
缓存实现示例:
@Service
public class UserServiceImpl implements UserService {
@Resource
private RedisTemplate<String, User> redisTemplate;
@Resource
private UserMapper userMapper;
// 缓存key前缀
private static final String USER_CACHE_PREFIX = "user:id:";
// 缓存过期时间(小时)
private static final long CACHE_EXPIRE_HOURS = 1;
@Override
@Transactional(readOnly = true)
public User getUserById(Integer userId) {
String key = USER_CACHE_PREFIX + userId;
// 1. 先查缓存
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 2. 缓存未命中,查数据库
user = userMapper.selectById(userId);
if (user != null) {
// 3. 设置缓存
redisTemplate.opsForValue().set(
key,
user,
CACHE_EXPIRE_HOURS,
TimeUnit.HOURS
);
// 4. 异步更新用户访问记录
CompletableFuture.runAsync(() ->
updateUserAccessTime(userId)
);
}
return user;
}
// 缓存一致性处理
@Override
@CacheEvict(key = "#user.id", condition = "#user.id != null")
public boolean updateUser(User user) {
return userMapper.updateById(user) > 0;
}
}
批量操作优化
使用批量操作可以大幅减少数据库交互次数,提高性能:
1.MyBatis批量插入示例:
<!-- 批量插入用户 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user (username, password, nickname, create_time)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(#{item.username}, #{item.password}, #{item.nickname},
<choose>
<when test="item.createTime != null">#{item.createTime}</when>
<otherwise>NOW()</otherwise>
</choose>)
</foreach>
</insert>
2.批量更新示例:
@Transactional
public int batchUpdateUserStatus(List<Integer> userIds, Integer status) {
return userMapper.batchUpdateStatus(userIds, status);
}
<!-- XML映射 -->
<update id="batchUpdateStatus">
UPDATE user SET status = #{status} WHERE id IN
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
5.3 扩展性优化
接口版本控制
良好的版本控制策略可以保证系统平滑升级:
1.URL路径版本控制:
@RestController
@RequestMapping("/api/v1/user")
public class UserControllerV1 {
@GetMapping("/list")
public Result<List<User>> listUsers() {
// V1版本实现
}
}
@RestController
@RequestMapping("/api/v2/user")
public class UserControllerV2 {
@GetMapping("/list")
public Result<PageInfo<User>> listUsers() {
// V2版本实现,返回分页数据
}
}
2.请求头版本控制:
@GetMapping("/user/list")
public Result<?> listUsers(@RequestHeader("X-API-Version") String version) {
if ("2.0".equals(version)) {
// 新版本逻辑
} else {
// 默认版本逻辑
}
}
多数据源支持
使用Spring的AbstractRoutingDataSource实现动态数据源切换:
1.配置多数据源:
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
return dynamicDataSource;
}
}
2.动态数据源路由:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
依赖注入优化
使用构造器注入可以避免循环依赖问题,提高代码可测试性:
1.推荐做法:
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RoleService roleService;
@Autowired
public UserServiceImpl(UserMapper userMapper, RoleService roleService) {
this.userMapper = userMapper;
this.roleService = roleService;
}
// 业务方法...
}
2.使用Lombok简化代码:
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final RoleService roleService;
// 自动生成构造器,无需手动编写
}
3.循环依赖解决方案:
// 使用@Lazy注解解决循环依赖
@Service
public class OrderServiceImpl implements OrderService {
private final UserService userService;
public OrderServiceImpl(@Lazy UserService userService) {
this.userService = userService;
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
