代理类的原理与作用及AOP概念
一、代理的概念与作用
生活中的代理
在生活中经常会与代理打交道,比如说一个重庆人想要买一联想电脑,他有两种选择:第一种是亲自去北京联想总部买电脑,第二种是在联想的重庆代理商处购买。这两种选择的目的是一样的,及主体业务的目标是一样的。在这种情况下,大多人都会选择第二种方案,因为虽然直接去联想总部电脑的价格可能会低于代理商,批发进货的成本和运输费的优势,比直接到北京总部买的总成本还是要低。
程序中的代理:
要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理、日志、计算方法的运行时间、事务管理、等等,该怎么做?
比如说这样一个需求,有一个已经定义好的类中有一个sayHello()方法,现在需要将这个方法的功能进行扩展,但是我们不能获得其源文件,这种情况下正确的做法是通过一个代理类解决。
步骤如下:
1) 编写一个与目标类具有相同接口的代理类
2) 代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码
目标类: 代理类:
class X{ Xproxy{
void sayHello(){ void sayHello(){
syso:HelloItheima; startTime
} X. sayHello(); X.sayHello();
} endTime;}}
代理架构图:
二、AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程(也叫面向方面),可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面,如下所示:
安全 事务 日志
StudentService ------|----------|------------|-------------
CourseService ------|----------|------------|-------------
MiscService ------|----------|------------|-------------
安全,事务,日志等功能要贯穿到好多个模块中,所以,它们就是交叉业务
对交叉业务的代码描述
method1 method2 method3
{ { {
------------------------------------------------------切面
.... .... ......
------------------------------------------------------切面
} } }
因为AOP的目标就是要使交叉业务模块化。可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的。
通过AOP思想的代码实现:
-----------------------------------------------------切面
func1 func2 func3
{ { {
.... .... ......
} } }
------------------------------------------------------切面
三、动态代理技术
要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式,那么这个工作会变得异常繁琐和庞大,这时就不能采用静态代理方式,需用用到动态代理技术。
◆ JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。
◆ JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。
◆ CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。
◆ 代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下四个位置加上系统功能代码:
1,在调用目标方法之前
2,在调用目标方法之后
3,在调用目标方法的前后
4,在处理目标方法异常的catch块中
分析JVM动态生成的类
1) 创建实现了Collection接口的动态类和查看其名称,分析Proxy.getProxyClass方法的各个参数。
2) 编码列出动态类中的所有构造方法和参数签名
3) 编码列出动态类中的所有方法和参数签名
代码如下:
package com.itheima.day3;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
public class ProxyTest {
/**
* @param args
*/
public static void main(String[] args) {
//得到一个动态类对象(类加载器通常用接口相同的类加载器)
Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
//得到代理类的名字
clazzProxy1.getName();
//得到类的构造方法及构造方法的参数
System.out.println("-------begin constructors--------");
Constructor[] constructors = clazzProxy1.getConstructors();
//循环获得所有构造方法
for(Constructor constructor : constructors ){
//获得构造方法的名字
String name = constructor.getName();
//在单线程中StringBuilder的效率更高,因为不用考虑线程安全
StringBuilder sBuilder = new StringBuilder(name);
sBuilder.append('(');
//构造方法的参数,因为可能是多个,所以用数组
Class[] clazzParms = constructor.getParameterTypes();
//取出每一个参数,并用','隔开
for(Class clazzParm : clazzParms){
sBuilder.append(clazzParm.getName()).append(',');
}
//去掉最后一个',',如果没有参数则不去
if(clazzParms != null && clazzParms.length != 0 )
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
System.out.println(sBuilder.toString());
}
/*-------------------------------------------------------------------*/
//得到类的方法及方法的参数
System.out.println("-------begin method--------");
Method[] methods = clazzProxy1.getMethods();
//循环获得所有方法
for(Method method : methods ){
//获得方法的名字
String name = method.getName();
//在单线程中StringBuilder的效率更高,因为不用考虑线程安全
StringBuilder sBuilder = new StringBuilder(name);
sBuilder.append('(');
//方法的参数,因为可能是多个,所以用数组
Class[] clazzParms = method.getParameterTypes();
//取出每一个参数,并用','隔开
for(Class clazzParm : clazzParms){
sBuilder.append(clazzParm.getName()).append(',');
}
//去掉最后一个',',如果没有参数则不去
if(clazzParms != null && clazzParms.length != 0 )
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
System.out.println(sBuilder.toString());
}
}
}
运行结果如下:
-------begin constructors--------
$Proxy0(java.lang.reflect.InvocationHandler)
-------begin method--------
add(java.lang.Object)
hashCode()
clear()
equals(java.lang.Object)
toString()
contains(java.lang.Object)
isEmpty()
addAll(java.util.Collection)
iterator()
size()
toArray([Ljava.lang.Object;)
toArray()
remove(java.lang.Object)
containsAll(java.util.Collection)
removeAll(java.util.Collection)
retainAll(java.util.Collection)
isProxyClass(java.lang.Class)
getProxyClass(java.lang.ClassLoader,[Ljava.lang.Class;)
getInvocationHandler(java.lang.Object)
newProxyInstance(java.lang.ClassLoader,[Ljava.lang.Class;,java.lang.reflect.InvocationHandler)
wait()
wait(long,int)
wait(long)
getClass()
notify()
notifyAll()
通过运行结果我们发现,生成的动态类只有一个有参的构造方法,参数为InvocationHandler,其方法全部为Collection接口和Object类的方法。
4) 创建动态类的实例对象,步骤如下
1,用反射获得构造方法
2,编写一个最简单的InvocationHandler类
3,调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去
4,打印创建的对象和调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告异常。
5,将创建动态类的实例对象的代理改成匿名内部类的形式编写
整体代码如下:
package com.itheima.day3;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
public class ProxyTest {
/**
* @param args
*/
public static void main(String[] args)throws Exception {
//得到一个动态类对象(类加载器通常用接口相同的类加载器)
Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
//得到代理类的名字
clazzProxy1.getName();
System.out.println("-------begin create instance object --------");
/*创建实际代理对象方式1,内部类方式*/
//获得clazzProxy带参数的构造方法
Constructor constructor = clazzProxy1.getConstructor(InvocationHandler.class);
//因为InvocationHandler是接口不能new,所以创建一个实现类
class MyInvocationHandler1 implements InvocationHandler{
//实现InvocationHandler的抽象方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return null;
}
}
//创建对象
Collection Proxy1 = (Collection)constructor.newInstance(new MyInvocationHandler1());
System.out.println(Proxy1);
//调用其不带返回值的方法
Proxy1.clear();
//调用其带返回值的方法,出现异常
//Proxy1.size();
/*创建实际代理对象方式2,匿名内部类方式*/
Collection Proxy2 = (Collection)constructor.newInstance(new InvocationHandler(){
//实现InvocationHandler的抽象方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return null;
}
});
Proxy2.clear();
//Proxy2.size();
System.out.println("--------方式3---------");
/*创建实际代理对象方式3,Proxy提供的newProxyInstance()方法*/
Collection Proxy3 = (Collection)Proxy.newProxyInstance(
//参数1,类加载器
Collection.class.getClassLoader(),
//参数2,接口
new Class[]{Collection.class},
//参数3,Handler对象
new InvocationHandler(){
//实现InvocationHandler的抽象方法
//指定目标,目标为一个ArrayList集合
ArrayList target = new ArrayList();
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//获取方法的执行时间
long beginTime = System.currentTimeMillis();
//调用目标方法,将其抽取出来,然后加入所需的代码
Object retVal = method.invoke(target, args);//可以通过过滤器对参数args进行过滤
long endTime = System.currentTimeMillis();
//打印调用的方法及执行时间
System.out.println(method.getName()+" runing time:"+(endTime-beginTime));
return retVal;
}
}
);
Proxy3.add("itheima");
Proxy3.add("beijing");
Proxy3.add("zhongguancun");
System.out.println(Proxy3.size());
}
}
运行结果:
-------begin create instance object --------
null
--------方式3---------
add runing time:1
add runing time:0
add runing time:0
size runing time:0
3
分析InvocationHandler对象的运行原理
动态生成的类实现了Collection接口(可以实现若干接口),生成的类有Collection接口中的所有方法和一个如下接受InvocationHandler参数的构造方法。构造方法接受一个InvocationHandler对象,接受对象了要干什么用呢?
我们在构造方法中接受一个参数,目的就是要记住这个参数,以便于以后运用它。因此该方法的内部代码如下:
$Proxy0 implements Collection
{
InvocationHandler handler;
public $Proxy0(InvocationHandler handler)
{
this.handler = handler;
}
}
我们每调用一次动态代理类的方法,它又会去调用handLer对象的invoke方法,因此在上面程序中实现Collection接口的动态类中的各个方法的代码如下:
$Proxy0 implements Collection
{
InvocationHandler handler;
public $Proxy0(InvocationHandler handler)
{
this.handler = handler;
}
//生成的Collection接口中的方法的运行原理
int size()
{
return handler.invoke(this,this.getClass().getMethod("size"),null);
}
void clear(){
handler.invoke(this,this.getClass().getMethod("clear"),null);
}
boolean add(Object obj){
handler.invoke(this,this.getClass().getMethod("add"),obj);
}
}
Client程序调用objProxy.add(“abc”)方法时,方法接受的三个参数:objProxy对象、add方法、“itheima”参数。
而handler对象也有三个参数,代码如下:
Class Proxy$ {
add(Object object) {
return handler.invoke(Object proxy, Method method, Object[] args);
}
}
其实这三个参数的对应关系为:
ibjProxy对象<----------------->Object proxy
add方法<-----------------> Method method
"itheima"参数<----------------->Object[] args
比如我们以上面程序中的代码Procy3.add("itheima");进行分析:
1)代理对象Proxy3调用add方法,传递了“itheima”参数;
2)add方法内部会找到InvocationHandler对象中的invoke方法,将代理对象proxy传进去,把add方法传入,将“itheima”参数传入。Handler对象的invoke方法返回了一个结果,并 将其给add方法,add方法又继续向外返回给调用的对象proxy3,即最终结果。其中invoke方法的返回值又来自于目标target。
问题1:Proxy1调用带返回值的方法出现异常
在代理对象Proxy1中,我们掉用其带返回值的方法size()时,会出现java.lang.NullPointerException异常。原因是当Proxy1对象调用size()方法时,size()方法内部会调用invoke(),而invoke返回的是null,而size()方法的返回值是int,所以会出现异常。而在Proxy3中,目标的返回值与代理的一致,所以不会出现异常。
问题解释2,Proxy3的类名为$Proxy0
在Proxy3中执行System.out.println(Proxy3.getClass().getName());代码,结果为$Proxy0。按照上面的分析,Proxy3调用getClass()方法时会将其交给目标,而目标的getClass()方法返回的应该是ArrayList才对。原因是,getClass方法是继承自Object类,而Proxy类只将继承自Object类的hashCode(),equals(),toString()方法委托给了Handler,而其它方法有它自己的处理方式。所以结果为$Proxy0
动态代理的工作原理图
编写可生成代理和插入通告的通用方法
在上面的程序中,我们的目标ArrayList对象和执行时间的获取都是通过硬编码的形式完成的,如果我们的目标Target和(时间获取)切面代码发生了改变,必须修改源代码,这样程序没有弹性,也不符合现实条件。为了解决这种问题我们可以抽取一个getProxy的方法,通过它接受目标,并返回给代理对象。我们不知道切面方法的名字,那么切面代码所在的方法必须实现一个接口,我们才能通过接口作为契约去调用它。也就是说把要执行的代码装到一个对象的某个方法中,然后把此对象作为参数传递,接收者只要调用这个对象的方法,即等于执行了外接提供的代码,前提是这个对象实现了某个接口。因此,在getProxy方法中需要接受实现方法的这个对象。
步骤如下:
1,定义getProxy方法接受目标参数和接口
2,定义一个Advice接口并让一个类myAdvice实现这个接口
3,在创建动态代理类对象的时候传入具体目标和接口的实现类对象
代码如下:
ProxyTest类(定义方法及测试)
package com.itheima.day3;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
public class ProxyTest {
/**
* @param args
*/
public static void main(String[] args)throws Exception {
//得到一个动态类对象(类加载器通常用接口相同的类加载器)
Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
//得到代理类的名字
clazzProxy1.getName();
System.out.println("--------方式3---------");
/*创建实际代理对象方式3,Proxy提供的newProxyInstance()方法*/
//指定目标,目标为一个ArrayList集合,方法里面的内部类要访问局部变量必须final修饰
final ArrayList target = new ArrayList();
//传入目标参数和方法所在的对象
Collection Proxy3 = (Collection)getProxy(target,new MyAdvice());
//调用代理对象的方法
Proxy3.add("itheima");
Proxy3.add("beijing");
Proxy3.add("zhongguancun");
System.out.println(Proxy3.size());
System.out.println(Proxy3.getClass().getName());
}
//因为类想不确定,所以用Object
private static Object getProxy(final Object target,final Advice advice) {
Object Proxy3 = Proxy.newProxyInstance(
//参数1,类加载器,与目标的类加载器一致
target.getClass().getClassLoader(),
//参数2,接口,跟目标接口一致
/*new Class[]{Collection.class},*/
target.getClass().getInterfaces(),
//参数3,Handler对象
new InvocationHandler(){
//实现InvocationHandler的抽象方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
/*
//获取方法的执行时间
long beginTime = System.currentTimeMillis();
//调用目标方法,将其抽取出来,然后加入所需的代码
Object retVal = method.invoke(target, args);
long endTime = System.currentTimeMillis();
//打印调用的方法及执行时间
System.out.println(method.getName()+" runing time:"+(endTime-beginTime));
return retVal;
*/
//调用advice接口的beforeMehod()方法
advice.beforeMethod(method);
//调用目标方法,将其抽取出来,然后加入所需的代码
Object retVal = method.invoke(target, args);
advice.afterMethod(method);
//打印调用的方法及执行时间
return retVal;
}
}
);
return Proxy3;
}
}
接口Advice
package com.itheima.day3;
import java.lang.reflect.Method;
public interface Advice {
//创建两个抽象方法
void beforeMethod(Method method);
void afterMethod(Method method);
}
接口的实现流MyAdvice
package com.itheima.day3;
import java.lang.reflect.Method;
//实现接口
public class MyAdvice implements Advice {
//实现方法beforeMethod
//两个方法都用到了beginTime,定义为成员变量
long beginTime = 0;
public void beforeMethod(Method method) {
// TODO Auto-generated method stub
System.out.println("到黑马培训来学习了");
beginTime = System.currentTimeMillis();
}
//实现方法afterMethod
public void afterMethod(Method method) {
// TODO Auto-generated method stub
System.out.println("从黑马毕业上班了");
long endTime = System.currentTimeMillis();
System.out.println(method.getName()+" running time of "+(endTime-beginTime));
}
}
运行结果如下:
--------方式3---------
到黑马培训来学习了
从黑马毕业上班了
add running time of 0
到黑马培训来学习了
从黑马毕业上班了
add running time of 0
到黑马培训来学习了
从黑马毕业上班了
add running time of 0
到黑马培训来学习了
从黑马毕业上班了
size running time of 0
3
$Proxy1