spring event機制

小強Zzz發表於2023-02-15

前言

在前端中我們經常使用事件機制,如滑鼠點選事件等,而在java中,java也為我們設定了事件機制。
我們先模仿前端的事件機制,使用程式碼進行簡單實現滑鼠點選事件。

class Mouse {
  private List<mouselistener> listeners = new ArrayList<mouselistener>();
  private int listenerCallbacks = 0;
   
  public void addListenerCallback() {
    listenerCallbacks++;
  }
   
  public int getListenerCallbacks() {
    return listenerCallbacks;
  }
   
  public void addListener(MouseListener listener) {
    listeners.add(listener);
  }
   
  public void click() {
    System.out.println("Clicked !");
    for (MouseListener listener : listeners) {
      listener.onClick(this);
    }
  }
}
 
interface MouseListener {
  public void onClick(Mouse source);
}

public class EventBasedTest {
 
  @Test
  public void test() {
    Mouse mouse = new Mouse();
    mouse.addListener(new MouseListener() {
      @Override
      public void onClick(Mouse mouse) {
        System.out.println("Listener#1 called");
        mouse.addListenerCallback();
      }
    });
    mouse.addListener(new MouseListener() {
      @Override
      public void onClick(Mouse mouse) {
        System.out.println("Listener#2 called");
        mouse.addListenerCallback();
      }
    });
    mouse.click();
  }
}

列印輸出如下所示

Clicked !
Listener#1 called
Listener#2 called

準備

實現java的Event需要實現三部分

  • 事件類:事件的載體。
  • 釋出者:釋出事件。
  • 監聽者:監聽並處理事件。

我們透過一個業務場景來具體實現java事件。假如我們有個需求,使用者建立成功後給使用者傳送一個郵件。這裡有兩個事情要做:

  • 建立使用者
  • 給使用者傳送郵件
    這個很簡單。

    @Service
    public class EmailService {
    
        @Transactional
        public void sendEmail(String email) {
          //send email
        }
    }
    
    
    @Service
    public class UserService {
        private final EmailService emailService;
        private final UserRepository userRepository;
    
        public UserService(EmailService emailService, UserRepository userRepository) {
          this.emailService = emailService;
          this.userRepository = userRepository;
        }
    
        @Transactional
        public User createUser(User user) {
          User newUser = this.userRepository.save(user);
          this.emailService.sendEmail(user.getEmail());
          return newUser;
        }
    }

    但這種實現是有問題的。我們想一下,這個功能的核心是建立使用者,而傳送郵件是一個副作用(傳送郵件不能影響使用者的建立),如果把這兩個操作放在一個事務中會有什麼問題?其實很明顯,如果建立使用者時丟擲異常,事務回滾,方法提前退出,那麼也不會傳送郵件,這是正常的。但是下面兩個場景是不可接受的:

    • 如果郵件傳送失敗,事務發生回滾,使用者建立失敗。
    • 如果郵件傳送成功後,事務提交失敗,使用者收到了郵件,可是使用者建立失敗,這不是我們所希望的。

我們先透過event機制進行業務解耦,同時達到易擴充套件的原則。

事件類

事件類為事件的載體,也就是所釋出者釋出的東西和監聽者接收的東西都只能是事件,我們所要在釋出者和監聽者之間傳遞的東西,也定義在我們的自定義事件類裡。
只要繼承ApplicationEvent,便是一個事件類。

public class UserCreatedEvent {
    private final User user;

    public UserCreatedEvent(User user) {
        this.user = user;
    }
    public User getUser() {
        return user;
    }
}

釋出者

釋出者釋出事件類,我們可以呼叫ApplicationEventPublisherpublishEvent()方法釋出事件類。

@Service
public class CustomerService {

    private final UserRepository userRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    public CustomerService(UserRepository userRepository, ApplicationEventPublisher applicationEventPublisher) {
        this.userRepository = userRepository;
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @Transactional
    public Customer createCustomer(User user) {
        User newUser = this.userRepository.save(user);
        final UserCreatedEvent event = new UserCreatedEvent(newUser);
        this.applicationEventPublisher.publishEvent(event);
        return newUser;
    }
}

監聽者

監聽者接受釋出者釋出的事件並處理。
實現也很簡單,在方法上加入@EventListener註解並接收事件類引數。

@Component
public class UserCreatedEventListener {

    private final EmailService emailService;

    public UserCreatedEventListener(EmailService emailService) {
        this.emailService = emailService;
    }

    @EventListener
    public void processUserCreatedEvent(UserCreatedEvent event) {
        this.emailService.sendEmail(event.getUser().getEmail());
    }
}

如果我們有兩個監聽者,可以透過@Order註解來控制兩個監聽者的執行順序。

雖然我們用EventListener的方式解耦了業務程式碼,看著很高大上,但是有興趣研究原始碼就可以發現,這在底層還是一個普通的方法呼叫,這並沒有達到我們的目的。

如果我們在Listener方法上加@Async讓其非同步執行,這跟不同的非同步呼叫並無二異,可能會導致先傳送簡訊後提交事務。這也不符合我們提出的要求。

事務隔離

使用@TransactionalEventListener註解,TransactionalEventListener註解是對EventListener的增強,被註解的方法可以在事務的不同階段去觸發執行,如果事件未在啟用的事務中釋出,除非顯式設定了 fallbackExecution() 標誌為true,否則該事件將被丟棄;如果事務正在執行,則根據其 TransactionPhase 處理該事件。

  • AFTER_COMMIT - 預設設定,在事務提交後執行
  • AFTER_ROLLBACK - 在事務回滾後執行
  • AFTER_COMPLETION - 在事務完成後執行(不管是否成功)
  • BEFORE_COMMIT - 在事務提交前執行
@Component
public class UserCreatedEventListener {

    private final EmailService emailService;

    public UserCreatedEventListener(EmailService emailService) {
        this.emailService = emailService;
    }

    @TransactionalEventListener
    public void processUserCreatedEvent(UserCreatedEvent event) {
        this.emailService.sendEmail(event.getUser().getEmail());
    }
}

這樣,我們在事務提交後執行傳送郵件方法。即保證了傳送郵件功能不對儲存使用者功能進行影響,也不會使得傳送郵件功能和儲存使用者功能沒有關聯。

總結

對修改關閉,對擴充套件開放。
參考:
Spring5原始碼解析-Spring框架中的事件和監聽器TransactionalEventListener使用場景以及實現原理,最後要躲個大坑
Spring Event的初步講解
訂閱釋出模式和觀察者模式的區別

相關文章