我说JDK动态代理+Class<T> = mybatis!谁赞成?谁反对?

95 阅读7分钟

本文主要有以下内容:

  • JDK动态代理实现
  • 动态代理+Class <T>模拟Mybatis是如何代理Mapper接口的?

动态代理

动态代理:是Java提供的一种机制,它允许在运行时创建一个代理对象,该代理对象能够拦截方法调用。动态代理分为JDK动态代理和CGLIB代理。

  • JDK动态代理:只能代理实现了接口的类。它通过Java的反射机制为目标对象生成代理类,在运行时将方法调用委托给InvocationHandler

  • CGLIB代理:通过继承目标类来创建代理对象。CGLIB使用的是底层的字节码操作(ASM库),通过生成目标类的子类并覆盖其中的方法来实现代理。CGLIB不要求目标类实现接口。

本文主要讲述JDK动态代理,要实现JDK代理需要如下的步骤:

  • 定义一个接口、该接口为被代理的对象
  • 实现定义的接口
  • 自定义handler类、此类需要实现InvocationHandler,重写invoke方法。
  • 创建代理对象

接下来就按着上述步骤一步一步的实现。首先定一个接口,为了后续行文方便则定义为UserMapper


public interface UserMapper {
    String findUserNameById(Integer userId);
}

接着实现这个接口:

public class UserMapperImpl implements UserMapper{
    @Override
    public String findUserNameById(Integer userId) {
        return "晚风也很温柔_";
    }
}

实现InvokeHandler,代码如下:

public class UserMapperInvokerHandler implements InvocationHandler {
    // 被代理的对象
    private Object target;
    public UserMapperInvokerHandler(Object target){
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("即将执行" + method.getName() + "方法");
        String result = (String) method.invoke(target, args);
        System.out.println("执行完" + method.getName() + "方法,返回结果为:" + result);
        return result;
    }
}

这个类的主要作用是在绑定执行目标以及在执行方法之前可以做一些增强逻辑。接着就创建代理对象。

  • proxy:代表创建的代理对象,即下面的proxyMapper对象
  • method:代表执行的方法,即findUserNameById()
  • args():代表方法的参数,即1
public static void main(String[] args) {
    UserMapper mapper = new UserMapperImpl();
    UserMapperInvokerHandler handler = new UserMapperInvokerHandler(mapper);
    UserMapper proxyMapper = (UserMapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(),
            new Class[]{UserMapper.class}, handler);
    System.out.println(proxyMapper.findUserNameById(1));
}

运行代码,结果如下图所示:

动态代理_Class<T>_JDK动态代理运行结果图.png

模拟MyBatis功能实现

Class类介绍

Class类是对java类的一个封装类,可以理解为一个java类可以被解析为一个Class的实例对象。比如说我们用User类描述用户,Dog类去描述小狗,Class类就是描述User类和Dog类的一个类。即它的一个实例代表了Java应用中的一个类或接口的结构信息。所有的类和接口在运行时都由一个Class对象来表示。通过Class类,Java程序可以获取有关类的元信息(成员变量、成员方法等),并通过反射机制可以进行动态操作,创建实例,调用方法等。

MyBatis

mybatis这个框架不管是初学者还是已经工作了的人,都知道是怎么使用的、大抵分为如下三步:

  • 定义对于mapper层接口
  • 编写xml文件中的sql
  • 在需要使用的地方依赖注入接口即可。

我不是标题党,但是用一篇文章实现mybatis的功能是不现实的,本文只实现其核心功能,即通过动态代理的方式去模拟调用接口方法的过程。因为我们在使用的时候,并没有按照上述的例子为每一个Mapper接口增加一个实现类!

首先分析一下工作:

  • 接口的定义会变吗?答案是否定的。
  • 在使用mybatis的实现我们没有增加mapper层接口的实现类。所以实现类我们不需要。
  • handler:肯定需要实现的,因为JDK动态代理依赖InvocationHandler的实现,即需要改造UserMapperInvokerHandler

