AOP 面向切面编程

AOP 面向切面编程

介绍

概括

面向过程编程:过程化的思想是业务转换成:1..;2..;3..;
面向对象编程:步骤转换成不同的业务逻辑对象
面向切面编程:AOP是针对面向对象编程的一种补充,切面编程在不影响原有功能(不违背开闭原则)的情况下完成一些额外的功能业务(比如日志)。切面编程的目的就是为了将业务目标进行而外的增强或者扩展

思想

file

和动态代理的关系

面向切面是指导思想,动态代理是实现手段
Spring 的 AOP 是基于 JDK动态代理 和 CGLIB动态代理 实现的

应用场景

需要对其进行增强就需要aop

日志操作

在不破坏开闭的情况下进行日志的记录

权限管理

在调用前判断是否有权限

事务管理

调用业务方法前开启事务,方法执行后提交事务

AOP术语

切面(Aspect)3

切面是用于编写切面逻辑的类,这个类很类似于JDK动态代理中的回调处理器或者cglib中的方法拦截器,主要就是将需要增强目标对象的功能代码编写在这个类中,而这些功能增强的代码就是切面逻辑。

通知/增强(Advice)4

增强的类型
增强就是对目标行为植入额外的逻辑代码,从而增强原有的功能。增强分为五种类型:

  1. 前置(目标方法调用前):需要实现 MethodBeforeAdvice 接口
  2. 后置(目标方法正常返回后):需要实现 AfterReturningAdvice 接口
  3. 环绕(在调用前后执行):需要实现 MethodInterceptor 接口,环绕需要手动调用目标方法
  4. 异常(出现异常调用,不执行后置):需要实现 ThrowsAdvice 接口
  5. 最终(始终执行) [Spring早期没有]

切入点(Pointcut)1

类似于坐标,目的是找到目标方法的哪些方法需要切入。可以使用切入点表达式查找

连接点(Joinpoint)2

目标对象的增强(被切入)方法被称为连接点,一个切入点对应多个连接点,而增强(被切入)方法可以有多个切入点

代理(Proxy)6

在运行时动态代理创建的对象叫代理对象,负责调用目标对象的方法并进行增强

目标(Target)5

被代理(增强)的对象

织入(Weaver)1-6的过程

织入 = (切入点和连接点[AspectJ的切入点表达式和注解] + 动态代理) 的过程
将切面中的增强逻辑应用到目标具体的连接点上并产生代理的过程称之为织入。

因此通常描述为“将通知织入到具体的目标”。

织入的时机可以分为以下几种:

  1. 类加载时织入,需要特殊的类加载器(LTW)

  2. 编译时织入,需要特殊的编译器(CTW)

  3. 运行时织入,通常使用JDK或者CGLIB在程序运行创建代理对象,

    spring就是基于运行时织入的。(注意:spring仅仅只是用到了AspectJ的切入点表达式和注解,但并没有使用AspectJ的类加载和编译时织入功能,而是使用JDK和CGLIB在运行时生成代理对象。)

总结

如何把这些术语结合起来
通过切入点表达式查找到多个连接点,然后切面增强逻辑根据查找到的连接点所在的目标对象通过动态代理进行增强

增强的类型

  1. 前置(目标方法调用前):需要实现 MethodBeforeAdvice 接口
  2. 后置(目标方法调用后):需要实现 AfterReturningAdvice 接口
  3. 环绕(在调用前后执行):需要实现 MethodInterceptor 接口
  4. 异常(出现异常调用,不执行后置):需要实现 ThrowsAdvice 接口
  5. 最终(始终执行) [Spring早期没有,传统的xml]依赖 AspectJ 实现

使用

有三种使用方式:传统的xml、依赖AspectJ的xml、依赖AspectJ的注解扫描

AspectJ 是 eclipse 开源组织编写的一套强大的AOP框架,它拥有特殊的编译器和类加载器,因此可以在编译时创建代理和类加载时创建代理,但由于 Spring 本身对 AOP 的实现是基于运行时创建代理,所以只能所以 JDK 和 CGLIB 来创建代理,但 Spring 却使用了 AspectJ 的切入点表达式以及相关的注解,使用起来更加简单和便捷
AspectJ Maven仓库

<dependencies>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.8</version>
    </dependency>
</dependencies>

传统的xml

代理工厂负责创建代理对象
装配代理,让 spring 在运行时动态创建一个代理对象
代理对象是通过 ProxyFactoryBean 这个代理工厂创建出来的
并且这个代理对象也会自动纳入纳入容中管理

<bean id="实体类的id" class="实体类的完整类名"/>

