一、程序中的代理原理
(一)要为已经存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理、日志、计算方法的运行时间、食物管理、等等
(二)编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码。
class X{
void sayHello(){
System.out.println("hello,itcast");
}
}
要在sayHello方法上增加一个统计执行这个方法需要时间的功能,前提是上边的类的源代码没有给,
做一个X的代理,
XProxy
{
void sayHello(){//既然是代理,就有相同的方法声明
starttime
X.sayHello();
endtime
}
}
之后就可以调用XProxy
代理架构图
1、客户端原来调用的是Target,现在然客户端调用代理。代理和目标是实现了相同的接口doSomeThing(),也就是说对外有相同的方法,
例如用Collection引用HashSet,Collection引用ArrayList
String className=props.getProperty("className");
Collection collections=(Collection)Class.forName(className).newInstance();//得到了类.调用不带参数的构造方法
Collection引用一个集合(Collection),Collection是一个接口,他会引用HashSet,也会引用ArrayList,但在源程序里面没出现这两个类的名字,只出现了接口,这样 就可以切换,不用改源代码就可以切换
2、在代理的方法里,一定要调用目标的对应的方法,但是在调用目标的方法的前边或者后边会加上一点系统功能代码,
类图:虚线表示实现接口
(三)工厂模式和配置文件的方式进行管理
则不需要修改客户端程序,在配置文件中配置是使用目标类、还是代理类,这样以后很容易切换。
例如,想要日志功能时就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想去掉系统功能也很容易
配置文件config.properties
中配置,到底用哪个类className=java.util.HashSet;
二、AOP 交叉业务的编程问题即为面向方面的编程(Aspect oriented program)
面向代理的编程现在应用很广
(一)系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面
例如:
类StudentService负责对学生进行管理,增删改查
类CourseService负责对课程进行管理,管理课程的增删改查
类MiscService对其他信息进行管理,例如管理老师的信息,教室的信息,
但不管什么类,里面都要有安全、事务、日志等功能,贯穿了好些模块当中去了,所以他们就成了交叉业务,他们好像不是一个独立的对象,
(二)用具体的程序代码描述交叉业务:
method1 method2 method3
{ {{
-------------------------------------------------------切面 安全、事务....功能
-------------------------------------------------------切面 安全、日志.....功能
} } }
一个问题域、一个面,变这样的程序就称之为AOP(面向方面的编程)
(三)交叉业务的编程问题即为面向方面的编程(Aspect oriented program)的目的就是要把交叉业务模块化
就是把交叉业务贯穿到好些业务模块中了,把这样的东西模块化,只写一份,其他的全自动应用上,而不是在每个地方都去写,写的时候就用代理的方法
我们不可能去改变目标的方法或者不可能深入到目标的方法内部中去修改,但让用户感觉在方法前/后加了代码,于是把这段代码移到方法外面,,运行的效果与上面一样
-------------------------------------------------------切面
func1 func2 func3
{ {{
... ... ...
} } }
-------------------------------------------------------切面
(四)使用代理技术正好可以解决(三)的问题
三、动态代理技术
a、要为系统中各种借口的类增加代理功能,需要太多的代理类,都采用静态代理方式,会是一件非常麻烦的事
b、JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类
c、JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。
d、CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库
如果有一个目标类本身没有实现接口,那么生成代理类的方法要不要跟目标类的方法一模一样,就是声明了、定义一模一样,要!
而这个目标类没有实现接口,那我通过什么方式来告诉java虚拟机:你跟我生成的类跟那个类有相同的方法列表。Java虚拟机干不了这个事,因为这个目标类没有接口,无法跟java虚拟机说生成这个类的接口方法,没接口,这时候有一个第三方库:CGLIB
CGLIB库可以动态生成一个类的子类,这个子类跟你有没有相同的方法,他继承了父类的所有方法,还不等于有父类方法的声明么,要有CGLIB库才可以生成没有接口的类的子类,现在CGLIB库不是sun公司的,是一个开源的
e、我们生成的目标,生成的代理的方法,去调用目标相应的方法,除了调用目标的方法,还干些额外的事:如果这个代理什么都不干,只是调用目标的方法这个代理就没什么价值,于是可以做些额外的事情
在代理方法中的位置加上系统功能代码:
1、在调用目标方法之前 2、在调用目标方法之后 3、在调用目标方法前后 4、在处理目标方法异常的catch块中
(一)分析JVM动态生成的类
1、用jdk Java虚拟机自己提供的API来生成动态类,用Java虚拟机API生成动态类一定要告诉他一个接口,
2、用的API为java.lang.reflect.Proxy:有个方法(getProxyClass(ClassLoader loader,Class<?>... interfacess),这个方法返回的是class字节码【 static Class<?>】),也就是在内存中造出了一个字节码,也就是说给你造出一个类。
但是在早字节码的时候,需要告诉他这个字节码实现了哪些接口(Class<?>... interfacess);
另外还有一个ClassLoader,我们每一个字class,就每一个字节码都能getClassLoader可以得到自己的类加载器,也就是说每一个Class一定是由一个类加载器加载进来的,就好比每一个人一定有一个妈妈,如果没有妈妈逻辑不通。可是。在java虚拟机内存里面操作一个字节码没有ClassLoader,没有通过类加载器加载,可是也要给他指定一个类加载器,所以创建时一定要指定一个类加载器,做一个动态类:
StringBuilder与StringBuffered的区别:
他们在运用上基本上一样,都是动态的往字符串上添加内容
在单线程里面StringBuilder效率高一些,多线程的情况就要用StringBuffered
现在这个线程肯定被一个线程操作,因为这个线程没有被定义到类里面,被多个线程共享
假设有5个线程调用main方法,那等于有5个main方法同时运行,
* 5个main方法同时运行就有5个StringBuilder变量,
* 个是个的变量,彼此不受影响
* 既然它不受别的线程影响,所以他就不用考虑安全的问题,不考虑安全,效率就高
* 如果此时用StringBuffered在这里,即使不受其他线程的影响,它还要考虑安全,这样效率就低
例:
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(')');
System.out.println(sBuilder.toString());
package cn.itcast.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
* @throws Exception
* @throws
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
/*
* 指定的接口是Collection
* 表达Collection这个接口其实就是用它的字节码Collection.class;
* 这个类加载器通常用接口相同的类加载器Collection.class.getClassLoader();
* 所以得到一个Class clazzProxy1
* 一般看到变量前面前缀为clazz就要考虑变量为一个字节码
* */
Class clazzProxy1=Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
/*
* 既然clazzProxy1是一个字节码,就可以getName
* 打印,这个类的名字叫$Proxy0
*/
System.out.println(clazzProxy1.getName());
System.out.println("-------------begin constructors list-------------");
/*
* 既然是类的字节码,看看类的身上有什么构造方法,每个构造方法接受的参数类型是什么样
* 也看看这个类身上有什么方法
* a、类的身上有什么构造方法,Constructor代表方法,
* 要的到所有的构造方法肯定是一个数组
* 得到一个字节码clazzProxy1以后,可以clazzProxy1.getConstructors()
* 就可以得到constructor的名字、参数列表constructor.getName();
* 希望可以打印出来叫:$Proxy0(),不带参数构造方法是这样
* 假设还有一个参数 $Proxy0(InvocationHandler x,int)
* 这时两个构造方法,第一个无参,第二个有参,
* 希望把他们打印出来,变量的名字并不重要,只是打印参数的类型和个数,
* 想以这种列表的形式进行打印:$Proxy0(), $Proxy0(InvocationHandler x,int)
* 拼出这个串
*
* 现在这个线程肯定被一个线程操作,因为这个线程没有被定义到类里面,被多个线程共享
*/
Constructor[] constructors=clazzProxy1.getConstructors();
for (Constructor constructor:constructors) {
//得到名字constructor.getName();
String name=constructor.getName();
//拼出这个串,初始值为name,也就是说等于$Proxy0()这部分
StringBuilder sBuilder=new StringBuilder(name);
sBuilder.append('(');
/*
* 在这里增加一对员工号就等于增加参数列表
*
*/
Class[] clazzParams=constructor.getParameterTypes();//得到构造函数的参数类型
for (Class clazzParam:clazzParams) {
sBuilder.append(clazzParam.getName()).append(',');//有多个参数列表肯定有','.$Proxy0(InvocationHandler x,int)
}
if (clazzParams!=null&&clazzParams.length==0) {
sBuilder.deleteCharAt(sBuilder.length()-1);//去掉最后一个','
}
sBuilder.append(')');
System.out.println(sBuilder.toString());
}
System.out.print("-------------begin methods list-------------");
Method[] methods=clazzProxy1.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(')');
System.out.println(sBuilder.toString());
}
}
结果:
$Proxy0
-------------begin constructors list-------------
$Proxy0(java.lang.reflect.InvocationHandler) //只有一个构造方法,并且要接收一个参数java.lang.reflect.参数的类型是 :InvocationHandler。生成的这个动态类有构造方//法,参数是InvocationHandler
-------------begin methods list-------------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;,)
newProxyInstance(java.lang.ClassLoader,[Ljava.lang.Class;,java.lang.reflect.InvocationHandler,)
getInvocationHandler(java.lang.Object,)
wait)
wait(long,int,)
wait(long,)
getClass)
notify)
notifyAll)
3、创建动态类的实力对象及其调用方法
a、用反射获得构造方法
b、编写一个简单的InvlcationHandler类
c、调用构造方法传进动态类的实力对象,并将编写的InvlcationHandler类的实力对象穿进去
d、打印创建的对象和调用对象的没有返回值的方法和getClass方法演示调用其他有返回值的方法报告了异常。
e将创建动态类的实例对象的代理改成匿名内部类的形式编写,
package cn.itcast.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
* @throws Exception
* @throws
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Class clazzProxy1=Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
System.out.println(clazzProxy1.getName());
System.out.println("-------------begin constructors list-------------");
Constructor[] constructors=clazzProxy1.getConstructors();
for (Constructor constructor:constructors) {
//得到名字constructor.getName();
String name=constructor.getName();
//拼出这个串,初始值为name,也就是说等于$Proxy0()这部分
StringBuilder sBuilder=new StringBuilder(name);
sBuilder.append('(');
/*
* 在这里增加一对员工号就等于增加参数列表
*/
Class[] clazzParams=constructor.getParameterTypes();//得到构造函数的参数类型
for (Class clazzParam:clazzParams) {
sBuilder.append(clazzParam.getName()).append(',');//有多个参数列表肯定有','.$Proxy0(InvocationHandler x,int)
}
if (clazzParams!=null&&clazzParams.length!=0) {
sBuilder.deleteCharAt(sBuilder.length()-1);//去掉最后一个','
}
sBuilder.append(')');
System.out.println(sBuilder.toString());
}
System.out.println("-----------begin create instance object-------");
/*
* 上面得到了字节码clazzProxy1
* clazzProxy1.newInstance()是得到无参的构造方法
* clazzProxy1.getConstructor(参数类型):是得到有参的构造方法(用反射获得)
* 在获得构造方法时用到的是 参数类型
* 构造方法接受一个参数:InvocationHandler.class,在
* 调用构造方法创建对象的时候就要传真正的参数:
*
*
* InvocationHandler是一个接口,就没法new,只能new他的实现类
*/
Constructor constructor=clazzProxy1.getConstructor(InvocationHandler.class);
//做个InvocationHandler的实现类MyInvocationHander1
class MyInvocationHander1 implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
return null;
}
}
/*用构造方法constructor去newInstance,传递一个对象new MyInvocationHander1()。。
调用构造方法传进动态类的实力对象,并将编写的InvlcationHandler类的实力对象穿进去*/
Collection proxy1=(Collection)constructor.newInstance(new MyInvocationHander1());
/*
* 打印的结果为null:
* 说明有两种可能:
* 1、一种是proxy1没有创建成功为null
* 2、一种是proxy1的toString方法返回了null。
* 所以proxy1.toString()可能会报告空指针异常
* 打印以后有一个假象,也有可能是null,也有可能空指针返回null
*/
System.out.println(proxy1);
proxy1.clear();//clear方法返回void,清除集合。如果此处不报告空指针异常肯定proxy1对象有了
// proxy1.size();//size方法有返回值,返回的是invoke的返回值,是null,而size是整数型的,所以不能成功调用
}
4,动态类实际应用
方法二
Collection proxy2=(Collection)constructor.newInstance(new InvocationHandler(){//跟上面同样效果
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
return null;
}});
Proxy的newProxyInstance(类加载器,接口,用字节码传递的实例对象)静态方法,机能有实力对象又能有字节码
注意:
- Proxy 前的强制转换必须为接口,否则会出现 java.lang.ClassCastException: $Proxy0 cannot be cast to
newProxyInstance()
返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序
InvocationHandler 是代理实例的调用处理程序 实现的接口。
invoke()
在代理实例上处理方法调用并返回结果。在与方法关联的代理实例上调用方法时,将在调用处理程序上调用此方法。
/*
* 类加载器就是接口的类加载器Collection.class.getClassLoader()
*/
Collection proxy3=(Collection)Proxy.newProxyInstance(Collection.class.getClassLoader(),
new Class[]{Collection.class},//接口,可能是一个也可能是多个
new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
ArrayList target=new ArrayList();//指定一个目标,每次调用add都要处理一次目标
return method.invoke(target, args);
}}
);
proxy3.add("zxx");
proxy3.add("lhm");
proxy3.add("bxd");
System.out.println(proxy3.size());
结果: 0
动态类生成的类的内部代码
1、动态类生成的类实现了Collection接口(可以实现若干接口),生成的类有Collection接口中的所有方法和一个如下接受InvocationHandler参数的构造方法。
2、构造方法接受一个InvocationHandler对象,接受对象了要干什么用呢?该方法内部的代码回事怎样的呢?
3、
Collection proxy3=(Collection)Proxy.newProxyInstance(Collection.class.getClassLoader(),
new Class[]{Collection.class},//接口,可能是一个也可能是多个
new InvocationHandler(){
ArrayList target=new ArrayList();//指定一个目标
@Override
/*
* invoke收到了客户端调用的三要素
* 现在还是调用方法method.invoke(target, args);
* 但是不是去调用代理对象,而是去调用目标,就是在目标身上在去执行刚才那个代理正在执行的方法
* 并且刚才代理传递的什么参数,继续把这个参数传递给目标的方法
* 目标执行完了返回一个值retVal
* 把这个值返回出去
* 代理的invoke方法就会受到这个返回值了
* invoke返回的结果就会返回给代理
* 代理proxy3调用add方法传递参数
* 代理add的返回值从invoke的返回值里面取
* 这样就把目标返回的结果返回到代理身上去了
*/
public Object invoke(Object proxy, Method method,//proxy当前正在调用代理对象,method调用这个代理对象的哪个方法
Object[] args) throws Throwable {//调用这个代理对象为它传递哪个参数
//调用目标前可以加代码
long beginTime=System.currentTimeMillis();
//可以在调用目标之前,把参数args改一改。比如写个过滤器,对原来传进来的参数进行修改,返回的结果也可以修改,当改的类型一定要一致
Object retVal=method.invoke(target, args);
//调用目标后可以加代码
long endTime=System.currentTimeMillis();
System.out.println(method.getName()+"running time:"+(endTime-beginTime));
return retVal;
}}
);
//Handler()返回的结果给了add方法,add方法继续向外返回,Handler()返回的结果来源于目标返回,目标返回的结果交给invoke
Object obj=proxy3.add("zxx");//调用代理的add方法
proxy3.add("lhm");
proxy3.add("bxd");
System.out.println(proxy3.size());
//Handler()返回的结果给了add方法,add方法继续向外返回,Handler()返回的结果来源于目标返回,目标返回的结果交给invoke
Object obj=proxy3.add("zxx");//调用代理的add方法
proxy3.add("lhm");
proxy3.add("bxd");
//System.out.println(proxy3.size());
System.out.println(proxy3.getClass().getName());
结果:$Proxy0
因为: getClass方法是从Object类继承过来的
hashCode,equals,toString三个方法委托给handler,其他的都不委托
下面
* 要把代码参数的形式传进来,可以把这些代码写成字符串,然后放进来,执行这些字符串表示的代码,在java中sclipe有这种方法
* 传递一个字符串进来,执行eval代码,这样的好处是可以在运行的时候放进来
* 设计代码在程序运行时可以临时设置,而不是编译代码时提前指定好
* 传递个对象给InvocationHandler,
* 只要把想要执行的对象放到InvocationHandler方法里面
* 调用代码就等于执行所给的代码
重构方法,选中
Collection proxy3=(Collection)Proxy.newProxyInstance(Collection.class.getClassLoader(),
new Class[]{Collection.class},//接口,可能是一个也可能是多个
new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method,//proxy当前正在调用代理对象,method调用这个代理对象的哪个方法
Object[] args) throws Throwable {//调用这个代理对象为它传递哪个参数
long beginTime=System.currentTimeMillis();
Object retVal=method.invoke(target, args);
long endTime=System.currentTimeMillis();
System.out.println(method.getName() +"running time:"+(endTime-beginTime));
return retVal;
}}
);
右键Refactor->Extract Method,起名
定义一个接口,新建一个Advice.java文件(希望你执行的系统功能,也是别人对你的一个建议,即将插入进来的,嵌入进来的建议,像这种系统功能一般起名Advice)
在ProxyTest.java里加上
ProxyTest.java
advice.afterMethod();
final ArrayList target=new ArrayList();//指定一个目标,把目标拿出来
Collection proxy3 = (Collection)getProxy(target,new MyAdvice());
private static Object getProxy(final Object target,final Advice advice) {//target目标,advice系统功能。生成代理
Object proxy3=Proxy.newProxyInstance(
target.getClass().getClassLoader(),
/* new Class[]{Collection.class},//接口,可能是一个也可能是多个*/
target.getClass().getInterfaces(),//与target实现相同的接口
new InvocationHandler(){
@Override
/*
* invoke收到了客户端调用的三要素
* 现在还是调用方法method.invoke(target, args);
* 但是不是去调用代理对象,而是去调用目标,就是在目标身上在去执行刚才那个代理正在执行的方法
* 并且刚才代理传递的什么参数,继续把这个参数传递给目标的方法
* 目标执行完了返回一个值retVal
* 把这个值返回出去
* 代理的invoke方法就会受到这个返回值了
* invoke返回的结果就会返回给代理
* 代理proxy3调用add方法传递参数
* 代理add的返回值从invoke的返回值里面取
* 这样就把目标返回的结果返回到代理身上去了
*/
public Object invoke(Object proxy, Method method,//proxy当前正在调用代理对象,method调用这个代理对象的哪个方法
Object[] args) throws Throwable {//调用这个代理对象为它传递哪个参数
/*//调用目标前可以加代码
long beginTime=System.currentTimeMillis();
//可以在调用目标之前,把参数args改一改。比如写个过滤器,对原来传进来的参数进行修改,返回的结果也可以修改,当改的类型一定要一致
Object retVal=method.invoke(target, args);
//调用目标后可以加代码
//System系统功能抽取成对象
long endTime=System.currentTimeMillis();
System.out.println(method.getName() +"running time:"+(endTime-beginTime));
return retVal;*/
advice.beforeMethod(method);
//可以在调用目标之前,把参数args改一改。比如写个过滤器,对原来传进来的参数进行修改,返回的结果也可以修改,当改的类型一定要一致
Object retVal=method.invoke(target, args);
//调用目标后可以加代码
//System系统功能抽取成对象
advice.afterMethod(method);
return retVal;
}}
);
return proxy3;
}
Advice.java
package cn.itcast.day3;
import java.lang.reflect.Method;
/*
* 一般有四种
* 再异常里执行的方法
* 在方法之前执行的
* 在方法之后执行的
* 在方法之前后执行的
*/
public interface Advice {
public void beforeMethod(Method method);
public void afterMethod(Method method);
}
MyAdvice.java
package cn.itcast.day3;
import java.lang.reflect.Method;
public class MyAdvice implements Advice {
long beginTime=0;
public void afterMethod(Method method) {
// TODO Auto-generated method stub
System.out.println("从传智播客毕业上班了");
long endTime=System.currentTimeMillis();
System.out.println(method.getName() +"running time:"+(endTime-beginTime));
}
public void beforeMethod(Method method) {
// TODO Auto-generated method stub
System.out.println("到传智播客学习");
beginTime=System.currentTimeMillis();
}
}
结果:
null
到传智播客学习
从传智播客毕业上班了
addrunning time:1
到传智播客学习
从传智播客毕业上班了
addrunning time:1
到传智播客学习
从传智播客毕业上班了
addrunning time:0
到传智播客学习
从传智播客毕业上班了
sizerunning time:0
3
$Proxy1
四、实现AOP功能的封装与配置
类似于spring的AOP框架
思路:
1、Object obj=BeanFactory.getBean("xxx")
工厂BeanFactory(产生javabean的factory),参数是Bean的名字,根据这个名字创建出javabean的对象,
a、BeanFactory的构造方法接受代表配置文件的输入流对象,配置文件格式如下:
xxx=java.util.ArrayList
xxx=cn.itcast.ProxyFactoryBean
xxx.target=java.util.ArrayList
xxx.advice=java.util.Myadvice
b、ProxyFacotryBean充当封装生成代理的工厂
创建工厂BeanFactory.java,专门创建javaBean对象,