基于 AspectJ 的 AOP 开发

AspectJ 是一个基于 Java 语言的 AOP 框架。Spring 2.0 以后新增了对 AspectJ 切点表达式支持,并且建议使用 AspectJ 方式来开发 AOP。

AspectJ 开发 AOP 有两种方式:注解方式和 XML 方式。

添加依赖

• 使用AspectJ 需要导入Spring AOP和 AspectJ相关jar包

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
46
47
48
49
50
51
52
53
54
55
56
57
<properties>
<spring.version>4.3.15.RELEASE</spring.version>
...
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>

注解方式

配置信息

使用注解方式需要开启 AspectJ 自动代理(<aop:aspectj-autoproxy/>):

1
2
3
4
5
6
7
8
9
10
<?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">

<aop:aspectj-autoproxy/>
</beans>

通知类型

@AspectJ 提供不同的通知类型:

  • @Before 前置通知,相当于BeforeAdvice;
  • @AfterReturning 后置通知,相当于AfterReturningAdvice;
  • @Around 环绕通知,相当于MethodInterceptor;
  • @AfterThrowing 异常抛出通知,相当于ThrowAdvice;
  • @After 最终final通知,不管是否异常,该通知都会执行;
  • @DeclareParents 引介通知,相当于IntroductionInterceptor。

定义切点

对于如何定义切点,在 AspectJ 中可以通过通知中的 value 属性来定义。属性值使用 execution 函数来定义切点的方法切入 。语法:

execution(<访问修饰符>?<返回类型><方法名>(<参数>)<异常>)  

问号表示可以有也可以没有。

例如:

  • 匹配所有类 public方法:execution(public * *(..))
  • 匹配指定包下所有类方法:execution(* com.imooc.dao.*(..)),不包含子包;
  • execution(* com.imooc.dao..*(..)), ..*表示包、子孙包下所有类;
  • 匹配指定类所有方法:execution(* com.imooc.service.UserService.*(..))
  • 匹配实现特定接口所有类方法:execution(* com.imooc.dao.GenericDAO+.*(..))
  • 匹配所有save开头的方法:execution(* save*(..))

案例

准备

定义一个 ProductDao 类,里面包含了五个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.imtt.aspectJ.demo1;

public class ProductDao {
public void save() {
System.out.println("保存商品");
}
public String update() {
System.out.println("修改商品");
return "ok";
}
public void delete() {
System.out.println("删除商品");
}
public void findOne() {
System.out.println("查询一个商品");
}
public void find() {
System.out.println("查询所有商品");
}
}

在配置文件中配置 Bean:

1
<bean id="productDao" class="com.imtt.aspectJ.demo1.ProductDao"/>

定义一个切面类,并在类名前加上 @Aspect 注解:

1
2
3
4
5
6
package com.imtt.aspectJ.demo1;

import org.aspectj.lang.annotation.*;

@Aspect
public class MyAspectAnn {}

在配置文件中配置 Bean:

1
<bean class="com.imtt.aspectJ.demo1.MyAspectAnn"/>

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.imtt.aspectJ.demo1;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo1 {
@Resource(name = "productDao")
private ProductDao productDao;

@Test
public void demo1() {
productDao.delete();
productDao.find();
productDao.save();
productDao.update();
productDao.findOne();
}
}

前置通知

在切面类中添加一个用于前置通知的方法,使用注解 @Before

1
2
3
4
5
6
//import org.aspectj.lang.JoinPoint;

@Before(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.save(..))")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知" + joinPoint);
}

方法中可以不传入 JoinPoint 对象,这里指示来说明一下 JoinPoint 对象 可以用来获得切点信息,即 execution 函数中的具体值。

后置通知

在切面类中添加一个用于后置通知的方法,使用注解 @AfterReturning

1
2
3
4
@AfterReturning(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.update(..))", returning = "result")
public void afterReturning(Object result) {
System.out.println("后置通知" + result);
}

可以通过 returning 属性来定义方法返回值。

环绕通知

在切面类中添加一个用于环绕通知的方法,使用注解 @Around

1
2
3
4
5
6
7
8
9
//import org.aspectj.lang.ProceedingJoinPoint;

@Around(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.delete(..))")
public Object round(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("环绕前通知");
Object o = joinPoint.proceed(); //执行目标方法
System.out.println("环绕后通知");
return o;
}

这里有两点需要说明地方:

  • 参数为 ProceedingJoinPoint 对象可以调用 peoceed() 方法来执行目标方法;如果不调用这个方法,那么目标方法就被拦截了。
  • @Around 注解的这个方法的返回值就是目标代理方法执行返回值。

异常抛出通知

在切面类中添加一个用于异常抛出通知的方法,使用注解 @AfterThrowing

1
2
3
4
@AfterThrowing(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.findOne(..))", throwing = "e")
public void afterThrowing(Throwable e) {
System.out.println("异常抛出通知" + e.getMessage());
}

