在实际开发中,Spring最常用到的是基于AspectJ的AOP开发方式。而AspectJ的使用通常有两种方式:注解方式和xml配置方式。

AspectJ简述

AspectJ是一个基于Java语言的AOP框架,在此之前都是使用传统的AOP开发,AspectJ是一个独立的AOP框架,由于它的方便效率,Spring2.0以后新增了对AspectJ切点表达式支持。

@AspectJ是AspectJ1.5新增的功能,通过JDK5注解的方式,允许直接在Bean类中定义切面。新版本的Spring框架建议使用AspectJ方式来进行开发AOP。

使用AspectJ需要导入Spring AOP和AspectJ相关的jar包,如spring-aop、aopalliance、aspects以及aspectj.weaver等。

基于注解方式的AspectJ开发AOP

第一步:新建一个webapp,名称为spring_aspectJ,接着在里面新建一个java源文件。

第二步:导入相应的包。首先在pom.xml文件中引入spring的基本开发包以及AspectJ开发包:

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
58
59
60
61
<!--Spring开发的基本包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<!--传统的AOP开发需要引入的两个包-->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

<!--AspectJ AOP开发需要引入的两个包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!--测试包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>

这里使用Spring进行开发,那么spring-corespring-beansspring-contextspring-expression这四个核心包不能缺少。与此同时进行AOP开发,aop联盟的包和Spring的aop包也要引入:aopalliancespring-aop。由于使用aspectJ,故需引入aspectJ框架的包aspectjweaver和Spring的aspects包spring-aspects。为了便于测试需要引入spring-testjunit包(注意如果想使用SpringJUnit4ClassRunner,则junit的版本必须是4.12及以上)。特别注意spring四个核心包及spring-test包的版本必须保持一致。

第三步:配置applicationContext文件。在resources下面新建一个applicationContext.xml文件。既然进行aop开发,因此配置文件中需要有aop相关的约束,新创建的applicationContext.xml文件是没有的,因此需要到spring-framework-4.2.0.RELEASE-dist\spring-framework-4.2.0.RELEASE\docs\spring-framework-reference\html\xsd-configuration.html页面中进行查找(此处是Spring的源码文件),找到40.2.7 the aop schema处的约束:

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">
<!-- bean definitions here -->
</beans>

修改配置文件中的代码为:

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"> <!-- bean definitions here -->

<!--开启AspectJ的注解开发,自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

这样就搭建完基于AspectJ的AOP开发环境了。

@AspectJ提供了不同类型的增强处理如下所示:@Before:前置通知,相当于BeforeAdvice;@AfterReturning后置通知,相当于AfterReturningAdvice;@Around环绕通知,相当于MethodInterceptor;@AfterThrowing异常抛出通知,相当于ThrowAdvice,@After最终通知,不管是否异常,该通知都会执行。还有一个较为复杂的引介通知@DeclareParents,它其实就相当于IntroductionInterceptor。

切入点表达式定义

你可以在通知中通过value属性来定义切点,此处是使用execution函数来定义切点。语法为:execution(<访问修饰符>?<返回类型><方法名>(<参数>)<异常>)。这个注解定义了增强处理的类型,还定义了作用到哪个类的哪个方法上,结合(*)的使用可以使其变得很灵活。

举几个例子,如匹配所有类public方法,此时为execution(public * *(..));匹配指定包下所有类的方法,此时为execution(* com.envy.dao.*(..))注意是不包含子包的,如果想表示包及子孙包下所有的类此时应当execution(*com.envy.dao..*(..));匹配指定类所有方法,此时为execution(*com.envy.service.UserService.*(..));匹配实现特定接口所有类方法,此时为execution(*com.envy.dao.UserDao+.*(..));匹配所有save开头的方法,此时为execution(*save*(..))

接下来进入正式的开发。首先要有目标类,这里提供一个ProductDao类(为了简单此处并没有实现接口):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ProductDao {
public void save(){
System.out.println("保存商品");
}

public void update(){
System.out.println("修改商品");
}

public void delete(){
System.out.println("删除商品");
}

public void findOne(){
System.out.println("查询某一个商品");
}

public void findAll(){
System.out.println("查询所有商品");
}
}

