如何进行AOP开发中的Java动态代理,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
1.AOP的概念:Aspect Oriented Programming 面向切面编程,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
2.OOP与AOP的区别:如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话;那么面向方面编程则是希望能够将通用需求功能从不相关的类当中分离出来。
3.AOP的应用范围:日志记录,性能统计,安全控制,事务处理,异常处理等等。
切面Aspect:切面就是一个关注点的模块化(横切到代码中的关注点),譬如:事务管理、日志记录、权限管理都是所谓的切面。 连接点Joinpoint:程序执行时的某个特定的点,在Spring中就是一个方法的执行。
通知Advice:通知就是在切面的某个连接点上执行的操作,也就是事务管理、日志记录等的具体代码。 切入点Pointcut:切入点是指描述某一类特定的连接点,也就是说指定某一类要织入(Weave)通知的方法。
Spring中的AOP编程的风格主要有基于XML配置文件、基于注解两种,我们这里以注解为例。
Spring的切入点匹配表达式使用AspectJ表达式(当然也可以使用正则表达式等),也就是所谓的 Aspects,如下所示: (1.)?表示可以不配置,也就是说只有方法的名字name-pattern、方法的参数param-pattern是必须的。对于param-pattern之外的其余部分,可以使用*作为通配符,表示任意,例如:execution (* *.m(..))就是执行任意返回值、任意类中的m方法时进行织入。
(2.)参数的通配配稍微复杂一些,其中(..)表示参数为0个或者多个,且类型任意;(*)表示任意类型的一个参数;(*,String)表示第一个参数为任意类型,第二个参数为String类型;什么都不写表示无参数。
(3.)多个表达式可以使用&&、||、!进行运算,因为表达式返回的是布尔值。
Spring中的AOP只针对方法,类型分为前置、后置、环绕、异常。 Spring的AOP是在运行时动态代理去完成,预编译方式的AOP实现主要有Aspect,需要有专门JAVA编译器,因为它是在编译器进行处理的。
反射机制的概念:在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
说直白一些,就是我们可以于运行时加载、探知、使用编译期间完全未知的类型,也就是具有看透任意的Class 的能力,哪怕这个Class是在运行时动态传给你的完全未知的。使用范围:当你要处理的类型无法在编译器确定,而是在运行时动态传入的。目前的MVC框架都采用这种机制。我们在Struts2框架中,如果想接收URL上的 参数?id=1&name=2&password=3,会很自然的在Action中写上如下的内容:及他们对应的set方法,然后你就可以在Action中获取上面的三个字段的值了。可是作为Struts2框架,Apache在开发的时候这个框架的时候,怎么知道我们Action里有这几个字段的呢?因为Struts2里使用了反射看透了你的Action。
代理设计模式:为其他对象提供一种代理以控制对这个对象的访问。说白了就是去掉客户不能看到的内容和服务或者增添客户需要的额外服务。
常用的代理设计模式:
(1.)包装:缺点就是当Animal接口中增加了新的方法,那么包装类中也必须增加这些新的方法。(2.)继承:缺点更明显,那就是不能实现对接口的所有子类的代理,与包装的模式相比,大大缩小了代理范围。但不管是那种方法,都是在编译期完成的代理,不能像Spring那样在运行期动态的对指定的类完成代理。
JAVA自带的动态代理是基于java.lang.reflect.Proxy、java.lang.reflect.InvocationHandler两个类来完成的,使用JAVA反射机制。
Proxy类中的几个方法都是静态的,通常,你可以使用如下两种模式创建代理对象:
第一种方式更加直接简便,并且隐藏了代理$Proxy0对象的结构,回调InvocationHandler就是拦截处理的地方。
JDK的动态代理会动态的创建一个$Proxy0的类,这个类继承了Proxy并且实现了要代理的目标对象的接口,但你不要试图在JDK中查找这个类,因为它是动态生成的。$Proxy0的结构大致如下所示:
从上面的类结构,你就可以理解为什么第二种创建代理对象的方法为什么要那么写了,以为它需要一个InvocationHandler作为构造参数。关于回调接口InvocationHandler:
它只有一个方法invoke()需要实现,这个方法会在目标对象的方法调用的时候被激活,你可以在这里控制目标对象的方法的调用,在调用前后插入一些其他操作(譬如:鉴权、日志、事务管理等)。Invoke()方法的后两个参数很好理解,一个是调用的方法的Method对象,另一个是方法的参数,第一个参数有些需要注意的地方,这个proxy参数就是我们使用Proxy的静态方法创建的动态代理对象,也就是$Proxy0的实例(这点你可以在Eclipse的断点调试中看到proxy的所属类型确实是$Proxy0)。由于$Proxy0在JDK中不是静态存在的,因此你不可以把第一个参数Object proxy强制转换为$Proxy0类型,因为你根本就无法从Classpath中导入$Proxy0。那么我们可以把proxy转为目标对象的接口吗?因为$Proxy0是实现了目标对象的所有的接口的,答案是可以的。但实际上这样做的意义不大,因为你会发现转换为目标对象的接口之后,你调用接口中的任何一个方法,都会导致invoke()的调用陷入死循环而导致堆栈溢出。这是因为目标对象的大部分的方法都被代理了,你在invoke()通过代理对象转换之后的接口调用目标对象的方法,依然是走的代理对象,也就是说当mammal.type()方法被激活时会立即导致invoke()的调用,然后再次调用mammal.type()方法,… …从而使方法调用进入死循环,就像无尽的递归调用。
那么invoke()方法的第一个参数到底干什么用的呢?其实一般情况下这个参数都用不到,除非你想获得代理对象的类信息描述,因为它的getClass()方法的调用不会陷入死循环。为什么getClass()不会呢?因为getClass()方法是final的,不可以被覆盖,所以也就不会被Proxy代理。但不要认为Proxy不可以对final的方法进行动态代理,因为Proxy面向的是Monkey的接口,而不是Monkey本身,所以即便是Monkey在实现Mammal、Primate接口的时候,把方法都变为final的,也不会影响到Proxy的动态代理。Proxy代理对象的过程如下所示
JDK的动态代理有个缺点,那就是不能对类进行代理,只能对接口进行代理,想象一下我们的Monkey如果没有实现任何接口,那么将无法使用这种方式进行动态代理(实际上是因为$Proxy0这个类继承了Proxy,JAVA的继承不允许出现多个父类)。但准确的说这个问题不应该是缺点,因为良好的系统,每一个类都是应该有一个接口的。
从上面知道$Proxy0是动态代理对象的所属类型,但由于这个类型根本不存在,我们如何鉴别一个对象是一个普通的对象还是动态代理对象呢?Proxy类中提供了isProxyClass(Class c)方法鉴别与此。CGLIB是一个开源的动态代理框架,它的出现补充了JDK自带的Proxy不能对类实现动态代理的问题。CGLIB是如何突破限制,对类也能动态代理的呢?这是因为CGLIB内部使用了另一个字节码框架ASM,类似的字节码框架还有Javassist、BCEL等,但ASM被认为是性能最好的一个。但这类字节码框架要求你对JAVA的Class文件的结构、指令集都比较了解,CGLIB对外屏蔽了这些细节问题。由于CGLIB使用ASM直接操作字节码,因此效率要比Proxy高,但这里所说的效率是指代理对象的性能,在创建代理对象时,Proxy是要比CGLIB效率高的。CGLIB动态代理的原理是使用ASM动态生成目标对象的子类,final方法不能被子类覆盖,自然也就不能被动态代理,这也是CGLIB的一个缺点。CGLIB被Hibernate、Spring等很多开源框架在内部使用,用于完成对类的动态代理,Spring中的很多XML配置属性的proxy-target-class,默认都为false,其含义就是默认不启用对目标类的动态代理,而是对接口进行动态代理。某些情况下,如果你想对Struts2的Action或者Spring MVC的Controller进行动态代理,你会发现默认Spring会报告找不到$Proxy0的xxx方法,这是因为一般我们都不会给控制层写一个接口,而是直接在实现类中写请求方法,这样JDK自带的Proxy是找不到这些方法的,因为他们不在接口中,此时你就要设置proxy-target-class=”true”,并引入CGLIB、ASM的类库,Spring的动态代理就可以正常工作了。关于如何进行AOP开发中的Java动态代理问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。