首先请思考一下以下代码执行的结果:

  • LogAop.java

//声明一个AOP拦截service包下的所有方法
@Aspect
public class LogAop {

@Around("execution(* com.demo.service.*.*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
try {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object ret = joinPoint.proceed();
//执行完目标方法之后打印
System.out.println("after execute method:"+method.getName());
return ret;
} catch (Throwable throwable) {
throw throwable;
}
}
}
  • UserService.java

@Service
public class UserService{

public User save(User user){
//省略代码
}

public void sendEmail(User user){
//省略代码
}

//注册
public void register(User user){
//保存用户
this.save(user);
//发送邮件给用户
this.sendEmail(user);
}
}
  • UserServiceTest.java

@SpringBootTest
public class UserServiceTest{

@Autowired
private UserService userService;

@Test
public void save(){
userService.save(new User());
}

@Test
public void sendEmail(){
userService.sendEmail(new User());
}

@Test
public void register(){
UserService.register(new User());
}
}

在执行save方法后,控制台输出为:

after execute method:save

在执行sendEmail方法后,控制台输出为:

after execute method:sendEmail

请问在执行register()方法后会打印出什么内容?

反直觉

 

这个时候可能很多人都会和我之前想的一样,在register方法里调用了save和sendEmail,那 AOP 会处理save和sendEmail,输出:

after execute method:save
after execute method:sendEmail
after execute method:register

然而事实并不是这样的,而是输出:

after execute method:register

在这种认知的情况下,很可能就会写出有bug的代码,例如:

@Service
public class UserService{
//用户下单一个商品
public void order(User user,String orderId){
Order order = findOrder(orderId);
pay(user,order);
}

@Transactional
public void pay(User user,Order order){
//扣款
user.setMoney(user.getMoney()-order.getPrice());
save(user);
//...其它处理
}
}

当用户下单时调用的order方法,在该方法里面调用了@Transactional注解修饰的pay方法,这个时候pay方法的事务管理已经不生效了,在发生异常时就会出现问题。

理解 AOP

 

我们知道 Spring AOP默认是基于动态代理来实现的,那么先化繁为简,只要搞懂最基本的动态代理自然就明白之前的原因了,这里直接以 JDK 动态代理为例来演示一下上面的情况。

关注公众号程序员小乐回复关键字“Java”获取Java面试题和答案。

由于 JDK 动态代理一定需要接口类,所以首先声明一个IUserService接口

IUserService.java
public interface IUserService{
User save(User user);
void sendEmail(User user);
User register(User user);
}

编写实现类

  • UserService.java

public class UserService implements IUserService{

@Override
public User save(User user){
//省略代码
}

@Override
public void sendEmail(User user){
//省略代码
}

//注册
@Override
public void register(User user){
//保存用户
this.save(user);
//发送邮件给用户
this.sendEmail(user);
}
}

编写日志处理动态代理实现

  • ServiceLogProxy.java

public static class ServiceLogProxy {
public static Object getProxy(Class<?> clazz, Object target) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object ret = method.invoke(target, args);
System.out.println("after execute method:" + method.getName());
return ret;
}
});
}
}

运行代码

  • Main.java

public class Main{
public static void main(String[] args) {
//获取代理类
IUserService userService = (IUserService) ServiceLogProxy.getProxy(IUserService.class, new UserService());
userService.save(new User());
userService.sendEmail(new User());
userService.register(new User());
}
}

结果如下:

after execute method:save
after execute method:sendEmail
after execute method:register

可以发现和之前 Spring AOP 的情况一样,register方法中调用的save和sendEmail方法同样的没有被动态代理拦截到,这是为什么呢,接下来就看看下动态代理的底层实现。

关注公众号程序员小乐回复关键字“offer”获取算法面试题和答案。

动态代理原理

 

其实动态代理就是在运行期间动态的生成了一个class在 jvm 中,然后通过这个class的实例调用真正的实现类的方法,伪代码如下:

