使用 Java AOP API 完成动态代理的一些注意事项

此生再无相见时 提交于 2020-04-07 02:08:27

###代码示例

Java原生API中,动态代理常用的API有两个:InvocationHandler接口和Proxy类

首先上代码StaffLoggerAspect.java

public class StaffLoggerAspect implements InvocationHandler {

    Object target;

    public Object getObject(Object object) {
        target = object;
        return Proxy.newProxyInstance(Staff.class.getClassLoader(), Staff.class.getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(proxy.getClass().getName());
        return method.invoke(target, args);
    }
}

Main类的main方法

public static void main(String[] args) {
    StaffLoggerAspect aspect = new StaffLoggerAspect();
    Staff staff1 = new Staff();
    staff1.setName("");
    System.out.println(staff1);
    Object staff2 = aspect.getObject(staff1);
    System.out.println(staff2);
}

输出结果

Staff{name='', age=0}
com.sun.proxy.$Proxy0
Staff{name='', age=0}

###注意事项

先来解释getObject方法,很多网上的教程都有某种函数用来做类似的事情,这个函数用来返回一个与被代理对象实现了相同接口的代理对象

注意它返回的是代理对象,而不是原对象。代理对象是Proxy类的子类(API文档),实现了被代理对象的所有接口,所以对这个函数结果进行强制转换的话必须转换成对应的接口类型而不是对象类型,对于没有接口的对象怎么办呢?只能转换成Object对象了,当然你能够通过代理使用的方法也只有Object自带的equals, toString之类的了,一些Object方法不会触发invoke方法,详见后边Proxy对象特征最后一条。代理对象的类名是预留类名详见Java API对于Proxy类的解释

###Java API Proxy 部分原文及解释

Proxy类的特征

A proxy class has the following properties:

  • Proxy classes are public, final, and not abstract. (即不可继承)
  • The unqualified name of a proxy class is unspecified. The space of class names that begin with the string "$Proxy" should be, however, reserved for proxy classes. (代理对象类名是没有明确定义的,但是以$Proxy开头的类名要给代理对象保留着,所以标准情况下如果发现某个class的类名以$Proxy开头,那它肯定是代理对象,具体可见输出结果)
  • A proxy class extends java.lang.reflect.Proxy. (代理对象全是这个类的子类,所以可以用instanceof来判断是否是代理对象)
  • A proxy class implements exactly the interfaces specified at its creation, in the same order.(代理对象和被代理对象所实现接口完全一致,连顺序也一致,顺序是做什么用的呢?原文档后边有一节标题是Methods Duplicated in Multiple Proxy Interfaces,这个顺序即是用来处理多接口具有相同函数声明的情况的,这里不详述)
  • If a proxy class implements a non-public interface, then it will be defined in the same package as that interface. Otherwise, the package of a proxy class is also unspecified. Note that package sealing will not prevent a proxy class from being successfully defined in a particular package at runtime, and neither will classes already defined by the same class loader and the same package with particular signers.(好长一串,意思是:如果公共接口,那么包名不确定,即使是包的内部接口,其它包访问不了,如果进行了代理,那么包里也会多出来这么一个代理类。后面说的跟代理无关了,说的是使用反射动态创建对象的话,包密闭是防不住色狼类的,完全阻止不了)
  • Since a proxy class implements all of the interfaces specified at its creation, invoking getInterfaces on its Class object will return an array containing the same list of interfaces (in the order specified at its creation), invoking getMethods on its Class object will return an array of Method objects that include all of the methods in those interfaces, and invoking getMethod will find methods in the proxy interfaces as would be expected.(因为之前说的一些原理,所以代理对象的getInterfaces返回被代理对象所有接口,顺序一致,代理对象的getMethod返回那些接口的方法)
  • The Proxy.isProxyClass method will return true if it is passed a proxy class-- a class returned by Proxy.getProxyClass or the class of an object returned by Proxy.newProxyInstance-- and false otherwise.(代理对象两种获取方式:Proxy.getProxyClass或者Proxy.newProxyInstance其它方式获取的Proxy.isProxyClass会返回false)
  • The java.security.ProtectionDomain of a proxy class is the same as that of system classes loaded by the bootstrap class loader, such as java.lang.Object, because the code for a proxy class is generated by trusted system code. This protection domain will typically be granted java.security.AllPermission.(代理对象权限极高,具有java.security.AllPermission,和java.lang.Object及其它的启动对象一致)
  • Each proxy class has one public constructor that takes one argument, an implementation of the interface InvocationHandler, to set the invocation handler for a proxy instance. Rather than having to use the reflection API to access the public constructor, a proxy instance can be also be created by calling the Proxy.newProxyInstance method, which combines the actions of calling Proxy.getProxyClass with invoking the constructor with an invocation handler.(代理类具有一个public单参数构造函数,需要InvocationHandler对象作为输入参数)

Proxy对象的特征

