在实际开发中,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-core
、spring-beans
、spring-context
、spring-expression
这四个核心包不能缺少。与此同时进行AOP开发,aop联盟的包和Spring的aop包也要引入:aopalliance
和spring-aop
。由于使用aspectJ,故需引入aspectJ框架的包aspectjweaver
和Spring的aspects包spring-aspects
。为了便于测试需要引入spring-test
和junit
包(注意如果想使用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方式优点就是配置相对集中,不需要修改代码,缺点就是开发较为繁琐。因此这两种方式的选用需要结合实际情况进行挑选。