持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
写在前面😘
大一电子信息工程新生,请多多关照,希望能在掘金记录自己的学习历程!
【Spring 学习笔记】 系列教程基于Spring 5.2.10.RELEASE讲解。
一、环境准备
添加AOP依赖
在pom.xml文件里添加Spring AOP和AspectJ的jar包依赖
<dependencies>
<!--包含Spring AOP:有基本的AOP功能-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--AspectJ框架有更强大的AOP功能-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
创建目标接口和实现类
/*UserService接口*/
public interface UserService {
//自我介绍方法
public string hello(String username,int age);
}
/*UserServiceImpl实现类*/
@Repository
public class UserServiceImpl implements UserService {
@Override
public String hello(String username, int age) {
return "username:" + username + ",age:" + age;
}
}
创建通知类
- 创建通知类,并指定切入点
/*通知类*/
@Component//将这个类定义成 Bean
@Aspect//将这个Bean定义为切面
public class MyAdvice {
//指定UserService类中的hello方法为切入点
@Pointcut("execution(* com.bighorn.service.UserService.hello(..))")
private void pt1(){}
}
创建Spring核心配置类
/*Spring核心配置类*/
@Configuration
@ComponentScan("com.bighorn") //开启注解扫描
@EnableAspectJAutoProxy //开启 AspectJ 的自动代理
public class SpringConfig {
}
编写运行程序
- 在
hello()方法中传入两个参数:"bighorn",18
public class App {
public static void main(String[] args) throws SQLException {
//获取配置类初始化容器
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
//从容器中获取UserService对象
UserService UserService = context.getBean(UserService.class);
//调用UserService的方法
String user = UserService.hello("bighorn", 18);
System.out.println(user);
}
}
二、JoinPoint和ProceedingJoinPoint
通知方法中如果有JoinPoint或者ProceedingJoinPoint参数,必须要把他们放在第一位!
JoinPoint接口
在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象
适用场景:前置通知、后置通知、异常通知、返回通知
常用API
| 方法名 | 功能 |
|---|---|
| Signature getSignature() | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[ ] getArgs() | 获取传入目标方法的参数数组 |
| Object getTarget() | 获取目标对象(被代理对象) |
| Object getThis() | 获取代理对象 |
- 目标方法名: joinPoint.getSignature().getName());
- 目标方法所属类的简单类名:joinPoint.getSignature().getDeclaringType().getSimpleName());
- 目标方法所属类的类名:joinPoint.getSignature().getDeclaringTypeName();
- 目标方法声明类型:Modifier.toString(joinPoint.getSignature().getModifiers()));
案例1
- 创建一个前置通知,在通知方法中传入JoinPoint参数,并调用一些方法获取目标方法的相关信息。
/*前置通知*/
@Before("pt1()")
public void before(JoinPoint jp) {
System.out.println("目标方法名为:" + jp.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + jp.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + jp.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(jp.getSignature().getModifiers()));
//获取传入目标方法的参数
Object[] args = jp.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}
//
System.out.println("目标对象:" + jp.getTarget());
System.out.println("代理对象:" + jp.getThis());
}
运行结果如下👇
注意点
仔细观察,发现代理对象自己和被代理对象输出的内存地址一样啊!
其实不然,两个不同的对象内存地址不可能一样,代理类其实内部调用了目标类的方法,上面的目标类和代理类打印调用的是toString(),但是代理类调用的其实是目标类的toString(),所以打印的地址一样。但是你如果用==比较结果就是false了。
我们也可以通过getClass()方法获取它们的class对象并打印出来,比较一下就会发现不同了。
System.out.println("目标对象:" + jp.getTarget().getClass());
System.out.println("代理对象:" + jp.getThis().getClass());
ProceedingJoinPoint接口
ProceedingJoinPoint接口 是JoinPoint的子接口,适用场景:只用在环绕通知中。
而且环绕通知方法中也必须有ProceedingJoinPoint这个形参,不然就没意义了。
proceed方法
- proceed()方法是有两个构造方法的
- 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
- 但是当需要修改原始方法的参数时,就只能采用有参数的proceed
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
- 有了这个特性后,我们就可以在环绕通知中
对原始方法的参数进行拦截过滤:如去除字符串最后一位的空格等操作。
案例2
对原始方法的第一个参数username进行拦截,把英文名变成大写!
/*环绕通知*/
@Around("pt1()")
public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
//获取传入目标方法的参数
Object[] args = pjp.getArgs();
//前置通知
System.out.println("目标方法执行前...原参数:"+Arrays.toString(args));
//将传入的英文名变成大写
args[0] = args[0].toString().toUpperCase();
//用新的参数args执行目标方法
result = pjp.proceed(args);
//返回通知
System.out.println("目标方法返回结果后...新参数:"+ Arrays.toString(args));
} catch (Throwable e) {
//异常通知
System.out.println("执行目标方法异常后...");
e.printStackTrace();
}
//后置通知
System.out.println("目标方法执行后...");
return result;
}
运行结果如下👇
三、获取返回值
对于目标方法的返回值,只有返回通知@AfterReturing和环绕通知@Around这两个通知类型可以获取,下面来介绍如何获取。
环绕通知获取返回值
- 使用
Object result = pjp.proceed( );获取返回值,result就是原始方法的返回值,我们不但可以获取,如果需要还可以进行修改。 - 在案例2中有类似代码:result = pjp.proceed(args)。这里就不演示了
返回通知获取返回值
- 在
@AfterReturning注解中,使用 returning 指定返回值名称,且需要与返回通知方法中形参名称保持一致。
/*返回后通知*/
@AfterReturning(value = "pt1()", returning = "ret")
public void afterReturning(Object ret) {
System.out.println("返回通知,获取原始方法返回值--->" + ret);
}
运行结果如下👇
四、获取异常
对于目标方法抛出异常,只有异常通知@AfterThrowing和环绕通知@Around这两个通知类型可以获取,下面来介绍如何获取。
环绕通知获取异常
- 使用
try-catch语句就能获取异常了,异常处理在catch中进行。在案例2中有类似代码,这里就不多赘述了。
try{
ret = pjp.proceed();
}catch(Throwable e){
//根据业务需求处理异常
e.printStackTrace();
}
异常通常获取异常
- 异常通常和返回通知类似,也要在注解上加一句话。不过
@AfterThrowing注解中是使用 throwing 指定异常名称,需要与方法形参名相同。
//异常后通知
@AfterThrowing(value = "pt1()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("异常通知获取到的异常"+t);
}
写在后面🍻
感谢观看啦✨
有什么不足,欢迎指出哦💖
掘金的运营同学审核辛苦了💗