SpringAop介绍及基本用法

AOP用一句话总结就是一种用来实现在应用程序运行期间动态地将某一段代码织入到指定方法指定位置并进行运行的一种编程方法。其底层实现方式是基于动态代理来完成的,本篇文章先介绍如何使用注解的方式来实现AOP功能,下篇文章将会分析其底层具体实现。

一、基本使用

实现一个计算器的功能,在计算前后打印参数及计算结果等参数信息。

配置类AopConfig

/**
* @author wangbin33
* @date Created in 18:45 2019/10/6
*/
@Configuration
@ComponentScan(basePackages = "com.wb.spring.aop")
// 开启AOP支持
// 该注解中会使用Import注解导入后置处理器及注册自定义Bean用来完成AOP功能
@EnableAspectJAutoProxy
public class AopConfig {
}

计算器测试类MathCalculator

/**
* @author wangbin33
* @date Created in 17:43 2019/10/6
*/
@Component
public class MathCalculator {
  public int div(int i, int j) {
    return i / j;
  }
}

测试切面类LogAspects

/**
* @author wangbin33
* @date Created in 18:02 2019/10/6
*/
@Aspect
@Component
public class LogAspects {
  // 声明切点,表示切到com.wb.spring.aop.MatchCalculator下的所有方法
  @Pointcut("execution(public int com.wb.spring.aop.MathCalculator.*(..))")
  public void pointCut() {}

  /** 环绕通知 */
  @Around("pointCut()")
  public Object doAround(ProceedingJoinPoint pjp) {
    // 获取被增强的目标对象,然后获取目标对象的class
    Class<?> targetClass = pjp.getTarget().getClass();
    System.out.println("执行Around,被增强的目标类为:" + targetClass);
    // 方法名称
    String methodName = pjp.getSignature().getName();
    System.out.println("执行Around,目标方法名称为:" + methodName);
    // 目标方法的参数类型
    Class[] parameterTypes = ((MethodSignature) pjp.getSignature()).getParameterTypes();
    // 目标方法的入参
    Object[] args = pjp.getArgs();
    System.out.println("执行Around,方法入参为:" + Arrays.toString(args));
    try {
      // 目标方法
      Method method = targetClass.getMethod(methodName, parameterTypes);
      System.out.println("执行Around,方法为:" + method);
      // 继续放行
      return pjp.proceed();
    } catch (Throwable e) {
      System.err.println("执行Around异常..." + e);
      return "error";
    }
  }
  /** 前置通知 */
  @Before("pointCut()")
  public void logStart(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    System.out.println(joinPoint.getSignature().getName() + "运行Before... 参数为:" + Arrays.asList(args));
  }
  /** 后置通知 */
  @After("pointCut()")
  public void logEnd(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature().getName() + "运行After...");
  }
  /** 返回通知 */
  @AfterReturning(value = "pointCut()", returning = "result")
  public void logReturn(JoinPoint joinPoint, Object result) {
    System.out.println(joinPoint.getSignature().getName() + "运行AfterReturning... 正常返回,结果为:" + result);
  }
  /** 异常通知 */
  @AfterThrowing(value = "pointCut()", throwing = "exception")
  public void logException(JoinPoint joinPoint, Exception exception) {
    System.out.println(joinPoint.getSignature().getName() + "运行AfterThrowing... 异常信息:" + exception);
  }
}

测试类TestMain

/**
* @author wangbin33
* @date Created in 18:48 2019/10/6
*/
public class TestMain {
  public static void main(String[] args) {
    ApplicationContext acx = 
        new AnnotationConfigApplicationContext(AopConfig.class);
    MathCalculator mathCalculator = 
    acx.getBean(MathCalculator.class);
    mathCalculator.div(2, 1);
  }
}

运行结果:

执行Around,被增强的目标类为:class com.wb.spring.aop.MathCalculator
执行Around,目标方法名称为:div
执行Around,方法入参为:[2, 1]
执行Around,方法为:public int com.wb.spring.aop.MathCalculator.div(int,int)
div运行Before... 参数为:[2, 1]
div运行After...
div运行AfterReturning... 正常返回,结果为:2

