声明:原创文章,转载请注明出处。https://www.jianshu.com/p/99122ea61f2f
设计模式系列:
30分钟学透设计模式1-单例模式的前世今生
30分钟学透设计模式2-随处可见的Builder模式
30分钟学透设计模式3-使用最多的Iterator模式
30分钟学透设计模式4-最简单的面向接口编程-简单工厂模式
30分钟学透设计模式5-从代理模式到AOP
一、概述
代理(Proxy),顾名思义,你不用去做,交给别人去做。这其中暗含的意思是,你去做的话,需要花费额外的精力;或者说别人具有做这件事的功能。
代理模式:一个类代表另一个类的功能。我们创建包含对象的另外一个对象,用以对外提供强化性的接口。
如果仅仅是,用一个对象包裹另一个对象,这种代理其实没有什么含义。其主要是对其功能性的增强。
怎么理解,看下文。
二、静态代理
有下面这样一个接口。
public interface Caculator {
int add(int x, int y);
}
其实现类为:
public class CaculatorImpl implements Caculator {
@Override
public int add(int x, int y) {
return x + y;
}
}
问题:现在有一些调用者要统计这个接口的耗时,要怎么处理呢?
方法1:修改实现类
public class CaculatorImpl implements Caculator {
@Override
public int add(int x, int y) {
long start = System.currentTimeMillis();
int ret = x + y;
System.out.println("total cost:" + (System.currentTimeMillis() - start));
return ret;
}
}
这样做不好,因为并不是所有的调用者都需要这个耗时数据,一刀切显然不合理。而且对原有代码进行了侵入。那还有其他方式吗?
方法2:调用者自己处理
调用者在调用add()
方法前后,自己处理耗时信息。这种显然也不合理。
除了这两种方法外,还有其他方法吗?老司机都是怎么做的呢?
方法3:静态代理
正如概述中说的,我们可以用一个对象来包裹原有的对象,并增强其功能。怎么理解,具体看代码。
public class CaculatorProxy implements Caculator {
private Caculator caculator;
public CaculatorProxy() {
caculator = new CaculatorImpl();
}
public int add(int x, int y) {
long start = System.currentTimeMillis();
int ret = caculator.add(x, y);
System.out.println("total cost:" + (System.currentTimeMillis() - start));
return ret;
}
public static void main(String[] args) {
Caculator caculator = new CaculatorProxy();
caculator.add(1, 2);
}
}
不难看出,原来的CaculatorImpl
对象,被包裹进了CaculatorProxy
中,并实现了一个加强版的add()
方法。
静态代理的特点
上面的方法3其实就是静态代理,是不是很简单。就是用Proxy代理,实现原有类的接口,并包裹下原有类,增强下功能。
优点:扩展原来的功能,不侵入原有代码。
缺点:不是功能性代理,水平扩展性差。
如果我们有另外一个接口,也需要对其方法进行耗时统计:
public interface HelloWorld {
void sayHello();
}
那我们还得像之前的CaculatorProxy()
那样重新写一个Proxy。之前的那个Proxy虽然也是对耗时统计,但却不能重复利用。
那怎么解决,功能性重用呢?
二、动态代理
1、使用JDK动态代理实现一个统计耗时代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class CostDynamicProxy implements InvocationHandler {
private Object target; // 被代理的目标对象
public CostDynamicProxy(Object target) {
this.target = target; // 注入
}
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("total cost:" + (System.currentTimeMillis() - start));
return result;
}
public T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
public static void main(String[] args) {
Caculator caculator = new CaculatorImpl();
CostDynamicProxy caculatorProxy = new CostDynamicProxy(caculator);
Caculator proxy = caculatorProxy.getProxy();
proxy.add(1, 2);
HelloWorld helloWorld = new HelloWorldImpl();
CostDynamicProxy helloWorldProxy = new CostDynamicProxy(helloWorld);
HelloWorld proxy1 = helloWorldProxy.getProxy();
proxy1.sayHello();
}
}
其实并不复杂,只不过把对某一具体的方法的代理,变成了对Java方法的代理。需要动态运行时替换进行代理。
这里面有两个重要的类,分别为:InvocationHandler
和Proxy
,感兴趣的小伙伴可以多了解了解。
重点:从这个JDK动态代理可以看出,其实现了一个功能性代理(统计耗时的代理),无论哪个类,哪个方法都可以通过其得出耗时,不需要单独为其创建一个代理。也就是解决了静态代理的功能性重用问题。
2、CGLib动态代理
既然JDK动态代理,都已经如此完美了。那这节还需要介绍什么呢?
上面需要代理的方法,其都是实现了一个接口。如果一个类并没有实现任何接口,那我们怎么去做?
public class Cal {
public int add(int x, int y) {
return x + y;
}
}
如果我们强行使用JDK动态代理,肯定会报错的。
所以说,我们可以使用CGLib来做动态代理。先看代码。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxy implements MethodInterceptor {
public T getProxy(Class clz) {
return (T) Enhancer.create(clz, this);
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
long start = System.currentTimeMillis();
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("total cost:" + (System.currentTimeMillis() - start));
return result;
}
public static void main(String[] args) {
CglibProxy cglibProxy = new CglibProxy();
Cal cal = cglibProxy.getProxy(Cal.class);
cal.add(1, 2);
}
}
需要实现MethodInterceptor()
并重写方法 intercept()
。
三、Spring AOP
AOP即面向切面编程。切面即表示从业务逻辑中脱离出来的横截面,比如上面提到的统计耗时,还有性能解控、日志记录、权限控制等等。
总体来讲,通过AOP,实现切面可以解决代码以及功能的耦合问题,让职责更加单一。逻辑更加清晰。
1、特点
- 让关注点代码与业务代码分离,可以动态地添加和删除在切面上的逻辑而不影响原来的执行代码。
- 模块之间的耦合度低;容易扩展;代码复用;
2、前置增强、后置增强、环绕增强
上面的例子中,如果我们仅仅实现long start = xxxx
,并且在调用add()
方法之前,那这就是一个前置增强。
同样地,如果我们仅仅实现System.out.println("total cost xxxx")
,并且在调用add()
方法之后,那这就是一个后置增强。
如果既有前置、也有后置,那就是环绕增强。
3、其他概念
- 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
- 切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象。面向切面编程,就是指对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。
- 连接点(joinpoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
- 切入点(pointcut):切入点在AOP中的通知和切入点表达式关联,指定拦截哪些类的哪些方法, 给指定的类在运行的时候动态的植入切面类代码。
- 通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
- 目标对象:被一个或者多个切面所通知的对象。
- 织入(weave):将切面应用到目标对象并导致代理对象创建的过程
- 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
- AOP代理(AOP Proxy):在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。
4、AOP的实现原理
- Spring AOP使用的为动态代理,即AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
- Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是
InvocationHandler
接口和Proxy
类。 - 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。