前言
前些天写了【详解什么是Spring AOP】虽然叫详解,但是里面其实还是留了很多内容没有说。于是就想着还是写几个辅助的文章,把前面博客里面的坑给填上,既然要做了就要做好对不对。那么这次要填的坑就是Spring AOP里面的Advice语义中的方法都应该怎么用,这部分的内容在官网【Declaring Advice】这部分的章节里也能够找到。更多Spring内容进入【Spring解读系列目录】。
Advice Sample
概念的正文已经在详解这个博客中详细的说过了,所以这里就不多罗嗦了。还是老样子,我们首先要引入必要的依赖,构建一个小demo。假设我们有一个接口Dao,有一个实现类DemoDao,有一个切面类MyAspect,一个测试类MainTest,一个配置类AppConfig。
<!-- Spring上下文是一个配置,向Spring框架提供上下文信息。 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--aspectj语法支持-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
@Configuration
@ComponentScan("com")
@EnableAspectJAutoProxy //开启AspectJ语法支持
public class AppConfig {
}
@Component
@Aspect
public class MyAspect {
@Pointcut("within(com.demo.dao.*)")
public void myPointCutWithin(){}
}
public interface Dao {
void print();
String printString(String str) throws Exception;
}
@Repository("demoDao")
public class DemoDao implements Dao{
@Override
public void print() {
System.out.println("print Empty");
}
@Override
public String printString(String str) throws Exception{ //throws抛出异常
System.out.println("print String:"+str);
return str;
}
}
public class MainTest {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext anno=new AnnotationConfigApplicationContext(AppConfig.class);
Dao dao= (Dao) anno.getBean("demoDao");
dao.printString("parameter");
}
}
Advice Type Sample
Before Advice
添加Before语句到MyAspect中,运行main()方法:
@Before("myPointCutWithin()")
public void beforeDao(){
System.out.println("this is a beforeDao");
}
运行结果,在方法执行前打印了。
this is a beforeDao
print String:param
After (Finally) Advice
添加After语句到MyAspect中,运行main()方法:
@After("myPointCutWithin()")
public void afterFinally(){
System.out.println("this is a afterFinally");
}
运行结果,在方法执行后打印了。
print String:param
this is a afterFinally
After Returning Advice
添加AfterReturning语句到MyAspect中,运行main()方法:
@AfterReturning("myPointCutWithin()")
public void afterWithReturn(){
System.out.println("this is a afterWithReturn");
}
运行结果,在方法执行后打印了。这个效果不容易和After (Finally)区别开来,
因为即便是void类型的也是返回了空,所以无论是什么都会打印的。
print String:param
this is a afterWithReturn
还有更进一步的方法,我们可以拿到返回值:
@AfterReturning(pointcut="myPointCutWithin()", returning = "str") //获取返回值
public void afterWithReturnValue(Object str){
System.out.println(str.toString()); //打印返回值
System.out.println("this is a afterWithReturnValue");
}
运行结果:
print String:param
param //连接点返回的值
this is a afterWithReturnValue
After Throwing Advice
添加AfterThrowing语句到MyAspect中,但是这个还是有些不一样的,因为我们必须要用throws语句抛出这个异常,才能被通知到,所以我们要改造一下DemoDao,让其能报一个空指针异常。
@AfterThrowing("myPointCutWithin()")
public void afterWithException(){
System.out.println("this is a afterWithException");
}
@Repository("demoDao")
public class DemoDao implements Dao{
Dao dao;
/****/
@Override
public String printString(String str) throws Exception{
System.out.println("print String:"+str);
dao.print(); //调用到这里抛出异常
return "String";
}
}
运行结果,在方法执行抛出异常以后被通知到了。做个提醒:有异常最终一定要try-catch处理,千万不要让你的代码裸奔,
养成良好的编码习惯是非常重要的。这里为了让大家看清楚,做了一个demo,实际上应该改造为下面的样子。
print String:param
this is a afterWithException
Exception in thread "main" java.lang.NullPointerException
at com.demo.dao.DemoDao.printString(DemoDao.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
******************捕获你的异常进行处理,杜绝裸奔。******************
public class MainTest {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext anno=new AnnotationConfigApplicationContext(AppConfig.class);
Dao dao= (Dao) anno.getBean("demoDao");
try {
dao.printString("param");
}catch (Exception e){
System.out.println("Do something");
}
}
}
输出结果:
print String:param
this is a afterWithException
Do something
这个方法也有一个升级版,可以允许你指定想要捕获的异常,比如这里指定捕获空指针异常被通知。这里如果写了别的异常比如官网提供的DataAccessException,通知就无法调用了:
//使用NullPointerException作为参数传递进来,或者使用更广泛的Exception,不过
@AfterThrowing(pointcut="myPointCutWithin()", throwing="ex") //指定空指针异常
public void afterWithException2(NullPointerException ex){
System.out.println("this is a target afterWithException2");
}
输出结果:
print String:param
this is a target afterWithException2
Do something
Around Advice
添加Around语句到MyAspect中:
@Around("myPointCutWithin()")
public void aroundNormal(ProceedingJoinPoint pjp){
System.out.println("this is a aroundNormal before");
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("this is a aroundNormal after");
}
运行结果:
this is a aroundNormal before
print String:param
this is a aroundNormal after
可以看到,这里面连接点是能够在通知中启动的,也就是说能够在通知中修改连接点的内容,这也是为什么官网说Around是最强大的通知,怎么修改呢?ProceedingJoinPoint 这个类已经把连接点描述清楚了,它继承了JoinPoint,所以拥有JoinPoint的一切方法。而JoinPoint正是Spring描述连接点的类,所以ProceedingJoinPoint 对象就能够获取到整个连接点的内容。比如getArgs()这个方法,就可以获取到连接点的参数,并封装成一个数组。那么我们要做的就很简单了,改变传参,修改数组的内容就好了啊。
@Around("myPointCutWithin()")
public void aroundChange(ProceedingJoinPoint pjp){
System.out.println("this is a aroundChange before");
Object[] objects=pjp.getArgs();
for (int i = 0; i < objects.length; i++) {
objects[i]="bbb"; //修改内容param为bbb
}
try {
pjp.proceed(objects);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("this is a aroundChange after");
}
运行结果:
this is a aroundChange before
print String:bbb
this is a aroundChange after
Advice的执行顺序
我们有这么多的Advice类型可以执行,那么他们的执行顺序怎么界定呢?全部都打开执行一下,除了After Throwing是必须异常通知外,其他的都是不影响的。
输出结果如下,可以很明显看出:Around before->before->after return->after->Around after.
this is a aroundChange before
this is a beforeDao
print String:bbb
this is a afterWithReturn
this is a afterFinally
this is a aroundChange after
Advice 使用表达式
我们之前的例子都是使用的切点的方法作为Advice的作用域,其实如果不想写切点方法也是可以的。Advice获取的只是切点方法的注解内容而已,所以这里是能够直接用表达式替代的,例如:
@Before("within(com.demo.dao.*)") //直接用表达式
public void beforeDao(){
System.out.println("this is a beforeDao");
}
输出:
this is a beforeDao
print String:param
和使用@Before("myPointCutWithin()")效果是一模一样的。