<bean id="实体类的切面类id" class="实体类的切面类的完整类名"/>
<!-- org.springframework.aop.framework.ProxyFactoryBean:FactoryBean实现,基于Spring BeanFactory中的bean构建AOP代理。 -->
<bean id="实体类的代理对象id(通常是实体类的id+Proxy)" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!-- target 属性:用于注入目标对象,这样spring才知道要为创建代理,目标对象可以直接使用 目标对象的id -->
    <property name="target" ref="实体类的id"/>
    <!-- interceptorNames 属性:配置切面,可以配置多个切面 -->
    <property name="interceptorNames">
        <list>
            <value>实体类的切面类id</value>
        </list>
    </property>
    <!-- proxyInterfaces 属性:用于注入接口信息,如果有接口,spring就会使用JDK动态代理
    如果目标对象没有实现任何接口(不用写),则spring会使用CGLIB来创建代理-->
    <property name="proxyInterfaces">
        <list>
            <value>edu.nf.ch18.service.UserService</value>
        </list>
    </property>
</bean>

调用(下面的方法也适用)

ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
context.getBean("实体类的代理对象id", 实体类.class);

注意

异常通知,根据官方文档说明改方法名必须叫做afterThrowing,并且必须包含一个Exception参数

依赖AspectJ的xml

<!-- AOP配置,proxy-target-class 属性设置为 true
 表示强制使用 CGLIB 生成代理,无论目标对象有没有实现接口-->
<aop:config>
    <!-- 配置切入点,Spring 使用了 AspectJ 的切入点表达式来实现了 AOP 中切入点的概念,
    通过切入点表达式可以找到需要增强的目标方法,而找到的这些目标方法就称之为连接点。
    id属性指定一个切入点的唯一标识,expression用于声明切入点表达式,切入点表达式的用法:
    execution(访问修饰符 包名.类名.方法名(参数类型))
     也可以使用通配符(*)的方法扩大切入点的范围 比如 包名.*.*(..) -->
    <aop:pointcut id="userPointcut" expression="execution(* org.example.User.*(..))"/>
    <!-- 配置通知器(也就是切面),使用 pointcut-ref 引用上面装配的切面的id -->
        <aop:advisor advice-ref="userAspect" pointcut-ref="userPointcut"/>-->
    <!-- 配置切面,ref 引用切面的bean的id,pointcut-ref 切入点表达式 -->
    <aop:aspect ref="serviceAspect">
        <!-- 执行顺序是随机的,是通过反射获取顺序决定的 -->
        <!-- 配置自定义通知,method 属性指定通知的方法名,后置通知 returning属性 需要对应返回值(返回值在参数中定义),异常通知 throwing属性 需要对应的Exception的参数 -->
        <aop:before method="before" pointcut-ref="userPointcut"/>
        <aop:after-returning method="afterReturning" pointcut-ref="userPointcut" returning="returnValue"/>
        <aop:around method="around" pointcut-ref="userPointcut"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="userPointcut" throwing="e"/>
        <aop:after method="after" pointcut-ref="userPointcut"/>
    </aop:aspect>
</aop:config>

依赖AspectJ的注解扫描

启动 AspectJ 注解处理器(配置类上的注解)

(proxyTargetClass = true):强制启动 CGLIB 生成代理

@EnableAspectJAutoProxy(proxyTargetClass = true)

切面类的设置

@Slf4j
@Component
// 标识为切面类
@Aspect
public class ServiceAspect {
    /**
     * 切入点表达式,用于找到连接点的表达式
     */
    @Pointcut("execution(* org.example.User.*(..))")
    public void pointcut() {
    }

    /**
     * 自定义前置通知,可以给一个参数
     * 这个参数为连接点
     * 通过这个连接点可以拦截目标方法的参数等信息
     *
     * @param jp
     */
    // 设置切入点
    @Before("pointcut()")
    public void before(JoinPoint jp) {
        log.info("自定义前置通知,参数" + jp.getArgs());
//        jp.getArgs()[0] = "修改参数";
    }

    /**
     * 后置通知
     *
     * @param jp          连接点
     * @param returnValue 目标方法的返回值
     */
    @AfterReturning(value="pointcut()",returning = "returnValue")
    public void afterReturning(JoinPoint jp, Object returnValue) {
        log.info("后置通知,参数" + jp.getArgs());
        log.info("返回值" + returnValue);
    }

    /**
     * 环绕通知
     *
     * @param jp 连接点,继承自 JoinPoint 接口
     * @return
     */
    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint jp) throws Throwable {
        log.info("环绕");
        return jp.proceed();
    }

    /**
     * 异常通知,当目标方法产生异常时会执行
     * 后置通知或者(方法执行后执行的通知)都不会生效
     * 除了最终通知
     *
     * @param jp
     * @param e
     */
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowing(JoinPoint jp, Exception e) {
        log.info("异常" + e.getMessage());
    }

    /**
     * 最终通知,不管有没有异常产生,都会执行
     *
     * @param jp
     */
    @After("pointcut()")
    public void after(JoinPoint jp) {
        log.info("最终通知");
    }
}