❝吹NB不负责:这可能是你从未见过的全新版本! ❞
引言
❝《雪地里的小画家》 下雪啦,下雪啦! 雪地里来了一群小画家。 小鸡画竹叶,小狗画梅花, 小鸭画枫叶,小马画月牙。 不用颜料不用笔, 几步就成一幅画。 青蛙为什么没参加? 他在洞里睡着啦。 ❞
还记得上小学时候的这篇课文吗?这是我记忆深刻的一篇语文课文,哈哈,在这里提出来让大家也回忆一下小学的故事。
这里面提到了小鸡,小狗,小马,小鸭,青蛙,他们都会在雪地里画画,我们以这些小动物为对象
,来说明一些问题吧。
静态代理
❝这些会画画小动物我们抽象出一个
画家 Painter
接口来,让小动物实现Painter
,完成paint()
方法。 ❞
小画家Painter
public interface Painter {
void paint();
}
小狗Puppy
画梅花
public class Puppy implements Painter {
@Override
public void paint() {
System.out.println("小狗画梅花");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
小马Pony
画月牙
public class Pony implements Painter {
@Override
public void paint() {
System.out.println("小马画月牙");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
❝两个就够了,其他几个小画家就不模拟了,手动捂脸~ ❞
老师Teacher
想要看Pony
画画:
public class Teacher {
public static void main(String[] args) {
new Pony().paint();
}
}
运行结果:
代码语言:javascript复制小马画月牙
Process finished with exit code 0
因为画的方法里有随机睡x秒
的业务处理逻辑,Teacher
现在想知道具体睡了多少秒,怎么办呢?
这还不简单,在paint()
方法中加开始、结束时间,然后相减就可以了:
public class Pony implements Painter {
@Override
public void paint() {
//加上时间记录,计算业务处理运行的时间
long start = System.currentTimeMillis();
System.out.println("小马画月牙");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("paint 画画耗时:" (end - start) "毫秒");
}
}
当然,小狗Puppy
的paint()
方法也要加这一段。
老师Teacher
的问题又来了,他还想让画画的时候记录下日志,那么可以做如下修改:
public class Pony implements Painter {
@Override
public void paint() {
//加上日志记录
System.out.println("日志:开始作画");
//加上时间记录,计算业务处理运行的时间
long start = System.currentTimeMillis();
System.out.println("小马画月牙");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("日志:画完了");
System.out.println("paint 画画耗时:" (end - start) "毫秒");
}
}
可以看出,如果想要Pony
在画画的时候添加一些诸如记录执行时间
,记录日志
这样的动作的时候,就要在实现paint()
方法的时候添加时间
、日志
这些东西。
但是,这不合理呀,我Pony
明明只需要处理画画的逻辑就行了啊!也简单,把时间处理
、日志处理
这些东西交给别人
去做,可以把别人
看成代理,这些代理分别持有paint()
方法,在代理内部实现画画之外的事情。
代理
❝
Teacher
将来只和代理
打交道,所以代理
必须也“会画画”的业务,除此之外,才是代理
处理特殊的业务。 so,代理可以看成是具有额外功能的Painter
,那就也让他实现Painter
接口,并且持有具体小画家(比如Pony
)对象(因为代理需要会画画) ❞
处理时间的代理TimeProxy
public class TimeProxy implements Painter {
private Pony pony;
public TimeProxy(Pony pony) {
this.pony = pony;
}
@Override
public void paint() {
long start = System.currentTimeMillis();
//调用小马画画
pony.paint();
long end = System.currentTimeMillis();
System.out.println("paint 画画耗时:" (end - start) "毫秒");
}
}
这时的Pony
开心了,只处理自己的逻辑即可,去掉时间、日志:
public class Pony implements Painter {
@Override
public void paint() {
System.out.println("小马画月牙");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
来,Teacher
走一遍,让代理给我办事:
public class Teacher {
public static void main(String[] args) {
new TimeProxy(new Pony()).paint();
}
}
运行结果:
代码语言:javascript复制小马画月牙
paint 画画耗时:3221毫秒
Process finished with exit code 0
大家想想,这样写有什么缺陷没有?
有的,这里只是持有了Pony
的对象,也就是说这个代理只能代理Pony
画画的时间处理,事实上,这个TimeProxy
也能代理Puppy
等其他小画家的,那如何做呢?
把具体的Pony
对象换成抽象的Painter
,Teacher
想看谁画画就给代理传哪个画家就行了!
改一下TimeProxy
:
public class TimeProxy implements Painter {
private Painter painter;
public TimeProxy(Painter painter) {
this.painter = painter;
}
@Override
public void paint() {
long start = System.currentTimeMillis();
painter.paint();
long end = System.currentTimeMillis();
System.out.println("paint 画画耗时:" (end - start) "毫秒");
}
}
这次调用小狗Puppy
来画:
new TimeProxy(new Puppy()).paint();
代码语言:javascript复制小狗画梅花
paint 画画耗时:2152毫秒
Process finished with exit code 0
very ok 了!别急,来把日志的代理也加进去。
代码语言:javascript复制public class LogProxy implements Painter {
private Painter painter;
public LogProxy(Painter painter) {
this.painter = painter;
}
@Override
public void paint() {
System.out.println("日志:开始作画");
painter.paint();
System.out.println("日志:画完了");
}
}
现在想一下,Teacher
该怎么调用这两个代理,既能打印运行时间,又能打印处理日志,还能画画?
我们看一下代理的构造方法,他里面传的是抽象的画家
,并不是具体的,而代理本身也是一种特殊的画家
-代理本身是实现Painter
这个接口的,所以调用的时候可以把代理作为参数传递到另一个代理
!!!
public class Teacher {
public static void main(String[] args) {
//new TimeProxy(new Puppy()).paint();
new TimeProxy(new LogProxy(new Puppy())).paint();
}
}
运行:
代码语言:javascript复制日志:开始作画
小狗画梅花
日志:画完了
paint 画画耗时:8489毫秒
Process finished with exit code 0
既有日志处理,又有时间处理,还有画画本身的逻辑处理,大功告成!
上面的例子诠释了一种设计模式-代理模式,这是一种静态代理模式。
动态代理
❝从前面的例子我们可以看到,静态代理只能作为某一特定的接口的代理,比如前面的
TimeProxy
只能代理Painter
。 像这种记录执行时间
的操作,应该可以应用于所有对象的方法上,具有普遍性,如果要实现把TimeProxy
使用到别的地方,其他Object,该怎么做呢? 分离代理行为与被代理对象,使用jdk的动态代理。 ❞
JDK的动态代理
jdk的Proxy
类来自于java.lang.reflect
包,没错,就是大名鼎鼎的反射
,反射是根据已经编译好的二进制字节码来分析类的属性和方法,只要给我一个.class
我就能分析出他的内容。
上代码:
代码语言:javascript复制public class Teacher {
public static void main(String[] args) {
Pony pony = new Pony();
Painter painter = (Painter) Proxy.newProxyInstance(
Pony.class.getClassLoader(),
Pony.class.getInterfaces(),//new Class[]{Painter.class}
new TimeProxyHandler(pony));
painter.paint();
}
}
Proxy.newProxyInstance
有三个参数,第一个是被代理类的类加载器,第二个是实现的接口数组,也可以写成:
new Class[]{Painter.class}
重点是第三个参数,该参数是一个InvocationHandler
,动态代理方法在执行时,会调用InvocationHandler类里面的invoke方法去执行。
类TimeProxyHandler
的具体实现:
public class TimeProxyHandler implements InvocationHandler {
private Pony pony;
public TimeProxyHandler(Pony pony) {
this.pony = pony;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object o = method.invoke(pony, args);
long end = System.currentTimeMillis();
System.out.println("执行耗时:" (end - start) "毫秒");
return o;
}
}
执行Teacher.main()
运行结果:
小马画月牙
执行耗时:7881毫秒
Process finished with exit code 0
以上我们是用JDK的动态代理可以分离代理行为和被代理的对象,这里的Pony
可以换成其他对象。
我的main方法里只调用了painter.paint();
啊,怎么连执行耗时:7881毫秒
这句话也打印出来了呢?
JDK动态代理原理分析
运行结果打印了执行耗时:7881毫秒
,说明程序必然运行了TimeProxyHandler
的invoke
方法,我们来分析一下下面这句
Painter painter = (Painter) Proxy.newProxyInstance(...
Proxy.newProxyInstance
这一句创建了一个中间类,我们通过如下手段System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")
把它弄出来看看:
public class Teacher {
public static void main(String[] args) {
Pony pony = new Pony();
//将proxy内部调用invoke方法 生成的中间类 保存下来
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Painter painter = (Painter) Proxy.newProxyInstance(
Pony.class.getClassLoader(),
new Class[]{Painter.class},
new TimeProxyHandler(pony));
painter.paint();
}
}
再次运行,发现项目目录多了这个:
打开看看,就能明白个差不多了
代码语言:javascript复制public final class $Proxy0 extends Proxy implements Painter {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
jdk帮我们生成的$Proxy0
继承Proxy
实现Painter
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.traveler100.dp.proxy.dynamicproxy.Painter").getMethod("paint");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
...
public final void paint() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
当我们main
里面调用painter.paint()
时,实际上执行了super.h.invoke(this, m3, (Object[])null)
,这里的m3
:
m3 = Class.forName("com.traveler100.dp.proxy.dynamicproxy.Painter").getMethod("paint");
来,一图胜千言:
cglib
❝引入Spring相关依赖包,org.springframework.cglib cglib底层也是基于asm实现的,并且它不需要实现任何接口。 ❞
来看效果:
代码语言:javascript复制/**
* cglib-code generate library
* cglib实现动态代理不需要实现接口
* 底层用的也是asm
* @author 行百里者
*/
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Pony.class);
enhancer.setCallback(new TimeMethodInterceptor());
Pony pony = (Pony) enhancer.create();
pony.paint();
}
}
class TimeMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(o.getClass().getSuperclass().getName());
System.out.println("before...");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("after");
return result;
}
}
class Pony {
public void paint() {
System.out.println("小马画月牙");
//随机睡10s以内,假装这是处理业务逻辑
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Spring AOP
❝现在我们知道了,动态代理可以对任何方法的任何地方切入代理所执行的逻辑,比如执行时间,记录日志,处理事务等。 我们可以在
Pony
的paint()
方法执行前切入before()
,在执行后切入after()
,也就是说可以在指定的点切入代理所要做的事情,这就是简单的面向切面了。 Spring AOP就是面向切面,AOP是Spring的核心之一。 ❞
下面用代码演示一下,AOP是怎么切入代理处理逻辑的。
Spring配置文件app_aop.xml
:
<bean id="pony" class="com.traveler100.dp.proxy.springaop.Pony"></bean>
<bean id="logProxy" class="com.traveler100.dp.proxy.springaop.LogProxy"></bean>
<aop:config>
<aop:aspect id="log" ref="logProxy">
<aop:pointcut id="onpaint" expression="execution(void com.traveler100.dp.proxy.springaop.Pony.paint())" />
<!-- 在Pony.paint()之前执行logProxy的before()方法 -->
<aop:before method="before" pointcut-ref="onpaint"/>
<!-- 在Pony.paint()之后执行logProxy的after()方法 -->
<aop:before method="after" pointcut-ref="onpaint"/>
</aop:aspect>
</aop:config>
LogProxy
public class LogProxy {
public void before() {
System.out.println("日志:开始作画");
}
public void after() {
System.out.println("日志:画完了");
}
}
Pony
还是那个Pony
,不赘述。
使用:
代码语言:javascript复制public class Teacher {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("app_aop.xml");
Pony pony = (Pony) ctx.getBean("pony");
pony.paint();
}
}
运行结果:
代码语言:javascript复制日志:开始作画
日志:画完了
小马画月牙
Process finished with exit code 0
Spring AOP
就是这么方便!!!
小结
❝代理模式应用得非常广泛,大到一个系统框架、企业平台,小到代码片段、事务处理,用到代理模式的概率是非常大的。 有了AOP大家写代理就更加简单了,有类似Spring AOP这样非常优秀的工具,拿来主义即可! 另外,我们看源代码,特别是调试时,只要看到类似$Proxy0这样的结构,我们不妨打开它看看,这样能够帮助我们更容易理解动态代理。 ❞