搞懂 AOP ,一篇文章就够了

AOP Alliance

在学习AOP之前,我们先来了解什么是 AOP Alliance,AOP是怎么诞生的?本着追本溯源的原则,我们一探究竟。

AOP是由AOP联盟 (AOP Alliance) 在2003-2004年前后(根据源码编写记录推断)制定的一套规范AOP实现的底层API。而AOP Alliance 是许多对aop和java有浓厚兴趣的软件开发人员联合成立的开源项目。通过这些底层的API,可以使得各个AOP实现及工具产品之间实现相互移植。这些API主要以标准接口的形式提供,是AOP编程思想要解决的横切交叉关注点问题各部件的最高抽象。Spring 的 AOP 框架中也直接以这些 API 为基础所构建的。

AOP Alliance 官网

AOP Alliance 在线javadoc API

我们下载 AOP联盟实现的AOP源码或者直接查看API可以发现,共包含4个包:

  • aop包,此包提供了最通用的aop接口,定义了一个表示通知 Advice 的标识接口,各种各样的通知都继承或实现了此接口,还包含了一个用于描述aop系统框架错误的运行时异常 AspectException 类。
  • intercept包,为拦截机制提供了一组接口,规范了aop核心概念中的连接点及通知类型。
  • instrument包,为程序提供了工具包
  • reflect包,提供了一组实现通用反射API的接口
AOP 理解

AOP(Aspect-Oriented Programming),面向切面编程,是面向对象(OOP)的一种补充,延续,也是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序可重用性,同时提高了开发效率。

上面是官方定义,描述的非常精准,但对于刚刚接触AOP概念的人来说,理解起来可能会有些吃力。那什么是AOP?举个栗子来说明一下。

一个项目的核心业务逻辑基本已经开发完成,这时,组长说我们项目都没有添加日志信息,我们把日志都加上吧,这时,应该如何做?修改业务逻辑类内部结构肯定不可能,不仅会导致原有功能出现故障的概率增大,还会出现业务逻辑代码和日志信息代码耦合在一起,代码整洁度也非常差。那我们把日志信息封装成方法,在业务逻辑类里进行调用也可以实现,还体现了封装的思想,这种方式无疑也是需要修改原有类内部结构的。而且并没有解决上述问题。为了解决这一类问题,AOP思想诞生了,AOP采用横向抽取机制,将分散在各个方法中的重复代码抽取出来,在程序编译或运行时再将这些提取出来的代码应用到需要执行的地方去。这种采用横向抽取机制的方式,传统的OOP显然是无法做到的,因为OOP只能实现父子类关系的纵向重用。虽然AOP是一种新的(相较于OOP)编程思想,但它只是OOP的延伸和补充,绝不是OOP的替代品。

比如,我们都需要在业务逻辑方法执行前后打印类似“…方法开始/结束执行”这样的日志信息。这时,我们就需要横向的向这些业务逻辑中添加一些日志信息,那如何横向添加,后面会有项目实战,但实战前我们需要学习一些理论知识。
在这里插入图片描述

AOP 概念

AOP 是解决一类问题的思想,而它的具体实现有两种最流行的方式,一个是我们熟知的 Spring AOP,一个是 AspectJ框架,都是AOP思想的具体实现。

Spring AOP & AspectJ

二者都是 AOP 思想的实现,Spring AOP 是纯 Java 实现的,不需要专门的编译过程和类加载器,在运行期间可以通过代理的方式向目标内植入增强的内容。而 AspectJ 是一个基于 Java 语言的 AOP 框架,在 Spring2.0 后,引入了对 AspectJ 的支持,并提供了一个专门的编译器在编译时提供横向代码的植入。