就这样简单,一个AOP的使用示例就成功运行完成了,那么示例中的Aspect,PointCut,@Before等都是什么鬼玩意呢?请继续向下看。

二、切面、切点及增强动作

2.1、切面

切面指的是由一系列切点及增强动作组成的模块对象,在使用过程中,如果有多个切面,可以通过Spring提供的Order接口或者@Order注解来定义切面的优先级,从而可以按照指定的顺序来对应用功能进行增强。例如上例中的LogAspects测试类就是一个切面,使用注解版的切面类,切面需要使用注解@Aspect来进行标注。

 

经常使用的场景:接口性能监控,事务管理,日志记录,读写分离等。

2.2、切点

切点也是一个抽象的概念,可以简单理解为数据库中的查询条件。例如在数据库中,某条数据满足一定的查询条件才会被查询出来,而如果某一个类中的方法满足切点中定义的条件时,切点对应的增强动作才会被执行。而这个切点对应的条件,成为切点表达式,切点表达式通常有如下的几种类型:

(1)execution表达式

该表达式的最小粒度为方法,使用语法如下:

execution(modifier returnType package.method(param) exception);

参数说明:

modifier:方法修饰符,例如:public,private等,可以省略
returnType:方法返回值类型,如:int,void
package:方法所在类的包名,可以省略
method:方法名称
param:方法参数
exception:方法抛出的异常,可以省略

示例1:

下面表达式会匹配到使用public修饰,返回值为int(如果写为*,则表示匹配任意返回值),类名称为MathCalculator中的所有方法,方法中可以带有参数,也可以不带参数

execution(public int com.wb.spring.aop.MathCalculator.*(..))

示例2:

下面表达式会匹配到返回值为任意类型,在com.wb.spring.aop.MathCalculator类中,并且不带参数的方法

execution(* com.wb.spring.aop.MathCalculator.*())

示例3:

下面表达式会匹配到返回值为任意类型,并且在com.wb.spring包及其子包下的所有类中名称为testService的没有参数的方法:

..通配符值的是匹配0个或者多个。

execution(* com.wb.spring..*.testService())

示例4:

下面表达式会匹配到返回值为任意类型,并且以Math开头的所有类中的所有第一个参数为String类型的的所有方法

execution(* com.wb.spring.aop.Math*.*(java.lang.String, ..))

(2)within类型

within声明的切点表达式最小粒度为类,匹配到表达式的所有类中的所有方法都会被增强,使用方法如下:

within(match-pattern)

示例1:

如下的表达式会匹配com.wb.spring.aop.MathCalculator类下的所有方法

within(com.wb.spring.aop.MathCalculator)

示例2:

如下的表达式会匹配到com.wb.spring.aop包下的所有类下的所有方法,但是不包括子包中的类

within(com.wb.spring.aop.*)

示例3:

如下的表达式会匹配到com.wb.spring.aop包及其子包下的所有类中的所有方法

within(com.wb.spring.aop..*)

(3)args类型

args类型不用关注类名和方法名,只需要关注方法中的参数类型和参数个数,但是如果指定参数类型时,需要指定类型对应的全路径,语法如下:

args(match-pattern)

示例1:

下面表达式可以匹配到只有一个String类型的参数对应的方法

args(java.lang.String)

示例2:

下面表达式会匹配到第一个参数为String类型,最后一个类型为Long类型的所有方法

args(java.lang.String,..,java.lang.Long)

(4)@within类型

within表示匹配指定的类,@within表示匹配带有指定注解的类。语法如下:

@within(annotation-pattern)

示例:

下面示例表示匹配所有标注有com.wb.spring.aop.MyAnnotation的类

@within(com.wb.spring.aop.MyAnnotation)

只要切面中使用@within声明了切入@MyAnnotation注解,则只要标注有该注解的类下面的方法,在运行期间都会被切到,一般用于类上面。

(5)@annotation类型

