AOP面向切面编程

一、AOP概述

1. AOP简介

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理

可以看之前写的动态代理,

https://www.cnblogs.com/mengd/p/13429797.html

  1. jdk动态代理:使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象,jdk动态代理要求目标类必须实现接口
  2. cglib动态代理:第三方的工具库,创建代理对象,原理是继承。 通过继承目标类,创建子类,子类就是代理对象。 要求目标类不能是final的, 方法也不能是final的

动态代理的作用:

  • 在目标类源代码不改变的情况下,增加功能
  • 减少代码的重复
  • 专注业务逻辑代码
  • 解耦合,让你的业务功能和日志分离,事务和非业务功能分离

2. 如何理解AOP

AOP(Aspect Orient Programming)面向切面编程

  • Aspect: 切面,给你的目标类增加的功能,就是切面,切面的特点: 一般都是非业务方法,独立使用的
  • Orient:面向
  • oop: 面向对象编程

理解:

  1. 需要在分析项目功能时,找出切面
  2. 合理的安排切面的执行时间(在目标方法前, 还是目标方法后)
  3. 合理的安全切面执行的位置,在哪个类,哪个方法增加增强功能

二、AOP编程术语

1. 切面(Aspect)

表示增强的功能, 就是一堆代码,完成某个一个功能,非业务功能

常见的切面功能有日志, 事务, 统计信息, 参数检查, 权限验证

2. 连接点(JoinPoint)

连接业务方法和切面的位置,就某类中的业务方法

3. 切入点(Pointcut)

指多个连接点方法的集合,多个方法

4. 目标对象(Target)

给哪个类的方法增加功能, 这个类就是目标对象

5. 通知(Advice)

通知表示切面功能执行的时间

一个切面有三个关键的要素:

  1. 切面的功能代码,切面是干什么
  2. 切面的执行位置,使用Pointcut表示切面执行的位置
  3. 切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后

三、Aspectj对AOP的实现

aop是一个规范,是动态的一个规范化,一个标准

aop的技术实现框架:

  1. spring:spring在内部实现了aop规范,能做aop的工作,我们项目开发中很少使用spring的aop实现。 因为spring的aop比较笨重
  2. aspectJ: 一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能

aspectJ框架实现aop有两种方式:

  • 使用xml的配置文件 : 配置全局事务
  • 使用注解,我们在项目中要做aop功能,一般都使用注解, aspectj有5个注解

1. Aspectj的通知类型

AspectJ 中常用的通知有五种类型

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知

2. Aspectj的切入点表达式

以上表达式共4个部分

execution(访问权限 方法返回值 方法声明(参数) 异常类型)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就
是方法的签名。

注意,表达式中黑色文字表示可省略部分,各部分间用空格分开

在其中可以使用以下符号:

常用的几个:

execution(public * * (..))

指定切入点的位置:任意的公共方法

execution(* set*(..))

指定切入点的位置:任何一个以set开始的方法

execution(* com.xyz.service.*.*(..))

指定切入点的位置:定义在service包里的任意类的任意方法

execution(* com.xyz.service..*.*(..))

指定切入点的位置:定义在service包或者子包里的任意类的任意方法

.. 出现在类名中时,后面必须跟*,表示包、子包下的所有类

execution(* *..service.*.*(..))

指定所有包下的service子包下所有类中所有的方法为切入点

3. Aspectj的开发环境

1. maven依赖

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--spring依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--aspectj依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

2. 引入AOP约束

在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的
AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式

四、AspectJ基于注解的AOP实现

1. 实现步骤

1. 定义业务接口与实现类

package com.md.b1;

/**
 * @author MD
 * @create 2020-08-09 10:55
 */
public interface SomeService {
    void doSome(String name,Integer age);
}

//-------------------------

package com.md.b1;

/**
 * @author MD
 * @create 2020-08-09 10:55
 */

// 目标类
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        // 给doSome方法增加一个功能,在执行之前输出时间
        System.out.println("目标方法doSome()");
    }
}

2. 定义切面类

类中定义了若干普通方法,将作为不同的通知方法,用来增强功能

注意点:

@Aspect

  • 这个注解是aspectj框架中的注解
  • 作用:表示当前类是切面类
  • 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
  • 位置:类定义的上面

定义方法,方法是实现切面功能的

方法的要求:

  • 公共方法
  • 方法名称自定义
  • 方法没有返回值
  • 方法可以有或没有参数,如果有参数,参数不是自定义的,有几个参数类型可以使用