然后就是去applicationContext.xml文件中对Bean进行管理,此处使用xml配置,当然使用注解也是可以的:

1
2
<!--目标类-->
<bean id="productDao" class="com.envy.aspectJ.demo1.ProductDao"/>

接着就是需要去定义一个切面类,名称为AspectJByAnno(使用@AspectJ来定义切面类),里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
* 切面类
**/
@Aspect
public class AspectJByAnno {

//前置通知类型
@Before(value="execution(* com.envy.aspectJ.demo1.ProductDao.*(..))")
public void before(){
System.out.println("前置通知");
}
}

当然别忘了去applicationContext.xml文件中对AspectJByAnno进行管理:

1
2
<!--切面类-->
<bean class="com.envy.aspectJ.demo1.AspectJByAnno"/>

接着创建一个名为SpringTest的类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 SpringTest {

@Resource(name="productDao")
private ProductDao productDao;

@Test
public void testOne(){
productDao.save();
productDao.delete();
productDao.update();
productDao.findAll();
productDao.findOne();
}
}

运行结果为:

1
2
3
4
5
6
7
8
9
10
前置通知
保存商品
前置通知
删除商品
前置通知
修改商品
前置通知
查询所有商品
前置通知
查询某一个商品

所有方法均前置通知了,若只需前置特定方法只需修改@Before(value="execution(* com.envy.aspectJ.demo1.ProductDao.*(..))")中的代码。

前置通知

