Spring-Aop基础知识

1 AOP概念和相关术语

Aspect Oriented Programming(AOP):面向切面编程,是面向对象编程(OOP)的一种补充,典型的就是Spring的事务管理。将比如,日志的记录,权限的校验,异常的处理这样非核心功能单独抽离出来,和核心功能解耦,横切在业务代码之上。

相关术语

  • Aspect

    切面,在spring的aop里面,切面通常是一个类,对横切关注点的抽象

  • Join point

    连接点,就是被拦截的点,简单来说,就是声明在什么时候,因为spring只支持方法的拦截,所以在spring中连接点可以理解为被拦截的方法

  • Pointcut

    切入点,也可以叫切点,一般配合切点表达式定位到哪些方法,哪些类,什么地方,是对连接点拦截的定义,可以拦截一个连接点,也可以拦截多个连接点

  • Advice

    通知,就是和Join point一起要做的事情,简单来说就是做什么事,就是拦截到连接点后,需要在这个连接点做什么事情

  • Weaving

    织入,就是将切面中的通知应用到目标对象(Target Object)上,生成代理对象(AOP Proxy)的过程

2 基于XML配置

使用XML配置文件使用aop,需要加入一下xml约束

1
2
3
<beans xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

注意:需要和之前的spring配置文件合在一起,需要bean的约束文件。

配置好以上的文件后,就可以在spring配置文件中使用aop了,所有的aop配置,都是在aop:config标签下,首先需要把代理目标类和增强类在ioc中注册,然后在aop中配置bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="coreService" class="com.moon.point.CoreService"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>
<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..*.*(..))"/>
<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置通知方法-->
<aop:before pointcut-ref="point" method="logExecuteTime"/>
</aop:aspect>
</aop:config>
</beans>

3 切点表达式

切点表达式,就是去匹配需要代理的类的方法,可以匹配多个或者单个方法,切点表达式书写在aop切点配置中

1
2
<!--配置aop切点 id:切点id  expression为切点表达式:表达式在execution()里书写-->
<aop:pointcut id="point" expression="execution(public * com.moon..*.*(..))"/>

正常方法签名对应的切点表达式

方法签名 public void e()
切点表达式 public void com.moon.point.CoreService.e(..)

涉及到的通配符

  • *:代表所有
  • ..:代表类,参数

常用的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* expression="execution(public * com.moon.point.CoreService.*(..))
该切点表示切如的方法为
修饰符 返回值 方法名()
public void com.moon.point.CoreService.e(..)
public * com.moon.point.CoreService.*(..)
* com.moon.*.*.*(..)
*/

//所有类型 所有返回值 com包下moon包下point包下CoreService的全部方法,方法的参数为任意
public * com.moon.point.CoreService.*(..)

//所有类型,所有返回值 com包下moon包下全部的包和类的全部方法,方法的参数为任意
* com.moon..*.*(..)

//所有类型,所有返回值 com包下moon包下全部的包和类的e方法,方法的参数为任意
* com.moon..*.e(..)

4 连接点

连接点指的就是根据切点拦截到的具体方法,因为spring的aop是方法级别,所以这里的连接点全部指的是具体的方法。

如果我们想要获取到连接点的话,我们需要导入另外一个依赖-AspectJ

4.1 AspectJ

aspectJ全名是Eclipse AspectJ,它来自于Eclipse基金会,是一个完整的aop方案框架,它可以拦截到的连接点就不是单纯的方法级别了,它全部都可以拦截,他和spring的区别是:

  • spring是方法级别,aspectJ是全级别

  • spring是动态织入,aspectJ是静态织入

    Spring使用JDK动态代理和Cglib动态代理,通过运行时在内存中生成零时的类文件来实现。

    AspectJ则是通过它自己的编译器进行编译,在编译时直接修改了编的二进制class文件

  • spring性能慢,AspectJ性能快

导入AspectJ依赖

Maven

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

Gradle

1
implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.6'

4.2 JoinPoint

依赖导好后,我们在自己的书写方法中就可以拿到连接点,JoinPoint连接点是所有切面方法都可以拿到的,在里面封装的我们方法的信息。

拿到连接点:

1
public void before(JoinPoint joinPoint){...}

操作连接点:

1
2
3
4
5
6
public void before(JoinPoint joinPoint){
System.out.println("目标对象为:"+joinPoint.getTarget());
System.out.println("目标方法签名为:"+joinPoint.getSignature());
Object[] args = joinPoint.getArgs();
System.out.println("目标方法参数个数为:"+args.length);
}

连接点方法:

