代理模式
需求: 有一辆坦克 , 想要记录坦克的移动时间和移动记录
原始代码如下
package com.cyc.design.proxy.v01;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
interface Movable {
void move();
}
方案一
通过在move的执行前后添加时间记录
package com.cyc.design.proxy.v02;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢? benchmark
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
long start = System.currentTimeMillis();
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public static void main(String[] args) {
new Tank().move();
}
}
interface Movable {
void move();
}
**问题:**方法虽然简单 ,但是却是要修改源码,如果不修改源码呢?
方案二
使用继承的方式,新建Tank2继承Tank类, 实现move接口, 在执行Tank类的move方法前和方法后, 添加时间记录
package com.cyc.design.proxy.v04;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Tank2().move();
}
}
class Tank2 extends Tank {
@Override
public void move() {
long start = System.currentTimeMillis();
super.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
interface Movable {
void move();
}
**问题:**熟悉了设计模式之后,应该记得要慎用继承,因为 继承耦合度太高,不够灵活,现在知识记录了坦克的运行时间,如果要记录坦克的行走记录,坦克发射的炮弹呢,难道还要写多个类去继承吗?
方案三
使用代理模式
新建TankTimeProxy同样实现moveAble接口,里面聚合了一个tank,记录一个起始时间和结束时间,中间调用的是tank的move方法。这种方式看起来就像是一个代理人,例如茅台酒代理人,买茅台酒,并不是去向酒厂就买,而是通过茅台酒代理人(经销商)去买,代理人再去向茅台厂购买。
package com.cyc.design.proxy.v05;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TankTimeProxy(new Tank()).move();
}
}
class TankTimeProxy implements Movable {
Tank tank;
public TankTimeProxy(Tank tank) {
this.tank = tank;
}
@Override
public void move() {
long start = System.currentTimeMillis();
tank.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
interface Movable {
void move();
}
**问题:**这个代理只能代理坦克的移动时间。
方案四
同样新建一个TankLogProxy代理,记录坦克的行进日志
package com.cyc.design.proxy.v06;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TankTimeProxy().move();
}
}
//时间代理
class TankTimeProxy implements Movable {
Tank tank;
@Override
public void move() {
long start = System.currentTimeMillis();
tank.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
//行进日志代理
class TankLogProxy implements Movable {
Tank tank;
@Override
public void move() {
System.out.println("start moving...");
tank.move();
System.out.println("stopped!");
}
}
interface Movable {
void move();
}
问题: 如何既能记录行进时间的日志又能记录行进日志的?
方法: 使用嵌套, 方向没错, 但是两个代理类里面都聚合了tank对象, 又怎么能将两个代理类聚合在一起呢?
方案五
两个代理不再代理具体的tank类,而是代理moveAble接口,这样的话,便可实现以上需求。
这也就是静态代理
package com.cyc.design.proxy.v07;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
*
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TankLogProxy(
new TankTimeProxy(
new Tank()
)
).move();
}
}
class TankTimeProxy implements Movable {
Movable m;
public TankTimeProxy(Movable m) {
this.m = m;
}
@Override
public void move() {
long start = System.currentTimeMillis();
m.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
class TankLogProxy implements Movable {
Movable m;
public TankLogProxy(Movable m) {
this.m = m;
}
@Override
public void move() {
System.out.println("start moving...");
m.move();
long end = System.currentTimeMillis();
System.out.println("stopped!");
}
}
interface Movable {
void move();
}
*问题: ** 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
方案六(动态代理)
分离代理行为与被代理对象
package com.cyc.design.proxy.v08;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
* v08:如果有stop方法需要代理...
* 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
* (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
* 分离代理行为与被代理对象
* 使用jdk的动态代理
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Tank tank = new Tank();
//reflection 通过二进制字节码分析类的属性和方法
Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
new Class[]{Movable.class}, //tank.class.getInterfaces()
new LogHander(tank)
);
m.move();
}
}
class LogHander implements InvocationHandler {
Tank tank;
public LogHander(Tank tank) {
this.tank = tank;
}
//getClass.getMethods[]
/**
* @param proxy 生成的代理对象, 也就是那个m
* @param method 要代理的方法
* @param args 方法需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method " + method.getName() + " start..");
Object o = method.invoke(tank, args);
System.out.println("method " + method.getName() + " end!");
return o;
}
}
interface Movable {
void move();
}
package com.cyc.design.proxy.v08;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
* v08:如果有stop方法需要代理...
* 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
* (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
* 分离代理行为与被代理对象
* 使用jdk的动态代理
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Tank tank = new Tank();
//reflection 通过二进制字节码分析类的属性和方法
Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
new Class[]{Movable.class}, //tank.class.getInterfaces()
new LogHander(tank)
);
m.move();
}
}
class LogHander implements InvocationHandler {
Tank tank;
public LogHander(Tank tank) {
this.tank = tank;
}
//getClass.getMethods[]
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method " + method.getName() + " start..");
Object o = method.invoke(tank, args);
System.out.println("method " + method.getName() + " end!");
return o;
}
}
interface Movable {
void move();
}
查看输出结果:
method move start..
Tank moving claclacla...
method move end!
这里使用到了JDK的动态代理, 点进去newProxyInstance
第一个参数表示, 需要哪一个ClassLoader将new 出来的代理对象加载到内存,第二个参数,表示这个代理对象需要实现哪些接口。第三个参数,Invocationhandler,调用处理器,指的是,被代理对象的方法被调用的时候, 需要做什么处理。
**问题: ** 根据代码, 观察发现, 并没有地方调用LogHander的invoke方法, method move start…和method move end!是如何输出的呢? 要弄清楚这个问题, 继续往下研究
方案七(动态代理)
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 用继承?
* v05:使用代理
* v06:代理有各种类型
* 问题:如何实现代理的各种组合?继承?Decorator?
* v07:代理的对象改成Movable类型-越来越像decorator了
* v08:如果有stop方法需要代理...
* 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型
* (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
* 分离代理行为与被代理对象
* 使用jdk的动态代理
*
* v09: 横切代码与业务逻辑代码分离 AOP
* v10: 通过反射观察生成的代理对象
* jdk反射生成代理必须面向接口,这是由Proxy的内部实现决定的
*/
public class Tank implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Tank tank = new Tank();
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//jdk 1.8
// System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true"); jdk 1.8以上
Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
new Class[]{Movable.class}, //tank.class.getInterfaces()
new TimeProxy(tank)
);
m.move();
}
}
class TimeProxy implements InvocationHandler {
Movable m;
public TimeProxy(Movable m) {
this.m = m;
}
public void before() {
System.out.println("method start..");
}
public void after() {
System.out.println("method stop..");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//Arrays.stream(proxy.getClass().getMethods()).map(Method::getName).forEach(System.out::println);
before();
Object o = method.invoke(m, args);
after();
return o;
}
}
interface Movable {
void move();
}
在当前项目目录下 , 会生成该代理对象的类, idea自动将其反编译,
查看
观察代码 , 发现他生成了move方法, 以为他调用动态代理时, 指定了被代理对象实现movable接口, 所以, 这里生成了move方法, 点击去Proxy类, 发现InvocationHandler就是上图中指定的当前LogProxyHandler
所以原理为: 当我们new代理类对象的时候, InvocationHandler传进去, 调用super.h就被指定为, 当m.move方法执行时 , 首选调用LogProxyHandler
JDK动态代理逻辑图
源码分析
- 从 Proxy.newProxyInstance点进去
- 重点是下面的getProxyConstructor这个方法,
- 点击进入getProxyConstructor
proxyCache.sub(intf).computeIfAbsent
这句话的意思是 , 每次生成代理的时候 , 如果这个代理已经在代理缓存池中存在 , 则直接从缓存池中拿。 computeIfAbsent , 意为 : 如果没有,则通过计算得出该代理。再往下, 有proxyBuild的build方法,这才是在去生成代理,点击进入build方法,
- 点击进入defineProxyClass
- 点击进入generateProxyClass
- gen.generateClassFile() 是直接操作编译生成的字节码文件,点击进入
- 在经过一系列的操作后,在methods.add(pm.generateMethod());将方法加入进去。
点击进入method.add方法
最终进入到asm, 归根结底 , JDK动态代理是又asm实现
ASM是一个通用的Java字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。 ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。 ASM提供与其他Java字节码框架类似的功能,但专注于性能。 因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用
JDK动态代理缺陷在于被代理的类必须实现某个接口
CGLIB动态代理
cglib 全称Code Generation Library ,是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。
代码实现
package com.cyc.design.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Random;
/**
* CGLIB实现动态代理不需要接口
*/
public class Main {
public static void main(String[] args) {
//增强器
Enhancer enhancer = new Enhancer();
//设置父类
enhancer.setSuperclass(Tank.class);
//设置回调
enhancer.setCallback(new TimeMethodInterceptor());
Tank tank = (Tank)enhancer.create();
tank.move();
}
}
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());
//输出 com.cyc.design.cglib.Tank, 说明cglib动态代理, 生成的是被代理对象的一个子类
//这么看来 ,cglib也有缺陷 如果被代理对象是final类型,那么cglib就无法动态代理该类
//cglib底层, 使用的也是asm
System.out.println("before");
Object result = null;
result = methodProxy.invokeSuper(o, objects);
System.out.println("after");
return result;
}
}
class Tank {
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出 com.cyc.design.cglib.Tank, 说明cglib动态代理, 生成的是被代理对象的一个子类,这么看来 ,cglib也有缺陷 如果被代理对象是final类型,那么cglib就无法动态代理该类
同样,和JDK动态代理一样,cglib底层, 使用的也是asm。
面对final类型的类 , asm就可以实现器动态代理,因为asm在读取完成器二进制码文件后, 可以对其进行修改,此时,可以删除器final关键字,便可对其实现动态代理。
spring aop
代码演示:
package com.cyc.design.spring.v1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* spring aop test
*/
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
Tank t = (Tank)context.getBean("tank");
t.move();
}
}
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
<bean id="tank" class="com.cyc.design.spring.v1.Tank"/>
<bean id="timeProxy" class="com.cyc.design.spring.v1.TimeProxy"/>
<aop:config>
<!--ref="timeProxy" 表示要切入的类是id为timeProxy指定的TimeProxy-->
<aop:aspect id="time" ref="timeProxy">
<!--切点位置-->
<aop:pointcut id="onmove" expression="execution(void com.cyc.design.spring.v1.Tank.move())"/>
<!--执行前,调用before方法-->
<aop:before method="before" pointcut-ref="onmove"/>
<!--执行后,调用after方法-->
<aop:after method="after" pointcut-ref="onmove"/>
</aop:aspect>
</aop:config>
</beans>
实现方式: 当Tank.move执行前 , spring 发现在配置文件中有该方法的切点, 并且有执行前和执行后要执行的方法。
这种配置文件的方式,如果需要进行切面的类很多,那么维护配置文件就是一件很麻烦的事。由此引出了下面的基于注解的方式。
基础注解的AOP
package com.cyc.design.spring.v2;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* spring aop test
*/
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("app_auto.xml");
Tank t = (Tank)context.getBean("tank");
t.move();
}
}
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
<aop:aspectj-autoproxy/>
<bean id="tank" class="com.cyc.design.spring.v2.Tank"/>
<bean id="timeProxy" class="com.cyc.design.spring.v2.TimeProxy"/>
</beans>
Spring
- AOP+IOC
- bean工厂+灵活装配+动态行为拼接(织入), 成就了spring在java框架中的一哥地位