Spring AOP调用本类方法没有生效的问题

栏目: IT技术 · 发布时间: 5年前

内容简介:首先请思考一下以下代码执行的结果:在执行在执行

背景

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

  • 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 方法里调用了 savesendEmail ,那 AOP 会处理 savesendEmail ,输出:

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 动态代理为例来演示一下上面的情况。

由于 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 方法中调用的 savesendEmail 方法同样的没有被动态代理拦截到,这是为什么呢,接下来就看看下动态代理的底层实现。

动态代理原理

其实动态代理就是在运行期间动态的生成了一个 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.savethis.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);
  }
}

附录


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2006-12-28 / USD 59.95

Web mining aims to discover useful information and knowledge from the Web hyperlink structure, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is n......一起来看看 《Web Data Mining》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具