首先是改造UserMapperInvokerHandler,为了让具有代理不同接口的能力,那么就需要引入泛型,为了方便理解,可以把Class类理解一个容器,如果不使用泛型,则创建代理对象时可以任意对象,比如我想要的是UserMapper,但是在使用时转型为DogMapper,那么就存在了问题。

public class ClassInvocationHandler implements InvocationHandler {
    private Class target;
    public ClassInvocationHandler(Class target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("执行了" + method.getName() + "方法");
        return null;
    }

    public Object newInstance() {
        return Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, this);
    }

    public static void main(String[] args) {
        ClassInvocationHandler handler = new ClassInvocationHandler(UserMapper.class);
        DogMapper mapper = (DogMapper) handler.newInstance();
    }
}

类似于这样的情况就会发生,从而造成bug。增加泛型、也就是增加了在编译期间的类型检查、可以有效的避免上述情况。修改的代码如下:

public class ProxyMapperHandler<T> implements InvocationHandler {

    private Class<T> interfaceClass;

    public ProxyMapperHandler(Class<T>  interfaceClass) {
        this.interfaceClass = interfaceClass;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("即将执行" + method.getName() + "方法");
        System.out.println(method.getName() + "被代理了");
        System.out.println("模拟方法执行完毕");
        return null;
    } 
}

在上面的示例中,我们在main方法里面手动创建了代理对象,这不优雅,在实际的使用过程中我们对这一步骤也是无感的,我们没显示的使用这一步,肯定是框架在其他地方帮我们做了,因此可以模拟使用如下的方式去创建handler对象。

public class ProxyMapperFactory<T> {
    private final Class<T> mapperInterface;
    public ProxyMapperFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    public T newInstance() {
        final ProxyMapperHandler<T> mapperProxy = new ProxyMapperHandler<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
                new Class[]{mapperInterface}, mapperProxy);
    }
}

新增加一个DogMapper接口:

public interface DogMapper {
    String findDogNameBy();
}

测试如下代码:

public static void main(String[] args) {
    DogMapper dogMapper = new ProxyMapperFactory<>(DogMapper.class).newInstance();
    dogMapper.findDogName();
    dogMapper.getDogAge();
    UserMapper userMapper = new ProxyMapperFactory<>(UserMapper.class).newInstance();
    userMapper.findUserNameById(1);
}

在这里就没有使用类型转换,这就是因为在newInstance方法里面返回时,通过泛型限定了返回类型。

动态代理_Class<T>_JDK动态代理运行dogMapperUserMapper结果图.png

可以看到结果符合预期,接下来就回到invoke这个方法里面:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    System.out.println("即将执行" + method.getName() + "方法");
    System.out.println(method.getName() + "被代理了");
    System.out.println("模拟方法执行完毕");
    return null;
  
}

可以看到这里并没有去调用method.invoke()方法,这里一切都是假的,而在使用框架的使用,这一部分的逻辑肯定是要去执行对应的方法。接下来就是把这一部分变为真的。

回顾一下MyBatis的使用流程,在mapper接口里面定义方法之后,在xml文件中的标签id要和其保持一致且在同一个namespace空间下唯一。因此我们可以推测,mybatis框架使用一个map,其keynamespace+id,其值为对这个标签的封装。因此可以模拟创建一个map,模拟mybatisxml文件解析的封装。

HashMap<String, String> sqlSessopn = new HashMap<>();

sqlSessopn.put("com.basic.java.reflect.tutorial.intf.DogMapper.findDogName","select dog_name from dog");
sqlSessopn.put("com.basic.java.reflect.tutorial.intf.DogMapper.getDogAge","select dog_age from dog");
sqlSessopn.put("com.basic.java.reflect.tutorial.intf.UserMapper.findUserNameById","select user_name from user where user_id = ?");

