0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Java Dynamic Proxy

0
Posted at

前言:Java标准库提供的动态代理(Dynamic Proxy)功能,允许在运行期动态创建一个接口的实例。动态代理实际上是通过JDK在运行期动态创建class字节码并加载的过程。

代理模式

关于代理模式

代理模式(Proxy pattern)是经典的23种设计模式之一,Java的动态代理就是代理模式。首先来看看什么是代理模式?下面是代理模式的UML图:
2.png

该模式的参与者:

  • Proxy
    • 保存一个引用使得代理可以访问实体。Proxy引用Subject。
    • 控制实体的存取,负责创建和删除它。
    • 提供一个与Subject的接口相同的接口,这样代理可以用来代替实体。
  • Suject
    • 定义RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy。
  • RealSubject
    • 定义Proxy所代表的的实体。

代理模式的例子

首先定义一个代表“可移动”交通工具的接口:

public interface Moveable {
    /**
     * 移动指定距离, 以kilometer为单位
     * @param km 移动的距离
     */
    void move(Integer km);
}

接下来创建一个接口实现类:Metro(地铁),该类实现了Moveable接口的move方法,并模拟了调用该方法后的运行时间。

public class Metro implements Moveable {
    @Override
    public void move(Integer km) {
        try {
            System.out.println("Metro is moving ...");
            Thread.sleep(km * 1000);  // 模拟移动时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后创建一个可以测试方法性能的PerformanceProxy:

public class PerformanceProxy implements Moveable {
    private Moveable m;
    public PerformanceProxy(Moveable m) {
        this.m = m;
    }
    @Override
    public void move(Integer km) {
        long start = System.currentTimeMillis(); // 记录方法调用开始时间
        m.move(km);                              // 方法调用
        long end = System.currentTimeMillis();   // 方法结束的时间
        System.out.println("耗时: " + (end - start)/1000.0); // 计算耗时
    }
}

测试代理模式:

public static void main(String[] args) {
    new PerformanceProxy(new Metro()).move(5);
}
---- 测试的结果 ----
Metro is moving ...
耗时: 5.005

这样一个简单的代理模式的例子就完成了。这个模式的好处是以后需要测试其他的交通工具(如飞机、高铁)的性能的时候,只需要让它们(飞机、高铁)实现Moveable接口,测试的时候将它们的实例传给PerformanceProxy代理即可(每个类不需要自己实现测试代码)。

这样,PerformanceProxy可以检测所有实现Moveable的实体类。如果还想实现其他代理,比如给某方法埋点(跟踪该方法的调用次数)、日志打印等等,只需要创建TrackingProxy、LoggingProxy等代理。当然因为这些代理都是实现的Moveable接口,所以它们可以链式使用:

public static void main(String[] args) {
    Plane plane = new Plane();
    PerformanceProxy pp = new PerformanceProxy(plane);
    LoggingProxy lp = new LoggingProxy(PerformanceProxy);
    lp.move();
}

静态代理

什么是静态代理?

像上面的例子,先编辑好源代码,自己手动实现代理,再对其编译。这样,在程序运行前,代理类就已经确定,代理类的.class文件(二进制文件)就已经存在了,这就是所谓的静态代理。

动态代理

Java标准库提供的动态代理(Dynamic Proxy)功能,允许在运行期动态创建一个接口的实例。动态代理实际上是通过JDK在运行期动态创建class字节码并加载的过程。将上面静态代理的例子用动态代理实现如下:

public static void main(String[] args) {
    final Metro metro = new Metro();
    Moveable m = (Moveable) Proxy.newProxyInstance(Metro.class.getClassLoader(), // 被代理的class loader
            new Class[]{Moveable.class}, // 传入要实现的接口
            new InvocationHandler() {  // 传入处理调用方法的InvocationHandler (匿名类)
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1
                    long start = System.currentTimeMillis();
                    Object res = method.invoke(metro, args); // 2
                    long end = System.currentTimeMillis();
                    System.out.println("耗时: " + (end - start)/1000.0);
                    return res; // 3
                }
            });
    m.move(5);
}

// 1. invoke中的参数proxy就是动态创建的proxy;method为接口的方法:可根据不同的方法做不同的处理。
// 2. method.invoke(metro, args)等价于这里的metro.move(),这和js中的function.call(obj, param)类似。
// 3. 2中方法的返回值,如果是void方法返回null,如果是基本数据类型则会自动装箱(例如:int会自动转换成Integer)。

上面的代码测试可以得到和上面静态代理同样的结果。从动态代理可以看出主要是调用了Proxy.newProxyInstance(...)操作以及InvocationHandlerinvoke的方法的实现。要查看JDK运行时做了什么,可以在测试代码上加上以下代码:

// env: JDK1.8 / IntelliJ IDEA 15 / macOS Majave
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

运行之后可以看到IDEA多了个com文件夹,动态生成的proxy就在里面:

# 动态生成代理的位置
...
|-- com/
| |--sun/
| | |--proxy/
| | | |--$Proxy0.class
...

下面是反编译$Proxy0.class后的部分代码:可以看出该代理继承于Proxy类并实现了Moveable接口。主要实现了Object类的equals、hashCode和toString基本方法和Moveable的接口方法move。


// ----- $Proxy0.class
public final class $Proxy0 extends Proxy implements Moveable {
    ...
    public $Proxy0(InvocationHandler var1) throws  { super(var1); }
    public final void move(Integer var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1}); // 1 
        } ...
    }

    public final boolean equals(Object var1) throws  { ... }
    public final String toString() throws  { ... }
    public final int hashCode() throws  { ... }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m3 = Class.forName("proxy.lizi.Moveable").getMethod("move", new Class[]{Class.forName("java.lang.Integer")});
            ...
        } ...
    }
}

