Java基础加强--分析代理类的作用与原理及AOP概念
举个例子:大家去商店买东西,商店就是厂家的一个代理。
--------------
代理的概念与作用
生活中的代理
武汉人从武汉的代理商手中买联想电脑和直接跑到北京传智播客旁边来找联想总部买电脑,你觉得最终的主体业务目标有什么区别吗?
基本上一样吧,都解决了核心问题,但是,一点区别都没有吗?从代理商那里买真的一点好处都没有吗?
在代理那里买会好一些,批发进货的成本和运输费的优势,比你自己直接到北京总部买的总成本要低吧。
所以通过代理还是能得到一些好处,主体目标还是和不经过代理是一样地完成了。
----------------
软件开发中也经常需要代理。
程序中的代理
人家在调用你这个X类的时候,还想在你方法里面多加一点功能。在执行你方法之前记下一个时间,在执行你方法之后再记下一个时间。
再用两个时间相减就统计出了这个方法执行了多长时间。不能改变X类中的代码。
现在做一个X的代理XProxy,它和X具有相同的方法声明,然后在方法内部添加一些辅助功能,调用X的方法。以后调用者就不需要调用X了,而是调用XProxy。
这就是代理的作用。
-------------------
要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理、日志、计算方法的运行时间、事务管理、等等,你准备如何做?
编写一个与目标类具有相同接口(也就是具有相同的方法)的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码。
(参看原理图)
这就是代理的原理。
-------------------------------------
如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类、还是代理类,这样以后很容易切换,
譬如,想要日志功能时就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想去掉系统功能也很容易。
这是代理的应用。
---------------------------------
面向代理的编程应用的还是很广泛的。
有一个术语,面向方面的编程(Aspect oriented program ,简称AOP)
那么什么叫做面向方面的编程呢?
StudentService类负责对学生进行管理。
CourseService类负责对课程进行管理。
MiscService负责管理其他的信息。
总之有各种各样的类,各管各的事,不管你管什么事,最后里面都有安全管理,事务管理,日志管理。
安全,事务,日志等功能要贯穿到好多个对象中(模块中),所以,它们就是模块间的交叉业务。
------------------
假如这三个类中都有一些方法,每类的个方法里面相同的地方都有一些共同的日志功能,事务功能等。。
那么看起来就像一个切面。想一个刀切到每一个对象里面去了。这时候不是一个对象而是一个面。所以这种编程就称之为面向方面的编程。
交叉业务的编程问题即为面向方面的编程(Aspect oriented program ,简称AOP),AOP的目标就是要使交叉业务模块化。
---------------------------
交叉业务贯穿到好多模块当中了。因此我要把这样的交叉业务模块化。模块化就需要用代理方式来实现。
我们不可能去改变目标的方法,但是我们可以让用户感觉到我就是在方法前或方法后加了代码。。怎么办呢?
可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的,如下所示:
使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术。
---------------
动态代理技术
要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式,将是一件非常麻烦的事情!写成百上千个代理类,是不是太累!
JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。
JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。
CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。
代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下四个位置加上系统功能代码:
1.在调用目标方法之前
2.在调用目标方法之后
3.在调用目标方法前后---1、2
4.在处理目标方法异常的catch块中
需要写一个示意代码进行辅助说明,例如
Class proxy{
void sayHello(){
………. 1
try{
target.sayHello();
}catch(Exception e){
………..4
}
…………. 2
}
}
----------------------------
Java基础加强--创建动态类及查看其方法列表信息
分析JVM动态生成的类
使用Java虚拟机API生成动态类,一定要实现一个接口。。
那么我们用的是哪一个API?
Proxy有一个静态方法用于创建动态类:
static Class<?> | getProxyClass(ClassLoader loader, Class<?>... interfaces) |
getProxyClass返回的是一个Class字节码,也就是在内存里面造出了一分字节码,也就是造出了一个类。但是在造出字节码的时候,要告诉这个字节码到底实现了那些接口。另外一个参数叫ClassLoader。
每一个Class字节码都可以getClassLoaderder得到自己的类加载器,也就是 说每一个Class一定是由一个类加载器加载进来的。那么在Java虚拟机内存里创造了一份字节码,这份字节码就没有被类加载器加载,可是也得必须为它指定一个类加载器,所以创建动态类的时候,一定要给它指定一个类加载器。那么这个类加载器用哪一个呢?可以随便指定,通常用与interfaces相同的类加载器。
----------------------------
下面通过Proxy创建一个动态类
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
public class ProxyTest {
public static void main(String[] args) {
Class clazzProxy = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
System.out.println(clazzProxy.getName());//创建出来的这个类的名字:$Proxy0
//既然有动态类的字节码clazzProxy,就可以看看这个类上有什么样的构造方法,每个构造方法接收的参数类型是什么样子的,还有这个类上的成员方法都有哪些。
//Constructor代表构造方法。
Constructor[] constructors = clazzProxy.getConstructors();
for(Constructor constructor : constructors){
String name = constructor.getName();
StringBuilder sBuilder = new StringBuilder(name);
sBuilder.append('(');
Class[] clazzParams = constructor.getParameterTypes();
for(Class clazzParam : clazzParams){
sBuilder.append(clazzParam.getName()).append(',');
}
if(clazzParams!=null && clazzParams.length != 0)
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
//$Proxy0(java.lang.reflect.InvocationHandler)
//生成的动态类,它只有一个构造方法,这个方法只接收一个参数,参数类型是InvocationHandler
System.out.println(sBuilder.toString());
}
System.out.println("----------begin methods list----------");
Method[] methods = clazzProxy.getMethods();
for(Method method : methods){
String name = method.getName();
StringBuilder sBuilder = new StringBuilder(name);
sBuilder.append('(');
Class[] clazzParams = method.getParameterTypes();
for(Class clazzParam : clazzParams){
sBuilder.append(clazzParam.getName()).append(',');
}
if(clazzParams!=null && clazzParams.length != 0)
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
//打印结果都是Collection和Object类里面的方法,如果实现了某一个接口,就有了那个接口里面的所有方法。
//同时这个动态类也是Object类的子类,所以也包含Object的所有方法。
System.out.println(sBuilder.toString());
}
}
}
-----------------
Java基础加强--创建动态类的实例对象及调用其方法
class MyInvocationHander1 implements InvocationHandler{
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
return null;
}
}
-------------------------------------
//clazzProxy.newInstance();会调用其不带参数的构造方法;但是动态类没有无参的构造方法。
//所以先要搞到动态类有参的构造方法,有了构造方法之后,再用构造方法去new一个Instance
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy1 = (Collection)constructor.newInstance(new MyInvocationHander1());
System.out.println(proxy1);//proxy1.toString返回值为null,而不是proxy1对象为null,为什么打印null呢?
//clear()没有返回值
proxy1.clear(); //调用Collection的clear方法--清除集合,运行后没有报空指针异常,说明proxy1对象已经创建成功了。
proxy1.size();//size()有返回值,这句话执行后出现java.lang.NullPointerException。为什么?
//到这里已经成功的获得了一个动态类,并且成功的用这个动态类去创建了一个实例对象,
//需要注意的是,那个动态类的构造方法要接收一个InvocationHandler对象。
--------------------
Java基础加强--完成InvocationHandler对象的内部功能。
刚才分两步,先搞到类,然后用类创建实例对象。能不能让创建动态类和创建其实例对象合二为一呢?
当热有啦。
JavaAPI给提供的一个例子:
-------------------
Proxy里面还提供了另外一个静态方法,直接搞出了字节码,又搞出了实例对象,一步到位。
static Object | newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) |
这个方法可以返回某个对象的代理对象。
这个方法的第一个参数:使用哪个装载器生成代理对象
第二个参数指定要实现的哪些接口
第三个参数指定生成的代理对象做什么事情(干什么事情也需要用对象来指定)
方法摘要 | |
invoke(Object proxy, Method method, Object[] args) |
这个接口由自己来实现,实现这个接口的invoke方法就能指定返回的代理对象干什么事(调用目标对象的方法以及在调用目标对象的方法的前后添加扩展功能)。
当别人调用代理对象的方法时,都是调用的你实现的invoke方法。
-------------
Collection proxy2 = (Collection) Proxy.newProxyInstance(
Collection.class.getClassLoader(),
new Class[]{Collection.class},//可能是要实现一个或多个接口,使用数组。这里不可以使用可变参数,因为可变参数必须位于参数列表的最后。
new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
return null;
}
}
);
//现在只创建了一个代理动态类的实例对象,还没有创建要被代理的目标。代理类是对目标类进行了扩展。内部还得调用目标类的方法
//代理类的对象要访问目标对象,调用目标对象的方法。所以就应该给代理类指定一个目标。要代理哪个目标,内部就调用那个目标的方法。
代理目的:当调运代理对象,然后在代理对象调运目标对象之前或者调运目标对象以后,我们可以干一些事,比如权限控制,日志记录等。
Collection proxy2 = (Collection) Proxy.newProxyInstance(
Collection.class.getClassLoader(),
new Class[]{Collection.class},//可能是要实现一个或多个接口,使用数组。这里不可以使用可变参数,因为可变参数必须位于参数列表的最后。
new InvocationHandler(){
//目标ArrayList也是实现了Collection接口
ArrayList target = new ArrayList();
@Override //当调用代理对象的方法时,都是调用的你实现的invoke方法。
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
long beginTime = System.currentTimeMillis();//调运目标对象之前做一些事
Object retVal = method.invoke(target, args);//调用目标对象的方法
Thread.sleep(1000); //调运目标对象之后之后做一些事
long endTime = System.currentTimeMillis();
System.out.println(method.getName()+"--running time--"+(beginTime-endTime));
return retVal;
}
}
);
//每调用一次代理对象的方法就会自动去调用InvocationHandler对象的invoke方法。
proxy2.add("zxx");
proxy2.add("lhm");//不直接调用目标对象,直接调用代理对象,然后让代理对象去调用目标对象。
proxy2.add("bxd");
System.out.println(proxy2.size());//3
---------------------
Java基础加强--分析InvocationHandler对象的运行原理
我们知道动态类有一个构造方法,构造方法接收了一个InvocationHandler对象
$Proxy0(java.lang.reflect.InvocationHandler)
接收 InvocationHandler对象要干什么用呢?该构造方法内部的代码会是怎样的呢?
只要构造方法接收一个参数,内部都是准备好一个成员变量,接收别人从构造方法传进来的参数。
接收这个参数就是记住这个参数,为了以后使用它。
$Proxy0 implements Collection
{
InvocationHandler handler;
public $Proxy0(InvocationHandler handler)
{
this.handler = handler;
}
}
---------------------
实现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);
}
}
-------------------------------
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy1 = (Collection)constructor.newInstance(new MyInvocationHander1());
刚才我们的构造方法接收了一个handler对象,现在调用proxy1.size();
size方法的内部就去调用那个handler对象的invoke方法。
int size()
{
return handler.invoke(this,this.getClass().getMethod("size"),null);
}
----------------------------------
InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法接收3个参数,这3个参数分别代表什么?
Client程序调用proxy1.add(“abc”)方法时,涉及三要素:objProxy对象、add方法、“abc”参数
proxy表示当前调用的是哪个代理对象。Method表示代理对象的哪个方法。args表示方法接收的参数。
这个方法内部怎么实现的呢?
在调用代理对象里面的方法的时候,涉及到3个要素,这个方法的内部就把这3个要素传递给了handler的invoke方法。
Class Proxy$ {
add(Object object) {
return handler.invoke(Object proxy, Method method, Object[] args);
}
}
-----------------
知道了代理的原理,再去解决上面的两个问题:
class MyInvocationHander1 implements InvocationHandler{
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
return null;
}
}
-----------
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
Collection proxy1 = (Collection)constructor.newInstance(new MyInvocationHander1());
proxy1.clear(); //运行后没有报空指针异常
System.out.println(proxy1);//proxy1.toString返回值为null,而不是proxy1对象为null,为什么打印null呢?
-------------------------
proxy1.size();//size()有返回值,这句话执行后出现java.lang.NullPointerException。为什么?
因为我们调用proxy1.size()方法的时候,size方法内部会调用MyInvocationHander1对象的invoke方法,invoke方法返回的结果是null。
size方法要的返回值是int,而你却返回了一个null,null不能转换成int,所以proxy1.size()这句话会报错。
------------------------
那么为什么clear方法没有报错?
Clear方法本来就返回一个void,而invoke方法返回了一个null。null就相当于无返回值,返回了一个void。
-----------------------
为什么System.out.println(proxy1);会打印出null呢?
这个问题和下面一个问题性质都是一样的。
Collection proxy2 = (Collection) Proxy.newProxyInstance(
Collection.class.getClassLoader(),
new Class[]{Collection.class},//可能是要实现一个或多个接口,使用数组。这里不可以使用可变参数,因为可变参数必须位于参数列表的最后。
new InvocationHandler(){
//目标ArrayList也是实现了Collection接口
ArrayList target = new ArrayList();
@Override //当调用代理对象的方法时,都是调用的你实现的invoke方法。
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
Object retVal = method.invoke(target, args);//调用目标对象的方法
return retVal;
}
}
);
System.out.println(proxy2.getClass().getName());//$Proxy0
/*按照代理的理论,调用代理对象的getClass()方法,它会转手给目标对象去执行getClass()方法并返回,
* 结果的返回值应该是目标的getClass()---java.util.ArrayList,可为什么返回的却是代理类呢?--$Proxy0*/
/*代理类的getClass方法是从Object类上继承来的,代理类也从Object类继承了好多方法。
* 但代理类只对hashCode、equals、toString 这3个方法进行重写,并委托给handler去做。
* 代理类不重写从 java.lang.Object 继承的代理实例的其他公共方法,所以这些方法的调用行为与其对 java.lang.Object 实例的操作一样。
* */
System.out.println(proxy1);打印这个对象的时候,会调用对象的toString方法,toString方法在代理类里被重写了,
因此在调用代理对象的toString方法时,它会自动调用MyInvocationHander1对象的invoke方法。所以会返回null。。
---------------
总结动态代理的工作原理:
客户端调用代理,代理的构造方法接收一个InvocationHandler对象
然后客户端调用代理的各个方法。代理的各个方法内部会把请求转发给通过构造方法传进来的InvocationHandler对象
这个InvocationHandler对象的invoke方法又把各个请求分发给目标的相应方法。