前言
1、导读
本文,通过三个案例模仿InvocationHandler拦截器和Proxy代理类的内部代码,助你更好的由浅入深的理解动态代理!
下面是被模仿两个类的源代码片段:
package java.lang.reflect;
//系统自带的拦截器接口
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args){
throws Throwable;
}
public static Object invokeDefault(Object proxy, Method method, Object... args){
……
}
}
package java.lang.reflect;
public class Proxy implements java.io.Serializable {
public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException {
……
}
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) {
……
}
}
Proxy类参数ClassLoader的作用 :它可以通过任意当前类获取。
示例如下所示:
ClassLoader loader = Test.class.getClassLoader();//注:Test可以是当前类的类名
Class c4 = loader.loadClass("com.succ.demo.Person");
更多关于反射的参考,点击进入反射的意义、用法、优缺点等 。
Proxy类参数interfaces的作用 :它是业务类所implements的某个统一方法/业务接口;
比如,public class Tank implements Moveable {} ,那么此处的interfaces就可以是Moveable 。
Proxy类参数InvocationHandler 的作用 :它就是具体的装饰类,也就是implements InvocationHandler的那个具体的实体类,主要用来包裹业务类的某个方法,起到修饰(过滤、拦截、校验等)作用。
比如:public class TimeInvocationHandler implements InvocationHandler {},那么此处的InvocationHandler 就可以是TimeInvocationHandler。
温馨提示:在实际开发中,这两个类可以直接用,而不必像下方案例中的写法,自己拼装代码。
2、InvocationHandler和Proxy的用法
InvocationHandler用法:
自定义的各种拦截、过滤器直接 implements InvocationHandler 接口,并在实现类中完成自己的拦截、过滤等;
意义:它存在的意义,就是定义一个通用的接口类,可以在指定类某个方法的外层添加自己的拦截、过虑、校验代码!
具体用法:参考案例二中的TimeInvocationHandler 即可!
Proxy用法:
首先要了解,Proxy它是个包装类,被包装的类叫做本体(负责执行方法),用法如下方案例二所示:
public class TankClient { public static void main(String[] args) throws Exception { Moveable tank=new Tank(); //动态生成一个实现了主类同一接口的代理类,并动态包装方法前后的代码 Moveable timeProx=(Moveable)Proxy.newProxyInstance(Moveable.class,new TimeInvocationHandler(tank)); System.out.println(timeProx); timeProx.move(); timeProx.stop(); //注:如果运行完上面代码,控制台打印:ClassNotFoundException。需要刷新一下工程里的代码把新自动生成的文件load到项目中。 } }
3、动态代理的使用场景
对方法执行前后(添加代码),进行监控(比如:方法运行[多少毫秒、记录日志、事务提交或回滚、权限监控、数据拦截、数据过滤]等)。
好处:让方法本体,与在该方法前后需要拦截、过虑、校验的方法隔离开,实现解耦。
4、是否使用动态代理对比
不采用代理:对一个类的多个方法进行监控时,重复的代码总是重复出现,不但破坏了原方法,如果要实现多个监控,将会对代码造成大量冗余。
同时,还导致业务代码,与非业务的监控代码掺杂在一起,不利于扩展和维护。
CarTimeProxy DogTimeProxy CatTimeProxy CowTimeProxy 代理类在无限制膨胀,就需要无限的修改业务代码。
采用代理后:原方法不需要做任何改动,操作的是原方法的代理对象,而原方法不用做任何修改,就实现了被监控。
5、动态代理原理
原理:面向切面编程(动态生成代理对象)。
1、这些监控代码,是一些共性代码,可以抽离出来,而不必和业务代码混杂一起。
2、对指定方法,可以同时进行日志跟踪、权限判断、事务处理、过滤等操作。
带着以上知识点,进入正题:
¥¥¥¥¥¥¥¥¥通过下方案例,可让你更好的、随心所欲、游刃有余的在合适的场景,使用动态代理类,InvocationHandler和Proxy这两个类!¥¥¥¥¥¥¥¥¥
案例一、硬性编码,最简单,最好理解,最冗余
实现原理:业务代码和拦截、过滤代码混在一起。
温馨提示
案例一看完后,可以先看案例三,回头再看案例二,因为一、三比较好理解,二稍来回拼接字符串,比较烧脑。
//1、定义统一的运动接口
public interface Moveable {
void move() ;
void stop() ;
}
//2、坦克类实现运动接口
public class Tank implements Moveable {
public Tank() {
super();
}
@Override
public void move() {
try {
System.out.println("tank is moving");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void stop() {
try {
System.out.println("tank is stopping");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//3、实现坦克的时间监控代理(注:main方法测试运行的时候,运行该代理,代理中传入 聚合类Tank就可以了)
public class TankTimeProxy implements Moveable {
Moveable tank ;//采用聚合,这里不用extends继承tank
public TankTimeProxy(Moveable tank) {
super();
this.tank = tank;
}
@Override
public void move() {
long start=System.currentTimeMillis();//方法运行的起始时间
System.out.println("run start time:"+start);
tank.move();//调用父类的move方法
long end=System.currentTimeMillis();//方法运行的截止时间
System.out.println("run end time:"+end);
System.out.println("运行了:"+(end-start)+"毫秒");
}
@Override
public void stop() {
long start=System.currentTimeMillis();//方法运行的起始时间
System.out.println("run start time:"+start);
tank.stop();//调用父类的stop方法
long end=System.currentTimeMillis();//方法运行的截止时间
System.out.println("run end time:"+end);
System.out.println("运行了:"+(end-start)+"毫秒");
}
}
//4、同理,实现对Tank方法的日志记录
public class TankLogProxy implements Moveable {
Moveable tank ;//采用聚合,这里不用extends继承tank
public TankLogProxy(Moveable tank) {
super();
this.tank = tank;
}
@Override
public void move() {
System.out.println("坦克启动,进入战场!");
System.out.println();
tank.move();//调用父类的move方法
System.out.println();
System.out.println("坦克撤离,离开战场~");
}
@Override
public void stop() {
System.out.println("坦克停下,进入补给状态!");
System.out.println();
tank.stop();//调用父类的stop方法
System.out.println();
System.out.println("补给完毕,结束休整~");
}
}
//5、对以上代理,进行测试
public class TankClient {
public static void LogAndTime(Tank tank) {
//日志代理包在最外面,内层是时间代理(外层的代理,最后执行)
Moveable timeProxy=new TankTimeProxy(tank);//时间代理包住“宿主坦克”
Moveable logProxy=new TankLogProxy(timeProxy);//日志代理再包住“时间代理”
logProxy.move();
printLine();
logProxy.stop();
}
public static void TimeAndLog(Tank tank) {
//时间代理包在最外面,内层是日志代理(外层的代理,最后执行)
Moveable logProxy=new TankLogProxy(tank);//日志代理包住“宿主坦克”
Moveable timeProxy=new TankTimeProxy(logProxy);//时间代理包住“日志代理”
timeProxy.move();
printLine();
timeProxy.stop();
}
public static void printLine() {
System.out.println("-------------------------------------------------------");
System.out.println("-------------------------------------------------------");
}
public static void main(String[] args) {
Tank tank=new Tank();
LogAndTime(tank);//日志记录在最外层
//TimeAndLog(tank);//时间记录在最外层
}
}
下面是测试结果
坦克启动,进入战场!
run start time:1616782198706
tank is moving
run end time:1616782203708
运行了:5002毫秒
坦克撤离,离开战场~
-------------------------------------------------------
-------------------------------------------------------
坦克停下,进入补给状态!
run start time:1616782203708
tank is stopping
run end time:1616782206708
运行了:3000毫秒
补给完毕,结束休整~
小节
优点:
1、上面的代码逻辑清晰,可读性很强。
2、如果被监控的地方比较少,扩展性尚可(如果需要被监控的类比较多的话,重复代码就会比较多)。
缺点:
1、有N个类需要被监控我们就要写N*(每个类有几种监控)个代理,比如:Tank 有TankLogProxy/TankTimeProxy/TankTransactionProxy等等,代码量比较大,一个Tank就会冒出很多个代理。
反思:
1、我们能否实现动态代理,动态生成代理类?
2、一个TimeProxy可以监控所有的满足该监控条件的类,比如:Tank、Cat、Dog、Cow等等。
3、监控代码,最好能分离出来,不和业务代码混淆在一起。
实现思路:
1、新建一个代理总类Proxy(JDK自带的有这个类,在本案例中我们模拟这个类)
2、在总代理类中要有一个newProxyInstance的方法,来通过入参动态生成我们需要的代理类(用该代理来运行我们需要运行的方法,比如:tank的move())。
public class Proxy { public static Object newProxyInstance(){ Object newObjectProxy=null; return newObjectProxy; } }
3、如上所示,我们是不是就可以通过该Proxy类,愉快的,动态生成我们需要的代理类了。(*—* 先自嗨一下)
注:由于逻辑比较简单(以上内容),但是实现动态代理的小细节比较多(如下内容),但是一旦写好Proxy以后的以后,就一劳永逸了。
首先我们要解决掉,几座小山头(如果基础比较扎实,可以略过下面的文字描述,直接看代码)。
1、把以上代理类(比如:TankTimeProxy )中的所有方法,都复制到Proxy的 newProxyInstance()方法内部,并对其进行字符串拼接。(工程量比较大,需仔细)。
2、拼接完字符串,发现:既然我要想实现动态代理,里面的部分代码是不能写死的。比如:对“方法”“”字符串的拼接 (参考上面 TankTimeProxy 类的move 方法、stop方法),在代理类中,我们可以用一个for循环,动态拼接方法。
3、我们在工程中新建的Java类,编译器会自动帮我们把编译后的class文件,放到bin目录下。对于我们自己动态生成的java类,需要我们手动编译。(特别提示:Class.forName("com.baidu.TankProxy").newInstance();,这种写法只能加载在内存中的类,比如:bin目录内的class)。我们手动动态创建的java文件,为了区分起见,避免不必要的冲突,需要单独放。此时我们就需要自己编译为class文件了。
4、Proxy类的newProxyInstance方法,为了更好的兼容,需要2个很重要的参数传入:Class interFace,InvocationHandler handler。前者 是实体类+代理类共同实现的接口类,后者是处理器总接口,具体要处理的是 log 还是transaction。
5、Proxy类的newProxyInstance方法中,可能会需要用到:创建目录、写数据到java文件中、URL方式加载文件、编译java文件为class
案例二、在理解案例一的基础上,进行字符串拼接和改装
具体实现:代码量不大,如果不容易看懂,请仔细阅读上面的提示。在理解的大原理基础上,就比较容易消化下面的代码。
注:
1、里面的路径可以随意指定Window系统下面的任意位置,在本案例中,统一为项目中的路径。
2、动态代理类这和jdk的保持一致$Proxy1
准备工作
先创建 监控处理器总接口InvocationHandler(JDK有这个类,此处为模拟写法),及其实现类之一TimeInvocationHandler。
import java.lang.reflect.Method;
//监控处理器,总接口(其他各种监控,都要implements 该接口)
public interface InvocationHandler {
/**
* @param objectProxy 代理对象(该参数主要是在动态代理类Proxy中调用,Invoke()入参写的是this,就是动态生成的代理类本身$Proxy1),可以理解为:TankTimeProxy TankLogProxy
* @param method
* 特别注意:其实现类,具体实现该方法是,方法内部invoke()第一个参数调用的应该是声明的聚合对象 Object target,比如:User、Cat、Tank
*/
void invoke(Object objectProxy,Method method);
void invoke(Object objectProxy,Method method,Object[] methodParameter );
}
//监控处理器,实现器之一,时间处理器
public class TimeInvocationHandler implements InvocationHandler {
public Object target;//被代理对象,比如:Tank
public TimeInvocationHandler() {
super();
}
public TimeInvocationHandler(Object target) {
super();
this.target = target;
}
@Override
public void invoke(Object objectProxy, Method method) {
this.invoke(target,method,null);
}
@Override
public void invoke(Object objectProxy, Method method, Object[] methodParameter) {
long start=System.currentTimeMillis();//方法运行的起始时间
System.out.println(method.getName()+" run start time:"+start);
try {
method.invoke(target, methodParameter);
} catch (Exception e) {
e.printStackTrace();
}
long end=System.currentTimeMillis();//方法运行的截止时间
System.out.println(method.getName()+" run end time:"+end);
System.out.println(method.getName()+"运行了:"+(end-start)+"毫秒");
}
}
Proxy类具体实现
String字符串拼接、文件写入、class编译等。特别注意:invoke()中的参数,传递是this,指的是当前动态生成的代理类。
public class Proxy {
final static String rt="\r\t";
//下面是复制的TankTimeProxy源码,仅仅用于对比查看,不用于案例实际操作。
public static Object newProxyInstance() throws Exception{
String src=
"package com.xp.proxy;"+rt+rt+
"public class TankTimeProxy implements Moveable {"+rt+
" Moveable tank ;//采用聚合,这里不用extends继承tank"+rt+
" public TankTimeProxy(Moveable tank) {"+rt+
" super();"+rt+
" this.tank = tank;"+rt+
" }"+rt+
" @Override"+rt+
" public void move() {"+rt+
" long start=System.currentTimeMillis();//方法运行的起始时间"+rt+
" System.out.println(\"tank run start time:\"+start);"+rt+
" "+rt+
" tank.move();//调用父类的move方法"+rt+
" "+rt+
" long end=System.currentTimeMillis();//方法运行的截止时间"+rt+
" System.out.println(\"tank run end time:\"+end);"+rt+
" System.out.println(\"运行了:\"+(end-start)+\"毫秒\");"+rt+
" }"+rt+
" "+rt+
" @Override"+rt+
" public void stop() {"+rt+
" long start=System.currentTimeMillis();//方法运行的起始时间"+rt+
" System.out.println(\"tank stop time:\"+start);"+rt+
" "+rt+
" tank.stop();//调用父类的stop方法"+rt+
" "+rt+
" long end=System.currentTimeMillis();//方法运行的截止时间"+rt+
" System.out.println(\"tank go again time:\"+end);"+rt+
" System.out.println(\"停了:\"+(end-start)+\"毫秒\");"+rt+
" }"+rt+
" "+rt+
"}";
System.out.println(src);
return null;
}
//该方法为真正的拼接方法,实际操作用这个方法。考虑到代码的可读性,里面拆分了几个纯粹的小方法。
public static Object newProxyInstance(Class interFace,InvocationHandler handler) throws Exception {
Method[] methods=interFace.getMethods();
String interName=interFace.getName();
//拼接字符串javaFileContent,并把拼接后的字符,写入到动态生成的$Proxy1.java文件中
String javaFileContent=
"package com.xp.proxy;"+rt+rt+
"import java.lang.reflect.Method;"+rt+
"public class $Proxy1 implements "+interName+" {"+rt+
" com.xp.proxy.InvocationHandler handler ; "+rt+
" "+rt+
" public $Proxy1(com.xp.proxy.InvocationHandler handler) {"+rt+
" super();"+rt+
" this.handler = handler;"+rt+
" }"+rt+
//getStringMethods(methods).toString()+
getStringMethodsByInterFace(interFace,methods).toString()+
"}";
Object newPro=doBirthJavaFileAndLoadFileClass(interFace,handler,javaFileContent);
return newPro;
}
public static void makeDir(String dir) {
File file = new File(dir);
if(!file.exists()) {
file.mkdirs();
System.out.println("创建路径完毕");
}
}
public static Object doBirthJavaFileAndLoadFileClass(Class interFace,InvocationHandler handler,String javaFileContent) throws Exception {
String projectPath=System.getProperty("user.dir");// G:\Workspaces\Eclipse4.7 Jee\ProxyMode
//makeDir(projectPath);
String fileName=projectPath+"\\src\\com\\xp\\proxy\\$Proxy1.java";//双斜杠,其中一个是转义字符
System.out.println("fileName= "+fileName);
File file=new File(fileName);
FileWriter writer=new FileWriter(file);
writer.write(javaFileContent);
writer.flush();
writer.close();
//step 1:编译
JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();//获取java自带的编译器,把Java文件编译为class
//System.out.println(compiler.getClass().getName());
StandardJavaFileManager fileManager= compiler.getStandardFileManager(null,null,null);
Iterable compilationUnits=fileManager.getJavaFileObjects(fileName);//获取编译单元对象
CompilationTask tasks=compiler.getTask(null, fileManager, null, null, null, compilationUnits);//获取编译任务
//注,此处需要点击Eclipse任务栏的Window-->show view,切换到Navigator模式才能看到编译后的class文件
tasks.call();//编译为class
fileManager.close();
//step 2:编译完毕,需要把class文件load到内存,然后从内存拿到对象。
//注:用classLoad的时候,必须load的是bin目录下的class(例如:X.class.forName("com.xp.TankTimeProxy").newInstance()这种写法),当前动态编译的class不在bin目录下
String localFilePath="file:\\"+projectPath+"\\src\\com\\xp\\proxy";//自动生成的java文件所在根目录
//String localFilePath="file:/"+projectPath;//自动生成的java文件所在根目录
System.out.println(localFilePath);
URL[]urls=new URL[] {new URL(localFilePath)};
URLClassLoader uLoader=new URLClassLoader(urls);
Class cl=uLoader.loadClass("com.xp.proxy.$Proxy1");//被动态生成的代理类
//System.out.println(c+" 666");
//注:cl.newInstance(),使用的前提是,对应的cl类里有一个空的构造方法。
Constructor c=cl.getConstructor(InvocationHandler.class);//自定义一个构造方法给cl,同时定义入参
Object newPro=c.newInstance(handler);//把被代理对象传入,生成新的对象
//newPro.move();
return newPro;
}
//该方法的存在,仅仅是为了下面的方法getStringMethodsByInterFace做铺垫,实际中不予运用
public static StringBuffer getStringMethods(Method[] methods) {
StringBuffer srtBf=new StringBuffer();
for (Method md : methods) {
String strMethod=
" @Override"+rt+
" public void "+md.getName()+"() {"+rt+
" long start=System.currentTimeMillis();"+rt+ //动态生成方法前后的处理,需要引入Invocation
" System.out.println(\"run start time:\"+start);"+rt+
" "+rt+
" obj."+md.getName()+"();"+rt+
" "+rt+
" long end=System.currentTimeMillis();"+rt+
" System.out.println(\"run end time:\"+end);"+rt+
" System.out.println(\"运行了:\"+(end-start)+\"毫秒\");"+rt+
" }"+rt+
" "+rt;
srtBf.append(strMethod);
}
return srtBf;
}
public static StringBuffer getStringMethodsByInterFace(Class interFace,Method[] methods) throws Exception{
StringBuffer srtBf=new StringBuffer();
for (Method md : methods) {
String strMethod=
" @Override"+rt+
" public void "+md.getName()+"() {"+rt+
" try{"+rt+
" Method m="+interFace.getName()+".class.getMethod(\""+md.getName()+"\");"+rt+
" handler.invoke(this,m);"+rt+ //这里直接传入md会报错,需要上一行代码特殊处理一下,处理为m
" }catch(Exception e){"+rt+
" e.printStackTrace();"+rt+
" } "+rt+
" }"+rt+
" "+rt;
srtBf.append(strMethod);
}
return srtBf;
}
}
对动态代理类进行测试
public class TankClient {
public static void main(String[] args) throws Exception {
Moveable tank=new Tank();
//动态生成一个实现了主类同一接口的代理类,并动态包装方法前后的代码
Moveable timeProx=(Moveable)Proxy.newProxyInstance(Moveable.class,new TimeInvocationHandler(tank));
System.out.println(timeProx);
timeProx.move();
timeProx.stop();
//注:如果运行完上面代码,控制台打印:ClassNotFoundException。需要刷新一下工程里的代码把新自动生成的文件load到项目中。
}
}
tank run start time:1616785209732
tank is moving
tank run end time:1616785214732
move运行了:5000毫秒
tank run start time:1616785214732
tank is stopping
tank run end time:1616785217733
stop运行了:3001毫秒
案例三、在实现动态代理的基础上,对UserMsg类的add操作进行一些监控
//1、 创建接口
public interface UserMgr {
void add();
}
//2、创建接口的实现类
public class UserMgrImpl implements UserMgr {
@Override
public void add() {
System.out.println("添加用户成功");
}
}
//3、创建操作处理器,具体添加监控逻辑代码
import java.lang.reflect.Method;
import com.xp.proxy.InvocationHandler;
public class TransactionInvocationHandler implements InvocationHandler {
public Object target;
public TransactionInvocationHandler() {
super();
}
public TransactionInvocationHandler(Object target) {
super();
this.target = target;
}
@Override
public void invoke(Object objectProxy, Method method) {
invoke(target,method,null);
}
@Override
public void invoke(Object objectProxy, Method method, Object[] methodParameter) {
System.out.println("transaction is begining");
try {
System.out.println("方法正在运行");
method.invoke(target);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("transaction is submited");
}
}
//4、创建测试类
public class UserClient {
public static void main(String[] args) throws Exception {
UserMgr mgr=new UserMgrImpl();
InvocationHandler h=new TransactionInvocationHandler(mgr);
UserMgr userProx=(UserMgr)Proxy.newProxyInstance(UserMgr.class,h );
userProx.add();
}
}
延伸案例:运行结果
transaction is begining
方法正在运行
添加用户成功
transaction is submited
总结:
绕了这么大圈,费了这么大劲,拼了半天字符串,调试了半天。
无非是想动态生成个代理类,并且不对原方法产生伤害,如你所愿,已展示完毕。
温馨提示:Proxy代理类,JDK6以上版本均已帮我们实现,本案例主要是阐述这种思想。实际开发中,用的会比较多。
尾言
最后:上传上案例中的目录结构,便于初学者理解吸收,欢迎大家交流,指导批评。
后记
一个方法外层嵌套多个监控器:
如果想实现动态代理,拼接字符串较为繁琐,本案例没有展示(如果展示的话,不难,但可读性不是很好)。
可喜的是,在案例一给你自己实现多层动态代理,提供了一种思路,感兴趣的话,可以在案例二基础上尝试一下。
附注
猜你可能感兴趣
1、JAV反射:反射的意义价值和用法 | 通过反射获取私有属性和方法 |反射的作用 | 反射的优缺点 | 反射破坏了封装性为什么还要用