乐趣区

关于java:Spring-AOP调用本类方法没有生效的问题

背景

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

  • 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);
  }
}

附录

从 JVM 中拿到动静代理生成的 class 文件
aop-understanding-aop-proxies

退出移动版