一 AOP由来
AOP全称Aspect-Oriented Programming,和OOP相对应,前者是面向切面编程,后者面向切面编程,若要更加直白的描述两者的区别,个人认为面向对象在纵向通过派生来实现个性化逻辑,而面向切面则在横向通过代理实现统一化逻辑。
事实上AOP的确也表示从业务逻辑中分离出来的横切逻辑,包括但不限于性能监控、日志记录、权限控制等等。这些功能在各个独立的业务功能模块中都可能存在,AOP就是为了将这部分公用且重复的逻辑从业务逻辑中剥离开来,实现代码解耦,让业务职责更加单一。
AOP的概念很早就有了,Java圈子里最出名的非AspectJ莫属,它的前身是AspectWerkz,而AspectWerkz早在2005年就停止更新了,所以说它是AOP的发源地并不为过。让AOP广为流传的则是Rod Johnson,这个人写了一个叫Spring的框架,因此一炮而红,他不仅在Spring中设计了一套IOC框架,还在此基础上设计了一套AOP框架,然而AOP效果并不理想,后来他采纳了网友们的建议在Spring中集成了AspectJ,所以才出现了如今Spring+AspectJ的经典组合。
二 如何实现AOP
假如我们现在要对一个函数执行过程进行性能监控,那么新人们大致会写出如下代码:
public class DemoClass {
public static void main(String[] args) {
DemoClass demoClass = new DemoClass();
demoClass.process();
}
private void process() {
long start = System.currentTimeMillis();
System.out.println("我被统计耗时了");
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
这种写死的代码有哪些缺陷呢?首先process方法应该专注于业务逻辑的实现,将性能统计代码强行插入其间使其职责混乱;其次当需要被统计的函数不仅仅为DemoClass的process函数时,需要修改各个函数逻辑,这违反了开闭原则;最后统计逻辑未必在生产中使用,此时再将函数调整,那么测试将变得毫无意义。
怎么解决?考虑如何在不侵入原函数逻辑的前提下,对函数功能进行增强——代理,没错就是上一章节提到的代理,那么将上面的实现调整为静态代理,我们需要先将目标函数抽象成为接口定义,然后实现代理类设计,那么调整后的代码应该长成下面的样子:
interface Process {
public void process();
}
class DemoClassProxy implements Process {
private Process impl;
public DemoClassProxy(Process impl) {
this.impl = impl;
}
@Override
public void process() {
long start = System.currentTimeMillis();
impl.process();
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
public class DemoClass implements Process {
public static void main(String[] args) {
Process processImpl = new DemoClassProxy(new DemoClass());
processImpl.process();
}
public void process() {
System.out.println("我被统计耗时了");
}
}
当然按照上一章节所说,静态代理是存在缺陷的,如果希望对多个类型的目标方法进行耗时统计,那么我们需要写很多Proxy,所以再次改进,用JDK动态代理实现:
class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
return result;
}
}
public class DemoClass implements Process {
public static void main(String[] args) {
Process processImpl = new DynamicProxy(new DemoClass()).getProxy();
processImpl.process();
}
public void process() {
System.out.println("我被统计耗时了");
}
}
如此一来但凡需要进行耗时统计的函数,只需要在应用程序调用处调整为DynamicProxy来返回接口实例对象即可,然而……如果被代理类没有实现任何接口呢?JDK动态代理无法做到非接口模式的代理,那么只能选择CGLIB动态代理了,重新调整:
class CGLIBProxy implements MethodInterceptor {
public CGLIBProxy() {
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> clazz) {
return (T) Enhancer.create(clazz, this);
}
@Override
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("耗时:" + (System.currentTimeMillis() - start));
return result;
}
}
public class DemoClass implements Process {
public static void main(String[] args) {
Process processImpl = new CGLIBProxy().getProxy(DemoClass.class);
processImpl.process();
}
public void process() {
System.out.println("我被统计耗时了");
}
}
到此为止,掰开了揉碎了,看似都在搞代理,这和AOP有啥关系呢?其实我们上面实现的就是AOP,在AOP中横向抽取出来的公共逻辑实际上就是原函数的一种增强,所以你会在SpringAOP中常常听到三个名词:
- 前置增强-BeforeAdvice
- 后置增强-AfterAdvice
- 环绕增强-AroundAdvice
稍微调整下上面的程序,我们将long start = System.currentTimeMillis()封装为函数before(), 将System.out.println(“耗时:” + (System.currentTimeMillis() - start))封装为函数after():
public long befor() {
return System.currentTimeMillis();
}
public void after(long start) {
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
那么前置增强就是before函数,后置增强就是after函数,环绕增强就是把这两个合起来。这样去理解AOP是不是就很清晰了?
三 Spring AOP
在了解了AOP的实现机制后,我们在来说说Spring是如何实现AOP的,原理自不必说,先说说如何使用。SpringAOP的使用分为编程式和配置式。
3.1 编程式
以编程模式使用SpringAOP要求依赖spring-aop组件,并且应用需要预先准备前置增强和后置增强接口的实现,写法如下:
class BeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置增强");
}
}
class AfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("后置增强");
}
}
public class DemoClass {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new DemoClass());
proxyFactory.addAdvice(new BeforeAdvice());
proxyFactory.addAdvice(new AfterAdvice());
DemoClass demoClass = (DemoClass) proxyFactory.getProxy();
demoClass.process();
}
public void process() {
System.out.println("我被测试着呢");
}
}
按如上方式就可以实现对DemoClass的process方法进行前后两次增强了,那么如果我们将MethodBeforeAdvice和AfterReturningAdvice两个接口合并是不是就实现了环绕增强咯,没错,只不过我们无需额外定义一个环绕接口了,org.aopalliance.intercept.MethodInterceptor已经帮我们做了这件事儿。这个接口不是Spring提供的,它归属AOP联盟,对接口的一个简单实现如下:
class AroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
before();
Object result = methodInvocation.proceed();
after();
return result;
}
private void before() {
System.out.println("前置增强");
}
private void after() {
System.out.println("后置增强");
}
}
上面的篇幅都是以代码方式实现的AOP,其本质上依然是通过代理的方式来对目标函数进行增强而已,所以说AOP本身没有什么很厉害的东西,接下来我们在看看Spring大肆宣扬的配置文件方式实现AOP(个人不是很接受大篇幅的配置,代码可读性不高,配置维护困难,看似降低了开发者工作成本,实际上也拦住了开发者晋升的渠道,大量使用反射性能低下,代码行数看似很少,性能却无法掌控,优化耗时几乎将前期节约的资源成本都耗尽了)。
3.2 配置式
说实话,这段不想写了,我是真的十分十分讨厌配置,就这么任性,反正Spring这套鬼东西网上资料多的是,看官有兴趣就自己搜搜看吧,我简单列一下SpringAOP中的一些关键点,大家按关键词搜索资料就好:
3.2.1 抛出增强
主要用于统一的异常处理,举个例子,程序中各处的异常处理要么继续向上抛出,要么拦截记录日志,这些对异常的处理行为是一致的,所以没有必要在各处实现,而且为了统一异常处理模式,莫不如直接将其剥离出来成为切面,抛出增强就是解决这个问题的。
3.2.2 引入增强
在这个小节之前都是对方法进行增强的,这个过程Spring称之为织入——Weaving。那么引入这个概念则是对类型进行增强,Spring称之为引入增强——IntroductionAdvice,啥意思?
- 存在接口A和B
- 存在类C实现了接口A
- 现在C不想实现接口B,但是又想通过C调用B接口的方法
够清楚了吗?话说回来我真是不知道Spring到底方便了开发者,还是坑了开发者,整个架构观念都偏的不行。
3.2.3 切面切点
再说说AOP中一个较难解决的问题,之前我们所写的案例都是单方法的,所以通过代理实现AOP也不复杂,但实际上很多类型的方法定义是非常多的,想对目标方法进行增强就必须要定位到目标方法,切面就是用来解决这个问题的。
所谓切面就是一组筛选过滤的条件,这些过滤条件加上目标类型就可以定位到具体的方法了,比如说目标类型DemoClass,过滤条件方法名为process,这个组合就叫切面——Advisor。
那么如何描述筛选过滤条件呢?切点呗——PointCut,那么总结起来切面将增强的逻辑和切点包装起来,Spring将其配置到ProxyFactory中,从而生成代理,以此来实现对目标类型的目标函数进行逻辑增强。
四 Spring+AspectJ
整个章节三其实并不能完全将SpringAOP描述清楚,我也懒得去写,比如说SpringAOP还有自动代理模式,可以扫描Bean,也可以扫描切面配置等等,万变不离其宗,就是各种配配配,配到最后文件乱了,开发者不仅编程水平没提升,连文件配置都干懵逼了,以至于Rod Johnson意识到在这样下去就特么的没办法忽悠了呀,得想办法把切面配置从Spring体系中干掉,然后他打开自己的留言板——嗯,既然网友们都喜欢AspectJ,那我也用吧。
至此Spring+AspectJ黄金搭档横空出世。大家都不用去配置各种切面文件了,改为各种注解来描述,程序员们载歌载舞,写注解归根到底也是写代码,总比写配置强,舒服。
这真的舒服吗?我个人看待这个事情还是举保留态度的,注解虽然以代码的方式来描述代码的元信息,但我们必须意识到注解的使用一定离不开反射,只要大量使用反射性能必然受损,合理和架构设计与所谓的精简代码相比,孰轻孰重很难说的清楚,个人更偏向于合理的架构设计,清晰合理的架构设计之下使用最基础的语法实现程序设计,不仅便于程序的阅读维护,也方便迁移。
用注解不过是替代了配置文件,不论何种实现其本质都是代理,所以这里不再重点描述AspectJ的用法,我列一个AspectJ提供的用于AOP的注解列表,大家参考下吧:
增强类型 | 接口 | 注解 | 配置 |
---|---|---|---|
前置增强:BeforeAdvice | MethodBeforeAdvice | @Before | aop:before |
后置增强:AfterAdvice | AfterReturningAdvice | @After | aop:after |
环绕增强:AroundAdvice | MethodInterceptor | @Around | aop:around |
抛出增强:ThrowsAdvice | ThrowsAdvice | @AfterThrowing | aop:after-throwing |
引入增强:IntroductionAdvice | DelegatingIntroductionInterceptor | @DeclareParents | aop:declare-parents |
五 小结
第一部分主要介绍了代理,是为第二部分铺路,第二部分简单的理一下Spring对AOP的支持,以及为何会出现配置、注解多种方式,那么在理清楚历史脉络之后,大家再去学习相关知识的时候就应该知道什么才是重点,说的更直接一些:
- 代理是根,想提升把代理吃透
- AOP是一种设计思想,SpringAOP沿用了Spring的配置体系,最终因配置过于复杂被抛弃
- AspectJ通过注解实现AOP,代码可读性更强,但是耦合度也随之变高
最后我会在第三部分自行设计一个AOP框架,反正已经了解了代理机制,和Spring以及AscpectJ对AOP的支持,那么如果我们在项目实施的过程中,想实现更加轻量,更加可控的AOP框架,莫不如自行设计一个。