Bootstrap

理解代理模式MyBatis就掌握了一半(详解动态代理原理)

 

目录

1.静态代理代码示例

2.动态代理代码示例

3.查看jdk动态代理类Class文件

4.代理类流程

5.代理类源码介绍

6.静态代理和动态代理

7.Mybatis动态代理的使用

7.1 mapper语句

7.2 Mybatis插件

7.3 Mybatis的连接池 

7.4 Mybatis的日志 


1.静态代理代码示例

先来个静态代理示例代码热热身,假如我要玩一个游戏,那么就可能需要以下几个步骤,登录,打怪,升级,那么我们就需要一个打游戏的接口了IGamePlayer。

public interface IGamePlayer {
    // 登录
    void login(String user, String password);
    // 打怪
    void killBoss();
    // 升级
    void upgrade();
}

那有接口就得有实现类把,来实现我说的登录、打怪、与升级对把,在这里就演示一下示例,输出是谁登录,是谁打游戏,是谁升级的。

public class GamePlayer implements IGamePlayer {
    private String name;

    public GamePlayer(String name) {
        this.name = name;
    }
    @Override
    public void login(String user, String password) {
        System.out.println(user + "登录成功!");
    }
    @Override
    public void killBoss() {
        System.out.println(this.name + "在打怪!");
    }
    @Override
    public void upgrade() {
        System.out.println(this.name + "升级成功!");
    }
}

然后用测试类看看输出

public static void main(String[] args) {
        IGamePlayer gamePlayer = new GamePlayer("杜大侠");
        gamePlayer.login("dd","123456");
        gamePlayer.killBoss();
        gamePlayer.upgrade();
}

那么假如现在我在上班,我又想我的游戏升级怎么办呢,对,找人帮我打,那么程序怎么实现呢?需要有一个代理类,帮我打游戏的代理类,而且是帮我打,所以登录是我的账号,最后升级什么的都得是我的

public class GamePlayerProxy implements IGamePlayer {
    // 他帮得是给我打游戏,所以需要知道玩家,持有对玩家得引用
    private IGamePlayer gamePlayer = null;

    public GamePlayerProxy(IGamePlayer gamePlayer) {
        this.gamePlayer = gamePlayer;
    }
    @Override
    public void login(String user, String password) {
        // 因为给我打游戏要登录我的账号
        this.gamePlayer.login(user, password);
    }
    @Override
    public void killBoss() {
        this.gamePlayer.killBoss();
    }
    @Override
    public void upgrade() {
        this.gamePlayer.upgrade();
    }
}

测试一下输出,发现和跟我自己玩的时候输出是一模一样的

public static void main(String[] args) {
        // 被代理对象的资源
        IGamePlayer gamePlayer = new GamePlayer("杜大侠");
        // 代理对象,把我的引用传给代理类,让它帮助我
        IGamePlayer gameProxy = new GamePlayerProxy(gamePlayer);
        gameProxy.login("dd", "123456");
        gameProxy.killBoss();
        gamePlayer.upgrade();
 }

那么上面代码示例静态代理模式会有问题,不好扩展不好维护,假如此时IGamePlayer接口又添加其他方法,那么GamePlayerProxy代理类也是要添加方法的,那假如原方法的参数改变了或者返回值改变了那么我们的GamePlayerProxy代理类也需要相应的改变,如果被代理对象再加一些额外的功能,但是代理对象类又不需要做,那么GamePlayerProxy代理类还必须要实现方法,这样的设计很头疼啊

那么再假如代理人帮我又打游戏,又要帮我挂qq,那么我是不是要既实现打游戏,又要实现挂qq类呢,那样代理类就会变得很臃肿,而且如果还有其他各种各有需要代理类干的活,那么就证明还会有无数的类,那就会造成类文件的膨胀。

综上所述,静态代理能帮助我们实现代理方式,但是还是有很多不足的,我们只是希望代理类帮我们做额外我们想做的事情,例如aop那种方式记录日志那种,那我们可不可以实现那种自动的生成的类,代码运行时生成的代理类,满足这两种就是动态代理,那么jdk就已经开发好了动态代理。我们来实现一下

2.动态代理代码示例

代理类要用动态代理的话一定要实现InvocationHandler,然后还是要有被代理对象的引用IGamePlayer和构造方法帮助我们传递被代理类的信息,那么实现一下在打怪之前先穿一下衣服把

public class DynamicProxyHandler implements InvocationHandler {

    private IGamePlayer gamePlayer = null;

    public DynamicProxyHandler(IGamePlayer gamePlayer) {
        this.gamePlayer = gamePlayer;
    }