@Before注解,可以在注解的方法中传入JointPoint对象,该对象用于注解目标类中的方法信息,即连接点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
* 切面类
**/
@Aspect
public class AspectJByAnno {

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

运行结果:

1
2
3
4
5
6
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
删除商品
修改商品
查询所有商品
查询某一个商品

可以很清楚的知道增强的具体是哪个方法。

后置通知

@AfterReturning注解,可以定义方法的返回值作为参数,也可以传入JointPoint对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
--------------ProductDao.java---------------

public void update(){
System.out.println("修改商品");
}

------------AspectJByAnno.java----------------

//后置通知类型
@AfterReturning(value="execution(* com.envy.aspectJ.demo1.ProductDao.update(..))")
public void afterReturning(){
System.out.println("后置通知");
}

运行结果:

1
2
3
4
5
6
7
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
后置通知
删除商品
修改商品
查询所有商品
查询某一个商品

接下来介绍如果待增强的方法需要返回值,此时依旧可以这样操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--------------ProductDao.java---------------

public String update(){
System.out.println("修改商品");
return "hello,此处已经修改了商品";
}

------------AspectJByAnno.java----------------

//后置通知类型
@AfterReturning(value="execution(* com.envy.aspectJ.demo1.ProductDao.update(..))", returning = "return")
public void afterReturning(Object return){
System.out.println("后置通知"+return);
}

运行结果:

1
2
3
4
5
6
7
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
删除商品
修改商品
后置通知hello,此处已经修改了商品
查询所有商品
查询某一个商品

注意上面returning的值,里面的return字符串可以随意取值,但是必须和下面的Object return保持一致。

环绕通知

@Around注解,此注解的方法要有返回值(返回值就是目标代理方法的返回值,一般使用Object作为返回值),同时需要传入参数:ProceedingJointPoing proceedingJoinPoint

1
2
3
4
5
6
7
8
9
10
------------AspectJByAnno.java----------------

//环绕通知类型
@Around(value="execution(* com.envy.aspectJ.demo1.ProductDao.delete(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("****环绕前通知****");
Object object = proceedingJoinPoint.proceed();
System.out.println("****环绕后通知****");
return object;
}

运行结果:

1
2
3
4
5
6
7
8
9
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
****环绕前通知****
删除商品
****环绕后通知****
修改商品
后置通知hello,此处已经修改了商品
查询所有商品
查询某一个商品

注意该注解还可以拦截目标代理方法的执行。如果你想目标方法被拦截,只需不调用ProceedingJointPoing的proceed方法即可。尝试注释掉Object object = proceedingJoinPoint.proceed();且将around返回类型修改为void后,再次运行一下:

1
2
3
4
5
6
7
8
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
****环绕前通知****
****环绕后通知****
修改商品
后置通知hello,此处已经修改了商品
查询所有商品
查询某一个商品

异常抛出通知

@AfterThrowing注解,此注解在代理方法抛出异常时将会执行(注意没有异常时不会发生),还可以通过设置throwing属性用于设置发生异常对象参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--------------ProductDao.java---------------

public void findOne(){
System.out.println("查询某一个商品");
int i =1/0;
}

------------AspectJByAnno.java----------------

//异常抛出通知类型
@AfterThrowing(value="execution(* com.envy.aspectJ.demo1.ProductDao.findOne(..))",throwing = "e")
public void afterThrowing(Throwable e){
System.out.println("****异常抛出****"+e);
}

运行结果:

1
2
3
4
5
6
7
8
9
10
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
****环绕前通知****
删除商品
****环绕后通知****
修改商品
后置通知hello,此处已经修改了商品
查询所有商品
查询某一个商品
****异常抛出****java.lang.ArithmeticException: / by zero

最终通知

@After注解,此注解不管目标方法中是否发生异常,该通知都会被执行。

1
2
3
4
5
6
7
------------AspectJByAnno.java----------------

//最终通知类型
@After(value="execution(* com.envy.aspectJ.demo1.ProductDao.findAll(..))")
public void after(){
System.out.println("****最终通知****");
}

运行结果:

1
2
3
4
5
6
7
8
9
10
前置通知execution(void com.envy.aspectJ.demo1.ProductDao.save())
保存商品
****环绕前通知****
删除商品
****环绕后通知****
修改商品
后置通知hello,此处已经修改了商品
查询所有商品
****最终通知****
查询某一个商品

定义切入点

在每个通知内定义切点会造成工作量大,不易维护,对于重复的切点可以使用@Pointcut进行定义。切点方法为:private void 无参数方法,方法名即为切入点名。注意当通知多个切点时,可以使用||进行连接。

1
2
3
4
5
6
7
8
9
10
------------AspectJByAnno.java----------------
//先定义切点
@Pointcut(value = "execution(* com.envy.aspectJ.demo1.ProductDao.save(..))")
private void mySave(){};

//前置通知使用上述切点
@Before(value="mySave()")
public void before(JoinPoint joinPoint){
System.out.println("前置通知"+joinPoint);
}

定义切入点具体操作为:在切面类中随意建一个方法,该方法没有实际意义,用来写切入点表达式。在上面代码中mySave方法指向了com.envy.aspectJ.demo1.ProductDao.save(..))方法。当前置通知需要指向这个方法时,只需value值写此方法即可。

基于XML方式的AspectJ开发AOP

第一二三步类似,注意第三步配置applicationContext2.xml文件:

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"> <!-- bean definitions here -->

<!-- XML配置方式完成AOP开发 -->

</beans>

第四步,新建一个名为com.envy.aspectJ.demo2的包,并在该包中新建一个接口CustomDao

1
2
3
4
5
6
7
public interface CustomDao {
public void save();
public void delete();
public void update();
public void findOne();
public void findAll();
}

同时新建一个该接口的实现类CustomDaoImpl:

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
public class CustomDaoImpl implements CustomDao {
@Override
public void save() {
System.out.println("**保存客户信息**");
}

@Override
public void delete() {
System.out.println("**删除客户信息**");
}

@Override
public void update() {
System.out.println("**修改客户信息**");
}

@Override
public void findOne() {
System.out.println("**查询某位客户信息**");
}

@Override
public void findAll() {
System.out.println("**查询所有客户信息**");
}
}

回到配置文件中,将目标类CustomDaoImpl进行管理:

1
2
<!--目标类-->
<bean id="customDaoImpl" class="com.envy.aspectJ.demo2.CustomDaoImpl"/>

接着定义一个切面类AspectJByXml:

1
2
3
4
//前置通知
public void beforeXMl(){
System.out.println("xml方式的前置通知");
};

