1. 初识PersistenceExceptionMyBatis的红色警报当你看到控制台突然抛出org.apache.ibatis.exceptions.PersistenceException时就像开车时仪表盘突然亮起故障灯。这个异常是MyBatis框架的通用异常包装器专门用来封装数据库操作过程中的各种问题。我见过不少开发者第一次遇到这个异常时手足无措的样子其实只要掌握正确的排查方法大多数情况下都能快速定位问题。这个异常最常见的场景就是在执行SQL查询时比如你可能会看到这样的错误堆栈org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{propertyid, modeIN, javaTypeclass java.lang.Object, jdbcTypenull}这种错误信息看似复杂但实际上MyBatis已经非常友好地为我们标注了关键信息。注意看###开头的行这是MyBatis特意格式化的错误提示会直接告诉你问题发生在查询数据库时原因是参数映射出了问题。2. 参数映射问题JdbcType的缺席审判2.1 当参数遇到null值在我处理过的案例中约40%的PersistenceException都与参数映射相关。比如下面这个典型错误!-- UserMapper.xml -- select idgetUserById parameterTypeint resultTypeUser SELECT * FROM user WHERE id #{id} /select当id参数传入null值时就可能抛出Error setting non null for parameter #1 with JdbcType null这是因为MyBatis无法推断出null参数对应的JDBC类型。解决方法很简单要么在参数中指定jdbcType#{id,jdbcTypeINTEGER}要么在全局配置中设置settings setting namejdbcTypeForNull valueNULL/ /settings2.2 参数个数不匹配的悬案另一种常见情况是参数个数不匹配比如// Mapper接口方法 User getUserByIdAndName(Param(id) Integer id, Param(name) String name); !-- XML配置 -- select idgetUserByIdAndName resultTypeUser SELECT * FROM user WHERE id #{id} /select执行时会报错Parameter index out of range (2 number of parameters, which is 1)这是因为XML中只用了id参数但方法却声明了两个参数。解决方法要么补全XML中的参数SELECT * FROM user WHERE id #{id} AND name #{name}要么修改接口方法参数个数。3. SQL语句的隐形杀手3.1 XML中的注释陷阱很多开发者不知道MyBatis的XML映射文件中某些注释会导致解析错误。比如select idgetUser resultTypeUser -- 这是错误的注释方式 SELECT * FROM user /* 这也是不推荐的注释 */ /select正确的注释方式应该是!-- 这是安全的XML注释 -- select idgetUser resultTypeUser !-- 行内注释也要用XML格式 -- SELECT * FROM user /select3.2 SQL语法问题即使你在数据库客户端中测试通过的SQL在MyBatis中也可能因为特殊字符导致问题。例如select idfindUsers resultTypeUser SELECT * FROM user WHERE name LIKE %${name}% /select这种写法不仅可能引发SQL注入风险当name包含特殊字符时还会导致语法错误。应该使用SELECT * FROM user WHERE name LIKE CONCAT(%, #{name}, %)4. 配置文件的蝴蝶效应4.1 数据库连接配置MySQL高版本(5.7)的SSL连接配置经常引发问题Communications link failure. The last packet successfully received from...解决方法是在连接URL中添加jdbc:mysql://localhost:3306/db?useSSLfalseserverTimezoneUTC4.2 映射文件定位问题我遇到过最隐蔽的一个bug是mapper.xml文件放错了目录导致始终报Error building SqlSession. The error may exist in XXXMapper.xml正确的资源目录结构应该是src/main/resources/ ├── com │ └── example │ └── mapper │ └── UserMapper.xml同时确保mybatis-config.xml中配置正确mappers mapper resourcecom/example/mapper/UserMapper.xml/ /mappers5. 类型处理的次元壁5.1 枚举类型转换处理枚举类型时经常会遇到Error setting non null for parameter #1 with JdbcType null解决方法是为枚举注册类型处理器MappedTypes(MyEnum.class) MappedJdbcTypes(JdbcType.VARCHAR) public class MyEnumTypeHandler extends BaseTypeHandlerMyEnum { // 实现处理方法 }然后在配置中注册typeHandlers typeHandler handlercom.example.MyEnumTypeHandler/ /typeHandlers5.2 集合类型处理当使用foreach标签时如果集合为null或空可能会报错。安全写法是select idgetUsersByIds resultTypeUser SELECT * FROM user where if testids ! null and !ids.isEmpty() id IN foreach collectionids itemid open( separator, close) #{id} /foreach /if /where /select6. 高级排查技巧6.1 日志分析三板斧开启完整日志在log4j.properties中添加log4j.logger.org.apache.ibatisDEBUG log4j.logger.java.sqlDEBUG关注SQL执行日志查找 Preparing:和 Parameters:开头的行异常堆栈分析从下往上看找到第一个你的代码出现的位置6.2 使用MyBatis内置工具MyBatis提供了SqlSessionFactoryBuilder的build方法可以传入自定义的InputStream方便测试时快速定位配置问题try (InputStream inputStream Resources.getResourceAsStream(mybatis-config.xml)) { SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); // 测试代码 } catch (Exception e) { e.printStackTrace(); }7. 实战案例解决复合问题最近我遇到一个典型复合案例异常信息如下PersistenceException: Error querying database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named userName in class java.lang.String经过分析发现问题是Mapper接口方法定义为User getUserByName(String userName);但XML中却使用了select idgetUserByName parameterTypestring resultTypeUser SELECT * FROM user WHERE user_name #{userName} /select解决方法有两种使用Param注解User getUserByName(Param(userName) String name);或者修改XML中的参数名SELECT * FROM user WHERE user_name #{name}8. 预防胜于治疗最佳实践统一编码规范团队统一Mapper接口与XML的命名规则参数检查重要参数添加非空校验单元测试对每个Mapper方法编写测试用例代码审查特别检查XML中的SQL注释和参数映射持续集成在CI流程中加入MyBatis代码检查插件记得上次我在项目中发现一个PersistenceException花了3小时才定位到是因为有人在XML中使用了!-- --注释--这种嵌套注释。所以现在我团队中都会定期进行MyBatis代码走查这类问题再没出现过。