@annotation和@within类似,只是@annotation中指定的注解一般用于某个类中的具体方法上,只要标注了@annotation中指定的注解,那么该方法在运行期间就会被切面拦截到。语法如下:

@annotation(annotation-pattern)

示例1:

下面示例表示匹配所有标注有com.wb.spring.aop.MyAnnotation的方法

@annotation(com.wb.spring.aop.MyAnnotation)

示例2:

例如下面的div方法执行时会被环绕增强,因为方法上方标注有@MyAnnotation注解,而切面表达式刚好是匹配到该注解。

注解类MyAnnotation

@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

测试类MyClass

@Component
public class MyClass {
  @MyAnnotation
  public int div(int a, int b) {
    return a + b;
  }
}

切面类MyAspect

@Aspect
@Component
public class MyAspect {
  @Around("@annotation(com.wb.spring.aop.MyAnnotation)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around... invoke ...");
    return pjp.proceed();
  }
}

(6)@args类型

@args表示使用了指定注解的类作为某个方法的入参,在这个方法被调用的时候,方法会被增强。语法如下:

@args(annotation-pattern)

示例1:

下面的示例表示匹配使用MyAnnotation注解标注的类作为参数的方法

@args(com.wb.spring.aop.MyAnnotation)

示例2:

使用切面功能对方法入参中包括Pen类型参数的方法进行增强。

测试类Pen

@Component
@MyAnnotation
public class Pen {
  public String writeText() {
    return "Hello world";
  }
}

测试类MyClass

@Component
public class MyClass {
  public String write(Pen pen) {
    return pen.writeText();
  }
}

切面类MyAspect

@Aspect
@Component
public class MyAspect {
  @Around("@args(com.wb.spring.aop.MyAnnotation)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around... invoke ...");
    return pjp.proceed();
  }
}

配置类AopConfig

@Configuration
@ComponentScan(basePackages = "com.wb.spring.aop")
// 开启AOP支持
@EnableAspectJAutoProxy
public class AopConfig {
}

测试类TestMain

public class TestMain {
  public static void main(String[] args) {
    ApplicationContext acx = 
       new AnnotationConfigApplicationContext(AopConfig.class);
    MyClass myClass = acx.getBean(MyClass.class);
    Pen pen = acx.getBean(Pen.class);
    myClass.write(pen);
  }
}

// 最后执行之后,会执行around方法.

2.3、增强动作

增强动作指的是程序执行过程中,如果所执行的方法满足某一个切点定义的条件之后,与切点对应的增强方法就会被触发而执行。有如下几种:

(1)@Around,环绕通知

这个注解的功能最丰富,使用这个注解标注的方法是用来实现业务逻辑代码的环绕增强,入参为ProceedingJoinPoint,可以用来调用业务模块的代码,调用之前的逻辑和调用之后的逻辑都可以在这个方法中实现,而且这个方法可以阻断业务模块的调用。

(2)@Before,前置通知

使用这个注解标注的方法会在业务代码执行之前先执行,不能阻断业务逻辑的执行,除非抛出异常。

(3)@After,后置通知

类似于finally,在所有的增强方法执行之后才会执行,无论业务逻辑是否有异常。

(4)@AfterReturning,返回通知

该注解标注的方法在业务逻辑代码执行之后执行。

(5)@AfterThrowing,异常通知

该注解标注的方法在业务逻辑抛出指定异常之后会执行。

2.4、增强动作的执行顺序

Around Before[@Before标注的方法方法执行前]
->Before[@Before标注的方法执行]
->目标方法[目标方法执行]
->Around After[目标方法执行之后,@After之前]
->After[@After标注的方法执行]
-> AfterReturning (如果有异常,则AfterThrowing)[返回或者异常]

至此,SpringAOP相关的基础用法介绍完毕,本篇文章通过一个简单的示例,说明了SpringAOP中常用的内容,例如切点,切面,切点表达式。后续文章将继续剖析SpringAOP的底层执行过程。欢迎评论转发!

文章属于作者原创,如果转发请标注文章来源:个人小站【www.jinnianshizhunian.vip