一、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
注解定义的路径不匹配 - 常见错误包括:大小写不一致(如
/userinfo
vs/userinfo
)、缺少或多余斜杠(如/api/user
vs/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.xml
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"> <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; } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论