面向切面编程(Aspect Oriented Programming),简称AOP。作为面向对象编程的一个强力补充,在业务系统中很少被关注,却随着Spring的出现而名声鹊起。
使用场景
历史文章中有介绍过一个在线调试系统。
图中的ServiceA、ServiceB、ServiceC的内部实现中有很多的逻辑。如果需要在各个业务逻辑中手动进行日志的判断和上报,势必会污染现有的代码,面临在线调试日志的代码分散,冗余,扩展性低等缺点。这时候AOP则可以很好的发挥它的作用。它主要用于横切现有业务逻辑,对其进行增强。
同理在一个大型系统中,总有着很多的基础性功能贯穿着所有的核心逻辑。
如上图,类似的需求。如果在系统初期就明确的需求,可以对核心逻辑设计一些可插拔的前置和后置逻辑进行实现。这需要有一些设计前瞻性,事实是很多系统初期设计并没有提供如此的机制,而很多类似的公共功能是上线后不断堆叠上去的。整体去修改代码,代价有点大,那么公共逻辑对现有逻辑的无侵入增强就变得很迫切了。这正是AOP所产生的原因,也是它最擅长的领域。
使用了AOP之后则不需要在每个逻辑中嵌入代码或者设计之初加入Hook机制。耗时监控、权限认证、事务控制、业务上报等逻辑,可以独立实现,然后通过切面,对核心逻辑进行织入。使做业务的人关注于业务而无需特别去关注一些公共的逻辑。
简单实现
展示一个不使用编程框架的例子。有既定的接口和实现类。
代码语言:javascript复制public interface IBookService { public void create(Book book); public void update(Book book);}
public class BookService implements IBookService {
@Override public void create(Book book) { System.out.println("create book"); } @Override public void update(Book book) { System.out.println("update book"); } }
对于每一次方法的调用,我们需要对其进行日志输出和耗时统计。这时候需要创建一个代理类。
代码语言:javascript复制import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;
public class TimeWatchProxy implements InvocationHandler {
private Object obj;
public static Object newInstance(Object obj) { return java.lang.reflect.Proxy.newProxyInstance (obj.getClass().getClassLoader(), obj .getClass().getInterfaces(), new TimeWatchProxy(obj)); }
private TimeWatchProxy(Object obj) { this.obj = obj; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result; try { System.out.println("before method " method.getName()); long start = System.nanoTime(); result = method.invoke(obj, args); long end = System.nanoTime(); System.out.println(String.format("%s took %d ns", method.getName(), (end-start)) ); } catch (InvocationTargetException e) { throw e.getTargetException(); } catch (Exception e) { throw new RuntimeException("unexpected invocation exception: " e.getMessage()); } finally { System.out.println("after method " method.getName()); } return result; }}
在需要使用到IBookService的地方都使用TimeWatchProxy来包装一下。
代码语言:javascript复制public class TimeWatchProxyApp { public static void main(String[] args) { IBookService bookService = (IBookService) TimeWatchProxy.newInstance(new BookService()); bookService.create(new Book()); bookService.update(new Book()); }}
运行后的执行结果是
代码语言:javascript复制before method createcreate bookcreate took 141900 nsafter method create
before method updateupdate bookupdate took 93400 nsafter method update
这里使用了JDK的动态代理直接实现了对IBookService的任何方法的横切。虽然这样的实现跟现有的AOP框架原理一致,但是非常简陋,也不是很具备普适性,需要改动使用者的代码。现行的框架都会有一个自动创建代理的过程,因此也引入一些概念和术语。
概念和术语
连接点(Join Point):表示程序执行中的一个点如方法的执行或者异常的处理。即上述图中,核心逻辑的4个需要进行织入增强的地方。它表示了核心逻辑中的一个方法或者整个类。
通知(Advice):一个作用在连接点上的动作。也就是上述图中的耗时监控、权限认证、事务控制、业务上报的实现。具体类型包含了前置、后置、环绕通知、返回通知、异常通知。
切入点(Pointcut):匹配连接点的谓词,用来表示一类连接点。它表示了通知需要作用的连接点。
切面(Aspect):组合了通知和切入点。表示哪些通知作用到哪些连接点。
织入(Weaving):把切面加入到对象,并创建出代理对象的过程。可分为静态织入和运行时织入。
框架实现
简单实现里面,通知的代码是在代理类实现的,这里需要单独抽象出来。
以Spring AOP为例,首先引入对应的依赖。
代码语言:javascript复制<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId></dependency><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.2</version></dependency><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version></dependency>
然后定义一个通知类
代码语言:javascript复制import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;
public class TimeWatchAdvice {
public void beforeMethod(JoinPoint joinPoint) { System.out.println("before method " joinPoint.getSignature().getName()); }
public void afterMethod(JoinPoint joinPoint) { System.out.println("after method " joinPoint.getSignature().getName()); }
public Object aroundMethod(ProceedingJoinPoint joinPoint) { try { long start = System.nanoTime(); Object result = joinPoint.proceed(); long end = System.nanoTime(); System.out.println(String.format("%s took %d ns", joinPoint.getSignature().getName(), (end - start))); return result; } catch (Throwable e) { throw new RuntimeException(e); } }}
接着是配置一个切面把切入点和通知类组合起来。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="bookService" class="com.lihongkun.labs.spring.container.aop.BookService"></bean>
<bean id="timeWatchAdvice" class="com.lihongkun.labs.spring.container.aop.TimeWatchAdvice" />
<aop:config> <aop:aspect ref="timeWatchAdvice"> <aop:pointcut id="serviceMethods" expression="execution(* com.lihongkun.labs.spring.container.aop.*Service.*(..))" />
<aop:before pointcut-ref="serviceMethods" method="beforeMethod" /> <aop:around pointcut-ref="serviceMethods" method="aroundMethod" /> <aop:after-returning pointcut-ref="serviceMethods" method="afterMethod" /> </aop:aspect> </aop:config></beans>
aop:aspect 定义一个切面,它指向了timeWatchAdvice,其包含的标签定义了pointcut,使用表达式对aop包下的Service后缀的类进行横切,分别实现了前置、环绕和后置通知。运行结果跟简单实现一致,如果需要多个通知,则可以定义多个切换来实现,更具备灵活性。
小结
面向切面编程,旨在通过对现有的功能进行切入,对其进行公共功能的增强,而不进行代码的侵入。它引入了切面、切入点和通知等定义。Spring AOP框架的使用可以在无需改动使用者的情况下,只需要进行配置则对现有的Bean的功能进行增强。