    /**
     * 那么现在在打怪之前想让它穿一个装备
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在打怪之前戴上装备,所以就需要判断是否是打怪方法
        if (method.getName().equals("killBoss")) {
            System.out.println("戴上麻痹戒指!");
        }
        // 调用被代理对象的方法
        return method.invoke(gamePlayer, args);
    }
}

测试输出信息

 public static void main(String[] args) {
        IGamePlayer gamePlayer = new GamePlayer("杜大侠");
        InvocationHandler handler = new DynamicProxyHandler(gamePlayer);
        IGamePlayer proxy = (IGamePlayer) Proxy.newProxyInstance(gamePlayer.getClass().getClassLoader(),
                new Class[]{IGamePlayer.class}, handler);
        System.out.println(proxy.getClass());
        proxy.login("dd", "123456");
        proxy.killBoss();
        proxy.upgrade();
    }

为其他对象提供一种代理以控制对这个对象的访问,也就是说我们不能做或者不想做的事情都可以委托他人来帮我们做,受益人是我们自己,在生活中有什么例子是代理模式呢,外卖、黄牛买票,黄牛买票最终也是我们自己坐车,外卖小哥帮你把外卖送到,也是你自己吃,代理模式是代理人帮你做一些事,最终体现都是在被代理人那里

可以了,简单的代码就能操作我们想要的结果,这样就简化多了,但是这个Proxy类做什么了呢,我们来输出一下Proxy,发现是$Proxy0的类名称,很奇怪

System.out.println(proxy.getClass());

3.查看jdk动态代理类Class文件

那么代码的动态代理演示完了,我们在来在main方法把这个类文件输出到磁盘里,在项目的根目录就会有这样的文件和Class类输出变量就会在相应的目录显示动态代理生成的类。

 // 输出变量
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

所有动态生成的类都继承Proxy

类里面有m0-m5的生成的方法

静态代码块里会把这几个m0-m5的都对应起来了,并且object对象的toString,hashCode啊,都是类必须要有的

还有我刚实现的自定义的方法,也都有相应的体现

咱们找下m5调用的体现,killBoss不就是我们打怪的接口么,然后h好熟悉啊,这个不就是我们代理测试实现的那个接口么,super就是调用父类,这里它的父类就是Proxy类,再调用invoke,invoke就是我们下图图三我们自己实现代理类而调用的方法,调用增强升级功能代码完毕,再调用

method.invoke(gamePlayer, args);被代理类的方法

4.代理类流程

Client假如就是main方法,doSomething就是调用任何方法它都会生成$Proxy类,$Proxy类调用invoke方法,走到我们自己定义的任何代理类方法调用invoke开始做增强代码逻辑的操作,然后再调用被代理对象的方法。

5.代理类源码介绍

看源码最好用ctrl + alt+b 哦!

那么这个jdk生成的代理类是用什么技术实现生成完让jvm直接能用呢?就是反射,大概看下源码

首先对我们传进来的接口InvocationHandler全部克隆了一遍

生成一个代理类class对象

根据代理类对象cl获取构造器

根据构造器和InvocationHandler实现类,创建代理类实例

核心就是Class<?> cl = getProxyClass0(loader, intfs);我们点进去看一下实现

接口定义了类不能超过65535,它怎么去拿class类对象的呢?

return proxyClassCache.get(loader, interfaces) 这里讲如果说缓存已经存在直接返回,否则用ProxyClassFactory创建。为什么有缓存呢,就是提高我们查找类的效率

接着来看下ProxyClassFactory,也是Proxy的内部类,首先代理名字前缀定义好了,很熟悉啊$Proxy,然后代理类名字计数器,从0开始,因为可能有多个代理类

真正创建class对象就是apply方法,首先进入到这个方法都是一些验证工作,跳过

验证所有的非public接口是不是在同一个package,如果是非public的,使用的包就是和被代理类一样的包名

如果都是public接口的话,包名统一都是com.sun.proxy

然后包名+类名前缀+序号生成代理类的名字

生成class字节码的文件

把代理类的字节码加载到jvm中,返回代理类class对象,是一个native方法

我们来看一下class字节码文件,分为4大类

generateClassFile主要就是把所有的方法组装成ProxyMethod对象, ProxyMethod对象就是方法名称、参数、返回类型等等的信息

还会添加从Object添加继承的方法

还会添加接口自定义的方法

第二步就是获得所有的字段和方法信息

第三步就是写入字节码(class)文件

写入jvm识别的方式,按照jvm规范书写的方式

6.静态代理和动态代理

静态带来是必须要有java源文件,通过java编译器转换为.calss文件,通过转换转为byte类加载器再进行加载。

动态代理直接再运行时就生成byte文件直接通过类加载器进行加载

还有一点jdk动态代理要求必须是实现接口那种方式,否则不能进行动态代理

Cglib支持不是接口的类能进行动态代理,但是Cglib不能代理被final修饰的方法

Springboot 2.x版本spring已经添加了cglib,如果是实现接口类型的就用jdk动态代理,如果不是接口类型就用Cglib

7.Mybatis动态代理的使用

7.1 mapper语句

这是一段Mybatis的一个查询数据库数据的代码,通过mapper找到配置文件的sql语句,执行sql语句获取数据,但是mapper.selectBlogById()它是没有实现类的,那是怎么实现操作方法的呢

打印一下类信息,发现此时的mapper其实是动态代理出来的类了

发现输出的是jdk的动态代理

那MyBatis中的hadler是什么呢,是MapperProxy里的invoke方法,也就是说能够不实现接口就直接调用来运行sql语句是通过了jdk动态代理实现的,sql操作都在mapperMethod里的execute里实现的

7.2 Mybatis插件

Mybatis拦截器啊,分页插件啊等一些其他插件都是使用了动态代理,那么mybats里有专门的Plugin代理类, 下图二Plugin类的invoke

 

7.3 Mybatis的连接池 

如果不指定spring管理数据库连接,mybatis也是有连接池的,而池的操作mybatis也是采用动态代理,为什么呢,因为connection的连接自己是不可能把自己还给池子里的,因为connection本身没有池啊!那么只能通过代理方式增强连接池的功能,代理类帮它把connection放回池操作等等。

invoke里的实现就是假如你要释放连接,那么判断方法是不是CLOSE,然后把连接放入dataSource容器,最后return也都是调回被代理方法的本身操作的方法

7.4 Mybatis的日志 

Mybatis会打印执行sql日记,肯定不能在业务里写,所以也需要代理模式

ConnectionLogger打印日志

随随便便就能找到Mybatis这么多代理实现方式,其实还有很多,,生成语句啊,结果啊等等。以上就是今天全部内容!

;