  • Given a proxy instance proxy and one of the interfaces implemented by its proxy class Foo, the following expression will return true:
proxy instanceof Foo

and the following cast operation will succeed (rather than throwing a ClassCastException):

(Foo) proxy

(首先要注意断句,看英文的话implemented by后边很容易搞错,应该是its proxy class / Foo, 整体上是这样的: Given a proxy instance proxy and one of its interfaces, for example Foo, ... Foo是接口,不是代理类的对象!!!根据之前proxy对象的说明,这一点本身没什么难理解的,它一加说明反而容易弄错)

  • Each proxy instance has an associated invocation handler, the one that was passed to its constructor. The static Proxy.getInvocationHandler method will return the invocation handler associated with the proxy instance passed as its argument.(废话,就是说想要拿到某个代理对象的InvocationHandler的话调用Proxy.getInvocationHandler方法)
  • An interface method invocation on a proxy instance will be encoded and dispatched to the invocation handler's invoke method as described in the documentation for that method. (代理对象上方法调用会发送到与之关联的InvocationHandler对象的invoke方法,注意所有方法调用都会被转发,需要在invoke里判断是不是你要弄的那个方法)
  • An invocation of the hashCode, equals, or toString methods declared in java.lang.Object on a proxy instance will be encoded and dispatched to the invocation handler's invoke method in the same manner as interface method invocations are encoded and dispatched, as described above. The declaring class of the Method object passed to invoke will be java.lang.Object. Other public methods of a proxy instance inherited from java.lang.Object are not overridden by a proxy class, so invocations of those methods behave like they do for instances of java.lang.Object.(hashCode, equals, toString方法也会转发到proxy对象InvocationHandler的invoke方法,其它没有转发!!!重要!!!要注意!!!)

使用说明

文档重要部分解释完了,这里补充一些实际使用时候用的上的包括思考方式等。

最开始我看到那个getObject里target设置的不是Proxy对象有点转不过来,其实要想清楚,过程是这样的:

  1. 你在外边用代理获得的是proxy对象,而不是原来类型的对象了,所以return的是代理对象,这样在调用接口的方法时才能转发过来
  2. 这里存储一个target是很有必要的,为什么呢,因为要保存被代理对象的信息。像代理对象创建的时候完全不需要被代理对象的实体,只需要其类的信息,而没有记录对象本身,示例代码里Staff的name的值有很多可能,我们对于不同staff对象做代理的话,希望被代理对象对应的getName不会出问题,那么我们需要一个原来对象的target来调用对应的方法,所以要记录一下,因此,通常情况下这个target在getObject被赋值,在invoke方法中被使用
  3. invoke方法第一个参数是代理对象,而且不要在invoke里调用代理对象上可以被转发的方法,比如你对toString进行了处理,然后你在invoke方法里还调用了proxy对象的toString方法(比如隐式的:System.out.println(proxy)这样),这种是绝对禁止的!因为你在代理对象上调用转发方法的话会引起再次调用invoke函数,从而导致像递归一样的结果,而大部分操作系统对递归层数是有限制的,会引发严重后果,所以如果没必要不要在invoke里调用代理对象的方法,但是你调用System.out.println(target)是安全的,因为会进行转发的只有代理对象的对应方法,被代理对象的直接方法调用是不经过转发的!!!

使用Java API动态代理的注意事项就简单说到这里,对于Method对象的使用和注意事项请自行查找相关API或教程,CGLib动态代理实现原理有所不同,相似性肯定有,但是请注意和Java原生API动态代理的区别(Java原生通过反射直接生成getInterfaces()得到的那些接口的一个对象,并不管被代理对象extends的部分,所以构造非常快,但是执行的时候性能低,因为需要各种转发;CGLib通过反射直接生成被代理对象的子类,所以不可用于final类的代理,因为这种类不可被继承,同时会把invoke的内容写入被生成的代理对象里,所以生成的时候会很慢,但是这种对象直接就是被代理对象相同类型的对象,毕竟是生成被代理类的子类,所以使用方便,被代理对象可以不需要接口,而且执行方法速度很快。因为原理区别比较大,CGLib肯定还有其它与Java API不同的特征,笔者暂时没有时间研究CGLib的特点,暂不详述)。在选择上,对于单例模式的对象,或者说相比于对象方法调用的次数,构造次数很少的对象建议用CGLib,对于需要大量构造(比如数据库的一条记录,网络服务的一个Request对象),相对而言每个对象方法调用次数不是很多的对象,建议使用Java原生API。

发现文中有不当之处,希望指正,尤其CGLib和Java API比较部分,是从原理出发的一些推断,并不确定,建议大家去看CGLib的相关文档。

笔者撰文不易,转载请注明出处,拜谢

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!