mybaits接口绑定机制实现原理

132 阅读4分钟

MyBatis 动态代理生成实现类详解

MyBatis 通过 动态代理 技术将 Mapper 接口与 SQL 映射绑定,开发者只需定义接口,无需手动编写实现类。以下是动态代理生成实现类的核心机制和流程解析:

一、动态代理的核心类

MyBatis 的动态代理实现主要依赖以下核心类:

类名作用
MapperProxy实现 InvocationHandler 接口,负责拦截接口方法调用并路由到 SQL 执行逻辑。
MapperProxyFactory工厂类,用于创建 MapperProxy 实例。
MapperRegistry管理所有 Mapper 接口与 MapperProxyFactory 的注册关系。
SqlSession提供 getMapper() 方法,触发动态代理对象的创建。

二、动态代理生成流程

以下为动态代理对象生成的核心步骤:

1. 注册 Mapper 接口
  • 初始化阶段:MyBatis 解析 mybatis-config.xml 和 Mapper 文件时,将接口的 Class 对象注册到 MapperRegistry

  • 关键源码(简化):

    public class MapperRegistry {
        private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
        
        public <T> void addMapper(Class<T> type) {
            knownMappers.put(type, new MapperProxyFactory<>(type));
        }
    }
    
2. 创建代理对象
  • 调用 SqlSession.getMapper()

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
  • 底层流程

    1. SqlSessionMapperRegistry 获取 MapperProxyFactory
    2. MapperProxyFactory 创建 MapperProxy 实例。
    3. 通过 Proxy.newProxyInstance() 生成代理对象。
3. 方法拦截与路由
  • MapperProxy.invoke() :代理对象拦截接口方法调用,执行以下逻辑:

    1. 判断是否为默认方法:Java 8+ 的默认方法直接调用,不代理。
    2. 缓存方法签名:将方法名、参数类型等转换为 MapperMethod(避免重复解析)。
    3. 执行 SQL:调用 MapperMethod.execute(),触发 SQL 解析和执行。

源码简析

public class MapperProxy<T> implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 处理默认方法
        if (method.isDefault()) {
            return invokeDefaultMethod(proxy, method, args);
        }
        // 转换为 MapperMethod
        MapperMethod mapperMethod = cachedMapperMethod(method);
        // 执行 SQL
        return mapperMethod.execute(sqlSession, args);
    }
}

三、方法签名与 SQL 映射的绑定

1. 方法签名解析
  • 方法唯一标识:由 接口全限定名 + 方法名 + 参数类型列表 组成。

  • 示例

    public interface UserMapper {
        User selectUserById(int id);           // 方法签名: com.example.UserMapper.selectUserById(int)
        List<User> selectUsersByName(String name); 
    }
    
2. SQL 语句查找
  • MappedStatement:MyBatis 在初始化阶段会将每个 SQL 标签(如 <select>)解析为 MappedStatement,并存储在 Configuration 对象中。

  • 查找逻辑

    String statementId = interfaceName + "." + methodName; // 如 "com.example.UserMapper.selectUserById"
    MappedStatement ms = configuration.getMappedStatement(statementId);
    
3. 参数与 SQL 绑定
  • 参数转换:通过 ParamNameResolver 解析方法参数名,并绑定到 SQL 的 #{param} 占位符。
  • 动态 SQL 处理:根据参数值动态生成最终 SQL(如 <if> 标签条件判断)。

四、动态代理的底层实现

1. JDK 动态代理 vs CGLIB
  • JDK 动态代理

    • MyBatis 使用 JDK 动态代理,仅支持接口代理
    • 代理对象实现 Mapper 接口,并持有 InvocationHandlerMapperProxy)。
  • 为何不用 CGLIB

    • Mapper 接口本身不需要实现类,CGLIB 代理类的方式不适用。
2. 代理对象示例
  • 生成的代理类(伪代码):

    public class $Proxy123 implements UserMapper {
        private InvocationHandler handler;
    ​
        public $Proxy123(InvocationHandler handler) {
            this.handler = handler;
        }
    ​
        public User selectUserById(int id) {
            Method method = UserMapper.class.getMethod("selectUserById", int.class);
            return (User) handler.invoke(this, method, new Object[]{id});
        }
    }
    

五、性能优化

1. 方法缓存
  • MapperMethod 缓存:每个方法对应的 MapperMethod 仅创建一次,避免重复解析方法签名。
  • 缓存位置MapperProxy 内部维护 Map<Method, MapperMethod>
2. 延迟加载
  • SqlSession 延迟绑定:代理对象在执行 SQL 时才会从 SqlSession 中获取数据库连接。

六、示例:动态代理全流程

1. 定义接口
public interface UserMapper {
    User selectUserById(int id);
}
2. XML 映射
<mapper namespace="com.example.UserMapper">
    <select id="selectUserById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>
3. 代理对象执行流程
// 获取代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
​
// 调用方法(触发动态代理)
User user = userMapper.selectUserById(1);
​
// 底层执行步骤:
// 1. MapperProxy 拦截方法调用,获取方法签名 "com.example.UserMapper.selectUserById"
// 2. 查找对应的 MappedStatement
// 3. 执行 SQL 并返回结果

七、注意事项

  1. 接口方法必须与 SQL 映射严格匹配

    • 若接口方法未在 XML 或注解中定义 SQL,启动时将抛出 BindingException
  2. 方法重载不支持

    • MyBatis 通过方法名唯一标识 SQL,不支持同一接口中的方法重载。
  3. 默认方法的限制

    • 默认方法(Java 8+)不会被代理,需手动调用其他方法。

总结

MyBatis 通过 JDK 动态代理 动态生成 Mapper 接口的代理对象,核心流程包括接口注册、代理对象创建、方法拦截与 SQL 执行。代理对象通过 MapperProxy 将方法调用路由到对应的 SQL 语句,结合 MappedStatementSqlSession 完成数据库操作。该机制实现了 接口与 SQL 的解耦,开发者只需关注接口定义和 SQL 编写,无需手动维护实现类,显著提升开发效率。