Spring AOP介绍与使用
1、AOP的概念
AOP的核心概念及术语
- 切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以
@Aspect
注解(@AspectJ 注解方式)来实现。 - 连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
- 通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
- 切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
- 引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现
IsModified
接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。 - 目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
- AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
- 织入(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。
AOP的通知类型(5种)
- 前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。
- 后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。
- 后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
- 后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。
- 环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。
AOP的应用场景
- 日志管理
- 权限认证
- 安全检查
- 事务控制
2、Spring AOP的配置(基于Maven)
1.导入依赖a
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd
">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mashibin_spring_01</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.0</version>
</dependency>
<!--springioc的名称空间-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
</dependencies>
</project>
2.编写配置(applictionContext.xml为配置文件)
-
创建applicationsContext.xml
-
配置applicationsContext.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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd "> <!--开启扫描包,用于注解--> <context:component-scan base-package="com.aop"></context:component-scan> <!--开启aop注解支持--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
3.创建各个类以及配置切入点表达式:
-
类的目录
-
calculator 接口(模拟计算器)
package com.aop.service; public interface calculator { public int add(int i,int j); public int sub(int i,int j); public int mult(int i,int j); public int div(int i,int j); }
-
Mycalculator类(实现计算器接口)
package com.aop.service; import org.springframework.stereotype.Service; @Service public class Mycalculator implements calculator { @Override public int add(int i, int j) { int result = i + j; return result; } @Override public int sub(int i, int j) { int result = i - j; return result; } @Override public int mult(int i, int j) { int result = i * j; return result; } @Override public int div(int i, int j) { int result = i / j; return result; } }
-
LogUtil类(日志类,也叫通知类),加入ioc注解和aop注解,还有切入点表达式
package com.aop.utils; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Controller; @Controller @Aspect public class LogUtil { @Before("execution(public int com.aop.service.Mycalculator.*(int,int))") public static void start(){ System.out.println("方法开始执行"); } @AfterReturning("execution( public int com.aop.service.Mycalculator.*(int,int))") public static void stop(){ System.out.println("方法执行完成"); } @AfterThrowing("execution( public int com.aop.service.Mycalculator.*(int,int))") public static void logException(){ System.out.println("方法出现异常"); } @After("execution( public int com.aop.service.Mycalculator.*(int,int))") public static void end(){ System.out.println("方法执行结束了......"); } }
-
给各个类添加注解和切入点表达式(上方代码已经添加)
-
给LogUtil添加@Component注解
-
给MyCalculator添加@Service注解
-
在LogUtil.java中添加@Aspect注解
-
在LogUtil类中设置方法在什么时候执行
-
设置下面方法在什么时候运行
- @Before:在目标方法之前运行:前置通知
- @After:在目标方法之后运行:后置通知
- @AfterReturning:在目标方法正常返回之后:返回通知
- @AfterThrowing:在目标方法抛出异常后开始运行:异常通知
- @Around:环绕:环绕通知
-
切入点白表达式:
*号:
1、匹配一个或者多个字符
execution( public int com.mashibing.inter.My*alculator.*(int,int))
2、匹配任意一个参数,
execution( public int com.mashibing.inter.MyCalculator.*(int,*))
3、只能匹配一层路径,如果项目路径下有多层目录,那么*只能匹配一层路径
4、权限位置(访问修饰符)不能使用*,如果想表示全部权限,那么不写即可
execution( * com.mashibing.inter.MyCalculator.*(int,*))
…号:
1、匹配多个参数,任意类型参数
execution( * com.mashibing.inter.MyCalculator.*(…))
2、匹配任意多层路径
execution( * com.mashibing…MyCalculator.*(…))
在写表达式的时候,可以有N多种写法,但是有一种最偷懒和最精确的方式:
最偷懒的方式:execution(* *(…)) 或者 execution(* *.*(…))
最精确的方式:execution( public int com.mashibing.inter.MyCalculator.add(int,int))
除此之外,在表达式中还支持 &&、||、!的方式
&&:两个表达式同时
execution( public int com.mashibing.inter.MyCalculator.*(…)) && execution(* *.*(int,int) )
||:任意满足一个表达式即可
execution( public int com.mashibing.inter.MyCalculator.*(…)) && execution(* *.*(int,int) )
!:只要不是这个位置都可以进行切入
&&:两个表达式同时
execution( public int com.mashibing.inter.MyCalculator.*(…))
-
4.侧式方法执行
import com.aop.service.calculator;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class aopTaet {
@Test
public void test1(){
//读取配置文件
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
//获取bean对象
calculator mycalculator = context.getBean( calculator.class);
//执行方法
mycalculator.add(1,2);
}
}
5,关于动态代理
- 当类存在接口实现时,会调用JDK的动态代理proxy
- 当类没有接口实现的时候,会调用Spring中的动态代理CGLIB
- 早期CGLIB速度会比JDK快,但是JDK再不断更新,而Spring一直停步不前,现在两者速度差不多
3、Spring AOP获取方法的详细信息
1.JoinPoint参数:获取方法名、参数列表等信息
@Before("execution(public int com.aop.service.Mycalculator.*(int,int))")
public static void start(JoinPoint joinPoint) {
// 获取方法签名
Signature signature = joinPoint.getSignature();
// 获取方法名
String name = signature.getName();
//获取方法参数
Object[] args = joinPoint.getArgs();
System.out.println(name+"方法开始执行--Before,方法参数是:" + Arrays.asList(args));
}
2.获取返回结果:returning(获取时,直接输出方法参数)
-
切入点表达式中添加returning,在方法中添加Object参数:
@AfterReturning(value = "execution( public int com.aop.service.Mycalculator.*(int,int))",returning = "result") public static void stop(JoinPoint joinPoint,Object result) { // 获取方法签名 Signature signature = joinPoint.getSignature(); // 获取方法名 String name = signature.getName(); System.out.println(name + "方法执行完成--AfterReturning,返回值:"+result); }
- 注意:切入点表达式中returning的属性必须参数Object的属性名称上保持一致(result)
3.获取异常信息:trowing (获取时,直接输出方法参数)
-
切入点表达式中添加trowing,在方法中添加Exception参数,但是两者的属性名必须一致
@AfterThrowing(value = "execution( public int com.aop.service.Mycalculator.*(int,int))",throwing = "e") public static void logException(JoinPoint joinPoint,Exception e) { // 获取方法签名 Signature signature = joinPoint.getSignature(); // 获取方法名 String name = signature.getName(); System.out.println(name+"方法出现异常--AfterThrowing,异常信息"+e); }
4.spring对通知方法的要求:
- spring对于通知方法的要求并不是很高,你可以任意改变方法的返回值和方法的访问修饰符,但是唯一不能修改的就是方法的参数,会出现参数绑定的错误,原因在于通知方法是spring利用反射调用的,每次方法调用得确定这个方法的参数的值。(即访问修饰符和返回值可以随意修改)
5.表达式方法的抽取: @Pointcut注解
-
作用:简化代码编写
-
步骤:
随便生命一个没有实现的返回void的空方法
给方法上标注@Potintcut注解
package com.mashibing.util; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; @Component @Aspect public class LogUtil { @Pointcut("execution( public int com.mashibing.inter.MyCalculator.*(int,int))") public void myPoint(){} @Before("myPoint()") public static void start(JoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); String name = joinPoint.getSignature().getName(); System.out.println(name+"方法开始执行,参数是:"+ Arrays.asList(args)); } @AfterReturning(value = "myPoint()",returning = "result") public static void stop(JoinPoint joinPoint,Object result){ String name = joinPoint.getSignature().getName(); System.out.println(name+"方法执行完成,结果是:"+result); } @AfterThrowing(value = "myPoint()",throwing = "exception") public static void logException(JoinPoint joinPoint,Exception exception){ String name = joinPoint.getSignature().getName(); System.out.println(name+"方法出现异常:"+exception.getMessage()); } @After("myPoint()") private int end(JoinPoint joinPoint){ String name = joinPoint.getSignature().getName(); System.out.println(name+"方法执行结束了......"); return 0; } }
6.环绕通知的使用(难理解)
@Around("mypoint()")
public Object myround(ProceedingJoinPoint proceedingJoinPoint){
Object[] args = proceedingJoinPoint.getArgs();
String name = proceedingJoinPoint.getSignature().getName();
Object proceed = null;
try {
System.out.println("环绕前置通知:"+name+"方法开始,参数是"+Arrays.asList(args));
//利用反射调用目标方法,就是method.invoke()
proceed = proceedingJoinPoint.proceed(args);
System.out.println("环绕返回通知:"+name+"方法返回,返回值是"+proceed);
} catch (Throwable e) {
System.out.println("环绕异常通知"+name+"方法出现异常,异常信息是:"+e);
}finally {
System.out.println("环绕后置通知"+name+"方法结束");
}
return proceed;
}
-
总结
总结:环绕通知的执行顺序是优于普通通知的,具体的执行顺序如下:
环绕前置–>普通前置–>目标方法执行–>环绕正常结束/出现异常–>环绕后置–>普通后置–>普通返回或者异常。
但是需要注意的是,如果出现了异常,那么环绕通知会处理或者捕获异常,普通异常通知是接收不到的,因此最好的方式是在环绕异常通知中向外抛出异常。
7.多切面的运行顺序:@Order注解
- 默认运行顺序是:根据切面类的首字母进行排序
- 自定义切面运行的顺序:
- 在切面类中添加@Order(int x);x数值越小,优先级越高
4、基于AOP的配置:采用配置方式的AOP配置
AOP基于注解的配置过于麻烦,一般不采用,但是还得有基础的了解。
配置文件:
<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
">
<!--开启扫描包-->
<context:component-scan base-package="com.mashibing"></context:component-scan>
<!--开启aop注解的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--注册bean对象-->
<bean id="logUtil" class="com.mashibing.util.LogUtil2"></bean>
<bean id="securityAspect" class="com.mashibing.util.SecurityAspect"></bean>
<bean id="myCalculator" class="com.mashibing.inter.MyCalculator"></bean>
<!--配置aop-->
<aop:config>
<!--抽取切面表达式-->
<aop:pointcut id="globalPoint" expression="execution(public int com.mashibing.inter.MyCalculator.*(int,int))"/>
<aop:aspect ref="logUtil">
<aop:pointcut id="mypoint" expression="execution(public int com.mashibing.inter.MyCalculator.*(int,int))"/>
<!--确认方法什么时候执行-->
<aop:before method="start" pointcut-ref="mypoint"></aop:before>
<aop:after method="end" pointcut-ref="mypoint"></aop:after>
<aop:after-returning method="stop" pointcut-ref="mypoint" returning="result"></aop:after-returning>
<aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"></aop:after-throwing>
<!--环绕通知-->
<aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
</aop:aspect>
<aop:aspect ref="securityAspect">
<aop:before method="start" pointcut-ref="globalPoint"></aop:before>
<aop:after method="end" pointcut-ref="globalPoint"></aop:after>
<aop:after-returning method="stop" pointcut-ref="globalPoint" returning="result"></aop:after-returning>
<aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"></aop:after-throwing>
<aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
</aop:aspect>
</aop:config>
</beans>