如果目标发生了异常,才会使用到这个异常抛出通知。可以通过 throwing 属性来获取异常信息。

最终通知

在切面类中添加一个用于最终通知的方法,使用注解 @After

1
2
3
4
@After(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.find(..))")
public void after() {
System.out.println("最终通知");
}

无论是否出现异常,最终通知总是会被执行的。

切点命名

如果有多个通知作用在了一个方法上并且时不时要修改当前方法使用什么通知,上面的这些注解方式将会导致大量修改。可以通过 @Pointcut 为切点命名来简化操作。

  • 对于重复的切点,可以使用 @Pointcut 进行定义;
  • 切点方法:private void 无参数方法,方法名为切点名;
  • 当通知多个切点时,可以使用 || 进行连接。

举个例子,在切面类中添加一个切点方法:

1
2
@Pointcut(value = "execution(* com.imtt.aspectJ.demo1.ProductDao.save(..))")
private void myPointCut1() {}

前置通知的注解可以修改为:

1
2
3
4
@Before(value = "myPointCut1()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知" + joinPoint);
}

XML 方式

准备

新建一个配置文件:

1
2
3
4
5
6
7
8
<?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">
</beans>

定义一个接口类 CustomerDao 及其实现类 CustomerDaoImpl:

AspectJ 开发 AOP 方式与是否需要实现接口无关。

1
2
3
4
5
6
7
8
9
package com.imtt.aspectJ.demo2;

public interface CustomerDao {
public void save();
public String update();
public void delete();
public void find();
public void findOne();
}
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
package com.imtt.aspectJ.demo2;

public class CustomerDaoImpl implements CustomerDao {
@Override
public void save() {
System.out.println("客户保存");
}
@Override
public String update() {
System.out.println("客户修改");
return "ok";
}
@Override
public void delete() {
System.out.println("客户删除");
}
@Override
public void find() {
System.out.println("客户查询多个");
}
@Override
public void findOne() {
System.out.println("客户查询一个");
}
}

在配置文件中配置 Bean:

1
<bean id="customerDao" class="com.imtt.aspectJ.demo2.CustomerDaoImpl"/>

定义一个切面类 MyAspectXml,包含与五种通知相关的方法:

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
package com.imtt.aspectJ.demo2;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspectXml {
public void before(JoinPoint joinPoint) {
System.out.println("xml前置通知" + joinPoint);
}
public void afterReturning(Object obj) {
System.out.println("xml后置通知" + obj);
}
public Object round(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("xml环绕前通知");
Object o = joinPoint.proceed(); //执行目标方法
System.out.println("xml环绕后通知");
return o;
}
public void afterThrowing(Throwable e) {
System.out.println("异常抛出通知" + e.getMessage());
}
public void after() {
System.out.println("最终通知");
}
}

在配置文件中配置 Bean:

1
<bean id="myAspectXml" class="com.imtt.aspectJ.demo2.MyAspectXml"/>

单元测试代码与前面的一样。

AOP配置

1
<aop:config></aop:config>

<aop:config></aop:config> 有三种配置:aspect、advisor 和 pointcut。

aspect 可以配置多个切面多个通知,advisor 是一个切面对应一个切入点,pointcut 配置多个切入点。

配置切入点:

<aop:config></aop:config> 使用 pointcut 来配置切面:

1
2
3
4
5
<aop:pointcut id="pointCut1" expression="execution(* com.imtt.aspectJ.demo2.CustomerDao.save(..))"/>
<aop:pointcut id="pointCut2" expression="execution(* com.imtt.aspectJ.demo2.CustomerDao.update(..))"/>
<aop:pointcut id="pointCut3" expression="execution(* com.imtt.aspectJ.demo2.CustomerDao.delete(..))"/>
<aop:pointcut id="pointCut4" expression="execution(* com.imtt.aspectJ.demo2.CustomerDao.findOne(..))"/>
<aop:pointcut id="pointCut5" expression="execution(* com.imtt.aspectJ.demo2.CustomerDao.find(..))"/>

配置切面

<aop:config></aop:config> 使用 aspect 来配置切面:

1
2
3
4
5
6
7
8
9
10
11
12
<aop:aspect ref="myAspectXml">
<!--前置通知-->
<aop:before method="before" pointcut-ref="pointCut1"/>
<!--后置通知-->
<aop:after-returning method="afterReturning" pointcut-ref="pointCut2" returning="obj"/>
<!--环绕通知-->
<aop:around method="round" pointcut-ref="pointCut3"/>
<!--异常抛出通知-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut4" throwing="e"/>
<!--最终通知-->
<aop:after method="after" pointcut-ref="pointCut5"/>
</aop:aspect>

配置了五种切面,通过 pointcut-ref 属性对应切入点。

坚持原创技术分享,您的支持将鼓励我继续创作!