package com.md.b1;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;

/**
 * @author MD
 * @create 2020-08-09 10:58
 */

@Aspect
public class MyAspect {

    // 前置通知,具体的在下变
   @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        // 就是你切面要执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
    }
}

3. 定义目标对象切面类对象

还是在src/main/resources下建立applicationContext.xml

整体结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--把对象交给spring容器,由spring容器统一创建,管理对象-->
    
    <!--声明目标对象-->
    <bean id="someService" class="com.md.b1.SomeServiceImpl"/>

    <!--声明切面类对象-->
    <bean id="myAspect" class="com.md.b1.MyAspect"/>

</beans>

4. 注册AspectJ自动代理

在上面文件的基础上添加

    <bean id="someService" class="com.md.b1.SomeServiceImpl"/>

    <bean id="myAspect" class="com.md.b1.MyAspect"/>


    <!--声明自动代理生成器:
        使用的是aspectj框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构,
        创建为代理对象。所以,目标对象就是被修改后的代理对象

        aspectj-autoproxy:会把spring容器中的所有目标对象,一次性都生成代理对象
    -->

    <aop:aspectj-autoproxy  />

<aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。
从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。
其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切
面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点

5. 测试类中的使用

package com.md;

import com.md.b1.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.security.provider.Sun;

/**
 * @author MD
 * @create 2020-08-09 15:28
 */
public class MyTest01 {

    @Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 从容器中获取目标对象,此时的目标对象是经过了aspectj修改后的代理对象
        SomeService proxy = (SomeService) ac.getBean("someService");

        //com.sun.proxy.$Proxy8 jdk动态代理
        //System.out.println(proxy.getClass().getName());

        // 通过代理的对象执行方法,实现目标方法执行,增强了功能
        proxy.doSome("张三",19);

//        前置通知,切面功能:在目标方法之前输出时间:Sun Aug 09 15:33:24 CST 2020
//        目标方法doSome()
    }
}

2. @Before前置通知

在目标方法执行之前执行

被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式

通过该参数,可获取切入点表达式、方法签名、目标对象等
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。

这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数

@Aspect
public class MyAspect {

//    @Before():前置通知注解
//    属性:value 是切入点表达式,表示切面功能执行的位置
//    位置:在方法的上面
//    特点:
//    1. 在目标方法之前执行
//    2. 不会改变目标方法的执行结果
//    3. 不会影响目标方法的执行



//    @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
//    public void myBefore(){
//        // 就是你切面要执行的功能代码
//        System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
//    }


//    @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
//    public void myBefore(){
//        // 就是你切面要执行的功能代码
//        System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
//    }


    /**
     * 指定通知方法中的参数 : JoinPoint
     * JoinPoint:业务方法,要加入切面功能的业务方法
     *    作用是:可以在通知方法中获取方法执行时的信息, 例如方法名称,方法的实参。
     *    如果你的切面功能中需要用到方法的信息,就加入JoinPoint.
     *    这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数
     */
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(JoinPoint jp){
        // 获取方法的完整定义
        System.out.println("方法的定义:"+jp.getSignature());
        System.out.println("方法的名称:"+jp.getSignature().getName());

        // 获取方法的实参
        Object[] args = jp.getArgs();
        for (Object arg:args){
            System.out.println("参数:"+arg);
        }


//        方法的定义:void com.md.b1.SomeService.doSome(String,Integer)
//        方法的名称:doSome
//        参数:张三
//        参数:19

        // 就是你切面要执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
    }
}

3. @AfterReturning后置通知

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值

该注解的 returning 属性就是用于指定接收方法返回值的变量名的

所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型

增加接口的方法

public interface SomeService {
    void doSome(String name,Integer age);
    String doOther(String name,Integer age);
}
//--------------------------------------------------
// 目标类
public class SomeServiceImpl implements SomeService {


    @Override
    public void doSome(String name,Integer age) {
        // 给doSome方法增加一个功能,在执行之前输出时间
        System.out.println("目标方法doSome()");
    }

    @Override
    public String doOther(String name,Integer age) {
        System.out.println("目标方法doOther()");
        return "a";
    }
}

定义切面

@Aspect
public class MyAspect {
    /**
     * 后置通知定义方法,方法是实现切面功能的。
     * 方法的定义要求:
     * 1.公共方法 public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法有参数的,推荐是Object ,参数名自定义
     */

    /**
     * @AfterReturning:后置通知
     *    属性:1.value 切入点表达式
     *         2.returning 自定义的变量,表示目标方法的返回值的。
     *          自定义变量名必须和通知方法的形参名一样。
     *    位置:在方法定义的上面
     * 特点:
     *  1。在目标方法之后执行的。
     *  2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
     *      Object res = doOther();
     *  3. 可以修改这个返回值,但不影响最后的调用结果
     *
     *  后置通知的执行
     *    Object res = doOther();
     *    参数传递: 传值, 传引用
     *    myAfterReturing(res);
     *    System.out.println("res="+res)
     *
     */

    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "res")
    public void myAfterReturing(Object res){

        if (res.equals("a")){
            // 你可以做一些功能
            System.out.println("登陆成功");
        }else{
            System.out.println("登陆失败");
        }

        
        // Object res:是目标方法执行后的返回值,可以根据返回值做切面功能处理
        System.out.println("后置通知:获取的返回值是:"+res);

        // 修改目标方法的返回值,是否影响最后的方法调用结果
        // 无影响,
        if (res != null){
            res = "hello Aspectj";
        }
    }
}

4. @Around环绕通知

在目标方法执行之前之后执行。

被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。

接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。

若目标方法有返回值,则该方法的返回值就是目标方法的返回值。

最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行

首先增加方法和实现

public interface SomeService {
    void doSome(String name,Integer age);
    String doFirst(String name,Integer age);
}
//-------------------------------------------
// 目标类
public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome(String name,Integer age) {

        // 给doSome方法增加一个功能,在执行之前输出时间
        System.out.println("目标方法doSome()");
    }


    @Override
    public String doOther(String name,Integer age) {
        System.out.println("目标方法doOther()");
        return "a";
    }

    @Override
    public String doFirst(String name,Integer age) {
        System.out.println("目标方法doFirst()");
        return "doFirst";
    }

}

切面类

@Aspect
public class MyAspect {
    /**
     * 环绕通知方法的定义格式
     *  1.public
     *  2.必须有一个返回值,推荐使用Object
     *  3.方法名称自定义
     *  4.方法有参数,固定的参数 ProceedingJoinPoint
     */

    /**
     * @Around: 环绕通知
     *    属性:value 切入点表达式
     *    位置:在方法定义的上面
     * 特点:
     *   1.它是功能最强的通知
     *   2.在目标方法的前和后都能增强功能。
     *   3.控制目标方法是否被调用执行
     *   4.修改原来的目标方法的执行结果。 影响最后的调用结果
     *
     
     *  环绕通知,等同于jdk动态代理的,InvocationHandler接口
     *
     *  参数:  ProceedingJoinPoint 就等同于 Method
     *         作用:执行目标方法的
     *  返回值: 就是目标方法的执行结果,可以被修改。
     *
     *  环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务
     */

//    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
//    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//
//        Object result = null;
//
//
//        System.out.println("环绕通知,在目标方法之前加入通知:现在时间:"+new Date());
//
//        // 1. 目标方法调用,等同于method.invoke(); 在这里等同于Object result = doFirst();
//        result = pjp.proceed();
//
//        // 2. 在目标方法前后加入功能
//        System.out.println("环绕通知,在目标方法之后提交事务");
//
//        // 3. 返回目标方法的执行结果
//        return result;
//
//    }




    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

	// 可以获取到调用方法的参数
        String name = "";
        // 获取第一个参数的值
        Object args[] = pjp.getArgs();
        if (args!=null && args.length > 1){
            Object arg = args[0];
            name = (String) arg;
        }


        // 实现环绕通知
        Object result = null;
        System.out.println("环绕通知,在目标方法之前加入通知:现在时间:"+new Date());


        // 1. 目标方法调用,等同于method.invoke(); 在这里等同于Object result = doFirst();
        if ("张三".equals(name)){
            // 符合条件,调用目标方法
            result = pjp.proceed();
        }


        // 2. 在目标方法前后加入功能
        System.out.println("环绕通知,在目标方法之后提交事务");


        // 还可以修改目标方法的执行结果,影响方法最后的调用结果
        if (result != null){
            result = "修改了";
        }


        // 3. 返回目标方法的执行结果
        return result;

    }

}

测试:

public class MyTest03 {

    @Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 从容器中获取目标对象
        SomeService proxy = (SomeService) ac.getBean("someService");



        // 通过代理的对象执行方法,实现目标方法执行,增强了功能
        String str = proxy.doFirst("张三",20);

        System.out.println(str);

    }
}

