面向切面编程

时间:2022-07-22
本文章向大家介绍面向切面编程,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

面向切面编程(Aspect Oriented Programming),简称AOP。作为面向对象编程的一个强力补充,在业务系统中很少被关注,却随着Spring的出现而名声鹊起。

使用场景

历史文章中有介绍过一个在线调试系统。

图中的ServiceA、ServiceB、ServiceC的内部实现中有很多的逻辑。如果需要在各个业务逻辑中手动进行日志的判断和上报,势必会污染现有的代码,面临在线调试日志的代码分散,冗余,扩展性低等缺点。这时候AOP则可以很好的发挥它的作用。它主要用于横切现有业务逻辑,对其进行增强。

同理在一个大型系统中,总有着很多的基础性功能贯穿着所有的核心逻辑。

如上图,类似的需求。如果在系统初期就明确的需求,可以对核心逻辑设计一些可插拔的前置和后置逻辑进行实现。这需要有一些设计前瞻性,事实是很多系统初期设计并没有提供如此的机制,而很多类似的公共功能是上线后不断堆叠上去的。整体去修改代码,代价有点大,那么公共逻辑对现有逻辑的无侵入增强就变得很迫切了。这正是AOP所产生的原因,也是它最擅长的领域。

使用了AOP之后则不需要在每个逻辑中嵌入代码或者设计之初加入Hook机制。耗时监控、权限认证、事务控制、业务上报等逻辑,可以独立实现,然后通过切面,对核心逻辑进行织入。使做业务的人关注于业务而无需特别去关注一些公共的逻辑。

简单实现

展示一个不使用编程框架的例子。有既定的接口和实现类。

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");    }    }

对于每一次方法的调用,我们需要对其进行日志输出和耗时统计。这时候需要创建一个代理类。

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来包装一下。

public class TimeWatchProxyApp {    public static void main(String[] args) {        IBookService bookService = (IBookService) TimeWatchProxy.newInstance(new BookService());        bookService.create(new Book());        bookService.update(new Book());    }}

运行后的执行结果是

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为例,首先引入对应的依赖。

<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>

然后定义一个通知类

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);        }    }}

接着是配置一个切面把切入点和通知类组合起来。

<?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的功能进行增强。