一文搞懂Java动态代理:为什么Mybatis Mapper不需要实现类?

524 阅读3分钟

前言:从“接口直接调用”的疑惑说起

使用过Mybatis的开发者都知道,在Mybatis中只需定义一个Mapper接口,无需编写实现类,就能直接调用其方法执行SQL操作。例如:

public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    User getUserById(int id);
}

// 直接调用接口方法(没有实现类!)
User user = userMapper.getUserById(1);

为什么接口不需要实现类就能工作?背后的核心机制正是Java动态代理。本文将深入剖析动态代理的实现原理,并解密Mybatis Mapper的“魔法”。


一、动态代理:Java的运行时魔法

1. 什么是动态代理?

动态代理是一种在程序运行时动态生成代理对象的技术,无需预先编写实现类。代理对象会拦截对目标方法的调用,并委托给InvocationHandler处理。

2. 两种实现方式

方式原理特点
JDK动态代理基于接口,利用ProxyInvocationHandler只能代理接口
CGLIB动态代理通过继承目标类,生成子类代理可代理类,需避免final方法

3. JDK动态代理示例

public interface HelloService {
    void sayHello();
}

public class JdkProxyDemo {
    public static void main(String[] args) {
        HelloService proxy = (HelloService) Proxy.newProxyInstance(
                HelloService.class.getClassLoader(),
                new Class[]{HelloService.class},
                (proxy1, method, args1) -> {
                    System.out.println("Before method call");
                    return null; // 实际调用逻辑
                });
        proxy.sayHello(); // 输出:Before method call
    }
}

关键点

  • 运行时生成名为$Proxy0的代理类
  • 调用方法时转发到InvocationHandler.invoke()

二、Mybatis Mapper的代理实现

1. Mybatis的核心流程

sequenceDiagram
    participant Client as 客户端
    participant MapperProxy as Mapper代理对象
    participant SqlSession as SqlSession
    participant Executor as 执行器
    participant DB as 数据库

    Client->>MapperProxy: 调用Mapper方法
    MapperProxy->>SqlSession: 转换方法调用
    SqlSession->>Executor: 执行SQL
    Executor->>DB: 发送SQL请求
    DB-->>Executor: 返回结果
    Executor-->>SqlSession: 处理结果
    SqlSession-->>MapperProxy: 返回映射对象
    MapperProxy-->>Client: 返回最终结果

2. 源码级解析

Mybatis通过MapperProxyFactory创建代理对象:

public class MapperProxyFactory<T> {
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),
                new Class[]{mapperInterface},
                mapperProxy);
    }
}

拦截逻辑核心代码MapperProxy.invoke()):

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 处理默认方法(Java 8+)
    if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
    }
    
    // 将方法转换为MappedStatement
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

三、关键设计剖析

1. 方法签名到SQL的映射

Mybatis通过以下方式建立方法-SQL映射:

  • XML映射文件<select id="getUserById">对应方法名
  • 注解方式@Select("SELECT ...")直接注解方法
  • 参数处理#{param}与方法参数绑定

2. 为什么不需要实现类?

  • 动态代理生成虚拟实现:代理对象在运行时处理所有方法调用
  • 方法签名即契约:方法名、参数、返回类型包含所有必要信息
  • SQL与代码解耦:SQL通过XML/注解配置,不硬编码在Java中

四、实战:手写简化版Mybatis代理

1. 定义核心组件

public class MybatisMiniProxy {
    public static <T> T getMapper(Class<T> mapperInterface) {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),
                new Class[]{mapperInterface},
                (proxy, method, args) -> {
                    // 解析SQL注解
                    Select select = method.getAnnotation(Select.class);
                    String sql = select.value()[0];
                    
                    // 模拟执行SQL
                    System.out.println("Execute SQL: " + sql);
                    System.out.println("Parameters: " + Arrays.toString(args));
                    
                    // 返回模拟对象
                    return new User(1, "test");
                });
    }
}

2. 使用示例

public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    User getUserById(int id);
}

public class Demo {
    public static void main(String[] args) {
        UserMapper mapper = MybatisMiniProxy.getMapper(UserMapper.class);
        User user = mapper.getUserById(1);
        System.out.println(user); // 输出User对象
    }
}

五、延伸思考与最佳实践

1. 动态代理的局限性

  • 接口新增方法时需要更新映射配置
  • 调试困难(代理类不可见)
  • 性能损耗(需权衡CGLIB与JDK代理)

2. Mybatis使用注意事项

  • Mapper接口扫描:确保@MapperScan配置正确
  • 方法签名严格匹配:参数名需与SQL占位符一致
  • 避免重载方法:可能导致映射歧义

结语:代理模式的力量

Mybatis通过动态代理将接口定义SQL实现解耦,实现了:

  • 声明式编程:只需关注What(要做什么),不用管How(如何做)
  • 架构灵活性:SQL可独立维护,支持热更新
  • 代码简洁性:消灭了传统DAO层的样板代码

理解这一设计,不仅能够更好地使用Mybatis,也为理解Spring等框架的AOP实现打下坚实基础。动态代理作为Java高级特性,堪称架构设计中“隐藏的瑞士军刀”。