getSignature() 获得目标对象的签名,就是当前拦截到的方法
getTarget() 获得目标对象
getThis() 获得正在执行的目标对象
getStaticPart() 获得此连接点的静态部分
getSourceLocation() 获得连接点对应的方法源地址,这里面的还包含的方法是使用不了的,Spring在重写时,直接抛出了不支持操作异常(UnsupportedOperationException)
toLongString() 获得连接点的扩展全命名
toShortString() 获得连接点的缩写命名
getKind() 返回该连接点的类型
getArgs() 返回连接点的参数数组

4.3 ProceedingJoinPoint

该连接点是上面连接点的加强版,但是只有环绕通知才可以获取到,它主要是可以操作连接点方法的运行。

获得ProceedingJoinPoint

1
public void before(ProceedingJoinPoint joinPoint){...}

操作连接点方法:proceed()

这也是这个连接点的核心,需要手动操作拦截的连接点方法,默认不执行

1
2
3
4
5
6
7
8
9
10
public void before(ProceedingJoinPoint joinPoint){
try {
// 执行连接点方法
Object proceed = joinPoint.proceed();
} catch (Throwable throwable) {

// 为了避免事务失效,所以要将异常跑出去ΩΩ
throw throwable;
}
}

5 通知的类型和使用

5.1 Before

前置通知,该通知是在连接点之前实现的,增强了连接点的前置方法,在前置方法中,我们可以拿到这个连接点,但是不可以对原来连接点方法的执行进行组织。

书写一个切面类,里面书写一个前置方法,在这个方法中我们拿到连接点

1
2
3
4
5
6
public class MyAspect {
// 前置方法
public void before(JoinPoint joinPoint) throws Throwable {
System.out.println("前置方法执行了,准备执行"+joinPoint.getSignature());
}
}

配置配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>

<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..Man.*(..))"/>

<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置前置通知,切面的方法 切点-->
<aop:before method="before" pointcut-ref="point" />
</aop:aspect>
</aop:config>
</beans>

结果:

5.2 After Advice

after后置方法是分为三类,主要是针对方法执行后三种状态,分别可以获得结果,捕获异常,finally执行。

在AfterAdvice中,同样也可以获得连接点JoinPoint

5.2.1 After-finally

我们使用的afterAdvice其实就是After-finally,是放在finally块中,不管方法执行结果是什么,都会执行

在After-finally中,同样也可以获得连接点JoinPoint

切面:

1
2
3
4
5
6
7
8
9
10
11
public class MyAspect {
// 前置方法
public void before(JoinPoint joinPoint) throws Throwable {
System.out.println("前置方法执行了,准备执行"+joinPoint.getSignature());
}

// 后置方法
public void after(JoinPoint joinPoint) throws Throwable {
System.out.println("后置方法执行了,执行完了"+joinPoint.getSignature());
}
}

XML配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>

<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..Man.*(..))"/>

<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置前置通知,切面的方法 切点-->
<aop:before method="before" pointcut-ref="point" />
<!--配置后置通知,切面方法 切点-->
<aop:after method="after" pointcut-ref="point" />
</aop:aspect>
</aop:config>
</beans>

结果:

5.2.2 After-returning

AfterReturning是放在连接点执行后面,可以获得连接点执行完返回的值,同样也可以拿到连接点JoinPoint

这里我们直接拿返回的值,就不拿连接点

切面:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyAspect {
// 前置方法
public void before(JoinPoint joinPoint) throws Throwable {
System.out.println("前置方法执行了,准备执行"+joinPoint.getSignature());
}

// 后置返回方法
public void afterReturning(Object result){
System.out.println("后置返回方法执行");
System.out.println("返回值为:"+result);
}
}

XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>

<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..Man.*(..))"/>

<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置前置通知,切面的方法 切点-->
<aop:before method="before" pointcut-ref="point" />
<!--配置后置返回通知,切面方法、切点、返回值接收对象-->
<aop:after-returning method="afterReturning"
pointcut-ref="point"
returning="result"/>
</aop:aspect>
</aop:config>
</beans>

结果:

这里的eat方法没有设置返回值,所以返回值为null

5.2.3 After-throwing

后置异常通知,顾名思义,就是在连接点方法发生异常的时候,才会执行,其实这个通知是放在catch块里面的,用来捕获异常。

注意:

  • 异常通知和返回结果通知是互斥的,俩者只有一个会被执行,如果出现异常就拿不到返回值,拿到返回值就说明没有异常。
  • 正常的后置通知,是放在finally块中,所以一定会被执行。