简而言之,Spring AOP 和 AspectJ 有不同的目标。
Spring AOP 是通过 Spring IOC提供了一个简单的AOP实现,以解决编码人员面临的常见问题。这并不是完整的AOP解决方案。
而 AspectJ 是最原始的 AOP 实现技术,提供了AOP的解决方案,AspectJ 更为健壮,相对于 Spring AOP 也显得更为复杂,值得注意的是,Aspect 能够被应用于所有的领域对象。

Weaving

Spring AOP 和 AspectJ 使用了不同的织入方式,这影响了他们在性能和易用性方面的行为。
AspectJ 使用了三种不同类型的织入:

  • 编译时织入:AspectJ编译器同时加载我们切面的源代码和我们的应用程序,并生成一个织入后的类文件作为输出。
  • 编译后织入:这就是所熟悉的二进制织入。它被用来编织现有的类文件和JAR文件与我们的切面。
  • 加载时织入:这和之前的二进制编织完全一样,所不同的是织入会被延后,直到类加载器将类加载到JVM。

AspectJ 使用的是编译期和类加载时进行织入,Spring AOP 利用的是运行时织入。
运行时织入,在使用目标对象的代理执行应用程序时,编译这些切面(JDK代理或CGLIB代理)

Jointpoint

前面介绍了Spring AOP基于代理模式。因此,它需要目标类的子类,并相应的应用横切关注点。但是也伴随着局限性,我们不能跨越“final”的类来应用横切关注点(或切面),因为它们不能被覆盖,从而导致运行时异常。

同样地,也不能应用于静态和final的方法。由于不能覆写,Spring的切面不能应用于他们。因此,Spring AOP由于这些限制,只支持执行方法的连接点。然而,AspectJ在运行前将横切关注点直接织入实际的代码中。 与Spring AOP不同,它不需要继承目标对象,因此也支持其他许多连接点。AspectJ支持如下的连接点:

Performance

考虑到性能问题,编译时织入比运行时织入快很多。Spring AOP是基于代理的框架,因此应用运行时会有目标类的代理对象生成。另外,每个切面还有一些方法调用,这会对性能造成影响。

AspectJ不同于Spring AOP,是在应用执行前织入切面到代码中,没有额外的运行时开销。

由于以上原因,AspectJ经过测试大概8到35倍快于Spring AOP

Spring AOP 和 AspectJ 的主要区别

Spring AOPAspectJ
实现方式纯Java实现基于Java语言语言实现
对编译器的要求不需和编译过程分离需要专门的编译器编译
织入时机只在运行时织入支持编译中和加载时织入
织入级别只支持方法级别织入支持字段,方法,构造函数,最终类/方法,等
实现方式只能通过 Spring IOC 管理 bean可以在所有域对象上实现
支持的连接点支持方法连接点支持所有连接点
性能比 AspectJ 差性能更好
学习成本更容易学习和应用相对于 Spring AOP 更复杂

前面提到了 Spring 2.0 以后引入了 AspectJ ,那Spring 自己不是实现了 AOP 吗?为什么还要引入 AspectJ?至于此问题,后面我们会做讨论。

AspectJ 有两种实现方式,一种是基于 XML,一种基于注解的方式,我们分别来看看实战中是如何使用的。

在实战前,我们必须知道,无论使用 Spring AOP 还是 AspectJ 哪种方式实现,都必须引入两个 jar包

  • spring-aop.jar
  • aspectjweaver.jar
项目实战-XML

1、新建一个 Spring 项目

引入 spring-aop,aspectjweaver 两个依赖,或者直接引入 spring-boot-starter-aop 即可。

2、定义业务逻辑类

public interface UserService {
    void addUser();
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        log.info("添加用户");
    }
}

3、新建 applicationContext.xml 文件

其中将目标类和切面类都注入到 IOC 容器。接着就是配置切点和增强。

  • AfterReturning 通知是程序成功执行后,才会织入。
  • AfterThrowing 通知是指程序有异常时织入
  • After 通知是无论程序执行成功与否都会织入。
<!--定义目标类-->
<bean id="userServiceImpl" class="com.fyy.aop.xml.UserServiceImpl"/>

<!--定义切面bean -->
<bean id="myAspect" class="com.fyy.aop.xml.MyAspect"/>

<aop:config>
    <!-- 1.1 配置切面 -->
    <aop:aspect id="aspect" ref="myAspect">
        <!-- 1.1.1 配置切点 -->
        <aop:pointcut id="myPointCut" expression="execution(* com.fyy.aop.xml.*.*(..))"/>
        <!-- 配置通知 -->
        <!-- 1.1.2 配置前置通知 -->
        <aop:before method="myBefore" pointcut-ref="myPointCut"/>
        <!-- 1.1.3 配置环绕通知 -->
        <aop:around method="myAround" pointcut-ref="myPointCut"/>
        <!-- 1.1.4 配置后置通知 -->
        <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut"/>
        <!-- 1.1.5 配置异常通知 -->
        <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut"/>
        <!-- 1.1.6 配置最终通知 -->
        <aop:after method="myAfter" pointcut-ref="myPointCut"/>
    </aop:aspect>
</aop:config>

4、定义切面类

其中,环绕通知:

  • 必须接收一个参数,类型为 ProceedingJoinPoint, 它是 JoinPoint 的子接口,表示可执行目标方法
  • 必须是 Object 类型的返回值
  • 必须 throws Throwable
public class MyAspect {
    public void myBefore(JoinPoint joinPoint) {
        log.info("前置通知");
    }
    
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("环绕通知开始");
        // 获取目标类
        Class<? extends ProceedingJoinPoint> aClass = proceedingJoinPoint.getClass();
        log.info("目标类:" + aClass);
        // 执行目标方法
        Object proceed = proceedingJoinPoint.proceed();
        log.info("目标方法执行结果:" + proceed);
        log.info("环绕通知结束");
        return proceed;
    }
    
    public void myAfterReturning(JoinPoint joinPoint) {
        log.info("后置通知");
    }

    public void myAfterThrowing(JoinPoint joinPoint) {
        log.info("异常通知");
    }

    public void myAfter(JoinPoint joinPoint) {
        log.info("最终通知");
    }
}

5、新建客户端类,加载配置文件,获取业务逻辑bean,调用方法。

public class Client {
    public static void main(String[] args) {
        // 定义配置文件路径
        String xmlPath = "applicationContext.xml";
        // 初始化spring容器,加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
        // 从容器中获取bean实例
        UserService userService = (UserService) applicationContext.getBean("userServiceImpl");
        userService.addUser();
    }
}

运行main方法,运行结果如下:

10:49:02.562 [main] INFO com.fyy.aop.xml.MyAspect - Before 通知
10:49:02.562 [main] INFO com.fyy.aop.xml.MyAspect - Around 通知开始
10:49:02.562 [main] INFO com.fyy.aop.xml.MyAspect - 目标类:class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
10:49:02.563 [main] INFO com.fyy.aop.xml.UserServiceImpl - 业务逻辑: 添加用户
10:49:02.563 [main] INFO com.fyy.aop.xml.MyAspect - 目标方法执行结果:null
10:49:02.563 [main] INFO com.fyy.aop.xml.MyAspect - Around 通知结束
10:49:02.563 [main] INFO com.fyy.aop.xml.MyAspect - AfterReturning 通知
10:49:02.563 [main] INFO com.fyy.aop.xml.MyAspect - After 通知
项目实战-注解式

1、定义业务逻辑

@RestController
public class UserController {
    @Resource
    private UserService userService;

    @GetMapping("/user")
    public void addUser() {
        userService.addUser();
    }
}

public interface UserService {
    void addUser();
}

@Component
@Slf4j
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        log.info("添加用户");
    }
}

2、定义切面类

@Aspect
@Slf4j
@Component
public class MyAspect {
    @Pointcut("execution(* com.fyy.aop.annotation.UserServiceImpl.*(..))")
    private void myPointcut() {
    }

