Spring注解开发05--------注解实现aop
什么是aop?
AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
从《Spring实战(第4版)》图书中扒了一张图:
从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
总而言之:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是通过动态代理实现的。
案例演示
一、导入相关依赖
<!-- Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.5</version>
</dependency>
<!-- aspect相关依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<!-- junit,单元测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
在这里有三点说明:
- Spring5.0之后,许多核心核心依赖都包含在
spring-webmvc
当中,所以导入spring-webmvc这个依赖,Spring的大部分功能就可以使用了。 - 要想使用Spring-Aop的功能,我们必须
aspectjweaver
这个依赖! Junit
我们应该都很熟悉了,是我们的单元测试类依赖。
二、我们自定义一个业务逻辑类(MathCalculate)。
这里我们就简单的定义一个除法的业务逻辑。
package com.xdw.aop;
public class MathCalculate {
public int div(int i, int j) {
System.out.println("MathCalculate...div...");
return i/j;
}
}
下面我们的要求是在业务逻辑进行的时候进行日志的输出(方法之前、方法之后以及方法抛出异常。。。)
三、定义我们的切面类
在编写我们通知类之前,有几个aop的注解我们需要了解以下:
@Before
前置通知,在目标方法执行之前运行@After
后置通知,在目标方法执行之后运行,不管方法是正常结束还是异常结束都会执行@AfterReturning
返回通知,在目标方法正常返回之后执行@AfterThrowing
异常通知,在目标方法运行出现异常时执行@Around
环绕通知,动态代理,我们可以直接手动推进目标方法运行(joinPoint.procced()
)@Aspect
放在切面类上,告诉Spring这是一个切面类。
下面我不会一下就把完整切面类代码贴出来,会一步一步对我们的代码进行完善!
试想一下,一开始你的代码是不是这样:
package com.xdw.aop;
import org.aspectj.lang.annotation.*;
// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {
@Before("execution(* com.xdw.aop..*.*(..))")
public void logStart() {
System.out.println("方法执行之前, 执行参数{}");
}
@After("execution(* com.xdw.aop..*.*(..))")
public void logEnd() {
System.out.println("方法执行结束");
}
@AfterReturning("execution(* com.xdw.aop..*.*(..))")
public void logRetur() {
System.out.println("方法执行结束,返回值是{}");
}
@AfterThrowing("execution(* com.xdw.aop..*.*(..))")
public void logException() {
System.out.println("方法执行异常,错误信息为{}");
}
}
execution(* com.xdw.aop..*.*(..))"
是一个切入点表达式,在上述代码中反复出现,十分麻烦。作为程序员,偷懒是我们的必备技能,我们可以将上述代码稍稍优化一下:
-
第一步,我们新建一个空方法,方法名随意指定,使用
@Pointcut
注解来修饰,告诉Spring这是一个切点@Pointcut("execution(* com.xdw.aop..*.*(..))") public void pointCut() {}
-
在我们的通知标签上引入该方法
// 本类引用,直接使用方法名即可 @Before("pointCut()") public void logStart() { System.out.println("方法执行之前, 执行参数{}"); } // 外部类引用,必须使用全类名 + 方法名 @After("com.xdw.aop.LogAspect.pointCut()") public void logEnd() { System.out.println("方法执行结束"); }
这里有两点需要说明: a.本类引用,直接使用方法名即可 b.外部类引用,必须使用全类名 + 方法名
最终,我们修改之后的方法如下:
package com.xdw.aop;
import org.aspectj.lang.annotation.*;
// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {
@Pointcut("execution(* com.xdw.aop..*.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void logStart() {
System.out.println("方法执行之前, 执行参数{}");
}
@After("com.xdw.aop.LogAspect.pointCut()")
public void logEnd() {
System.out.println("方法执行结束");
}
@AfterReturning("pointCut()")
public void logRetur() {
System.out.println("方法执行结束,返回值是{}");
}
@AfterThrowing("pointCut()")
public void logException() {
System.out.println("方法执行异常,错误信息为{}");
}
}
四、将我们的业务逻辑类与切面类注入IOC容器中
package com.xdw.config;
import com.xdw.aop.LogAspect;
import com.xdw.aop.MathCalculate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy // 开启注解自动注入
@Configuration
public class MainConfigOfAop {
@Bean
public MathCalculate mathCalculate() {
return new MathCalculate();
}
@Bean
public LogAspect logAspect() {
return new LogAspect();
}
}
在这里,我们一定要在我们的配置类上添加@EnableAspectJAutoProxy
注解,开启基于注解的aop模式,否则aop不会生效。在未来,我们会看到很多的@Enablexxxx注解,它们的作用都是开启某一项功能,来替换我们以前的那些繁琐的配置文件。
五、测试
编写测试方法测试。
@Test
public void test01() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAop.class);
MathCalculate mathCalculate = applicationContext.getBean(MathCalculate.class);
mathCalculate.div(3,1);
}
运行结果如下:
从运行结果中,我们可以看到,我们的执行参数并没有被打印出来,因为在我们的代码中,根本没有获取参数!我们应该如何在代码中获取参数信息呢?
六、完善切面类,获取方法名及参数
-
我们可以在切面方法中添加
JoinPoint
类型的参数,来获取方法名称、参数等信息@Before("pointCut()") public void logStart(JoinPoint joinPoint) { System.out.println(joinPoint.getSignature().getName() + "方法执行之前, 执行参数{"+ Arrays.asList(joinPoint.getArgs()) + "}"); }
joinPoint.getSignature().getName()
获取方法名称,joinPoint.getArgs()
获取方法参数! -
我们可以在
@AfterReturing
注解中,设置returning
属性,来指向我们自定义的接收结果的参数@AfterReturning(value="pointCut()", returning = "result") public void logReturn(JoinPoint joinPoint, Object result) { System.out.println(joinPoint.getSignature().getName() + "方法执行结束,返回值是{"+ result +"}"); }
-
我们可以在
@AfterThrowing
注解中,设置throwing
属性,来指定我们自定义的接收结果的参数@AfterThrowing(value = "pointCut()", throwing = "e") public void logException(JoinPoint joinPoint, Exception e) { System.out.println(joinPoint.getSignature().getName() + "方法执行异常,错误信息为{" + e.getMessage() + "}"); }
修改之后的切面类如下:
package com.xdw.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import java.util.Arrays;
// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {
@Pointcut("execution(* com.xdw.aop..*.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void logStart(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() + "方法执行之前, 执行参数{"+ Arrays.asList(joinPoint.getArgs()) + "}");
}
@After("com.xdw.aop.LogAspect.pointCut()")
public void logEnd(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() + "方法执行结束");
}
@AfterReturning(value="pointCut()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
System.out.println(joinPoint.getSignature().getName() + "方法执行结束,返回值是{"+ result +"}");
}
@AfterThrowing(value = "pointCut()", throwing = "e")
public void logException(JoinPoint joinPoint, Exception e) {
System.out.println(joinPoint.getSignature().getName() + "方法执行异常,错误信息为{" + e.getMessage() + "}");
}
}
JoinPoint参数一定要放在参数列表的第一位,否则Spring是无法识别的,就会报错。
七、再次测试
我们先正常测试:
我们再将除数设为0:
发现参数、返回值、错误信息都能正常打印出来了。
至此AOP测试搭建成功!
小结
上述测试环境在搭建时,虽然繁琐,但是归纳总结起来,其实只有三步:
- 将业务逻辑类与切面类都注入至容器中,并告诉Spring哪个是切面类(
@Aspect
) - 在切面类上的每个切面方法上标注通知注解,告诉Spring该方法何时何地运行(切入点表达式)
- 开启基于注解的Aop模式
@EnableAspectJAutoProxy
切入点(execution)表达式说明:
我们以我们代买中的表达式execution(* com.xdw.aop..*.*(..))
为例,整个表达式可以分为五个部分:
- execution(): 表达式主体。
- 第一个
*
号:表示返回类型,*
号表示所有的类型。 - 包名:表示需要拦截的包名,后面的两个句点
..
表示当前包和当前包下的所有子包。 - 第二个
*
号:表示类名,*
号表示所有的类。 *(..)
:最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点..
表示任何参数。