上面代码中(1位置)的super.h就是手动编写的InvocationHandler实例,m3可以从后面的代码看出就是move方法。所以调用动态代理的move实际就是调用InvocationHandler实例的invoke方法。下面的时序图简易的描述了上述动态代理的创建及执行过程:
times.png

注:由JDK源码可了解到动态代理的实现的方式是:Java的反射机制+ASM(一个可操作字节码的框架)。反射机制可动态获取类及其方法,但仅限于获取及调用;ASM是可以直接操作字节码(.class文件),以此来实现动态创建代理类。

动态代理的应用例子

动态代理的运用之MyBatis

MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。使用MyBatis时一般只需要写好Mapper.xml配置及接口文件XXXMapper.java,如下:

<!-- XXXMapper.xml -->
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
    <select id="selectById" resultMap="userMap">
        SELECT * from sys_user where id = #{id}
    </select>
    <select id="selectAll" resultType="tk.mybatis.simple.model.SysUser"> 
    	select id,
		user_name userName
	from sys_user
    </select>
    ...
</mapper>

// ---- UserMapper.java
public interface UserMapper {
	/**
	 * 通过 id 查询用户
	 * 
	 * @param id
	 * @return
	 */
	SysUser selectById(Long id);
	
	/**
	 * 查询所有用户
	 * 
	 * @return
	 */
	List<SysUser> selectAll();
}

Mapper接口没有实现类却能被正常调用接口方法,这是因为MyBaits在Mapper接口上使用了动态代理的。基本原理就是通过获取XML文件的配置信息再根据Mapper接口定义实现其接口方法。

动态代理运用之Spring

Spring框架的核心是IoC(Inversion of Control)和AOP(Aspect Oriented Programming)。Spring bean的依赖注入(DI:可以通过XML方式和注解方式)以及面向切面的方法织入,实质是使用了cglib的动态代理。cglib(Code Generation Library)的原理是动态生成了一个要代理类的子类,子类重写要代理的类的所有不是final的方法,在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。cglib的底层也是使用了ASM框架。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?