
简介本资源是一份面向Java Web初学者与中级开发者的三层架构系统性学习文档聚焦企业级Web应用的结构设计与SSH框架整合实践。内容深入剖析表示层Struts实现MVC解耦、业务逻辑层Spring IoC/DI与AOP管理及数据访问层Hibernate ORM映射与HQL查询并结合Spring 2.5Struts 2.0Hibernate 3.2典型版本组合提供轻量级、高可维护的J2EE开发方案。资源为单个Word文档.doc格式全文约102KB涵盖框架原理、组件职责划分、集成机制图解及实际项目落地案例结构清晰、术语规范便于对照理解与课后复盘。目前已有2376人学习下载适合希望夯实分层思想、掌握SSH协同开发流程、提升代码解耦能力的开发者系统研读。1. Java Web 三层架构不是分包目录而是职责隔离的生存法则你有没有遇到过这样的项目改一个用户登录逻辑结果要动 JSP、Action、Service、DAO、Hibernate 映射文件、SQL 脚本最后连事务配置都得重测上线前一小时发现UserAction里硬编码了数据库连接池参数而UserService又直接 new 了一个UserDaoImpl——这不是代码是雷区地图。Java Web 三层架构表示层 / 业务逻辑层 / 数据访问层从来就不是教科书里“把类扔进 controller、service、dao 三个包”这么简单。它是一套经过十年以上企业级项目血洗验证的职责契约视图不碰 SQLService 不写 HTMLDAO 不管事务边界Spring 容器才是唯一能说“谁该依赖谁”的仲裁者。本文讲的 SSHStruts 2.0 Spring 2.5 Hibernate 3.2组合不是过时的古董堆砌而是把 MVC 解耦、IoC 控制流、ORM 映射这三根钢索拧成一股绳的实战范式。它解决的不是“能不能跑”而是“改需求时敢不敢删代码”——当你在struts-config.xml里删掉一个action却不用动applicationContext.xml里的 bean 定义当你把UserDAO接口换成 MyBatis 实现UserService一行代码都不用改这才是三层架构真正落地的信号。适合正在维护老系统、被耦合代码拖垮的中级 Java 工程师也适合刚从 Spring Boot 转回传统架构、搞不清ActionServlet和DispatcherServlet根本区别的新人。别急着建 Maven 模块先搞懂这三层之间谁向谁发号施令、谁对谁负最终责任。2. 表示层Struts 2.0 的 Controller 不是转发器而是请求生命周期的守门人Struts 2.0 的核心不是Action类而是FilterDispatcher或StrutsPrepareAndExecuteFilter拦截的整个 HTTP 请求生命周期。它强制你在struts.xml里声明“这个 URL 由谁处理、处理完跳哪、出错去哪”把流程控制权从 Java 代码里彻底剥离。这不是偷懒是防止UserAction.execute()里突然冒出new UserService().updatePassword(...)这种反模式。2.1 Struts 配置的本质用 XML 描述请求路由与结果映射struts.xml不是配置文件是请求契约说明书。以下是一个用户列表功能的标准配置package nameuser extendsstruts-default namespace/admin action namelistUser classcom.simon.oa.user.controller.UserAction methodlist result namesuccess/WEB-INF/jsp/user/list.jsp/result result nameerror/WEB-INF/jsp/error.jsp/result result nameinput/WEB-INF/jsp/user/input.jsp/result /action /package逻辑说明namespace/admin将所有 action 统一挂载到/admin/*路径下避免 URL 冲突class指向具体 Action 类但注意——这个类必须由 Spring 容器管理后文详述否则无法注入 Servicemethodlist表明调用UserAction.list()方法而非默认execute()三个result分别对应不同返回值success是正常流程error是业务异常如权限不足input是表单校验失败后回显页面。参数说明namelistUser是 action 的逻辑名客户端访问路径为/admin/listUser.action.action后缀由struts.action.extension配置默认启用result的name必须与 Action 方法return success;中的字符串严格匹配大小写敏感。2.2 Action 类的设计铁律零业务逻辑、零数据访问、只做三件事UserAction的唯一职责是接收请求参数 → 调用 Service → 返回结果字符串。它不该有Autowired注解Spring 2.5 不支持注解驱动靠 XML 注入也不该有sessionFactory字段。标准写法如下// com.simon.oa.user.controller.UserAction.java public class UserAction extends ActionSupport { private UserService userService; // 仅声明引用不实例化 private ListUser userList; // 用于页面展示的 VO 对象列表 private String userId; // 接收 URL 参数 ?userId123 // 必须提供 setter 方法供 Struts 框架通过反射注入参数 public void setUserId(String userId) { this.userId userId; } // 必须提供 getter 方法供 JSP 页面通过 ${userList} 访问 public ListUser getUserList() { return userList; } // 业务方法只调用 Service不处理任何业务规则 public String list() { try { this.userList userService.findAllUsers(); return SUCCESS; // 对应 struts.xml 中的 namesuccess } catch (Exception e) { addActionError(获取用户列表失败 e.getMessage()); return ERROR; // 对应 nameerror } } }逻辑说明extends ActionSupport提供了addActionError()、getText()等基础能力setUserId()是 Struts 自动调用的参数绑定入口URL 中?userId123会自动赋值给userId字段getUserList()是 JSP EL 表达式${userList}的访问入口Struts 会通过反射调用此方法获取数据list()方法内只做userService.findAllUsers()调用所有分页、权限校验、缓存逻辑必须下沉到 Service 层。参数说明SUCCESS和ERROR是ActionSupport中定义的静态常量值为success和error必须与struts.xml中result name...的 name 一致addActionError()的错误信息会自动传递给error.jsp页面无需手动request.setAttribute()。2.3 视图层JSP 不是模板引擎而是纯数据渲染器JSP 页面里禁止出现if (user.getRole().equals(ADMIN))这类业务判断。所有逻辑必须前置到 Action 或 Service。标准list.jsp应只做三件事引入 Struts 标签库、遍历userList、输出 HTML 表格。% taglib prefixs uri/struts-tags % html headtitle用户列表/title/head body h2用户管理/h2 s:if testuserList ! null userList.size() 0 table border1 trthID/thth姓名/thth邮箱/th/tr s:iterator valueuserList varuser tr tds:property value#user.id//td tds:property value#user.name//td tds:property value#user.email//td /tr /s:iterator /table /s:if s:else p暂无用户数据/p /s:else /body /html逻辑说明s:if和s:iterator是 Struts 2 的标签库test属性执行 OGNL 表达式#user.id中的#表示当前迭代对象s:property输出对象属性自动处理 null 安全整个页面没有 Java 脚本% %没有 SQL没有 if-else 业务分支。参数说明valueuserList中的userList是 Action 中getUserList()返回的 ListStruts 会将其放入 ValueStackvaruser将每次迭代的对象绑定到#user供后续标签使用s:else是s:if的配套标签不可省略。3. 业务逻辑层Spring 2.5 的 IoC 容器不是工厂而是对象关系的宪法Spring 2.5 的核心价值不在“简化开发”而在用 XML 配置强行规定对象之间的依赖契约。它让UserAction不能直接new UserServiceImpl()必须通过容器注入让UserService不能直接new UserDaoImpl()必须声明接口依赖。这种“不许你自由恋爱必须由组织介绍”的设计才是解耦的根基。3.1 applicationContext.xml用bean定义对象的出生证与婚姻证applicationContext.xml是 Spring 容器的宪法每个bean标签定义一个对象的创建方式、生命周期和依赖关系。以下是用户模块的核心配置!-- 1. 数据源DBCP 连接池 -- bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/oa_db?useUnicodetrueamp;characterEncodingUTF-8/ property nameusername valueroot/ property namepassword value123456/ property namemaxActive value20/ /bean !-- 2. SessionFactoryHibernate 的核心工厂 -- bean idsessionFactory classorg.springframework.orm.hibernate3.LocalSessionFactoryBean property namedataSource refdataSource/ property namemappingResources list valuecom/simon/oa/user/model/User.hbm.xml/value /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQLDialect/prop prop keyhibernate.hbm2ddl.autoupdate/prop prop keyhibernate.show_sqltrue/prop /props /property /bean !-- 3. DAO 实现类继承 HibernateDaoSupport -- bean iduserDao classcom.simon.oa.user.dao.hibernate.UserDaoImpl property namesessionFactory refsessionFactory/ /bean !-- 4. Service 接口实现类 -- bean iduserService classcom.simon.oa.user.service.impl.UserServiceImpl property nameuserDao refuserDao/ /bean !-- 5. Struts Action必须由 Spring 管理才能注入 Service -- bean iduserAction classcom.simon.oa.user.controller.UserAction scopeprototype property nameuserService refuserService/ /bean逻辑说明iddataSource定义数据源 Beandestroy-methodclose确保容器关闭时释放连接LocalSessionFactoryBean是 Spring 封装的 Hibernate 工厂mappingResources指定.hbm.xml映射文件路径注意斜杠方向userDaoBean 的sessionFactory属性通过refsessionFactory注入形成依赖链userService依赖userDaouserAction依赖userService最终形成Action → Service → DAO → SessionFactory → DataSource的完整链条scopeprototype是关键——Struts 每次请求都会创建新 Action 实例必须设为 prototype否则多线程下会共享状态。参数说明property namexxx中的name必须与目标类的 setter 方法名一致如setUserService(UserService userService)refxxx引用其他 Bean 的id非class名list和props是 Spring 的集合注入语法prop key...对应 Properties 文件的键值对。3.2 Service 接口与实现用接口隔离变化用实现专注逻辑UserService接口定义契约UserServiceImpl实现细节。这是 Spring IoC 能生效的前提——依赖必须面向接口而非实现类。// com.simon.oa.user.service.UserService.java public interface UserService { ListUser findAllUsers(); User findUserById(String id); void updateUser(User user); } // com.simon.oa.user.service.impl.UserServiceImpl.java public class UserServiceImpl implements UserService { private UserDao userDao; // 依赖 DAO 接口非实现类 public void setUserDao(UserDao userDao) { this.userDao userDao; } Override public ListUser findAllUsers() { // 业务逻辑可加日志、权限检查、缓存等横切关注点 System.out.println(【Service】开始查询所有用户); ListUser users userDao.findAll(); // 调用 DAO 接口方法 System.out.println(【Service】查询到 users.size() 个用户); return users; } Override public User findUserById(String id) { // 业务逻辑ID 校验、空值处理 if (id null || id.trim().length() 0) { throw new IllegalArgumentException(用户ID不能为空); } return userDao.findById(id); } }逻辑说明UserServiceImpl中setUserDao()是 Spring 注入入口userDao字段类型是接口UserDao而非UserDaoImplfindAllUsers()方法内只做日志和调用所有数据库操作委托给userDao.findAll()findUserById()做了 ID 校验这是 Service 层该承担的职责Action 只做参数接收DAO 只做数据存取。参数说明Override注解确保实现类方法与接口签名一致throw new IllegalArgumentException()是业务异常会被 Struts 捕获并跳转到error.jspSystem.out.println()是调试日志实际项目应替换为 Log4j。3.3 事务管理不是加个Transactional而是用 AOP 切面织入Spring 2.5 不支持注解式事务Transactional是 2.5 之后版本才引入必须用 AOP 配置。事务边界必须圈定在 Service 方法上DAO 层绝不开启事务。!-- 在 applicationContext.xml 中追加事务配置 -- bean idtransactionManager classorg.springframework.orm.hibernate3.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean aop:config aop:pointcut idserviceMethods expressionexecution(* com.simon.oa.user.service.impl.*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refserviceMethods/ /aop:config tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method namefind* read-onlytrue/ tx:method nameget* read-onlytrue/ tx:method name* propagationREQUIRED/ /tx:attributes /tx:advice逻辑说明aop:pointcut定义切点匹配com.simon.oa.user.service.impl包下所有类的所有方法aop:advisor将事务通知txAdvice织入切点tx:method指定事务规则find*和get*方法设为只读read-onlytrue提升性能其他方法*使用REQUIRED传播行为如果已有事务则加入否则新建。参数说明propagationREQUIRED是最常用行为确保updateUser()方法在事务中执行read-onlytrue会禁用 Hibernate 的脏检查减少数据库压力transaction-managertransactionManager引用上面定义的事务管理器 Bean。4. 数据访问层Hibernate 3.2 的 .hbm.xml 不是配置而是对象与表的婚书Hibernate 3.2 的核心是.hbm.xml映射文件它用 XML 声明 Java 类与数据库表的字段对应关系。这不是可选配置而是 ORM 的法律依据。一旦映射出错save()会静默失败load()会返回 null——没有报错只有沉默的崩溃。4.1 User.hbm.xml用class定义类与表的绑定用id定义主键策略User.hbm.xml必须与User.java同包且文件名严格匹配类名 .hbm.xml。以下是标准模板?xml version1.0 encodingUTF-8? !DOCTYPE hibernate-mapping PUBLIC -//Hibernate/Hibernate Mapping DTD 3.0//EN http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd hibernate-mapping packagecom.simon.oa.user.model class nameUser tablet_user catalogoa_db !-- 主键映射 -- id nameid columnuser_id typestring generator classassigned/ /id !-- 属性映射 -- property namename columnuser_name typestring length50 not-nulltrue/ property nameemail columnuser_email typestring length100 not-nullfalse/ property namecreateTime columncreate_time typetimestamp not-nulltrue/ /class /hibernate-mapping逻辑说明hibernate-mapping package...指定默认包class nameUser中的name是类名非全限定名tablet_user指定数据库表名catalogoa_db指定数据库名MySQL 中可省略id定义主键columnuser_id是数据库字段名typestring对应 Java 的String类型generator classassigned/表示主键由程序赋值如 UUID非数据库自增property映射普通字段length50对应数据库VARCHAR(50)not-nulltrue对应NOT NULL约束。参数说明nameid必须与User.java中的字段名private String id;严格一致columnuser_id必须与数据库字段名完全相同区分大小写typestring是 Hibernate 内置类型非 Java 类型常见值有string、integer、timestamp、booleannot-nulltrue会在save()时校验若为 null 则抛PropertyValueException。4.2 User.javaPOJO 必须满足 JavaBean 规范且无业务逻辑User.java是纯粹的数据载体必须有无参构造器、getter/setter 方法且字段类型与.hbm.xml中type一致。// com.simon.oa.user.model.User.java public class User implements Serializable { private static final long serialVersionUID 1L; private String id; private String name; private String email; private Date createTime; // 必须有无参构造器Hibernate 反射创建对象时调用 public User() {} // getter/setter 方法IDE 可自动生成 public String getId() { return id; } public void setId(String id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getEmail() { return email; } public void setEmail(String email) { this.email email; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime createTime; } }逻辑说明implements Serializable是必须的Hibernate 会序列化对象serialVersionUID防止类版本不兼容无参构造器public User() {}是 Hibernate 创建对象的入口所有字段必须有 public getter/setter且命名符合 JavaBean 规范如getId()对应id字段Date类型对应.hbm.xml中typetimestamp。参数说明private String id;的类型String必须与id中typestring匹配private Date createTime;的类型Date必须与property中typetimestamp匹配Hibernate 会自动转换serialVersionUID值可任意但建议固定以避免反序列化问题。4.3 DAO 接口与实现用 HibernateDaoSupport 封装模板用 getHibernateTemplate() 执行操作DAO 层的接口定义数据操作契约实现类继承HibernateDaoSupport复用 Hibernate 模板。// com.simon.oa.user.dao.UserDao.java public interface UserDao { ListUser findAll(); User findById(String id); void save(User user); void update(User user); void delete(User user); } // com.simon.oa.user.dao.hibernate.UserDaoImpl.java public class UserDaoImpl extends HibernateDaoSupport implements UserDao { Override public ListUser findAll() { // 使用 HibernateTemplate 查询所有 User return (ListUser) getHibernateTemplate().find(from User); } Override public User findById(String id) { // 根据主键查询HQL 中的 User 是类名非表名 return (User) getHibernateTemplate().get(User.class, id); } Override public void save(User user) { // 保存对象返回主键若 generator 为 assigned则返回 null getHibernateTemplate().save(user); } Override public void update(User user) { getHibernateTemplate().update(user); } Override public void delete(User user) { getHibernateTemplate().delete(user); } }逻辑说明HibernateDaoSupport提供getHibernateTemplate()方法封装了Session获取、事务管理、异常转换等模板逻辑find(from User)是 HQL 查询User是 Java 类名非数据库表名get(User.class, id)根据主键查询id是主键值save()和update()方法不返回主键generatorassigned时若需返回应改用saveOrUpdate()并配合GeneratedValue。参数说明find(from User)中的User必须与.hbm.xml中class nameUser一致get(User.class, id)的User.class是 Java 类id是主键值类型必须与id中type匹配getHibernateTemplate().save(user)会触发 INSERT SQLupdate(user)触发 UPDATE。5. 避坑指南SSH 项目启动失败的五个高频现场与抢救方案SSH 组合框架的调试不是看日志是读日志里的异常栈顶第一行。90% 的启动失败源于 XML 配置的拼写错误、路径错误或依赖缺失。以下是我在线上环境反复踩过的五个坑按发生频率排序每条都附带真实现象、根本原因和秒级修复法。5.1 现象Tomcat 启动卡死在Initializing Spring root WebApplicationContext无后续日志原因applicationContext.xml中sessionFactory的mappingResources路径错误导致 Hibernate 加载.hbm.xml时抛出FileNotFoundException但 Spring 默认吞掉该异常只打印初始化开始日志。解决打开applicationContext.xml检查property namemappingResources下的value路径是否与实际文件位置一致。例如若User.hbm.xml放在src/com/simon/oa/user/model/目录则value必须写为com/simon/oa/user/model/User.hbm.xml注意斜杠方向Windows 下也用/。血泪经验用 IDE 的 “Go to Declaration” 功能CtrlClick点击value内容看能否跳转到文件跳不到就是路径错。5.2 现象访问/admin/listUser.action报 404Tomcat 日志显示No result defined for action com.simon.oa.user.controller.UserAction and result input原因struts.xml中action的class属性指向的类名与实际类不匹配或该类未被 Spring 容器管理即applicationContext.xml中缺少对应bean定义。Struts 找不到 Action 实例便默认返回input结果而input结果未在result中定义。解决第一步确认struts.xml中classcom.simon.oa.user.controller.UserAction的全限定名与 Java 文件路径完全一致包括大小写第二步检查applicationContext.xml是否存在bean iduserAction classcom.simon.oa.user.controller.UserAction且scopeprototype。玄学排查在UserAction构造器里加System.out.println(UserAction created!);启动时看控制台是否打印不打印说明 Spring 根本没创建它。5.3 现象UserAction.list()执行时报NullPointerException堆栈指向this.userService.findAllUsers()原因UserAction类中的userService字段未被 Spring 注入因为applicationContext.xml中userActionBean 的property nameuserService的ref值写错或userServiceBean 的id与ref不匹配。解决检查applicationContext.xml中两处idbean iduserService ...的id必须与bean iduserAction内property nameuserService refuserService/的ref值完全一致。翻车现场曾把refuserService写成refuserservice小写 sXML 不区分大小写但 Spring 区分导致注入失败。5.4 现象userDao.findAll()返回空 List但数据库明明有数据开启show_sqltrue后发现 SQL 是select * from User表名大写原因.hbm.xml中class tablet_user的table属性写错或User.java的类名与class nameUser不一致导致 Hibernate 生成错误的 SQL 表名。MySQL 默认表名小写select * from User会找不到表。解决打开User.hbm.xml确认class tablet_user的table值与数据库实际表名show tables;查看完全一致同时确认class nameUser的name与User.java的类名一致。后悔药在hibernateProperties中添加prop keyhibernate.default_schemaoa_db/prop强制指定 schema。5.5 现象userService.updateUser(user)执行后数据库无变化show_sqltrue显示无 UPDATE 语句原因事务未生效。常见于applicationContext.xml中aop:config的expression切点未匹配到 Service 方法或tx:advice的tx:method未覆盖updateUser方法名。解决第一步确认UserServiceImpl类是否在com.simon.oa.user.service.impl包下切点表达式execution(* com.simon.oa.user.service.impl.*.*(..))严格匹配包路径第二步检查tx:method name*是否在updateUser方法名匹配范围内*匹配所有没问题第三步在updateUser方法开头加System.out.println(updateUser called);看是否执行不执行说明 AOP 未织入。终极排查在UserServiceImpl上加Transactional需升级 Spring 版本临时验证若此时事务生效证明原 AOP 配置有误。6. 进阶技巧用 Hibernate 的merge()替代update()解决 detached object 异常在 SSH 架构中最常见的运行时异常是org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session。这通常发生在用户修改表单后提交UserAction接收到一个User对象来自 JSP 表单绑定然后调用userService.updateUser(user)而UserServiceImpl内部又调用userDao.update(user)。问题在于这个user对象是游离态detached的它曾被某个 Session 加载过但当前 Session 并不认识它。update()方法要求对象必须是游离态但 Hibernate 发现当前 Session 已加载了同 ID 的另一个User实例比如之前查过一次于是拒绝更新。6.1 为什么update()会失败而merge()能成功update()的语义是“把这个游离对象的状态同步到数据库并让它重新变为持久态”。但它有个隐藏前提当前 Session 不能已存在同 ID 的持久化对象。而merge()的语义是“不管当前 Session 有没有同 ID 对象都把传入对象的状态复制到 Session 中的对应持久化对象上然后返回那个持久化对象”。它不关心传入对象的状态只关心如何把数据刷进数据库。// UserDaoImpl.java 中的错误写法导致 NonUniqueObjectException Override public void update(User user) { getHibernateTemplate().update(user); // 危险 } // 正确写法用 merge() 替代 update() Override public User merge(User user) { return (User) getHibernateTemplate().merge(user); // 安全 }逻辑说明merge(user)会先检查当前 Session 是否已存在 ID 相同的User实例。如果存在就把user的属性值复制过去如果不存在就先load()一个新实例再复制。最终返回的是 Session 中的那个持久化对象而不是传入的游离对象。这样既保证了数据更新又避免了 Session 冲突。参数说明merge()返回值是Object需强转为User传入的user对象在调用后仍是游离态不应再使用merge()会触发 UPDATE SQL效果与update()相同但更健壮。6.2 在 Service 层统一处理将update()封装为merge()的安全代理为了不让每个 DAO 都暴露merge()应在 Service 层做一层封装把业务方法的语义从“更新对象”明确为“合并状态”。// UserServiceImpl.java Override public void updateUser(User user) { // 业务逻辑校验、日志等 if (user.getId() null) { throw new IllegalArgumentException(用户ID不能为空); } System.out.println(【Service】准备合并用户 user.getName()); // 调用 DAO 的 merge()而非 update() User mergedUser userDao.merge(user); System.out.println(【Service】合并完成ID mergedUser.getId()); }逻辑说明updateUser()方法名保持不变对外契约但内部实现改为调用userDao.merge()mergedUser是 Session 中的持久化对象可用于后续操作如发消息、记日志user对象在merge()后不再有效不应再调用其 getter。参数说明user.getId()校验放在merge()前因为merge(null)会抛NullPointerExceptionSystem.out.println()中的mergedUser.getId()能正确打印证明合并成功。6.3 验证 merge() 是否生效三步断言法要确认merge()真正解决了问题不能只看日志要用三步断言步骤操作预期结果说明1. 断言数据库更新修改用户姓名后提交查数据库SELECT * FROM t_user WHERE user_id123;user_name字段值已变更证明 SQL 执行成功2. 断言 Session 状态在UserServiceImpl.updateUser()中merge()后加System.out.println(isPersistent: getHibernateTemplate().getSessionFactory().getCurrentSession().contains(mergedUser));输出isPersistent: true证明mergedUser已是当前 Session 的持久化对象3. 断言无异常抛出连续两次提交同一用户模拟并发编辑观察 Tomcat 日志无NonUniqueObjectException只有正常日志证明merge()彻底规避了游离对象冲突提示getHibernateTemplate().getSessionFactory().getCurrentSession()获取当前线程绑定的 Sessioncontains(obj)返回true表示obj是该 Session 的持久化对象。这是验证merge()效果的黄金标准。从那以后我每次写 DAO 的更新方法都强制本文还有配套的精品资源点击获取