Spring AOP 基于AspectJ
简介
AspectJ是一个基于Java语言的AOP框架,Spring2.0以后新增了对AspectJ切点表达式支持。因为Spring1.0的时候Aspectj还未出现;
AspectJ1.5中新增了对注解的支持,允许直接在Bean类中定义切面。新版本的Spring框架建
议我们都使用AspectJ方式来开发AOP,并提供了非常灵活且强大的切点表达式 ;
当然无论使用Spring自己的AOP还是AspectJ相关的概念都是相同的;
注解配置
依赖导入:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.2.RELEASE</version> </dependency>
通知类型
@AspectJ提供的通知类型:
@Before 前置通知 在原始方法执行前执行
@AfterReturning 后置通知 在原始方法执行前执行
@Around 环绕通知 彻底拦截原始方法的执行,执行前后都可以增加逻辑,也可以不执行原始方法
@AfterThrowing抛出通知,执行原始方法出现异常时执行
@After 最终final通知,不管是否异常,原始方法调用后都会执行
@DeclareParents 引介通知,相当于IntroductionInterceptor (了解即可)
定义切点
通过execution函数来定义切点
语法:execution(访问修饰符 返回类型 方法名 参数 异常)
表达式示例:
匹配所有类public方法:execution(public * *(..))
第一个*表示返回值 ..表示任意个任意类型参数
匹配指定包下所有方法: execution(* cn.xxx.dao.*(..))
第一个想*表示忽略权限和返回值类型
匹配指定包下所有方法:execution(* cn.xxx.dao..*(..))
包含子包
匹配指定类所有方法: execution(* cn.xxx.service.UserService.*(..))
匹配实现特定接口所有类方法 : execution(* cn.xxx.dao.GenericDAO+.*(..))
匹配所有save开头的方法: execution(* save*(..))
前置通知
pom依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.2.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.2.RELEASE</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.2.RELEASE</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> </dependencies> </project>
xml需要添加aop名称空间及xsd:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> <!-- 启用aspectj --> <aop:aspectj-autoproxy/> <!-- 目标--> <bean id="personDao" class="com.yh.demo1.PersonDao"/> <!-- 切面--> <bean class="com.yh.demo1.MyAspect"/> </beans>
test:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class Test1 { @Autowired PersonDao personDao; @Test public void test(){ personDao.delete(); personDao.update(); } }
切面类:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class MyAspect { //表示PersonDao下所有方法都作为切点 @Before(value = "execution(* com.yh.demo1.PersonDao.*(..))") public void beforeAdvice(){ System.out.println("before code run....."); } }
当我们需要获取切点信息(被增强的代码)时,可以在通知添加参数,想下面这样
@Aspect public class MyAspect { @Before(value = "execution(* com.yh.demo1.PersonDao.*(..))") public void beforeAdvice2(JoinPoint point){ System.out.println("before code run2....." + point); } }
后置通知:
//当需要获取原始方法的返回值时可以在注解中添加returning参数来指定参数名 Aspectj会自动将返回值放到参数中 @AfterReturning(value = "execution(* com.yh.demo1.PersonDao.delete(..))",returning = "result") public void afterAdvice(Object result){ System.out.println("删除方法执行后 ..... 返回值为:"+ result); }
后置通知可以获取目标方法的返回值
环绕通知:
@Around(value = "execution(* com.yh.demo1.PersonDao.insert(..))") public void aroundAdvice(ProceedingJoinPoint point) throws Throwable { //code............ System.out.println("环绕前置.."); //执行原始方法 __当需要获取返回值时可以声明变量接收 Object result = point.proceed(); System.out.println("原始方法返回值: "+result); //code............ System.out.println("环绕后置.."); }
环绕通知与其他通知最大的区别在于环绕通知可以控制是否调用原始方法
注意:参数类型必须为ProceedingJoinPoint,否则 无法执行原始方法,
异常通知
@AfterThrowing(value = "execution(* com.yh.demo1.PersonDao.save(..))",throwing = "e") public void exceptionHandler(JoinPoint point,Exception e){ System.out.println(point + " 方法出现"+e.getMessage()+"异常"); }
当方法中出现时才会执行该通知,若需要获取异常信息,可在注解中添加throwing指定参数名称
我们可以使用环绕+异常通知来处理数据库事务,在环绕中开启事务以及提交事务,异常通知中回滚事务,当然Spring已经对事务进行了封装不需要自己写
最终通知
@After(value = "execution(* *delete(..))") public void afterRun(){ System.out.println("最终"); }
最终通知叫做after 即调用原始方法之后执行无论原始方法中是否出现异常
而后置叫做afterReturning表示在成功返回后才会执行执行
带有逻辑符的表达式:
在表达式中可以使用户逻辑操运算符,与&&
或||
非!
示例:
/* execution(* cn.xxx.service.UserDao.insert(..))||execution(* cn.xxx.service.UserDao.delete(..)) execution(* cn.xxx.service.UserDao.*nsert(..))&&execution(* cn.xxx.service.UserDao.inser*(..)) !execution(* cn.xxx.service.UserDao.insert(..)) */
切点命名
假设有多个通知应用在同一个切点上时,我们需要重复编写execution表达式,且后续要修改切点时则多个通知都需要修改,维护起来非常麻烦,我们可以通过给切点指定名称从而完成对切点的重复使用和统一操作,以提高开发维护效率;
//定义命名切点 方法名称即切点名称 @Pointcut(value = "execution(* com.yh.demo1.PersonDao.save(..))") private void savePointcut(){} @Pointcut(value = "execution(* com.yh.demo1.PersonDao.delete(..))") private void deletePointcut(){}
多个通知应用到同一个切点:
//使用命名切点 @Before(value = "savePointcut()") public void beforeAdvice(){ System.out.println("before code run....."); } //使用命名切点 @Around(value = "savePointcut()") public void beforeAdvice2(ProceedingJoinPoint point) throws Throwable { System.out.println("环绕前"); point.proceed(); System.out.println("环绕后");
一个通知应用到多个切点
//同一个通知对应多个切点 @After(value = "savePointcut()||deletePointcut()") public void afterAdvice(){ System.out.println("after code run....."); }
XML配置
XML配置所需的jar 以及各个对象之间的依赖关以及表达式的写法都是一样的,仅仅是换种方式来写而已;
xml:
<!--目标--> <bean id="studentDao" class="com.yh.demo2.StudentDao"/> <!--通知--> <bean id="advices" class="com.yh.demo2.XMLAdvice"/> <!--织入信息--> <aop:config> <!--切点定义--> <aop:pointcut id="select" expression="execution(* com.yh.demo2.StudentDao.select(..))"/> <!--切面定义--> <aop:aspect ref="advices"> <aop:before method="before" pointcut-ref="select"/> <aop:after-returning method="afterReturning" pointcut-ref="select" returning="result"/> <aop:after method="after" pointcut-ref="select" /> <aop:after-throwing method="exception" pointcut-ref="select" throwing="e"/> <aop:around method="around" pointcut-ref="select"/> </aop:aspect> <!--入侵式通知 即通知需要实现指定接口 两种不能同时使用 --> <aop:advisor advice-ref="advice2" pointcut-ref="select"/> </aop:config> <!--入侵式通知Bean--> <bean id="advice2" class="com.yh.demo2.XMLAdvice2"/>
通知类:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.JoinPoint; public class XMLAdvice { public void before(JoinPoint pointcut){ System.out.println("前置通知 切点:"+pointcut); } public void afterReturning(JoinPoint point,Object result){ System.out.println("后置通知 切点:"+point); } public void after(JoinPoint point){ System.out.println("最终通知 切点:"+point); } public void exception(JoinPoint point,Throwable e){ System.out.println("异常通知: " + e+"切点:"+point); } public void around(ProceedingJoinPoint point) throws Throwable { System.out.println("环绕前"); point.proceed(); System.out.println("环绕后"); } }
你会发现 ,无论是XML还是注解都不需要手动指定代理,以及目标对象,Aspectj会从切点中获取目标对象信息并自动创建代理;
AspectJ是目前更流行的方式,具体采用XML还是注解需要根据项目具体情况,小组协作开发推荐xml;