public class $Proxy0 implements IUserService{

//这个类就是之前动态代理里的new InvocationHandler(){}对象
private InvocationHandler h;
//从接口中拿到的register Method
private Method registerMethod;

@Override
public void register(User user){
//执行前面ServiceLogProxy编写好的invoke方法,实现代理功能
h.invoke(this,registerMethod,new Object[]{user})
}
}

回到刚刚的main方法,那个userService变量的实例类型其实就是动态生成的类,可以把它的 class 打印出来看看:

IUserService userService = (IUserService) ServiceLogProxy.getProxy(IUserService.class, new UserService());
System.out.println(userService.getClass());

输出结果为:

xxx.xxx.$Proxy0

在了解这个原理之后,再接着解答之前的疑问,可以看到通过代理类的实例执行的方法才会进入到拦截处理中,而在InvocationHandler#invoke()方法中,是这样执行目标方法的:

//注意这个target是new UserService()实例对象
Object ret = method.invoke(target, args);
System.out.println("after execute method:" + method.getName());

在register方法中调用this.save和this.sendEmail方法时,this是指向本身new UserService()实例,所以本质上就是:

User user = new User();
UserService userService = new UserService();
userService.save(user);
userService.sendEmail(user);

不是动态代理生成的类去执行目标方法,那必然不会进行动态代理的拦截处理中,明白这个之后原理之后,就可以改造下之前的方法,让方法内调用本类方法也能使动态代理生效,就是用动态代理生成的类去调用方法就好了,改造如下:

  • UserService.java

public class UserService implements IUserService{

//注册
@Override
public void register(User user){
//获取到代理类
IUserService self = (IUserService) ServiceLogProxy.getProxy(IUserService.class, this);
//通过代理类保存用户
self.save(user);
//通过代理类发送邮件给用户
self.sendEmail(user);
}
}

运行main方法,结果如下:

after execute method:save
after execute method:sendEmail
after execute method:save
after execute method:sendEmail
after execute method:register

可以看到已经达到预期效果了。

Spring AOP 中方法调用本类方法的解决方案

同样的,只要使用代理类来执行目标方法就行,而不是用this引用,修改后如下:

@Service
public class UserService{

//拿到代理类
@Autowired
private UserService self;

//注册
public void register(User user){
//通过代理类保存用户
self.save(user);
//通过代理类发送邮件给用户
self.sendEmail(user);
}
}

好了,问题到此就解决了,但是需要注意的是Spring官方是不提倡这样的做法的,官方提倡的是使用一个新的类来解决此类问题,例如创建一个UserRegisterService类:

@Service
public class UserRegisterService{
@Autowired
private UserService userService;

//注册
public void register(User user){
//通过代理类保存用户
userService.save(user);
//通过代理类发送邮件给用户
userService.sendEmail(user);
}
}

