ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MyBatis Reflector 反射工具箱源码解析:JavaBean 属性元数据的核心引擎

2026/9/20 13:03:15 拓冰建站 浏览量
MyBatis Reflector 反射工具箱源码解析:JavaBean 属性元数据的核心引擎 MyBatis Reflector 反射工具箱源码解析JavaBean 属性元数据的核心引擎【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunterMyBatis 的 ORM 能力高度依赖反射机制将结果集列值写入实体属性、读取实体属性绑定 SQL 参数、解析user.name这类嵌套属性表达式都离不开一套对 JavaBean 元数据的高效封装。org.apache.ibatis.reflection.Reflector正是这套机制的基石它把一个类的构造方法、getter/setter 方法、字段、可读写属性一次性解析并缓存供MetaClass、MetaObject、ObjectWrapper等上层组件直接复用。阅读本篇你将掌握 Reflector 的字段设计与构造流程、方法收集与去重算法、getter 冲突解决策略以及为什么 MyBatis 要求实体类必须提供无参构造方法。本文以仓库文档 Mybatis-Reflector.md 为骨架结合 反射工具箱和TypeHandler系列 中关于ReflectorFactory、ObjectFactory的讲解以及MetaObject、ObjectWrapper的调用链给出源码级解读。Reflector 类结构与字段设计MyBatis 的反射相关内容统一存放在org.apache.ibatis.reflection包下Reflector是该包的核心类。先看它的字段定义public class Reflector { /** 实体类.class */ private final Class? type; /** 可读属性 */ private final String[] readablePropertyNames; /** 可写属性值 */ private final String[] writablePropertyNames; /** set 方法列表 */ private final MapString, Invoker setMethods new HashMap(); /** get 方法列表 */ private final MapString, Invoker getMethods new HashMap(); /** set 的数据类型 */ private final MapString, Class? setTypes new HashMap(); /** get 的数据类型 */ private final MapString, Class? getTypes new HashMap(); /** 构造函数 */ private Constructor? defaultConstructor; /** 缓存数据, 大写KEY */ private MapString, String caseInsensitivePropertyMap new HashMap(); }这些字段可以分成四类理解字段类型职责typeClass?被解析的 JavaBean 字节码对象readablePropertyNames/writablePropertyNamesString[]可读有 getter与可写有 setter的属性名数组setMethods/getMethodsMapString, Invoker属性名到 setter/getter 调用器Invoker的映射setTypes/getTypesMapString, Class?属性名到其 setter/getter 参数返回类型的映射defaultConstructorConstructor?无参构造方法caseInsensitivePropertyMapMapString, String大写属性名到原始属性名的映射用于大小写不敏感查找值得注意的设计点这里没有直接缓存Method对象而是缓存Invoker。Invoker是org.apache.ibatis.reflection.invoker包下的抽象有MethodInvoker、GetFieldInvoker、SetFieldInvoker等实现统一封装了通过方法反射调用与直接读写字段两种路径上层调用方无需关心属性背后是方法还是字段。构造流程一次解析六步完成Reflector的构造方法接收一个类的字节码在构造过程中完成全部元数据解析public Reflector(Class? clazz) { type clazz; // 1. 解析无参构造方法 addDefaultConstructor(clazz); // 2. 解析所有 getter 方法 addGetMethods(clazz); // 3. 解析所有 setter 方法 addSetMethods(clazz); // 4. 兜底为没有 getter/setter 的字段生成字段级读写 addFields(clazz); // 5. 由 getMethods/setMethods 的 keySet 生成可读/可写属性名数组 readablePropertyNames getMethods.keySet().toArray(new String[0]); writablePropertyNames setMethods.keySet().toArray(new String[0]); // 6. 构建大小写不敏感映射统一使用 Locale.ENGLISH 转大写 for (String propName : readablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } for (String propName : writablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } }解析顺序体现了 MyBatis 的设计思路先方法后字段方法优先于字段。如果一个属性既有 setter 方法又有同名 public 字段以 setter 为准字段解析只是对没有对应 getter/setter的属性做兜底。解析完成后可读属性名数组与可写属性名数组分别来源于getMethods和setMethods的 keySet这也意味着只有 getter 没有 setter 的属性是只读的反之是只写的。addDefaultConstructor无参构造方法的获取addDefaultConstructor从类的所有声明构造方法中过滤出参数长度为 0 的那一个private void addDefaultConstructor(Class? clazz) { // 获取类里面的所有构造方法 Constructor?[] constructors clazz.getDeclaredConstructors(); // 过滤得到空参构造 constructor - constructor.getParameterTypes().length 0 Arrays.stream(constructors).filter(constructor - constructor.getParameterTypes().length 0) .findAny().ifPresent(constructor - { this.defaultConstructor constructor; }); }注意这里使用的是getDeclaredConstructors()它返回本类声明的全部构造方法包括 private而不是getConstructors()只返回 public 构造方法。配合上层的Reflector.canControlMemberAccessible()判断MyBatis 在 Java 9 模块化环境下同样可以控制非 public 构造方法的可访问性。配套的getDefaultConstructor()方法则负责安全取出该构造方法这也是为什么实体类必须写无参构造的根因public Constructor? getDefaultConstructor() { if (defaultConstructor ! null) { return defaultConstructor; } else { // 如果没有空参构造抛出的异常 throw new ReflectionException(There is no default constructor for type); } }当实体类只声明了带参构造方法而没有显式写出无参构造时defaultConstructor为 null任何需要实例化该实体的操作例如 ResultSetHandler 创建结果对象都会抛出ReflectionException(There is no default constructor for ...)。这正是 MyBatis 官方要求实体类保留无参构造的原因也是很多初学者遇到的 There is no default constructor 异常的来源。下面的调试截图展示了断点停在addDefaultConstructor时constructors数组中People类的两个构造方法其中无参构造的parameterTypes长度为 0addGetMethods 与 PropertyNamergetter 方法收集addGetMethods负责收集类中所有 getter 方法其判定标准有两个无参且方法名符合 getter 命名规范private void addGetMethods(Class? clazz) { // 反射方法 MapString, ListMethod conflictingGetters new HashMap(); Method[] methods getClassMethods(clazz); // JDK8 filter 过滤get 开头的方法 Arrays.stream(methods).filter(m - m.getParameterTypes().length 0 PropertyNamer.isGetter(m.getName())) .forEach(m - addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m)); resolveGetterConflicts(conflictingGetters); }其中是否符合 getter 命名规范由org.apache.ibatis.reflection.property.PropertyNamer判定public static boolean isGetter(String name) { // 在语义上 is 开头的也是get开头的 return (name.startsWith(get) name.length() 3) || (name.startsWith(is) name.length() 2); }两点细节get开头的方法名长度必须大于 3即get之后还必须有属性名排除get本身is开头的方法名长度必须大于 2用于兼容 boolean 属性的isXxx()命名。PropertyNamer.methodToProperty()负责把方法名转换为属性名例如getName()→name、isActive()→active。同一个属性可能同时存在getName()和isName()如getName()与isName()返回类型不同这些方法会先按属性名分组存入conflictingGettersMapString, ListMethod再由resolveGetterConflicts统一解决冲突。addSetMethods的收集逻辑与 getter 侧对称使用PropertyNamer.isSetter()判定set开头且长度大于 3并把setMethods与setTypes填充完整。getClassMethods全类可见方法收集与去重getClassMethods是 getter/setter 收集的底层支撑它把类自身、所有父类、所有接口中的可见方法统一收集起来并去重private Method[] getClassMethods(Class? clazz) { // 方法唯一标识: 方法 MapString, Method uniqueMethods new HashMap(); Class? currentClass clazz; while (currentClass ! null currentClass ! Object.class) { // getDeclaredMethods 获取 public, private, protected 方法 addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods()); // we also need to look for interface methods - // because the class may be abstract // 当前类是否继承别的类(实现接口)如果继承则需要进行操作 Class?[] interfaces currentClass.getInterfaces(); for (Class? anInterface : interfaces) { // getMethods 获取本身和父类的 public 方法 addUniqueMethods(uniqueMethods, anInterface.getMethods()); } // 循环往上一层一层寻找最后回到 Object 类的上级为 null 结束 currentClass currentClass.getSuperclass(); } CollectionMethod methods uniqueMethods.values(); return methods.toArray(new Method[0]); }循环从当前类开始向上遍历到Object为止Object本身的方法不参与收集因为Object的方法不是 JavaBean 属性访问器当前类使用getDeclaredMethods()能拿到 public/private/protected 所有可见方法接口使用getMethods()因为接口方法天然是 public每次循环用currentClass.getSuperclass()向上提升一级确保父类方法也被纳入之所以要遍历接口是因为抽象类可能未实现接口方法但子类需要继承这些方法签名。addUniqueMethods桥接方法过滤单个类或接口的方法通过addUniqueMethods写入集合private void addUniqueMethods(MapString, Method uniqueMethods, Method[] methods) { for (Method currentMethod : methods) { // 桥接方法 if (!currentMethod.isBridge()) { // 方法的唯一标识 String signature getSignature(currentMethod); // check to see if the method is already known // if it is known, then an extended class must have overridden a method if (!uniqueMethods.containsKey(signature)) { uniqueMethods.put(signature, currentMethod); } } } }这里过滤了桥接方法bridge method。桥接方法是 Java 编译器在泛型擦除时为保持多态而自动生成的合成方法如List的泛型子类会生成Object签名的桥接方法它们与业务方法签名重复必须剔除。注释 if it is known, then an extended class must have overridden a method 揭示了去重的目的当父类方法被子类重写后getDeclaredMethods()会同时返回父子两个方法通过签名去重可以保证子类实现优先后写入覆盖先写入。getSignature方法唯一签名方法签名的生成规则是返回值类型#方法名称:参数列表/** * 方法唯一标识,返回值类型#方法名称参数列表 */ private String getSignature(Method method) { StringBuilder sb new StringBuilder(); Class? returnType method.getReturnType(); if (returnType ! null) { sb.append(returnType.getName()).append(#); } sb.append(method.getName()); Class?[] parameters method.getParameterTypes(); for (int i 0; i parameters.length; i) { sb.append(i 0 ? : : ,).append(parameters[i].getName()); } return sb.toString(); }签名格式示例java.lang.String#toString无参方法和void#setName:java.lang.String带参方法。多参数时以逗号分隔如void#setUser:java.lang.Integer,java.lang.String。返回值类型参与签名意味着同名同参不同返回值的方法会被视为不同签名——这与 JVM 方法签名的规范一致也为后续resolveGetterConflicts处理返回值冲突埋下伏笔。仓库文档中的调试记录展示了完整过程解析People类时uniqueMethods最终包含toString、setName、getName等方法的签名映射而让Man extends People implements TestManInterface后循环先收集Man自身的方法hello()、inte()随后currentClass上移到People最终uniqueMethods中父类方法也全部就位验证了继承链与接口方法都会被完整收集。resolveGetterConflictsgetter 冲突解决一个属性可能对应多个候选 getter例如继承体系里父类返回Object、子类返回String的同名方法或getXxx()与isXxx()并存。resolveGetterConflicts负责在这些候选中挑选唯一的胜出者private void resolveGetterConflicts(MapString, ListMethod conflictingGetters) { for (EntryString, ListMethod entry : conflictingGetters.entrySet()) { Method winner null; String propName entry.getKey(); boolean isAmbiguous false; for (Method candidate : entry.getValue()) { if (winner null) { winner candidate; continue; } Class? winnerType winner.getReturnType(); Class? candidateType candidate.getReturnType(); if (candidateType.equals(winnerType)) { if (!boolean.class.equals(candidateType)) { // 同类型且非 boolean无法区分标记歧义 isAmbiguous true; break; } else if (candidate.getName().startsWith(is)) { // boolean 类型isXxx 命名优先于 getXxx winner candidate; } } else if (candidateType.isAssignableFrom(winnerType)) { // OK getter type is descendant // 候选类型是胜者类型的父类保留更具体的胜者 } else if (winnerType.isAssignableFrom(candidateType)) { // 候选类型更具体候选胜出 winner candidate; } else { // 两者互不兼容标记歧义 isAmbiguous true; break; } } addGetMethod(propName, winner, isAmbiguous); } }冲突解决的三条规则返回类型相同非 boolean 类型视为歧义冲突抛ReflectionExceptionIllegal overloaded getter method with ambiguous type for property ...boolean 类型则优先选择is开头的方法返回类型存在继承关系始终选择更具体的子类型isAssignableFrom判定因为子类型提供的信息更精确返回类型互不兼容判定为歧义冲突抛出异常。addGetMethod(propName, winner, isAmbiguous)会把胜出方法的Method包装成MethodInvoker放入getMethods把返回类型放入getTypes若isAmbiguous为 true 则直接抛出ReflectionException。这就是当你在类里写了Object getX()和String getX()两个同名 getter 时MyBatis 启动阶段就会报错的底层原因。addFields字段兜底解析经过方法解析后仍有一部分属性既没有 getter 也没有 setter例如使用字段直接赋值的场景。addFields对字段做兜底处理并且递归处理父类字段private void addFields(Class? clazz) { Field[] fields clazz.getDeclaredFields(); for (Field field : fields) { if (!setMethods.containsKey(field.getName())) { // issue #379 - removed the check for final because JDK 1.5 allows // modification of final fields through reflection (JSR-133). (JGB) // pr #16 - final static can only be set by the classloader int modifiers field.getModifiers(); if (!(Modifier.isFinal(modifiers) Modifier.isStatic(modifiers))) { addSetField(field); } } if (!getMethods.containsKey(field.getName())) { addGetField(field); } } if (clazz.getSuperclass() ! null) { addFields(clazz.getSuperclass()); } }关键决策点只有当setMethods/getMethods中不存在该属性名时才为字段生成字段级读写保证方法优先字段级 setter 排除了final static字段pr #16注释final static 只能由类加载器设置注释同时引用了issue #379JDK 1.5JSR-133允许通过反射修改 final 实例字段因此这里没有排除 final 字段通过clazz.getSuperclass()递归父类的私有字段同样被纳入GetFieldInvoker/SetFieldInvoker在需要时设置setAccessible(true)。属性查看与大小写不敏感映射解析完成后一个Reflector实例便完整承载了目标类的元数据。仓库文档给出的解析结果截图目标类Man展示了最终状态readablePropertyNames包含name、listwritablePropertyNames仅含name—— 说明list只有 getter 没有 setter是只读属性setMethods/getMethods中存储的是MethodInvokercaseInsensitivePropertyMap同时保存NAME→name、LIST→list的大小写映射。caseInsensitivePropertyMap的作用在 ORM 映射中非常重要数据库列名通常不区分大小写而 Java 属性名遵循驼峰命名。当resultMap或自动映射中的列名大小写与属性名不完全一致时findProperty会先按原始名查找失败后利用caseInsensitivePropertyMap实现大小写不敏感的匹配public String findPropertyName(String name) { return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH)); }该方法在MetaClass中经由findProperty(String name, boolean useCamelCaseMapping)暴露给上层使用。ReflectorFactory反射元数据的缓存工厂每次new Reflector(clazz)都伴随着全量反射扫描成本不低。因此 MyBatis 提供了ReflectorFactory接口与默认实现DefaultReflectorFactory对解析结果做缓存public interface ReflectorFactory { boolean isClassCacheEnabled(); void setClassCacheEnabled(boolean classCacheEnabled); /** 通过 JavaBean 的 clazz 获取该 JavaBean 对应的 Reflector */ Reflector findForClass(Class? type); } public class DefaultReflectorFactory implements ReflectorFactory { private boolean classCacheEnabled true; /** 大部分容器及工厂设计模式的管用伎俩key 为 JavaBean 的 clazzvalue 为对应的 Reflector 实例 */ private final ConcurrentMapClass?, Reflector reflectorMap new ConcurrentHashMap(); Override public Reflector findForClass(Class? type) { if (classCacheEnabled) { // synchronized (type) removed see issue #461 return reflectorMap.computeIfAbsent(type, Reflector::new); } else { return new Reflector(type); } } // ...isClassCacheEnabled / setClassCacheEnabled 的实现 }设计要点reflectorMap采用ConcurrentHashMap天然线程安全computeIfAbsent(type, Reflector::new)是若不存在则计算并缓存的原子操作注释issue #461说明曾用synchronized(type)后因死锁风险移除classCacheEnabled开关允许关闭缓存如某些动态生成类的场景关闭后每次findForClass都直接new Reflector(type)接口 默认实现 可继承扩展仓库文档展示了CustomReflectorFactory extends DefaultReflectorFactory的自定义方式是 MyBatis 一贯的扩展设计。ReflectorFactory实例由Configuration持有configuration.getReflectorFactory()并通过MetaClass、MetaObject逐层传递全框架共享同一份反射缓存。上层应用MetaClass、MetaObject 与 ObjectWrapperReflector并不直接暴露给业务代码而是被上层三个组件消费MetaClass静态工具类MetaClass.forClass(clazz, reflectorFactory)内部reflectorFactory.findForClass(clazz)获取Reflector再转发findProperty、getGetterNames、getSetInvoker、getGetInvoker等方法用于描述类的元信息ObjectWrapperBeanWrapper包装 JavaBean、MapWrapper包装 Map、CollectionWrapper包装集合的抽象接口见 Mybatis-ObjectWrapper。其中BeanWrapper内部持有MetaClass其getBeanProperty/setBeanProperty正是通过metaClass.getGetInvoker(...)/getSetInvoker(...)取出Reflector缓存的Invoker完成属性读写MetaObjectMetaObject.forObject(...)根据对象类型选择ObjectWrapper实现见 Mybatis-MetaObject并对外提供getValue(a.b.c)、setValue(a.b.c, value)这类支持嵌套路径与索引访问的 API是 MyBatis 处理#{user.name}、list[0].id等表达式的基础。ObjectFactory实例化的另一块拼图Reflector只负责发现无参构造方法真正调用构造方法实例化对象的是ObjectFactory。默认实现DefaultObjectFactory.instantiateClass(...)的逻辑是无参时直接type.getDeclaredConstructor().newInstance()带参时根据constructorArgTypes匹配构造方法并传入constructorArgs。若遇到IllegalAccessException如非 public 构造会先判断Reflector.canControlMemberAccessible()再决定是否setAccessible(true)重试。完整的ObjectFactory接口与DefaultObjectFactory实现可参考 反射工具箱和TypeHandler系列。ReflectorObjectFactory的分工可以总结为Reflector 负责静态元数据有哪些构造、哪些属性、怎么读写ObjectFactory 负责动态实例化怎么 new 出对象。二者配合MyBatis 才能在没有 Spring 容器介入的情况下从一行 SQL 结果集里凭空创建出完整的业务对象。实践总结从源码到日常开发结合整条调用链几个可以直接指导日常开发的结论实体类务必保留无参构造ResultSetHandler创建结果对象、DefaultObjectFactory.create()实例化对象都依赖Reflector.getDefaultConstructor()缺失即抛ReflectionException同名同参、返回类型冲突的 getter 会导致启动失败resolveGetterConflicts只允许返回类型存在继承关系或boolean 的 isXxx 与 getXxx 并存两种情形其余一律抛异常boolean 属性推荐isXxx()命名冲突时is开头的方法优先被选中字段级读写是兜底方案private字段即使没有 getter/setter也会被GetFieldInvoker/SetFieldInvoker通过setAccessible(true)接管这也是 MyBatis 允许字段映射如resultMap直接映射字段的底层支持属性名大小写不敏感映射配置中的属性名与实际 JavaBean 属性名大小写不一致时caseInsensitivePropertyMap会兜底匹配解析结果全局缓存DefaultReflectorFactory的ConcurrentHashMap保证了每个类只解析一次这就是为什么 MyBatis 大量使用反射却依然能保持启动与执行性能的重要原因。如果你想进一步追踪这套反射工具箱在框架中的实际调用位置可以顺藤摸瓜阅读 Mybatis-MetaObject属性读写入口、Mybatis-ObjectWrapperBean/Map/Collection 三种包装以及 反射工具箱和TypeHandler系列工厂与类型转换完整串起 MyBatis 基础支持层的反射脉络。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考