//环绕通知,在目标方法之前加入通知:现在时间:Mon Aug 10 20:52:07 CST 2020
//        目标方法doFirst()
//        环绕通知,在目标方法之后提交事务
//        修改了

5. @AfterThrowing 异常通知

在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象

相当于try-catch中catch里面执行的

增加业务方法

public interface SomeService {
    void doSome(String name,Integer age);

    void doSecond();
}


//实现类-----------------------------------------
@Override
    public void doSecond() {
         System.out.println("执行业务方法doSecond()" + (10/0));
    }

切面类:

@Aspect
public class MyAspect {
    /**
     * 异常通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法有个一个Exception, 如果还有是JoinPoint,*/

    /**
     * @AfterThrowing:异常通知
     *     属性:1. value 切入点表达式
     *          2. throwinng 自定义的变量,表示目标方法抛出的异常对象。
     *             变量名必须和方法的参数名一样
     * 特点:
     *   1. 在目标方法抛出异常时执行的
     *   2. 可以做异常的监控程序, 监控目标方法执行时是不是有异常。
     *      如果有异常,可以发送邮件,短信进行通知
     *
     *  执行就是:
     *   try{
     *       SomeServiceImpl.doSecond(..)
     *   }catch(Exception e){
     *       myAfterThrowing(e);
     *   }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
        //发送邮件,短信,通知开发人员
    }
}

6. @After最终通知

无论目标方法是否抛出异常,该增强均会被执行

相当于try-catch-finally中finally里面执行的

增加方法及实现

public interface SomeService {
    void doSome(String name,Integer age);
    void doSecond();

    void doThird();
}

//------------------------
  @Override
    public void doThird() {
        System.out.println("执行业务方法doThird()"+ (10/0));
    }

切面类

@Aspect
public class MyAspect {
    /**
     * 最终通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法没有参数,  如果还有是JoinPoint,*/

    /**
     * @After :最终通知
     *    属性: value 切入点表达式
     *    位置: 在方法的上面
     * 特点:
     *  1.总是会执行
     *  2.在目标方法之后执行的
     *
     *  try{
     *      SomeServiceImpl.doThird(..)
     *  }catch(Exception e){
     *
     *  }finally{
     *      myAfter()
     *  }
     *
     */
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
     }

}

7. @Pointcut定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式

其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法

五、总结

前面的概念有些绕,看了代码就比较清晰了,感觉和python的装饰器很像,只不过py的没有这么绕,就是为已经存在的对象添加额外的功能

总的来说就是在一个方法前或一个方法后执行一些通用的方法,提高效率,把那些业务的方法写一块,那些非业务的方法或那些业务方法经常使用的方法写成切面类,使用方便还便于管理

1. 使用aspectj框架实现aop

使用aop:目的是给已经存在的一些类和方法增加额外的功能,前提是不改变原来类的代码

  1. 新建maven项目
  2. 加入依赖:spring依赖和aspectj依赖以及junit单元测试
  3. 创建目标类
    • 接口和它的实现类,要做的是给类中的方法增加功能
  4. 创建切面类:普通类
    • 在类的上面加入@Aspect
    • 在类中定义方法,这个方法就是切面要执行的功能代码
    • 在方法的上面加入aspectj中的通知注解。例如:@Before
    • 还需要指定切入点表达式,execution()
  5. 创建spring的配置文件,声明对象,把对象交给容器统一管理,声明对象可以使用注解或者<bean>
    • 声明目标对象
    • 声明切面类对象
    • 声明aspectj框架中的自动代理生成器标签,自动代理生成器:用来完成代理对象的自动创建功能
  6. 创建测试类
    • 从spring容器中获取目标对象(实际上就是代理对象),通过代理执行,实现aop的功能增强

2. Review



强制使用cglib代理

 目标类有接口,还想用cglib代理
        proxy-target-class="true" : 这句话就是告诉框架,要使用cglib动态代理
        <aop:aspectj-autoproxy proxy-target-class="true" />

相关文章

这篇文章主要介绍了spring的事务传播属性REQUIRED_NESTED的原...
今天小编给大家分享的是一文解析spring中事务的传播机制,相...
这篇文章主要介绍了SpringCloudAlibaba和SpringCloud有什么区...
本篇文章和大家了解一下SpringCloud整合XXL-Job的几个步骤。...
本篇文章和大家了解一下Spring延迟初始化会遇到什么问题。有...
这篇文章主要介绍了怎么使用Spring提供的不同缓存注解实现缓...