聊聊SpringAOP那些不为人知的秘密
点击关注公众号,Java干货及时送达
引出AOP
SpringAOP是Spring框架中非常重要的一个概念,AOP,意为面向切面编程。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
来看一个例子,首先我们创建一个接口:
public interface CalculateService {
int add(int x, int y);
int reduce(int x, int y);
int multi(int x, int y);
int division(int x, int y);
}
然后创建实现类:
@Service
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int x, int y) {
System.out.println(x + " + " + y + " = " + (x + y));
return x + y;
}
@Override
public int reduce(int x, int y) {
System.out.println(x + " - " + y + " = " + (x - y));
return x - y;
}
@Override
public int multi(int x, int y) {
System.out.println(x + " * " + y + " = " + (x * y));
return x * y;
}
@Override
public int division(int x, int y) {
System.out.println(x + " / " + y + " = " + (x / y));
return x / y;
}
}
此时我们从容器中获取这个组件并调用计算方法:
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
CalculateService calculateService = context.getBean("calculateServiceImpl", CalculateService.class);
calculateService.add(1,1);
calculateService.reduce(1,1);
calculateService.multi(1,1);
calculateService.division(1,1);
}
运行结果:
1 + 1 = 2
1 - 1 = 0
1 * 1 = 1
1 / 1 = 1
现在需求变了,我们需要在输出语句的前后分别打印当前系统的时间,如果让你实现,你会怎么做呢?最笨的办法就是硬编码,直接在每个方法里添加打印时间的代码即可:
@Service
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int x, int y) {
System.out.println("计算前的时间:" + LocalDateTime.now());
System.out.println(x + " + " + y + " = " + (x + y));
System.out.println("计算后的时间:" + LocalDateTime.now());
return x + y;
}
@Override
public int reduce(int x, int y) {
System.out.println("计算前的时间:" + LocalDateTime.now());
System.out.println(x + " - " + y + " = " + (x - y));
System.out.println("计算后的时间:" + LocalDateTime.now());
return x - y;
}
@Override
public int multi(int x, int y) {
System.out.println("计算前的时间:" + LocalDateTime.now());
System.out.println(x + " * " + y + " = " + (x * y));
System.out.println("计算后的时间:" + LocalDateTime.now());
return x * y;
}
@Override
public int division(int x, int y) {
System.out.println("计算前的时间:" + LocalDateTime.now());
System.out.println(x + " / " + y + " = " + (x / y));
System.out.println("计算后的时间:" + LocalDateTime.now());
return x / y;
}
}
运行结果:
计算前的时间:2022-01-21T14:35:21.806
1 + 1 = 2
计算后的时间:2022-01-21T14:35:21.806
计算前的时间:2022-01-21T14:35:21.806
1 - 1 = 0
计算后的时间:2022-01-21T14:35:21.806
计算前的时间:2022-01-21T14:35:21.806
1 * 1 = 1
计算后的时间:2022-01-21T14:35:21.806
计算前的时间:2022-01-21T14:35:21.806
1 / 1 = 1
计算后的时间:2022-01-21T14:35:21.806
这样虽然实现了需求,但是不够优雅,而且如果接口方法有变动,我们就需要修改实现类的代码,那么有没有一种办法能够将这些打印时间的需求抽离出来,然后让其在指定的方法执行前后分别执行呢?SpringAOP就能够帮助我们完成这一想法。
SpringAOP改造代码实现
@Aspect
@Component
public class CalculateAspectJ {
@Before("execution(* com.wwj.spring.demo.aop.CalculateService.add(..))")
public void printBefore(){
System.out.println("计算前的时间:" + LocalDateTime.now());
}
}
这段代码里面涉及到的知识点比较多,下面我会一一介绍,先来看看效果:
计算前的时间:2022-01-21T14:45:41.579
1 + 1 = 2
1 - 1 = 0
1 * 1 = 1
1 / 1 = 1
看输出结果好像打印时间只在add方法生效了,这是为什么呢?我们主要的关注点就是下面的这个组件:
@Aspect
@Component
public class CalculateAspectJ {
@Before("execution(int com.wwj.spring.demo.aop.CalculateService.add(..))")
public void printBefore(){
System.out.println("计算前的时间:" + LocalDateTime.now());
}
}
对于传统的OOP编程,我们的开发流程是从上至下的,比如转账操作,我们需要在取款、查询业务、转账三个操作中验证用户的信息是否正确:而AOP打破了这种限定,它以一种横向的方式进行编程,就像砍树一样,如下图:可以看到经过AOP的改造后,原先要写三遍的验证用户代码只需要写一次了,它就像一根针,把代码织入到了业务中。再回过头来看看刚才的组件:
@Aspect
@Component
public class CalculateAspectJ {
@Before("execution(int com.wwj.spring.demo.aop.CalculateService.add(int,int))")
public void printBefore(){
System.out.println("计算前的时间:" + LocalDateTime.now());
}
}
其中@Aspect注解用于声明当前类为一个切面,当一个类被声明为切面后,Spring便会将该类切入到某个切点中,而切点就是我们需要改造的方法,那么如何指定切面作用于哪些切点上呢,我们需要借助切点表达式:
execution(int com.wwj.spring.demo.aop.CalculateService.add(int,int))
切点表达式以execution
开头,值为方法的全名,包括返回值、包名、方法名、参数,Spring将根据切点表达式去匹配需要切入的方法,不过一般情况下切点表达式并不会写得这么精确,通常配合通配符一起使用,如:
execution(* com.wwj.spring.demo.aop.CalculateService.*(..))
它表示匹配CalculateService接口下任意返回值任意参数的任意方法,也就是说,该接口下的所有方法都将被处理,当我们使用通配符方式配置时,运行结果如下:
计算前的时间:2022-01-21T16:07:23.250
1 + 1 = 2
计算前的时间:2022-01-21T16:07:23.250
1 - 1 = 0
计算前的时间:2022-01-21T16:07:23.250
1 * 1 = 1
计算前的时间:2022-01-21T16:07:23.250
1 / 1 = 1
通知类型
将代码逻辑织入到业务中的流程还有一个专业的概念,叫通知,从上面的运行结果我们不难发现,切面只在方法执行之前生效了,这是因为我们使用了@Before注解,它表示的是通知类型中的前置通知,Spring中共有5种通知类型:
@Before:前置通知,在目标方法执行前执行 @After:后置通知,在目标方法执行后执行,无论是否出现异常 @AfterReturning:返回通知,在目标方法执行后执行,出现异常则不执行 @AfterThrowing:异常通知,在目标方法出现异常后执行 @Around:环绕通知,围绕方法执行,它能实现以上四种通知的效果
由此可知,若是想在目标方法执行之后实现某些功能,则需要使用后置通知,添加一个配置:
@After("execution(* com.wwj.spring.demo.aop.CalculateService.*(..))")
public void printAfter() {
System.out.println("计算前的时间:" + LocalDateTime.now());
}
运行结果:
计算前的时间:2022-01-21T16:14:00.002
1 + 1 = 2
计算后的时间:2022-01-21T16:14:00.002
计算前的时间:2022-01-21T16:14:00.002
1 - 1 = 0
计算后的时间:2022-01-21T16:14:00.002
计算前的时间:2022-01-21T16:14:00.002
1 * 1 = 1
计算后的时间:2022-01-21T16:14:00.002
计算前的时间:2022-01-21T16:14:00.002
1 / 1 = 1
计算后的时间:2022-01-21T16:14:00.002
其它几种类型的通知用法也是如此,只需改变注解名字即可,不过在每种通知中都有一些其它细节,下面我们一一介绍。
前置通知
前置通知@Before,它会在目标方法执行之前执行,所以按道理我们可以在前置通知中获取目标方法的一些信息,比如方法名、方法入参等,好在Spring已经考虑到了,为我们提供了JoinPoint来获取,来看例子:
@Before("execution(* com.wwj.spring.demo.aop.CalculateService.*(..))")
public void printBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List