    @Before("myPointcut()")
    public void myBefore() {
        log.info("前置通知");
    }

    @Around("myPointcut()")
    public Object myAround(ProceedingJoinPoint point) throws Throwable {
        String methodName = point.getSignature().getName();
        log.info("环绕通知,执行" + methodName + "方法前");
        //选择执行
        point.proceed();
        log.info("环绕通知,执行" + methodName + "方法后");
        return point;
    }

    @AfterReturning("myPointcut()")
    public void myAfterReturning() {
        log.info("后置通知");
    }

    @AfterThrowing("myPointcut()")
    public void myAfterThrowing() {
        log.info("异常通知");
    }

    @After("myPointcut()")
    public void myAfter() {
        log.info("最终通知");
    }
}

3、测试接口

2021-03-20 11:42:43.327  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.MyAspect          : Around通知: 执行addUser方法前
2021-03-20 11:42:43.327  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.MyAspect          : Before 通知
2021-03-20 11:42:43.340  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.UserServiceImpl   : 业务逻辑: 添加用户
2021-03-20 11:42:43.341  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.MyAspect          : AfterReturning 通知
2021-03-20 11:42:43.341  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.MyAspect          : After 通知
2021-03-20 11:42:43.341  INFO 13116 --- [nio-8080-exec-2] com.fyy.aop.annotation.MyAspect          : Around通知: 执行addUser方法后
各 Advice 的执行顺序

一个切面时,Advice 的执行顺序是 Around - Before - After - After Returning / After Throwing,具体的通知执行模型如下:

在这里插入图片描述
多个切面时,Advice 的执行顺序,通知执行模型如下:
在这里插入图片描述

使用 Spring AOP 为什么还要引 AspectJ 的依赖?

先来看看 Spring 官网原话:

Thus, for example, the Spring Framework’s AOP functionality is normally used in conjunction with the Spring IoC container. Aspects are configured by using normal bean definition syntax (although this allows powerful “auto-proxying” capabilities). This is a crucial difference from other AOP implementations. You cannot do some things easily or efficiently with Spring AOP, such as advise very fine-grained objects (typically, domain objects). AspectJ is the best choice in such cases. However, our experience is that Spring AOP provides an excellent solution to most problems in enterprise Java applications that are amenable to AOP.

Spring AOP never strives to compete with AspectJ to provide a comprehensive AOP solution. We believe that both proxy-based frameworks such as Spring AOP and full-blown frameworks such as AspectJ are valuable and that they are complementary, rather than in competition. Spring seamlessly integrates Spring AOP and IoC with AspectJ, to enable all uses of AOP within a consistent Spring-based application architecture. This integration does not affect the Spring AOP API or the AOP Alliance API. Spring AOP remains backward-compatible

意思就是,spring aop 通常与 spring ioc 一起使用,通过使用bean定义语法配置,这是与其他 AOP 实现的一个重要区别。虽然使用 spring aop 不能轻松的完成某些事情,例如建立非常细粒度的对象,这种情况下,AspectJ 是最佳选择,spring aop 为 Java 应用程序中的大多数问题提供了解决方案,从这个角度来说,spring aop 还是不错的解决方案。

spring aop 从不试图与 AspectJ 竞争以提供全面的AOP解决方案。我们相信基于代理的框架(如spring aop)和成熟的框架(如 AspectJ)都是有价值的,它们是互补的,而不是竞争。Spring 将 Spring AOP和 IoC 与 AspectJ 无缝集成,以便在一致的基于 Spring 的应用程序体系结构中实现AOP的所有使用。这种集成不会影响 Spring AOP API 或 AOP Alliance API。spring aop 保持向后兼容。

现在明白了,Spring 致力于寻找 AOP 的解决方案,而 spring aop + AspectJ 以互补的关系共同供 Java 开发者使用。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值