Spring AOP調用本類方法沒有生效的問題

背景

首先請思考一下如下代碼執行的結果:html

  • 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方法後,控制檯輸出爲:java

after execute method:save

在執行sendEmail方法後,控制檯輸出爲:git

after execute method:sendEmail

請問在執行register()方法後會打印出什麼內容?github

反直覺

這個時候可能不少人都會和我以前想的同樣,在register方法裏調用了savesendEmail,那 AOP 會處理savesendEmail,輸出:spring

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

然而事實並非這樣的,而是輸出:jvm

after execute method:register

在這種認知的狀況下,極可能就會寫出有bug的代碼,例如:ide

@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方法的事務管理已經不生效了,在發生異常時就會出現問題。this

理解 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

相關文章
相關標籤/搜索