切面:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyAspect {
// 前置方法
public void before(JoinPoint joinPoint) throws Throwable {
System.out.println("前置方法执行了,准备执行"+joinPoint.getSignature());
}

// 后置异常方法
public void afterThrowing(Exception e){
System.out.println("后置异常方法执行");
System.out.println("异常信息为:"+e.getMessage());
}
}

XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>

<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..Man.*(..))"/>

<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置前置通知,切面的方法 切点-->
<aop:before method="before" pointcut-ref="point" />
<!--配置后置返回通知,切面方法
切点
异常接收对象-->
<aop:after-throwing method="afterThrowing"
pointcut-ref="point"
throwing="e"/>
</aop:aspect>
</aop:config>
</beans>

这里对man类,代理的目标类进行修改,抛一个异常

1
2
3
4
5
6
7
public class Man implements Person{
@Override
public void eat() {
System.out.println("吃饭");
throw new RuntimeException("它没异常,我制造异常,哎,就是玩");
}
}

结果:

5.3 Around Advice

AroundAdvice,环绕通知,该通知的特点是可以自定义代理,很类似一个代理类,把连接点方法直接给我们,我们可以选择执行或者不执行,默认是不执行的,这里我们拿的连接点就需要是ProceedingJoinPoint,该连接点仅支持环绕通知。

切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyAspect {
// 环绕通知方法
public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
try {
System.out.println("前置通知");
// 通过proceed控制目标方法的执行
Object proceed = joinPoint.proceed();
//返回值后置通知
System.out.println("返回值是"+proceed+",返回值后置通知");
return proceed;
} catch (Throwable throwable) {
System.out.println("异常通知:异常信息为:"+throwable.getMessage());
// 为了避免事务失效,所以要将异常跑出去ΩΩ
throw throwable;
} finally {
System.out.println("后置通知");
System.out.println("");
}
}
}

XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>
<!--切面类-->
<bean id="aspect" class="com.moon.aspect.MyAspect"/>
<!--配置aop-->
<aop:config>

<!--配置aop切点-->
<aop:pointcut id="point" expression="execution(public * com.moon..Man.*(..))"/>

<!--配置切面切入-->
<aop:aspect ref="aspect">
<!--配置环绕通知-->
<aop:around method="around" pointcut-ref="point"/>
</aop:aspect>
</aop:config>
</beans>

结果:

5.4 执行顺序

6 基于注解的AOP配置

6.1 XML配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!--需要被代理的类-->
<bean id="Man" class="com.moon.point.Man"/>

<!--开启组件扫描-->
<context:component-scan base-package="com.moon"/>
<!--开启aop注解模式-->
<aop:aspectj-autoproxy/>
</beans>

6.2 前后置通知

使用java类来配置一个切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 加入组件,交给ioc管理
@Component
// 声明是一个切面类
@Aspect
public class AnnotationAspect {
/**
* 配置切入点
*/
@Pointcut("execution(* com.moon.point.Man.*(..))")
private void pointCut(){}

/**
* 前置通知,将前面配置的切入点引入
*/
@Before("pointCut()")
public void beforeAdvice(){
System.out.println("This is beforeAdvice!");
}

/**
* 后置通知,将前面配置的切入点引入
*/
@After("pointCut()")
public void afterAdvice(){
System.out.println("This is afterAdvice!");
}

/**
* 后置返回通知,可以指定返回的参数的接收值
* @param result 返回参数接收
*/
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturning(Object result){
System.out.println("This is afterReturning! Result is "+result);
}
/**
* 后置异常通知,可以指定异常参数的接收
* @param e 异常接收
*/
@AfterThrowing(value = "pointCut()",throwing = "e")
public void afterThrowing(Exception e){
System.out.println("This is afterReturning! Exception is "+e.getMessage());
}
}

结果:

6.3 环绕通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 加入组件,交给ioc管理
@Component
// 声明是一个切面类
@Aspect
public class AnnotationAspect {
/**
* 配置切入点
*/
@Pointcut("execution(* com.moon.point.Man.*(..))")
private void pointCut(){}

/**
* 环绕通知配置,引入切点
* @param point 连接点
* @return 执行后返回值
* @throws Throwable 方法执行异常
*/
@Around("pointCut()")
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable {
try {
// 前置通知
System.out.println("This is around-beforeAdvice");
// 执行方法,拿到返回值
Object proceed = point.proceed();
// 后置返回值方法
System.out.println("This is around-afterReturningAdvice! result is " + proceed);
return proceed;
} catch (Throwable e) {
// 后置异常方法
System.out.println("This is around-afterThrowingAdvice! Exception Message is " + e.getMessage());
throw e;
} finally {
// 后置finally方法
System.out.println("This is around-afterFinallyAdvice!");
}
}
}

结果: