Spring(五)AspectJ
AspectJ是一个Java语言实现的AOP框架。
支持注解配置和xml配置。
1.注解配置
还是三部曲:导包,写配置,测试。
(1)jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.3</version>
</dependency>
(2)配置
将目标类和通知类放在Spring容器中;
假设现在有一个calculate类,里面有add+,sub-,multi*,div/四个方法:
目标类:
使用注解放入Spring容器中。
package com.dh.target;
import org.springframework.stereotype.Component;
@Component
public class Calculator {
public void add(){
System.out.println("add......");
}
public void sub(){
System.out.println("sub......");
}
public void multi(){
System.out.println("multi......");
}
public void div(){
System.out.println("div......");
}
}
通知类:
首先需要了解通知的类型:
- @Before:前置通知;
- @AfterReturning:后置通知;
- @AfterThrowing:抛出通知;
- @After:最终通知;
- @Around:环绕通知。
使用注解放在Spring容器中。
使用Aspect注解,标识为该类是通知。
package com.dh.aspect;
import org.aspectj.lang.annotation.*;
@Aspect
@Component
public class MyAspect {
//execution()存放的是定义切点的表达式
//指定类下的所有方法
@Before(value = "execution(* com.dh.target.Calculator.*(..))")
public void before(){
System.out.println("before");
}
@AfterReturning(value = "execution(* com.dh.target.Calculator.*(..))")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing(value = "execution(* com.dh.target.Calculator.*(..))")
public void afterThrowing(){
System.out.println("afterThrowing");
}
@After(value = "execution(* com.dh.target.Calculator.*(..))")
public void after(){
System.out.println("after");
}
}
配置文件:扫描注解
<?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: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/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.dh"/>
</beans>
测试:
(3)Spring单元测试:
导入jar包:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.3</version>
<scope>test</scope>
</dependency>
import com.dh.target.Calculator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ContextConfiguration("classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class MyTest01 {
@Autowired
public Calculator calculator;
@Test
public void test01(){
calculator.add();
System.out.println("---------------");
calculator.sub();
System.out.println("---------------");
calculator.multi();
System.out.println("---------------");
calculator.div();
}
}
结果:
before
add......
afterReturning
after
---------------
before
sub......
afterReturning
after
---------------
before
multi......
afterReturning
after
---------------
before
div......
afterReturning
after
可以看到,通知已经被织入到指定的切点了。
再试试看程序出异常,在add方法中添加int i = 1/0;
before
add......
afterThrowing
after
可以发现,当程序出现异常时,不会执行afterReturning了,而是会执行afterThrowing,并且就算出现异常了,也一定会执行after。
通知方法中的参数
在这些方法中,有一些参数可以帮助我们获得更多的信息:
package com.dh.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
//前置通知可以获取切点信息
@Before(value = "execution(* com.dh.target.Calculator.*(..))")
public void before(JoinPoint joinPoint){
System.out.println("切点信息:"+joinPoint);
System.out.println("before");
}
//后置通知可以获取返回值
//需要在表达式后面指定
@AfterReturning(value = "execution(* com.dh.target.Calculator.*(..))",returning = "result")
public void afterReturning(Object result){
System.out.println("返回值:"+result);
System.out.println("afterReturning");
}
//抛出通知可以获取异常的信息
//也需要在表达式后面指定
@AfterThrowing(value = "execution(* com.dh.target.Calculator.*(..))",throwing = "e")
public void afterThrowing(Exception e){
System.out.println("异常信息:"+e);
System.out.println("afterThrowing");
}
//最终通知
@After(value = "execution(* com.dh.target.Calculator.*(..))")
public void after(){
System.out.println("after");
}
}
只测试add方法,结果:
切点信息:execution(void com.dh.target.Calculator.add())
before
add......
返回值:null
afterReturning
after
指定异常:
切点信息:execution(void com.dh.target.Calculator.add())
before
add......
异常信息:java.lang.ArithmeticException: / by zero
afterThrowing
after
表达式示例
匹配所有类public⽅法: execution(public (..)) ,第⼀个表示返回值 ..表示任意个任意类型的参数
匹配指定包下所有类所有⽅法: execution(* com.xxx.dao..(..)) ,第⼀个*表示忽略权限和返回值类型
匹配指定包下所有类所有⽅法: execution(* com.xxx.dao..*(..)) ,包含⼦包
匹配指定类所有⽅法: execution(* com.xxx.service.UserService.*(..))
匹配实现特定接⼝所有类⽅法 : execution(* com.xxx.dao.GenericDAO+.*(..))
匹配所有save开头的⽅法: execution(* save*(..)) *
匹配某个指定的⽅法: execution(* com.dh.dao.StudentService.save(..))
匹配带有⼀个整型参数的⽅法 ("execution(* *(int))")
定义切点,实现重用
像上述方法中,每一个方法的切点表达式都是一样的,我们可不可以把它提取出来呢?答案是可以的.
书写一个空白的方法,使用@Pointcut注解,写入要重用的表达式,然后在需要的地方使用value="方法()"即可。
@Pointcut(value = "execution(* com.dh.target.Calculator.*(..))")
public void pointCut(){
}
//前置通知可以获取切点信息
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint){
System.out.println("切点信息:"+joinPoint);
System.out.println("before");
}
环绕通知
细心的你一定发现了,上面好像没有涉及到环绕通知。
没错,它被拎出来了。
@Around(value = "pointCut()")
public void around(ProceedingJoinPoint pjp){
try {
System.out.println("环绕前置...");
//执行目标方法
Object proceed = pjp.proceed();
System.out.println("方法返回值:"+proceed);
System.out.println("环绕后置...");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
查看结果:
环绕前置...
切点信息:execution(void com.dh.target.Calculator.add())
before
add......
返回值:null
afterReturning
after
方法返回值:null
环绕后置...
有异常时:
环绕前置...
切点信息:execution(void com.dh.target.Calculator.add())
before
add......
异常信息:java.lang.ArithmeticException: / by zero
afterThrowing
after
可以看到,此时只执行了环绕前置。
当注释Object proceed = pjp.proceed();
时:
环绕前置...
环绕后置...
惊!!!
所以明白为神马把它拎出来了吧。
环绕通知一个人差不多就可以干完所有通知的活,并且还能强大到决定是否执行切点方法。
总结
总结一下:
-
最容易记住的就是,最终通知@After无论如何都会被执行的;
-
然后:@Before前置通知和@Around中执行方法前的语句也都会被执行的;
-
@AfterReturing和@AfterThrowing只会二选一的出现;
-
当出现了异常之后,剩下的都不会执行了,只会执行@After通知里的内容;
-
@Around可以决定切点方法是否执行。
2.xml配置
注解事务虽然很简单,只需要导入jar包,将目标类和通知类放入容器,然后开启注解,在通知类上加上@Aspect注解即可。
但是还是那句话,别人的类我们就不能加注解了,而且在团队协作开发的时候,xml会比注解配置更好一点点,因为可以直接在xml中看到你的配置信息,而不需要一个一个类的去找注解。
所以,xml配置还是十分有必要的。
xml的配置其实步骤和注解差不多,只是注解能完成的事情要放在xml文件中罢了。
第一步的导包完成;
第二步的配置xml文件:
<?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:context="http://www.springframework.org/schema/context"
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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 目标类-->
<bean id="calculator" class="com.dh.target.Calculator"/>
<!-- 通知类-->
<bean id="myAspect2" class="com.dh.aspect.MyAspect2"/>
<!-- 织入信息-->
<aop:config>
<!-- 切点信息-->
<aop:pointcut id="point" expression="execution(* com.dh.target.Calculator.*(..))"/>
<!-- 切面信息-->
<aop:aspect ref="myAspect2">
<aop:before method="before" pointcut-ref="point"/>
<aop:after-returning method="afterReturning" returning="result" pointcut-ref="point"/>
<aop:after-throwing method="afterThrowing" throwing="e" pointcut-ref="point"/>
<aop:around method="around" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
</aop:aspect>
</aop:config>
</beans>