简介:本资源是面向高校计算机专业学生与JavaEE初学者的期末复习题库,聚焦MyBatis框架核心知识点,助力系统梳理ORM原理、SQL映射配置及常见考点。题库覆盖初识MyBatis、核心配置、动态SQL、关联查询等关键章节,题型丰富,含填空、判断、选择、简答四类,并附标准答案与解析,特别强化对 / / 元素、mybatis-config.xml结构、resultMap映射机制、SQL注入防范(#{}/${}辨析)、concat()函数局限性及MyBatis工作流程(SqlSessionFactory→SqlSession→Executor→MappedStatement)等高频易错点的考查。资源为1个41KB的DOCX文档,内容排版清晰、题目编号规范、答案分项标注,便于打印复习或碎片化自测。目前已有2646人学习下载,是备考JavaEE课程期末考试、夯实MyBatis基础、查漏补缺的高实用性教辅材料。
1. 这不是题海战术,而是一份能真正帮你把 MyBatis 和 Spring 框架“焊进肌肉记忆”的 JavaEE 期末复习题库
你是不是也经历过:考前翻了三遍《JavaEE 开发实战》,笔记记了二十页,结果一看到“<resultMap>的<id>和<result>区别在哪”就卡壳?或者对着@Pointcut和@Around发呆,分不清哪个是切点、哪个是增强?这不是你学得不认真——而是大多数复习资料把框架讲成了名词解释汇编,缺了真实开发中那层“为什么这么设计、不这么写会翻车、改一个参数怎么崩”的实操肌理。
这份《JavaEE期末复习题库》不是题海,它是一套按真实开发脉络反向拆解的诊断式习题集。它从 MyBatis 删除操作必须事务提交这个血泪经验出发,一路覆盖到 Spring MVC 执行流程中HandlerAdapter到底把ModelAndView交给谁(不是ViewReslover,是DispatcherServlet!),每道题都对应一个你在写 DAO 层时踩过的坑、在配applicationContext.xml时删错标签导致启动失败的瞬间。填空题里藏着concat()函数防注入的玄学陷阱;判断题第2题直击“只加 JAR 包就能跑?”这个新手幻觉;简答题答案里那句“<selectKey>的order属性设为BEFORE还是AFTER,取决于主键生成策略”,就是某次模拟项目X上线前两小时调通 ID 生成逻辑的关键线索。它适合两类人:一是临考前想用最少时间抓住主干逻辑的备考者,二是刚写完第一个 CRUD 却对SqlSession生命周期一头雾水的入门开发者。题库不教你怎么背,它逼你动手去想:如果namespace写错了,MyBatis 报什么错?如果@Transactional没加在 Service 层,DAO 层的delete()真的会回滚吗?
2. MyBatis 核心机制落地:从 XML 配置到 SQL 执行链路的完整闭环
2.1<mapper>的 namespace 不是摆设:它是 MyBatis 查找 SQL 的唯一身份证
MyBatis 不是靠文件名找 SQL,而是靠<mapper>标签里的namespace属性。这个值必须全局唯一,且要和 DAO 接口的全限定类名完全一致。比如你的接口是com.example.dao.UserDao,那么映射文件开头必须写:
<mapper namespace="com.example.dao.UserDao"> <select id="findUserById" resultType="com.example.entity.User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>注意:
namespace值必须和接口类名严格匹配,包括大小写和包路径。如果写成com.example.dao.userDao(小写 d)或com.example.dao.UserDaoImpl(多写了 Impl),MyBatis 在运行时根本找不到这个接口,抛出Invalid bound statement (not found)异常——这是初学者最常遇到的“配置没错但死活不执行”的第一大坑。
namespace的作用远不止标识。它和<select>、<insert>等元素的id属性拼起来,构成 MyBatis 内部的 SQL 唯一标识符(即statementId)。当你在代码里调用sqlSession.selectOne("com.example.dao.UserDao.findUserById", 1)时,MyBatis 就是靠这个字符串去缓存里查对应的MappedStatement对象。所以,namespace是整个 SQL 映射体系的根,漏掉或写错,后续所有操作都失去坐标。
2.2<delete>元素背后:为什么删除操作必须手动 commit,而查询不用?
填空题第一题问“MyBatis 的删除操作在映射文件中是通过配置【 】元素来实现的”,答案是<delete>。但这只是表象。真正关键的是:<delete>对应的SqlSession.delete()方法本身不触发事务提交,它只把 SQL 语句塞进 JDBC 的 PreparedStatement,真正的执行和提交由外部控制。
看一段典型代码:
SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserDao userDao = sqlSession.getMapper(UserDao.class); userDao.deleteUser(1001); // 这行只执行 delete SQL,未提交 sqlSession.commit(); // 必须显式调用 commit,否则数据不入库 } catch (Exception e) { sqlSession.rollback(); // 出错必须 rollback,否则连接可能被污染 } finally { sqlSession.close(); }参数说明:
sqlSession.openSession()默认开启自动提交(autoCommit=false),这意味着所有 DML 操作(INSERT/UPDATE/DELETE)都处于事务暂挂状态,直到你调用commit()或rollback()。而SELECT操作属于 DQL,不涉及事务,所以sqlSession.selectOne()调用后立即返回结果,无需 commit。这是 JDBC 规范决定的,MyBatis 只是忠实封装。如果你用的是 Spring 声明式事务(@Transactional),那commit()就由 Spring AOP 代理在方法退出时自动完成,但底层逻辑没变——还是那个SqlSession在干活。
2.3#{}vs${}:模糊查询里藏着的 SQL 注入生死线
填空题第3题问“使用 MySQL 中的【 】函数进行字符串拼接,可以有效防止模糊查询时可能出现的 SQL 注入问题”,标准答案是concat()。但这里有个巨大误导:concat()本身并不能防止 SQL 注入,它只是 MySQL 提供的一个字符串函数;真正防注入的是 MyBatis 的#{}占位符机制。
看两个写法对比:
<!-- 危险!用 ${} 拼接,直接导致 SQL 注入 --> <select id="searchUserByName" resultType="User"> SELECT * FROM user WHERE name LIKE '%${name}%' </select> <!-- 安全!用 #{} + concat(),预编译参数化 --> <select id="searchUserByNameSafe" resultType="User"> SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%') </select>当传入参数name = "admin' OR '1'='1"时:
- 第一种写法会被解析成
WHERE name LIKE '%admin' OR '1'='1%',SQL 逻辑被篡改; - 第二种写法中,
#{name}会被 JDBC 预编译为?,实际执行时参数以二进制方式传入,CONCAT('%', ?, '%')中的?永远是字符串字面量,不会参与 SQL 解析。
避坑 / 常见问题 / 排查
现象 1:模糊查询返回空结果,但数据库里明明有数据。
原因:用了${name}但前端传参带了前后空格,如" zhang ",拼成'% zhang %'导致匹配失败。
解决:统一在 Java 层 trim 参数,或改用#{name}+CONCAT,让数据库处理拼接。现象 2:
CONCAT('%', #{name}, '%')在 Oracle 数据库报错。
原因:CONCAT是 MySQL 特有函数,Oracle 用||或CONCAT(仅支持两参数)。
解决:用 MyBatis 的<bind>标签做数据库无关处理:<bind name="likeName" value="'%' + name + '%'" /> <where> <if test="likeName != null">name LIKE #{likeName}</if> </where>现象 3:
<select>返回null,但日志显示 SQL 执行成功。
原因:resultType写错了类名,如resultType="user"(小写),而实际类是User,MyBatis 找不到类型处理器。
解决:检查resultType是否为全限定类名或已注册别名(如user别名需在mybatis-config.xml中配置<typeAlias alias="user" type="com.example.entity.User"/>)。现象 4:
<update>执行后int返回值为 0,但数据库记录确实被改了。
原因:SQL 语句里WHERE条件没匹配到任何行,UPDATE影响行为 0,MyBatis 如实返回。
解决:在业务层检查返回值,为 0 时抛自定义异常或打告警日志,避免“静默失败”。
2.4mybatis-config.xml的核心骨架:为什么environments和mappers缺一不可
mybatis-config.xml是 MyBatis 的总开关,它的结构不是随意的。最简可用配置必须包含两个顶级节点:<environments>和<mappers>。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 1. 环境配置:定义数据源和事务管理器 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!-- 2. 映射器加载:告诉 MyBatis 去哪找 SQL --> <mappers> <mapper resource="com/example/dao/UserDao.xml"/> </mappers> </configuration><environments>节点定义了 MyBatis 连接数据库的“通道”,没有它,SqlSessionFactoryBuilder构建工厂时会直接抛NullPointerException。<mappers>则是 SQL 的“地图”,它指向所有Mapper.xml文件的位置。如果漏掉<mappers>,sqlSession.getMapper(UserDao.class)会返回null,因为 MyBatis 根本不知道你的 SQL 写在哪。
参数说明:
<environments>的default属性指定默认激活的环境(如development),可切换为test或production;<dataSource>的type="POOLED"表示使用连接池(推荐),UNPOOLED每次新建连接,性能极差;<mapper>的resource属性是类路径相对地址,必须和Mapper.xml在 classpath 下的实际位置一致(如src/main/resources/com/example/dao/UserDao.xml)。
3. Spring Bean 生命周期与依赖注入:从 XML 配置到注解驱动的演进真相
3.1<bean>的scope属性:singleton 不是“单例”,prototype 也不是“每次 new”
填空题第4题问“对于【 】作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 实例后,Bean 的实例就交给客户端代码来管理”,答案是prototype。但这句话容易引发误解——prototypeBean 真的“每次请求都 new 一个”吗?不完全是。
看这段配置:
<bean id="userService" class="com.example.service.UserService" scope="prototype"/>当你调用applicationContext.getBean("userService")时,Spring 确实会每次都 new 一个新实例。但关键在于:这个“每次”是指你主动调用getBean()的次数,而不是 HTTP 请求次数。如果UserService被注入到一个singleton的 Controller 里:
@Controller public class UserController { @Autowired private UserService userService; // 这里注入的 userService 是 singleton 还是 prototype? }答案是:Controller 初始化时,Spring 会调用一次getBean("userService")创建一个 prototype 实例,并把它注入进去。之后无论多少次 HTTP 请求,Controller 里的userService字段始终是同一个对象。因为prototype的生命周期由容器控制,一旦注入完成,容器就不再管理它。
所以
prototype的真实含义是:Spring 容器每次调用getBean()时,都返回一个新实例;但它不保证这个实例在应用中只被用一次。真正需要“每次请求一个新 Bean”的场景,要用request或session作用域(Web 环境下),它们由 Spring Web 容器管理。
3.2@Autowired的装配逻辑:byType 优先,但 name 冲突时会 fallback 到 byName
填空题第1题问“所谓自动装配,就是将一个 Bean 自动的注入到到其他 Bean 的【 】中”,答案是Property(属性)。但@Autowired的工作原理比“设个属性值”复杂得多。
Spring 默认按类型(byType)装配。比如:
@Service public class UserServiceImpl implements UserService { ... } @Service public class OrderServiceImpl implements OrderService { ... } @Controller public class UserController { @Autowired private UserService userService; // ✅ 成功:找到唯一的 UserService 类型 Bean }但如果存在多个同类型 Bean:
@Service("userServiceV1") public class UserServiceImplV1 implements UserService { ... } @Service("userServiceV2") public class UserServiceImplV2 implements UserService { ... }此时@Autowired private UserService userService;会抛NoUniqueBeanDefinitionException。解决办法有两个:
- 加
@Qualifier("userServiceV1")指定 Bean 名; - 或者在字段上直接用
@Resource(name="userServiceV1")(JSR-250 标准,按 name 查)。
参数说明:
@Autowired的required=false属性表示该依赖可选,即使找不到也不报错(字段为 null);@Qualifier的 value 值必须和@Service("xxx")的 name 一致;@Resource是 Java 标准,name属性对应 Bean 的 id/name,type属性对应类型,优先级name > type。
3.3<bean>的autowire属性:XML 时代的手动装配与自动装配之争
选择题第1题问“Spring 的<bean>元素中的 autowire 属性取值不包括以下”,选项 D 是byId。这是对的,因为autowire只支持no、byName、byType、constructor、autodetect五种,没有byId。
autowire="byName"的意思是:Spring 会查找容器中所有 Bean,把名字(id 或 name)和当前 Bean 的属性名相同的那个注入进来。比如:
<bean id="userService" class="com.example.service.UserServiceImpl"/> <bean id="userController" class="com.example.controller.UserController" autowire="byName"/>UserController类里必须有一个名为userService的 setter 方法:
public class UserController { private UserService userService; public void setUserService(UserService userService) { // 方法名必须是 set + 首字母大写的属性名 this.userService = userService; } }避坑 / 常见问题 / 排查
现象 1:autowire="byName"失效,属性仍是 null。
原因:<bean>的id和属性名大小写不一致,如id="UserService"(大写 U),而属性是private UserService userService;(小写 u)。
解决:严格遵循 JavaBean 规范,id全小写,如userService。现象 2:
autowire="byType"找到多个 Bean,启动报错。
原因:容器中有两个UserService实现类,且都没指定primary="true"。
解决:在其中一个<bean>上加primary="true",或改用@Primary注解。现象 3:
<bean>配置了init-method,但方法没被调用。
原因:init-method指定的方法名拼写错误,或该方法不是 public 无参方法。
解决:检查方法签名是否为public void init() {},且init-method="init"中的值与方法名完全一致。现象 4:
@Component扫描不到类,applicationContext.getBean()报NoSuchBeanDefinitionException。
原因:@ComponentScan的 basePackages 没包含该类所在包,或类上没加@Component及其衍生注解(@Service、@Repository)。
解决:确认@ComponentScan("com.example")覆盖了com.example.service,且UserServiceImpl类上有@Service。
3.4@Controller、@Service、@Repository:不只是语义标签,更是 AOP 的切面锚点
填空题第2题问“@Controller通常作用在控制层,如 Spring MVC 的【 】”,答案是Controller。但这些注解的价值远超“标记位置”。
@Repository是最关键的——它被 Spring 定义为数据访问异常转换的切面锚点。当你在 DAO 层抛出SQLException,Spring 的PersistenceExceptionTranslationPostProcessor会捕获它,并转换成 Spring 统一的DataAccessException子类(如DuplicateKeyException)。这个转换只对加了@Repository的类生效。
@Repository // 必须加!否则 SQLException 不会被转换 public class UserDaoImpl implements UserDao { @Override public void insertUser(User user) { // JDBC 操作,可能抛 SQLException } }@Service和@Controller则是事务管理的默认切面目标。@Transactional注解默认只对public方法生效,且要求目标类是 Spring 管理的 Bean(即加了@Service)。如果UserService没加@Service,你加了@Transactional也没用——因为 Spring 无法为它生成代理。
所以,这些注解是 Spring AOP 的“钩子”。不加
@Repository,你就拿不到 Spring 的异常体系;不加@Service,@Transactional就是废纸。
4. Spring AOP 与 Spring MVC 执行链:从切面织入到请求分发的底层穿透
4.1@Aspect切面的执行顺序:为什么@Before和@After不能替代@Around
填空题第5题问“AOP 术语中【 】表示 AOP 框架在特定的切入点执行的增强处理”,答案是Advice(通知)。但Advice有五种类型,其中@Around是最强大也最容易误用的。
看一个典型日志切面:
@Aspect @Component public class LoggingAspect { @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); // ⚠️ 必须调用 proceed(),否则目标方法不执行! long end = System.currentTimeMillis(); System.out.println(joinPoint.getSignature() + " took " + (end - start) + "ms"); return result; } }@Around的核心是proceed()方法——它代表目标方法的执行点。如果你忘了调用它,或者在proceed()前抛异常,目标方法永远不会运行。而@Before和@After是被动的:@Before在目标方法前执行,@After在目标方法后执行(无论成功失败),但它们无法控制目标方法是否执行、无法修改参数、无法修改返回值。
参数说明:
ProceedingJoinPoint是@Around独有的参数类型,joinPoint.getSignature()获取方法签名,joinPoint.getArgs()获取参数数组;@Before的JoinPoint参数不能调用proceed();@AfterReturning的returning属性可接收返回值,如@AfterReturning(pointcut = "...", returning = "result")。
4.2 Spring MVC 的DispatcherServlet:不是“前端控制器”,而是整个流程的中央调度器
填空题第4题问“Spring MVC 提供了一个前端控制器【 】”,答案是DispatcherServlet。但它的角色远不止“拦截请求”。
DispatcherServlet是 Spring MVC 的心脏,它内部持有一组组件:
HandlerMapping:根据 URL 查找Handler(即@Controller中的方法);HandlerAdapter:适配不同类型的Handler(如@RequestMapping方法、HttpRequestHandler);ViewResolver:根据逻辑视图名(如"success")解析成真实视图(如/WEB-INF/views/success.jsp);HandlerExceptionResolver:统一处理异常;LocaleResolver:管理国际化。
整个流程是DispatcherServlet主导的:
HTTP Request → DispatcherServlet → HandlerMapping → HandlerAdapter → Handler → ModelAndView → ViewResolver → View → Response关键细节:
HandlerAdapter并不直接把ModelAndView交给ViewResolver。HandlerAdapter执行完Handler后,把ModelAndView返回给DispatcherServlet,然后DispatcherServlet再调用ViewResolver。填空题第10章判断题第1题说“HandlerAdapter将ModelAndView对象返回给ViewReslover”,这是错的——ViewReslover是笔误,正确是ViewResolver,且调用方是DispatcherServlet。
4.3@RequestMapping的 method 属性:GET/POST 不是可选,而是安全边界
填空题第1题问“@RequestMapping注解类型用于映射【 】”,答案是URL。但@RequestMapping的method属性绝不是装饰品。
@RequestMapping(value = "/user", method = RequestMethod.POST) public String createUser(@RequestBody User user) { ... }如果不加method = RequestMethod.POST,这个方法会响应所有 HTTP 方法(GET、POST、PUT、DELETE)。这会导致:
- 安全风险:恶意用户用 GET 访问
/user,可能触发创建操作; - 语义混乱:RESTful 设计中,
POST /user是创建,GET /user是查询,混用破坏契约。
Spring 4.3+ 提供了快捷注解:@PostMapping、@GetMapping,它们本质就是@RequestMapping(method = RequestMethod.XXX)的语法糖。
避坑 / 常见问题 / 排查
现象 1:@PostMapping方法被 GET 请求触发,返回 405 Method Not Allowed。
原因:浏览器地址栏输入 URL 默认是 GET,而方法只允许 POST。
解决:前端用<form method="post">或 AJAX POST 请求;后端加@GetMapping处理查询。现象 2:
@RequestMapping("/user/{id}")的{id}参数取不到,@PathVariable为 null。
原因:@RequestMapping的value和@PathVariable的value不一致,如@RequestMapping("/user/{userId}")但@PathVariable("id")。
解决:确保@PathVariable的 name 和 URL 模板中的占位符名完全一致。现象 3:
@RequestBody接收 JSON 报 400 Bad Request。
原因:缺少@EnableWebMvc或WebMvcConfigurationSupport,导致HttpMessageConverter未注册。
解决:在配置类上加@EnableWebMvc,或确认spring-webmvcJAR 已引入。现象 4:
DispatcherServlet启动时报No mapping found for HTTP request。
原因:web.xml中DispatcherServlet的url-pattern配置为/,但静态资源(js/css)也被拦截,且没配置mvc:resources。
解决:在 Spring MVC 配置中加<mvc:resources mapping="/static/**" location="/static/"/>,或改url-pattern为/app/*。
4.4ModelAndView的真相:它不是返回值,而是 Spring MVC 的内部通信协议
填空题第5题问“Controller 执行完成后,会返回一个【 】对象”,答案是ModelAndView。但现代开发中,你几乎不会显式 new 它。
// 传统写法(不推荐) @RequestMapping("/list") public ModelAndView listUsers() { List<User> users = userService.findAll(); ModelAndView mav = new ModelAndView("userList"); // 逻辑视图名 mav.addObject("users", users); // 模型数据 return mav; } // 现代写法(推荐) @RequestMapping("/list") public String listUsers(Model model) { model.addAttribute("users", userService.findAll()); return "userList"; // 直接返回逻辑视图名 }ModelAndView是 Spring MVC 1.x 时代的产物,它把模型(Model)和视图(View)耦合在一个对象里。现在更推荐用Model参数(或ModelMap、Map)传递数据,用String返回视图名。这样更轻量,也符合单一职责原则。
参数说明:
Model参数由 Spring 自动注入,addAttribute("key", value)等价于model.put("key", value);返回的String是逻辑视图名,ViewResolver会根据前缀后缀拼成真实路径,如prefix="/WEB-INF/views/" + "userList" + suffix=".jsp"→/WEB-INF/views/userList.jsp。
5. 动态 SQL 与事务管理:MyBatis 与 Spring 协同工作的边界与雷区
5.1<foreach>的 collection 属性:为什么 Map 的 key 是 “map”,而不是 “params”
填空题第2题问“在使用 MyBatis 的<foreach>时,如果传入的参数是多个的时候,就需要把它们封装成一个 Map 了,这时候 collection 属性值就为 Map 的【 】”,答案是键。但这里的“键”指 Map 的 key,不是任意字符串。
看一个典型场景:前端传userId=1001和status=active,DAO 方法需要接收两个参数:
public List<User> findUsersByStatusAndId(@Param("userId") Long userId, @Param("status") String status);@Param("userId")的作用是告诉 MyBatis:把这个参数放到 Map 里,key 是"userId"。所以 MyBatis 内部会构造一个 Map:{"userId": 1001, "status": "active"}。
在 XML 中写<foreach>时:
<select id="findUsersByStatusAndId" resultType="User"> SELECT * FROM user WHERE status = #{status} AND id IN <foreach item="id" collection="userId" open="(" separator="," close=")"> #{id} </foreach> </select>错!
collection="userId"是错的。collection的值必须是 Map 的 key,而userId是值,不是 key。正确写法是collection="list"(如果传的是 List)或collection="array"(如果传的是数组)。但这里传的是两个独立参数,MyBatis 会把它们包装成一个 Map,key 是@Param指定的值,所以collection应该是"userId"?不,还是错。
真相是:<foreach>的collection属性只适用于参数本身就是集合(List/Array/Map)的情况。如果参数是多个基本类型,应该用@Param包装,然后在<foreach>中用collection="param1"指向那个@Param的 key。但 MyBatis 3.4+ 更推荐用@Param后,在 SQL 中直接引用#{param1},而不是<foreach>。
所以,这道题的答案“键”指的是:当你手动构造 Map 作为参数时,collection的值就是这个 Map 的某个 key。例如:
Map<String, Object> params = new HashMap<>(); params.put("idList", Arrays.asList(1001, 1002, 1003)); params.put("status", "active"); userDao.findUsers(params);XML 中:
<foreach item="id" collection="idList" open="(" separator="," close=")"> #{id} </foreach>collection="idList"—— 这里的idList就是 Map 的 key。
5.2<set>元素的智能逗号消除:为什么它比手写 SQL 更安全
填空题第5题问“使用<set>和<if>元素相结合的方式来组装 update 语句时,<set>元素就会动态前置【 】关键字,同时也会消除 SQL 语句中最后一个多余的【 】”,答案是SET和逗号。
看一个典型更新语句:
<update id="updateUser"> UPDATE user <set> <if test="name != null and name != ''">name = #{name},</if> <if test="email != null and email != ''">email = #{email},</if> <if test="age != null">age = #{age},</if> </set> WHERE id = #{id} </update>如果没有<set>,你得手写:
UPDATE user <if test="name != null">name = #{name},</if> <if test="email != null">email = #{email},</if> <if test="age != null">age = #{age}</if> -- 最后一个不能加逗号! WHERE id = #{id}这极易出错。<set>的作用是:自动在开头加SET关键字,并自动删除内部所有<if>生成的 SQL 片段末尾的逗号。它等价于:
<trim prefix="SET" suffixOverrides=","> <if test="name != null and name != ''">name = #{name},</if> <if test="email != null and email != ''">email = #{email},</if> <if test="age != null">age = #{age},</if> </trim>suffixOverrides=","表示删除结尾的逗号,prefix="SET"表示开头加SET。
参数说明:
<trim>的prefix是前缀(如SET、WHERE),suffix是后缀(如AND 1=1),prefixOverrides删除开头的指定字符串(如"AND "),suffixOverrides删除结尾的指定字符串(如",")。
5.3 Spring 事务传播行为:为什么@Transactional加在 Service 层,DAO 层却不用管
填空题第4题问“MyBatis 的更新操作也需要进行事务提交”,答案是对。但 MyBatis 本身不管理事务,它只提供SqlSession的commit()方法。事务管理是 Spring 的事。
@Transactional的核心是TransactionInterceptor,它在方法执行前后织入事务逻辑。当UserService.updateUser()被调用时:
UserService.updateUser() → TransactionInterceptor.before() → 开启事务 → UserDao.updateUser() → SqlSession.update() → TransactionInterceptor.afterReturning() → commit()如果UserDao.updateUser()抛异常,TransactionInterceptor.afterThrowing()会调用rollback()。
关键边界:
@Transactional只对public方法生效,且要求调用方和被调用方都是 Spring 代理对象。如果UserService内部直接new UserDaoImpl(),事务失效。所以 DAO 层不需要、也不应该加@Transactional——它只负责执行 SQL,事务由上层 Service 统一控制。
避坑 / 常见问题 / 排查
现象 1:@Transactional方法内调用另一个@Transactional方法,内层事务不生效。
原因:Spring 事务基于代理,内部方法调用是 this.xxx(),绕过了代理。
解决:提取内层逻辑到另一个@Service类,或用ApplicationContext获取代理对象调用。现象 2:
@Transactional(rollbackFor = Exception.class)仍不回滚。
原因:抛出的是Error(如OutOfMemoryError)或RuntimeException的子类(如NullPointerException),而rollbackFor默认只对RuntimeException回滚。
解决:rollbackFor = Exception.class已正确,检查是否真的抛出了Exception,而非RuntimeException。现象 3:同一事务中多次
save(),只有第一次生效。
原因:save()方法里用了@Modifying但没加@Transactional,或EntityManager的 flush 模式问题。
解决:确认所有 DML 操作都在一个@Transactional方法内,且@Modifying配合@Query使用。现象 4:
@Transactional在测试类中不生效。
原因:JUnit 测试类没加 `@RunWith(Spring
本文还有配套的精品资源,点击获取