除此之外,我们知道在Java的反射中提供了一个Method类封装Java类中的方法,基于此我们是不是也可以使用一个类完成对CURD标签的封装。代码如下:

public class MapperMethod {
  
private final SqlCommand command;
public MapperMethod(SqlCommand command) {
    this.command = command;
}

public Object execute(Map<String, String> sqlSession, Object[] args) {
    switch (command.getType()) {
        case 1:
            System.out.println(sqlSession.get(command.getName()));
            break;
        case 2:
            System.out.println(sqlSession.get(command.getName()));
            break;
        case 3:
            System.out.println(sqlSession.get(command.getName()));
            break;
        case 4:
            System.out.println(sqlSession.get(command.getName()));
            break;
        default:
            return null;
    }
    return null;
}

public static class SqlCommand {

    private final String name;

    /**
     * 1:insert
     * 2:select
     * 3:update
     * 4:delete
     */
    private final Integer type;

    public SqlCommand(Class<?> mapperInterface, Method method) {
        String statementName = mapperInterface.getName() + "." + method.getName();
        if (statementName.contains("insert")) {
            this.name = statementName;
            this.type = 1;
        } else if (statementName.contains("select") || statementName.contains("find") || statementName.contains("get")) {
            this.name = statementName;
            this.type = 2;
        } else if (statementName.contains("update")) {
            this.name = statementName;
            this.type = 3;
        } else {
            this.name = statementName;
            this.type = 4;
        }
    }

    public String getName() {
        return name;
    }

    public Integer getType() {
        return type;
    }
}
}

SqlCommand之中返回方法的操作类型即对应insert,update,delete,select标签,这里由于是模拟、则随意一点。代码比较简单就不做额外的解释了。

接下来就是把这个SqlSession传递给每一个类就可以了。

public class ProxyMapperFactory<T> {

    private final Class<T> mapperInterface;
    private Map<String,String> sqlSession;

    public ProxyMapperFactory(Class<T> mapperInterface, Map<String,String> sqlSession) {
        this.mapperInterface = mapperInterface;
        this.sqlSession = sqlSession;

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

    private Class<T> interfaceClass;

    private Map<String,String> sqlSession;
    public ProxyMapperHandler(Class<T>  interfaceClass, Map<String,String> sqlSession) {
        this.interfaceClass = interfaceClass;
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        MapperMethod.SqlCommand sqlCommand = new MapperMethod.SqlCommand(interfaceClass, method);
        return new MapperMethod(sqlCommand).execute(sqlSession,args);
    }

}

此时完整的测试代码如下:

 public static void main(String[] args) {

        HashMap<String, String> sqlSessopn = new HashMap<>();

        sqlSessopn.put("com.basic.java.reflect.tutorial.intf.DogMapper.findDogName","select dog_name from dog");
        sqlSessopn.put("com.basic.java.reflect.tutorial.intf.DogMapper.getDogAge","select dog_age from dog");
        sqlSessopn.put("com.basic.java.reflect.tutorial.intf.UserMapper.findUserNameById","select user_name from user where user_id = ?");

        DogMapper dogMapper = new ProxyMapperFactory<>(DogMapper.class, sqlSessopn).newInstance();
        dogMapper.findDogName();
        dogMapper.getDogAge();
        UserMapper userMapper = new ProxyMapperFactory<>(UserMapper.class,sqlSessopn).newInstance();
        userMapper.findUserNameById(1);
    }

动态代理_模拟xml.png

到这里基本上本文就要结束了、因为只需要在switch...case里面去实现获取数据连接、根据sql标签执行对应的解析以及方法,在执行sql语句返回结果基本上就实现了超级mini版的mybatis

当然框架在这一方面的实现肯定比我这个sqlSession复杂得多。大体的思路是这个样子,具体的细节就需要仔细推敲了。

附上xmind总结:

动态代理_模拟_xmind.png