Spring AOP调用本类方法为什么没有生效的更多相关文章

  1. spring aop 动态代理批量调用方法实例

    今天项目经理发下任务,需要测试 20 个接口,看看推送和接收数据是否正常.因为对接传输的数据是 xml 格式的字符串,所以我拿现成的数据,先生成推送过去的数据并存储到文本,以便验证数据是否正确,这时候 ...

  2. 源码解析Spring AOP的加载与生效

    本次博主主要进行Spring AOP这里的解析,因为在工作中使用后,却不知道背后的实现原理并在使用的过程中发现了一些认知缺陷,所以决定写这么一篇文章以供大家参考参考,进入正题. 本次博主使用了@Asp ...

  3. Spring AOP在函数接口调用性能分析及其日志处理方面的应用

    面向切面编程可以实现在不修改原来代码的情况下,增加我们所需的业务处理逻辑,比如:添加日志.本文AOP实例是基于Aspect Around注解实现的,我们需要在调用API函数的时候,统计函数调用的具体信 ...

  4. Spring AOP深入理解之拦截器调用

    Spring AOP深入理解之拦截器调用 Spring AOP代理对象生成回想 上一篇博客中:深入理解Spring AOP之二代理对象生成介绍了Spring代理对象是怎样生成的,当中重点介绍了JDK动 ...

  5. spring aop 内部调用问题解决

    方法1: 基于 proxy 的 spring aop 带来的内部调用问题可以使用 AopContext.currentProxy() 强转为当前的再调用就可以解决了 例如: 错误用法:public A ...

  6. spring aop 之链式调用

    关关雎鸠,在河之洲.窈窕淑女,君子好逑. 概述 AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横 ...

  7. Spring AOP无法拦截内部方法调用

    当在同一个类中,A方法调用B方法时,AOP无法工作的问题 假设一个接口里面有两个方法: package demo.long; public interface CustomerService { pu ...

  8. java:struts框架2(方法的动态和静态调用,获取Servlet API三种方式(推荐IOC(控制反转)),拦截器,静态代理和动态代理(Spring AOP))

    1.方法的静态和动态调用: struts.xml: <?xml version="1.0" encoding="UTF-8"?> <!DOCT ...

  9. Spring AOP开发时如何得到某个方法内调用的方法的代理对象?

    Spring AOP开发时如何得到某个方法内调用的方法的代理对象? 问题阅读起来拗口,看代码 在方法中调用其他方法很常见,也经常使用,如果在一个方法内部调用其他方法,比如 public class U ...

随机推荐

  1. 社交网络分析的 R 基础:(五)图的导入与简单分析

    如何将存储在磁盘上的邻接矩阵输入到 R 程序中,是进行社交网络分析的起点.在前面的章节中已经介绍了基本的数据结构以及代码结构,本章将会面对一个实质性问题,学习如何导入一个图以及计算图的一些属性. 图的 ...

  2. 【POJ2942】Knights of the Round Table(二分图 点双联通分量)

    题目链接 大意 给定\(N\)个点与\(M\)个关系,每个关系表示某两个点间没有直接的边相连,求不在所有奇环上的点的个数. (\(1\le N\le 1e3,1\le M\le 1e6\)) 思路 考 ...

  3. Git重命名远程分支

    一.重命名本地分支 将hot_fix分支重命名为bug_fix git branch -m hot_fix bug_fix 二.推送重命名后的本地分支到远程仓库 git push origin bug ...

  4. Feign 动态URL 解决记录

    Feign中使用动态URL请求 (应当是spring-cloud-starter-openfeign,不知道和一般的feign有何差别) 在spring项目下,假设有这样个Feign的消费接口,原来写 ...

  5. IDEA中快速排除maven中的依赖

    选中该模块 点击show dependenties 切换试图 选中要排除的依赖,右击 选择Execlude,然后选择需要在哪个模块添加排除依赖 完成

  6. const 和指针之间的姻缘

    const和指针到底有何姻缘呢? char const *p = NULL; //char const 和 const char 是一样的,p 是一个指向常整型的指针变量 ,指针变量的值不能改变 ch ...

  7. 最强端口扫描器Nmap

    实验目的 利用nmap命令探测出目标系统开放的端口和服务类型. 实验原理 Nmap是一个综合的.功能全面的端口扫描工具主机发现:nmap可以用来查找目标网络中在线主机.默认情况下,nmap通过icmp ...

  8. MM32F0020 GPIO驱动LED灯(MM32F0020 GPIO Toggle)

    目录: 1.MM32F0020简介 2.MM32F0020系统时钟配置 3.MM32F0020的GPIO外设配置及其初始化 4.使用官网的Systick定时器做延时 5.MM32F0020 GPIO驱 ...

  9. Oracle之表和字段的注释

    给表名加上注释 --给表名加上注释的语法结构 --语法结构:COMMENT ON TABLE 英文表名 IS '中文注释' COMMENT ON TABLE DEPT IS '部门表'; 给字段加上注 ...

  10. C# 重载运算符--不合理设计,只支持静态

    什么叫做重载运算符 比如:int x=1; int y=2; int total=x+y; 我们比较喜欢看上面这种写法,而不是这种, int x=1; int y=1; int total=int.a ...