1. 从一次映射报错说起:为什么别名这件事值得单独聊
第一次被resultType的别名坑到,是在一个查询接口上。SQL 写得好好的,实体类字段也对得上,结果启动就抛了一个ClassNotFoundException,提示找不到str这个类型。当时我盯着 XML 看了半天,resultType="str"明明是我随手写的简写,心里还想着“字符串嘛,简写一下应该没问题”。结果 MyBatis 根本不认,它只认自己内置的那套别名表。
这件事让我意识到,resultType的别名不是随便起的,它背后有一张固定的注册表。你写string可以,写str就不行;你写int可以,写integer也可以,但写i就不行。这套规则看起来简单,但真正用起来,细节比想象中多。
这篇内容就围绕resultType自带的数据类型别名展开。我会把内置别名表完整梳理一遍,讲清楚它们是怎么注册进去的,为什么有的别名有多个写法,以及在真实项目里怎么用才不容易出错。适合已经写过 MyBatis 但没系统整理过别名的开发者,也适合刚接触 MyBatis、想少踩几个坑的新手。读完你至少能搞清楚三件事:内置别名到底有哪些、它们从哪来、以及什么时候该用别名什么时候不该用。
2. 内置别名表的全貌:一张表看清所有自带类型
2.1 基本类型与包装类型的别名对照
MyBatis 在初始化的时候,会通过TypeAliasRegistry这个注册中心,把一批常用类型注册进去。这些注册是硬编码在框架源码里的,不需要你做任何配置就能直接用。下面这张表是我从实际使用中整理出来的,覆盖了日常开发中绝大多数场景。
| 别名 | 映射的 Java 类型 | 常见使用场景 |
|---|---|---|
string | java.lang.String | 查询单列字符串、计数结果转字符串 |
byte | java.lang.Byte | 小整数、状态标记 |
long | java.lang.Long | 主键 ID、时间戳 |
short | java.lang.Short | 小范围数值 |
int/integer | java.lang.Integer | 计数、状态码、普通数值字段 |
double | java.lang.Double | 金额、比例、浮点计算 |
float | java.lang.Float | 精度要求不高的浮点数 |
boolean | java.lang.Boolean | 逻辑判断、开关标记 |
date | java.util.Date | 时间字段 |
decimal/bigdecimal | java.math.BigDecimal | 金额、高精度计算 |
bigint | java.math.BigInteger | 超大整数 |
object | java.lang.Object | 不确定返回类型时的兜底 |
map | java.util.Map | 返回键值对结构 |
hashmap | java.util.HashMap | 同上,更具体的实现 |
list | java.util.List | 返回列表结构 |
arraylist | java.util.ArrayList | 同上,更具体的实现 |
collection | java.util.Collection | 集合类型兜底 |
iterator | java.util.Iterator | 迭代器场景 |
这张表里有个细节值得注意:int和integer都指向Integer,但int本身在 Java 里是基本类型。MyBatis 在这里做了统一处理,不管你写哪个,最终拿到的都是包装类型。这个设计是有意为之的,因为 MyBatis 在反射调用的时候,基本类型和包装类型混用容易出问题,统一成包装类型能省掉很多麻烦。
2.2 为什么有的类型有多个别名
你可能会好奇,为什么int和integer都能用,decimal和bigdecimal也都能用。这不是随意设计的,而是为了照顾不同开发者的书写习惯。有人习惯写 Java 里的短名,有人习惯写 SQL 里的类型名,MyBatis 干脆两个都注册上,谁用哪个都行。
但要注意,这种“多别名”的情况只出现在少数几个类型上。大部分类型只有一个固定别名,比如string就是string,你写str或者String都不行。这一点和大小写也有关系——MyBatis 在注册别名的时候,会把别名统一转成小写,所以你在 XML 里写resultType="STRING"或者resultType="String",实际上也能匹配到。但为了代码可读性,建议统一用小写。
2.3 别名注册的底层逻辑
TypeAliasRegistry的工作方式其实很直接:内部维护一个Map<String, Class<?>>,key 是小写别名,value 是对应的 Class 对象。注册的时候调用registerAlias方法,解析的时候调用resolveAlias方法。整个过程没有任何魔法,就是查表。
框架启动时,Configuration类的构造函数里会调用TypeAliasRegistry的注册方法,把上面那张表里的类型全部塞进去。这个动作发生在你写的第一行 MyBatis 配置被解析之前,所以你在 XML 里可以直接用这些别名,不需要额外声明。
理解这一点很重要,因为它解释了一个常见疑问:为什么我自定义的别名有时候不生效?答案往往是你注册的时机太晚了,或者注册的 registry 和解析用的不是同一个实例。这个坑我在后面会详细展开。
3. 别名在 XML 与注解中的实际写法差异
3.1 XML 映射文件里的 resultType 用法
在 XML 里,resultType是最常见的别名使用场景。写单表查询的时候,如果返回的是一个简单类型,直接用别名最省事。
<select id="countUsers" resultType="int"> SELECT COUNT(*) FROM user </select> <select id="findUserName" resultType="string"> SELECT name FROM user WHERE id = #{id} </select>这两条语句里,int和string都是内置别名。你不需要写完整的java.lang.Integer或者java.lang.String,MyBatis 会自动帮你解析。这种写法在返回单列结果的时候特别方便,代码看起来也干净。
但这里有个容易忽略的点:resultType="int"返回的实际上是Integer对象,不是基本类型int。如果你在 Java 接口里把返回值声明成int,MyBatis 会自动拆箱;如果声明成Integer,就直接用。两种写法都能跑,但建议接口和 XML 保持一致,避免不必要的装箱拆箱。
3.2 注解方式下的别名使用
用注解写 MyBatis 的时候,别名同样适用。比如:
@Select("SELECT COUNT(*) FROM user") int countUsers(); @Select("SELECT name FROM user WHERE id = #{id}") String findUserName(@Param("id") Long id);注解方式下,resultType不是显式写的,而是通过方法返回值推断出来的。MyBatis 会根据返回类型自动选择对应的 TypeHandler。这时候别名表的作用就体现在内部解析上——框架需要知道int对应哪个 Handler,String对应哪个 Handler,这套映射关系就是靠别名表串起来的。
如果你在注解里用@ResultType显式指定,也可以写别名:
@Select("SELECT * FROM user WHERE id = #{id}") @ResultType("map") Map<String, Object> findUserMap(@Param("id") Long id);这种写法在返回动态结构的时候很有用,比如你不确定查询结果有哪些列,用map别名就能直接接住。
3.3 两种方式的取舍建议
XML 和注解各有适用场景。XML 适合复杂 SQL、动态条件多的查询,别名的使用也更直观;注解适合简单查询、快速开发,别名的存在感没那么强,但底层逻辑是一样的。
我的经验是:如果返回类型是简单类型(int、string、long这些),两种方式都无所谓;如果返回类型是复杂对象或者集合,XML 的可读性更好,因为你可以清楚地看到resultType写的是什么。注解方式下,返回类型藏在方法签名里,时间久了容易忘。
4. 自定义别名:什么时候需要,怎么注册才不出错
4.1 自定义别名的典型场景
内置别名覆盖了 JDK 里的常用类型,但项目里总有自己定义的实体类。比如你有一个UserVO、一个OrderDTO,每次写resultType都要写完整包名,又长又容易写错。这时候就可以注册自定义别名。
<typeAliases> <typeAlias type="com.example.project.model.UserVO" alias="UserVO"/> <typeAlias type="com.example.project.model.OrderDTO" alias="OrderDTO"/> </typeAliases>注册之后,XML 里就可以直接写resultType="UserVO",不用再写全限定名。这个功能在实体类多的项目里能省不少事。
还有一种更省事的做法是包扫描:
<typeAliases> <package name="com.example.project.model"/> </typeAliases>这样会把包下所有类都注册成别名,别名默认是类名首字母小写。比如UserVO会注册成userVO,OrderDTO会注册成orderDTO。注意这里的大小写规则——默认是首字母小写,其余不变,不是全小写。这一点和内置别名的全小写规则不一样,容易搞混。
4.2 注册时机与顺序的坑
自定义别名最容易踩的坑是注册时机。MyBatis 在解析 XML 的时候,如果遇到一个还没注册的别名,会直接抛异常。所以typeAliases的配置必须出现在使用它的select标签之前。
在 XML 配置文件里,MyBatis 对标签顺序有要求,typeAliases必须放在settings之后、typeHandlers之前。如果你放错了位置,启动就会报错。这个错误信息有时候不太直观,可能只告诉你某个标签顺序不对,不会直接说“你的别名没注册上”。
用 Spring Boot 集成的时候,顺序问题通常由自动配置处理掉了,但如果你手动配置SqlSessionFactoryBean,就要注意typeAliasesPackage属性的设置时机。这个属性会在构建SqlSessionFactory的时候被解析,如果设置得太晚,同样不生效。
4.3 别名冲突与覆盖问题
自定义别名和内置别名重名的时候,会发生什么?答案是后注册的覆盖先注册的。因为TypeAliasRegistry底层就是一个 Map,put 操作会覆盖旧值。
这意味着如果你把某个实体类的别名注册成string,那内置的string就会被覆盖掉,后续所有resultType="string"的地方都会解析到你的实体类上。这种问题非常隐蔽,因为代码不会报错,但运行结果完全不对。
我的建议是:自定义别名尽量用有辨识度的名字,比如加个前缀或者用完整的类名。不要图省事用str、obj这种短名,既容易和内置别名冲突,也容易和别人的代码冲突。
5. 别名解析失败的排查链路:从报错到定位
5.1 常见报错信息解读
别名解析失败的时候,MyBatis 通常会抛ClassNotFoundException或者TypeException。报错信息里会带上你写的那个别名,比如:
Cause: java.lang.ClassNotFoundException: Cannot find class: str看到这个报错,第一反应应该是:str不是内置别名,也没有注册过自定义别名。这时候你要做的是检查两件事:一是这个别名是不是拼写错了,二是如果它是自定义别名,注册配置有没有生效。
还有一种报错是TypeException: Could not resolve type alias 'xxx',这个更直接,明确告诉你别名解析不了。排查思路是一样的。
5.2 逐步排查的完整过程
我一般按这个顺序排查:
- 确认别名拼写:先看是不是把
string写成了str,把integer写成了intger。这种低级错误其实最常见。 - 确认别名是否内置:对照内置别名表,看看你写的别名在不在里面。不在的话,要么改用内置别名,要么去注册自定义别名。
- 确认自定义别名注册位置:如果是自定义别名,检查
typeAliases配置有没有放在正确的位置,包扫描的包名有没有写对。 - 确认注册时机:如果是手动配置
SqlSessionFactory,检查typeAliasesPackage是不是在构建之前设置的。 - 确认有没有冲突覆盖:如果别名看起来注册了但解析结果不对,检查是不是被同名别名覆盖了。
这个顺序是从简单到复杂,大部分问题在前两步就能解决。
5.3 一个真实的排查案例
之前有个同事遇到一个问题:他的resultType="user"一直报找不到类型,但他明明在typeAliases里注册了User类,别名写的是user。我帮他看的时候,发现他的配置是这样的:
<typeAliases> <typeAlias type="com.example.model.User" alias="User"/> </typeAliases>别名写的是大写User,但 XML 里用的是小写user。MyBatis 在注册的时候会把别名转成小写,所以注册进去的 key 是user,按理说应该能匹配上。但问题出在他用的 MyBatis 版本比较老,那个版本还没有做小写转换,导致注册的是User,解析的是user,匹配不上。
解决办法很简单:把别名统一写成小写。这件事之后,我养成了一个习惯——所有自定义别名一律小写,不管类名是什么。
6. 别名与 TypeHandler 的配合:别只看表面
6.1 别名解析之后发生了什么
resultType解析出 Class 对象之后,MyBatis 还要根据这个 Class 去找对应的TypeHandler。TypeHandler负责把 JDBC 类型和 Java 类型互相转换。比如string别名解析出String.class,然后StringTypeHandler被调用来处理结果集。
这个过程是自动的,你不需要手动指定 Handler。但理解这一点有助于排查一些奇怪的问题。比如你写resultType="date",返回的java.util.Date可能只包含日期部分,时间部分被截断了。这不是别名的问题,而是DateTypeHandler的行为——它默认只处理java.sql.Date,时间部分会丢失。如果你需要完整的时间戳,应该用resultType="java.util.Date"配合自定义 Handler,或者直接用timestamp相关的处理方式。
6.2 别名选择对结果的影响
不同的别名会触发不同的 Handler,最终影响返回值的类型和精度。举几个实际例子:
resultType="int"和resultType="long"在数值范围上没区别,但返回的 Java 类型不同,接口声明要对应上。resultType="decimal"返回BigDecimal,精度不丢失;resultType="double"返回Double,可能有精度问题。金额计算一定要用decimal。resultType="map"返回的 Map 是HashMap,列名作为 key,值作为 value。如果查询结果有空值,Map 里对应的 key 可能是 null,取的时候要注意判空。
这些细节在写代码的时候容易忽略,但出了问题往往很难查。我的建议是:涉及金额用decimal,涉及时间用明确的java.util.Date或LocalDateTime,涉及计数用int或long,不要图省事随便写。
6.3 自定义 TypeHandler 与别名的关系
如果你写了自定义TypeHandler,并且想通过别名来使用它,需要在注册 Handler 的时候指定对应的 Java 类型。别名本身不直接关联 Handler,它只是帮你找到 Class,Handler 的匹配还是靠 Class 类型。
<typeHandlers> <typeHandler handler="com.example.handler.JsonTypeHandler" javaType="com.example.model.JsonData"/> </typeHandlers>这样注册之后,当resultType解析到JsonData这个类时,MyBatis 会自动选用你的JsonTypeHandler。别名在这里的作用是让你少写包名,Handler 的选择逻辑不受影响。
7. 几个容易忽略的细节与个人经验
7.1 大小写敏感性的实际表现
前面提到过,MyBatis 在注册别名的时候会转小写,所以解析的时候也不区分大小写。但这个行为在不同版本里可能有差异。我实测过几个版本,大部分情况下resultType="STRING"和resultType="string"都能跑,但为了保险起见,还是统一用小写。
另外,自定义别名如果通过包扫描注册,默认别名是类名首字母小写,这个规则和内置别名的全小写规则不一样。比如UserVO注册成userVO,不是uservo。如果你在 XML 里写resultType="uservo",是匹配不上的。这个坑我踩过一次,后来就养成了包扫描之后先确认别名是什么的习惯。
7.2 别名在动态 SQL 中的表现
动态 SQL 里用别名和普通 SQL 没区别,resultType的解析发生在语句构建阶段,和动态标签无关。但有一种情况要注意:如果你在<if>标签里动态改变resultType,MyBatis 是不支持的。resultType必须是静态的,不能在运行时改变。
如果你确实需要根据条件返回不同类型,可以考虑用resultType="map"兜底,然后在 Java 层做转换。或者拆成多个查询方法,每个方法用不同的resultType。
7.3 我个人的使用建议
用了这么多年 MyBatis,关于别名我有几条自己的原则:
- 内置别名能用就用,但不要用太偏的。
string、int、long、decimal、date、map这几个覆盖了大部分场景,其他的用到再查。 - 自定义别名一律小写,并且加项目前缀或者用完整类名,避免冲突。
- 金额字段永远用
decimal,不要用double。 - 返回单列的时候用别名最方便,返回多列的时候老老实实写实体类或者用
map。 - 如果别名解析报错,先查拼写,再查注册,最后查冲突。这个顺序能解决九成以上的问题。
这些经验没有什么高深的理论,都是从一次次报错和排查中攒下来的。别名这个东西,平时不起眼,但真出问题的时候,如果不了解它的机制,很容易卡住。希望这篇内容能帮你把这块知识补上,下次遇到类似问题能快速定位。