接着回到配置文件中,开始配置切面类及AOP配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--配置切面类-->
<bean id="aspectJByXml" class="com.envy.aspectJ.demo2.AspectJByXml"/>

<!--AOP配置-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pointcut1" expression="execution(* com.envy.aspectJ.demo2.CustomDao.save(..))"/>
<!--配置AOP的切面-->
<aop:aspect ref="aspectJByXml">
<!--配置前置通知-->
<aop:before method="beforeXMl" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>

此处是以配置前置通知为例进行介绍说明:aop:pointcut标签用于配置切入点,id为切入点id,expression是切点表达式;aop:aspect标签用于配置AOP的切面(一般是多个切入点和多个通知的组合,而aop:advisor通常是一个切入点和一个通知的组合)ref是切面类;而aop:before用于配置前置通知,method是AspectJByXml切面类中配置的前置通知方法的名称, pointcut-ref是选择切入点。

由于xml方式和注解方式最大的不同则是xml文件,因此这里就不过多介绍,直接附上每一个通知的代码。AspectJByXml文件:

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
public class AspectJByXml {
//前置通知
public void beforeXMl(JoinPoint joinPoint){
System.out.println("xml方式的前置通知"+joinPoint);
};

//后置通知
public String afterReturningXMl(Object object){
System.out.println("xml方式的后置通知");
return "********后置通知******"+object;
};

//环绕通知
public Object aroundXML(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("xml方式的环绕前通知");
Object obj=proceedingJoinPoint.proceed();
System.out.println("xml方式的环绕后通知");
return obj;
}

//异常抛出通知
public void afterThrowingXML(Throwable e){
System.out.println("****xml方式的异常抛出****"+e);
}

//最终通知
public void afterXML(){
System.out.println("xml方式的最后通知");
}

}

ApplicationContext2.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
33
34
35
36
37
38
39
40
<?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 definitions here -->

<!-- XML配置方式完成AOP开发 -->

<!--配置目标类-->
<bean id="customDaoImpl" class="com.envy.aspectJ.demo2.CustomDaoImpl"/>

<!--配置切面类-->
<bean id="aspectJByXml" class="com.envy.aspectJ.demo2.AspectJByXml"/>

<!--AOP配置-->
<aop:config>
<!--配置切入点(哪些类的哪些方法需要增强)-->
<aop:pointcut id="pointcut1" expression="execution(* com.envy.aspectJ.demo2.CustomDao.save(..))"/>
<aop:pointcut id="pointcut2" expression="execution(* com.envy.aspectJ.demo2.CustomDao.delete(..))"/>
<aop:pointcut id="pointcut3" expression="execution(* com.envy.aspectJ.demo2.CustomDao.update(..))"/>
<aop:pointcut id="pointcut4" expression="execution(* com.envy.aspectJ.demo2.CustomDao.findOne(..))"/>
<aop:pointcut id="pointcut5" expression="execution(* com.envy.aspectJ.demo2.CustomDao.findAll(..))"/>


<!--配置AOP的切面-->
<aop:aspect ref="aspectJByXml">
<!--配置前置通知-->
<aop:before method="beforeXMl" pointcut-ref="pointcut1"/>
<!--配置后置通知-->
<aop:after-returning method="afterReturningXMl" pointcut-ref="pointcut2" returning="object"/>
<!--配置环绕通知-->
<aop:around method="aroundXML" pointcut-ref="pointcut3"/>
<!--配置异常抛出通知-->
<aop:after-throwing method="afterThrowingXML" pointcut-ref="pointcut4" throwing="e"/>
<!--配置最终通知-->
<aop:after method="afterXML" pointcut-ref="pointcut5"/>
</aop:aspect>
</aop:config>
</beans>

对比

其实无论是注解方式还是XML配置方式,它们各有优缺点。注解方式优点就是开发便捷,缺点就是在对其进行维护时需要修改代码;XML方式优点就是配置相对集中,不需要修改代码,缺点就是开发较为繁琐。因此这两种方式的选用需要结合实际情况进行挑选。