Phân tích mã nguồn Spring - Cơ chế lắng nghe sự kiện

Gói context trong Spring là sự mở rộng của chức năng gói beans, ví dụ như mở rộng container cơ bản BeanFactory thành ngữ cảnh ApplicationContext. ApplicationContext không chỉ chứa toàn bộ chức năng cơ bản của BeanFactory mà còn cung cấp nhiều chức năng mở rộng khác. Bài viết này sẽ phân tích cơ chế lắng nghe sự kiện mà Spring cung cấp, sử dụng thiết kế mẫu quan sát (observer pattern). Không dài dòng, chúng ta bắt đầu ngay vào nội dung chính.

I. Định nghĩa cơ chế lắng nghe sự kiện trong Spring

Những ai đã sử dụng MQ hoặc hiểu về thiết kế mẫu quan sát có thể biết rằng để thực hiện cơ chế lắng nghe sự kiện, cần có bốn thành phần cốt lõi: sự kiện, nhà sản xuất sự kiện và người tiêu dùng sự kiện, cùng với một bộ điều khiển quản lý mối quan hệ đăng ký lắng nghe giữa nhà sản xuất, người tiêu dùng và sự kiện.

Trong Spring, cơ chế lắng nghe sự kiện được thực hiện chủ yếu thông qua sự kiện, trình lắng nghe sự kiện, nhà phát hành sự kiện và bộ phát sóng sự kiện.

1.1. Sự kiện trong Spring (ApplicationEvent)

Trong Spring, có một lớp trừu tượng ApplicationEvent làm lớp cha cho tất cả các sự kiện, lớp này chứa tham chiếu đến ApplicationContext hiện tại, từ đó có thể xác định mỗi sự kiện được tạo ra từ container Spring nào.

1.2. Trình lắng nghe sự kiện trong Spring (ApplicationListener)

Trình lắng nghe sự kiện trong Spring cũng có một giao diện cấp cao nhất là ApplicationListener, chỉ có một phương thức onApplicationEvent(E event). Khi sự kiện mà trình lắng nghe này đang lắng nghe xảy ra, phương thức này sẽ được thực thi.

1.3. Nhà phát hành sự kiện trong Spring (ApplicationEventPublisher)

Nhà phát hành sự kiện trong Spring cũng có một giao diện cấp cao nhất là ApplicationEventPublisher, chỉ có một phương thức publishEvent(Object event). Gọi phương thức này có thể kích hoạt sự kiện trong Spring.

1.4. Bộ phát sóng sự kiện trong Spring (ApplicationEventMulticaster)

Bộ điều khiển cốt lõi của sự kiện trong Spring được gọi là bộ phát sóng sự kiện, giao diện là ApplicationEventMulticaster. Bộ phát sóng có hai chức năng chính:

Chức năng một: Đăng ký trình lắng nghe sự kiện vào bộ phát sóng, như vậy bộ phát sóng sẽ biết trình lắng nghe nào đang lắng nghe sự kiện gì, và biết sự kiện nào có những trình lắng nghe nào đang lắng nghe.

Chức năng hai: Phát sóng sự kiện đến trình lắng nghe sự kiện. Khi có sự kiện xảy ra, cần thông qua bộ phát sóng để phát sóng đến tất cả các trình lắng nghe sự kiện, vì nhà sản xuất chỉ cần quan tâm đến việc sản xuất sự kiện, không cần quan tâm đến sự kiện này được tiêu thụ bởi những trình lắng nghe nào.

II. Sử dụng cơ chế lắng nghe sự kiện trong Spring

Hãy lấy hệ thống thông báo làm ví dụ. Giả sử có một tình huống như sau: Khi một người dùng đăng ký dịch vụ thành công, lúc này cần thực hiện nhiều thao tác như gửi email xác nhận, cập nhật thông tin trong cơ sở dữ liệu, và nếu có sử dụng mã giảm giá thì cần cập nhật trạng thái của mã giảm giá. Về cơ bản, một thao tác đăng ký dịch vụ cần thực hiện ba thao tác cập nhật dữ liệu. Ba thao tác này thực tế lại không có mối liên quan gì với nhau, vì vậy có thể sử dụng ba trình lắng nghe sự kiện đăng ký dịch vụ để xử lý logic kinh doanh tương ứng, lúc này có thể sử dụng cơ chế lắng nghe sự kiện của Spring để mô phỏng thực hiện tình huống này.

  1. Đầu tiên, định nghĩa một sự kiện đăng ký dịch vụ ServiceEvent, sự kiện này chứa mã đăng ký, mã dịch vụ và mã giảm giá sử dụng, mã nguồn như sau:
 1 /**
 2  * @Desc: Sự kiện đăng ký dịch vụ tùy chỉnh
 3  */
 4 public class ServiceEvent extends ApplicationEvent {
 5 
 6     /** Mã đăng ký*/
 7     private String registrationCode;
 8     /** Mã dịch vụ*/
 9     private String serviceCode;
10     /** Mã giảm giá*/
11     private String discountCode;
12 
13     /** Hàm tạo sự kiện*/
14     public ServiceEvent(ApplicationContext source, String registrationCode, String serviceCode, String discountCode) {
15         super(source);
16         this.registrationCode = registrationCode;
17         this.serviceCode = serviceCode;
18         this.discountCode = discountCode;
19     }
20 }
  1. Định nghĩa lần lượt trình lắng nghe đăng ký, trình lắng nghe dịch vụ và trình lắng nghe giảm giá để lắng nghe sự kiện đăng ký dịch vụ, xử lý tương ứng, mã nguồn như sau:
 1 /**
 2   * @Desc: Trình lắng nghe đăng ký (dùng để lưu thông tin đăng ký)
 3   */
 4 public class RegistrationListener implements ApplicationListener<ServiceEvent>  {
 5 
 6     @Override
 7     public void onApplicationEvent(ServiceEvent event) {
 8         System.out.println("Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:" + event.getRegistrationCode());
 9         //TODO Xử lý lưu thông tin đăng ký
10     }
11 }
 1 /**
 2  * @Desc: Trình lắng nghe dịch vụ (dùng để cập nhật thông tin dịch vụ)
 3  */
 4 public class ServiceListener implements ApplicationListener<ServiceEvent> {
 5 
 6     @Override
 7     public void onApplicationEvent(ServiceEvent event) {
 8         System.out.println("Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:" + event.getServiceCode());
 9         //TODO Xử lý cập nhật dịch vụ
10     }
11 }
/**
 * @Desc: Trình lắng nghe giảm giá (dùng để sử dụng mã giảm giá)
 */
public class DiscountListener implements ApplicationListener<ServiceEvent> {

    @Override
    public void onApplicationEvent(ServiceEvent event) {
        if(event.getDiscountCode()!=null) {
            System.out.println("Trình lắng nghe giảm giá nhận được sự kiện đăng ký, mã giảm giá là:" + event.getDiscountCode());
            //TODO Xử lý sử dụng mã giảm giá
        }else {
            System.out.println("Đăng ký:"+event.getRegistrationCode()+"không sử dụng mã giảm giá");
        }
    }
}
  1. Nhà phát hành sự kiện và bộ phát sóng sự kiện không cần định nghĩa tùy chỉnh, sử dụng mặc định của Spring là được.

  2. Bây giờ sự kiện, trình lắng nghe sự kiện, nhà phát hành sự kiện và bộ phát sóng sự kiện đã định nghĩa xong, tiếp theo chỉ cần đăng ký mối quan hệ lắng nghe giữa trình lắng nghe sự kiện và sự kiện vào container Spring là được, cách thực hiện rất đơn giản, thực chất là đăng ký trình lắng nghe sự kiện như một bean vào container Spring, như sau:

1 <beans>
2    
3    <!-- Các bean khác -->
4 
5     <bean id="registrationListener" class="com.example.spring.event.RegistrationListener"/>
6     <bean id="serviceListener" class="com.example.spring.event.ServiceListener"/>
7     <bean id="discountListener" class="com.example.spring.event.DiscountListener"/>
8 </beans>
  1. Mã kiểm tra như sau:
 1 public static void main(String[] args){
 2         /** 1. Khởi tạo container Spring */
 3         ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
 4         /** 2. Tạo sự kiện đăng ký dịch vụ*/
 5         for (int i=0;i < 5;i++){
 6             String registrationCode = "REG_" + i;
 7             String serviceCode = "SVC_" + i;
 8             String discountCode = null;
 9             if(i%2==0) {
10                 // Số chẵn sử dụng mã giảm giá
11                 discountCode = "DISC_" + i;
12             }
13             ServiceEvent serviceEvent = new ServiceEvent(context, registrationCode, serviceCode, discountCode);
14             /**3. ApplicationContext thực hiện giao diện ApplicationEventPublisher, nên có thể trực tiếp sử dụng ApplicationContext để gửi sự kiện*/
15             context.publishEvent(serviceEvent);
16         }
17     }

Mã kiểm tra rất đơn giản, bước đầu tiên là khởi tạo container Spring, bước thứ hai là tạo sự kiện đăng ký dịch vụ, bước thứ ba là gửi sự kiện trực tiếp thông qua ApplicationContext, lúc này ba trình lắng nghe sự kiện có thể lắng nghe được sự kiện đăng ký và xử lý, kết quả kiểm tra như sau:

 1 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_0
 2 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_0
 3 Trình lắng nghe giảm giá nhận được sự kiện đăng ký, mã giảm giá là:DISC_0
 4 
 5 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_1
 6 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_1
 7 Đăng ký:REG_1không sử dụng mã giảm giá
 8 
 9 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_2
10 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_2
11 Trình lắng nghe giảm giá nhận được sự kiện đăng ký, mã giảm giá là:DISC_2
12 
13 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_3
14 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_3
15 Đăng ký:REG_3không sử dụng mã giảm giá
16 
17 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_4
18 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_4
19 Trình lắng nghe giảm giá nhận được sự kiện đăng ký, mã giảm giá là:DISC_4

Lưu ý: Cơ chế lắng nghe sự kiện của Spring được xử lý đồng bộ, tức là nhà sản xuất phát hành sự kiện và người tiêu dùng tiêu thụ sự kiện được thực thi trên cùng một luồng, vì vậy trong ví dụ này, mặc dù sự kiện đăng ký được ba trình lắng nghe sự kiện lắng nghe riêng biệt, nhưng tổng thời gian thực thi phương thức không giảm đi, và nếu bất kỳ trình lắng nghe nào ném ra ngoại lệ, cũng sẽ ảnh hưởng đến các trình lắng nghe khác, vì vậy mỗi trình lắng nghe sự kiện khi xử lý tin nhắn phải bắt ngoại lệ sự kiện, hoặc chuyển thành xử lý bất đồng bộ bên trong.

Ví dụ, chuyển đổi trình lắng nghe như sau:

  1. In luồng hiện tại tại thời điểm phát hành và tiêu thụ sự kiện; 2. Trình lắng nghe giảm giá không thực hiện kiểm tra null, sẽ dẫn đến ngoại lệ khi discountCode là null, kết quả kiểm tra như sau:
 1 Luồng phát hành sự kiện là:main
 2 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_0, luồng là:main
 3 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_0, luồng là:main
 4 Trình lắng nghe giảm giá nhận được sự kiện đăng ký, cập nhật giảm giá:null, luồng là:main
 5 Exception in thread "main" java.lang.NullPointerException
 6     at com.example.spring.event.DiscountListener.onApplicationEvent(DiscountListener.java:15)
 7     at com.example.spring.event.DiscountListener.onApplicationEvent(DiscountListener.java:10)
 8     at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
 9     at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
10     at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
11     at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403)
12     at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
13     at com.example.spring.MainTest.main(MainTest.java:29)

Từ kết quả kiểm tra có thể thấy, luồng phát hành sự kiện và tất cả các trình lắng nghe tiêu thụ sự kiện đều thực thi trên cùng một luồng, trong ví dụ này là luồng main, vì vậy toàn bộ cơ chế lắng nghe sự kiện của Spring thực chất là hoạt động đồng bộ, đều do luồng phát hành sự kiện xử lý.

Hơn nữa, một khi trình lắng nghe nào đó ném ra ngoại lệ, tương đương với toàn bộ luồng đều bị dừng, không chỉ các trình lắng nghe sau không thể tiêu thụ sự kiện, mà cả luồng phát hành sự kiện cũng bị ảnh hưởng. Khi logic kinh doanh giữa nhiều trình lắng nghe không ảnh hưởng lẫn nhau, có thể sử dụng phương án tối ưu, như hình minh họa:

Lúc này có thể chuyển trình lắng nghe sự kiện thành cách xử lý bất đồng bộ, tức là tiếp nhận tin nhắn sự kiện đồng bộ, nhưng xử lý logic kinh doanh chuyển thành xử lý bất đồng bộ, ví dụ trình lắng nghe đăng ký trong ví dụ trên được chuyển đổi như sau:

 1 public class RegistrationListener implements ApplicationListener<ServiceEvent>  {
 2 
 3     @Override
 4     public void onApplicationEvent(ServiceEvent event) {
 5         /** Đảm bảo trình lắng nghe sự kiện không ném ra ngoại lệ thông qua try/catch*/
 6         try {
 7             new Thread(new Runnable() {
 8                 @Override
 9                 public void run() {
10                     System.out.println("Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:" + event.getRegistrationCode() + ", luồng là:" + Thread.currentThread().getName());
11                     //TODO Xử lý lưu thông tin đăng ký
12                 }
13             }).start();
14         }catch (Exception e){
15             e.printStackTrace();
16         }
17     }
18 }

Bằng cách tạo luồng mới để thực thi logic kinh doanh bất đồng bộ, hoặc giao logic kinh doanh cho pool luồng thực thi cũng được.

  1. Đối với cùng một sự kiện nếu có nhiều trình lắng nghe sự kiện, vì là đồng bộ, chắc chắn sẽ có sự khác biệt về thứ tự thực thi, Spring mặc định thứ tự thực thi sự kiện là theo thứ tự tải bean, ví dụ trong ví dụ này, thứ tự cấu hình trong XML là RegistrationListener -> ServiceListener -> DiscountListener,

Thứ tự thực thi cuối cùng cũng là thứ tự này, nhưng rõ ràng cách sắp xếp ngầm định này rất dễ khiến các nhà phát triển bỏ qua, vì vậy Spring cung cấp thêm cách sắp xếp, đó là để trình lắng nghe thực hiện giao diện Ordered hoặc giao diện con của Ordered là PriorityOrdered

Giao diện Ordered chỉ có một phương thức

/** Ưu tiên cao nhất*/
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

/** Ưu tiên thấp nhất*/
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

/** Lấy giá trị ưu tiên, giá trị越小 ưu tiên càng cao*/
int getOrder();

Giao diện con PriorityOrdered của Ordered kế thừa từ giao diện Ordered nhưng không có bất kỳ phương thức thực hiện nào, tức là vai trò của PriorityOrdered chỉ là một vai trò đánh dấu.

Trong cơ chế lắng nghe sự kiện của Spring, thứ tự ưu tiên là ưu tiên sắp xếp theo PriorityOrdered trước, sau đó mới sắp xếp theo Ordered, cuối cùng sắp xếp theo thứ tự tải bean.

Ví dụ với ba trình lắng nghe trong ví dụ trên, vì RegistrationListener được tải đầu tiên nên mặc định là thực thi đầu tiên, lúc này chúng ta lần lượt thực hiện giao diện Ordered và PriorityOrdered trên ServiceListener và DiscountListener, như sau:

1 public class ServiceListener implements ApplicationListener<ServiceEvent>, Ordered {
2     @Override
3     public int getOrder() {
4         /** Sử dụng ưu tiên cao nhất*/
5         return Ordered.HIGHEST_PRECEDENCE;
6     }
7 }
1 public class DiscountListener implements ApplicationListener<ServiceEvent>, PriorityOrdered {
2 
3     @Override
4     public int getOrder() {
5         /**Sử dụng ưu tiên thấp nhất*/
6         return Ordered.LOWEST_PRECEDENCE;
7     }
8 }

Ở đây ServiceListener thực hiện giao diện Ordered, ưu tiên là ưu tiên cao nhất; DiscountListener thực hiện giao diện PriorityOrdered, đặt ưu tiên là ưu tiên thấp nhất, kết quả thực thi như sau:

1 Trình lắng nghe giảm giá nhận được sự kiện đăng ký, cập nhật giảm giá:REG_0
2 Trình lắng nghe dịch vụ nhận được sự kiện đăng ký, cập nhật dịch vụ:SVC_0
3 Trình lắng nghe đăng ký nhận được sự kiện đăng ký, mã đăng ký là:REG_0

Có thể thấy trình lắng nghe thực hiện giao diện PriorityOrdered là DiscountListener thực thi đầu tiên, trình lắng nghe thực hiện giao diện Ordered là ServiceListener thực thi thứ hai, trình lắng nghe không thực hiện giao diện sắp xếp là RegistrationListener thực thi cuối cùng.

Ở đây mặc dù cả DiscountListener và ServiceListener đều thực hiện phương thức getOrder(), và ServiceListener đặt là ưu tiên cao nhất, DiscountListener đặt là ưu tiên thấp nhất, nhưng vẫn sẽ thực thi DiscountListener trước, đây chính là ưu điểm của việc thực hiện giao diện PriorityOrdered.

Trình lắng nghe thực hiện giao diện PriorityOrdered, bất kể giá trị ưu tiên như thế nào, chắc chắn sẽ thực thi trước trình lắng nghe thực hiện giao diện Ordered, cũng chính là lý do giao diện PriorityOrdered không có bất kỳ phương thức thực hiện nào, giao diện này chỉ để đánh dấu, đánh dấu trình lắng nghe này được thực thi ưu tiên nhất.

III. Nguyên lý thực thi cơ chế lắng nghe sự kiện trong Spring

Qua ví dụ trên, đã hiểu được logic xử lý của sự kiện và trình lắng nghe sự kiện, nhưng vẫn còn nhiều vấn đề cần khám phá, chẳng hạn như quá trình phát hành sự kiện, trình lắng nghe như thế nào để lắng nghe tin nhắn v.v., lúc này cần bắt đầu từ việc khởi tạo ApplicationContext.

Khi ApplicationContext khởi tạo, có hai bước cốt lõi liên quan đến trình lắng nghe sự kiện, một là khởi tạo bộ phát sóng sự kiện, hai là đăng ký tất cả các trình lắng nghe sự kiện

 1 /** Các quy trình khác*/ 
 2 
 3 /** Khởi tạo bộ phát sóng sự kiện*/
 4 initApplicationEventMulticaster();
 5         
 6 /** Các quy trình khác*/
 7 
 8 /** Đăng ký trình lắng nghe sự kiện*/
 9 registerListeners();
10 
11 /** Các quy trình khác*/

3.1. Phân tích mã nguồn khởi tạo bộ phát sóng sự kiện

 1 /** Đối tượng bộ phát sóng sự kiện của container Spring*/
 2     private ApplicationEventMulticaster applicationEventMulticaster;
 3 
 4     /** beanName tương ứng với bộ phát sóng sự kiện*/
 5     public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";
 6 
 7     /** Khởi tạo bộ phát sóng sự kiện*/
 8     protected void initApplicationEventMulticaster() {
 9         //1. Lấy đối tượng BeanFactory của container Spring
10         ConfigurableListableBeanFactory beanFactory = getBeanFactory();
11         //2. Lấy bean của bộ phát sóng sự kiện từ BeanFactory, nếu tồn tại thì là bộ phát sóng sự kiện tùy chỉnh của người dùng
12         if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
13             //2.1. Gán giá trị cho bộ phát sóng sự kiện của container
14             this.applicationEventMulticaster =
15                     beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
16             if (logger.isTraceEnabled()) {
17                 logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
18             }
19         }
20         else {
21             //3. Nếu không có tùy chỉnh, thì khởi tạo đối tượng bộ phát sóng sự kiện mặc định SimpleApplicationEventMulticaster
22             this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
23             //4. Đăng ký bean này
24             beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
25             if (logger.isTraceEnabled()) {
26                 logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
27                         "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
28             }
29         }
30     }

Từ mã nguồn có thể thấy, logic khởi tạo bộ phát sóng sự kiện khá đơn giản, chính là quá trình gán giá trị cho đối tượng bộ phát sóng sự kiện applicationEventMulticaster của container, nếu trong beanFactory tồn tại bean dùng để tùy chỉnh thì sử dụng tùy chỉnh, nếu không có tùy chỉnh thì tạo mới bộ phát sóng sự kiện mặc định SimpleApplicationEventMulticaster, sau đó gán giá trị cho đối tượng applicationEventMulticaster.

3.2. Phân tích mã nguồn đăng ký trình lắng nghe sự kiện

 1 /** Đăng ký trình lắng nghe sự kiện*/
 2     protected void registerListeners() {
 3         //1. Duyệt qua các trình lắng nghe sự kiện được tạo bằng cách mã hóa, thêm vào bộ phát sóng sự kiện
 4         for (ApplicationListener<?> listener : getApplicationListeners()) {
 5             //2. Lấy bộ phát sóng sự kiện hiện tại, thêm trình lắng nghe sự kiện
 6 <strong>            getApplicationEventMulticaster().addApplicationListener(listener);
</strong> 7         }
 8 
 9         //3. Lấy tất cả các bean thực hiện giao diện ApplicationListener từ BeanFactory, duyệt qua và thêm vào bộ phát sóng sự kiện
10         String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
11         for (String listenerBeanName : listenerBeanNames) {
12             getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
13         }
14 
15         //3. Lấy các sự kiện cần phát hành sớm
16         Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
17         this.earlyApplicationEvents = null;
18         if (earlyEventsToProcess != null) {
19             for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
20                 //5. Duyệt qua và phát sóng các sự kiện cần phát hành sớm
21                 getApplicationEventMulticaster().multicastEvent(earlyEvent);
22             }
23         }

Từ mã nguồn có thể thấy, logic đăng ký trình lắng nghe sự kiện cũng không phức tạp, chủ yếu là tìm tất cả các trình lắng nghe sự kiện từ container, sau đó gọi phương thức addApplicationListener của bộ phát sóng sự kiện để thêm trình lắng nghe sự kiện vào bộ phát sóng sự kiện, tiếp theo lại xem logic thêm vào của bộ phát sóng sự kiện, mã nguồn như sau:

 1 @Override
 2     public void addApplicationListener(ApplicationListener<?> listener) {
 3         synchronized (this.retrievalMutex) {
 4             // Thêm trình lắng nghe sự kiện vào bộ sưu tập trình lắng nghe bên trong applicationListeners
 5             Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
 6             if (singletonTarget instanceof ApplicationListener) {
 7                 this.defaultRetriever.applicationListeners.remove(singletonTarget);
 8             }
 9             this.defaultRetriever.applicationListeners.add(listener);
10             this.retrieverCache.clear();
11         }
12     }

Có thể thấy logic đăng ký khá đơn giản, chính là thêm đối tượng Listener vào bộ sưu tập bên trong của bộ phát sóng sự kiện để lưu trữ, như vậy bộ phát sóng sự kiện đã lưu trữ tất cả các trình lắng nghe sự kiện trong container.

3.3. Nguyên lý phát hành và tiêu thụ sự kiện

Sự kiện được phát hành thông qua phương thức publishEvent của lớp thực hiện ApplicationEventPublisher, ApplicationContext chính là lớp thực hiện giao diện này, vì vậy khi sử dụng Spring có thể trực tiếp sử dụng thể hiện ApplicationContext để gọi phương thức publishEvent để phát hành sự kiện, mã nguồn như sau:

 1 /** Phát hành sự kiện
 2      * @param event: Đối tượng sự kiện
 3      *  */
 4     @Override
 5     public void publishEvent(Object event) {
 6         publishEvent(event, null);
 7     }
 8 
 9     /** Phát hành sự kiện
10      * @param event: Đối tượng sự kiện
11      * @param eventType: Loại sự kiện
12      * */
13     protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
14         Assert.notNull(event, "Event must not be null");
15 
16         /** 1. Đóng gói sự kiện phát hành thành đối tượng ApplicationEvent (vì tham số truyền vào là Object, có thể không kế thừa ApplicationEvent) */
17         ApplicationEvent applicationEvent;
18         if (event instanceof ApplicationEvent) {
19             applicationEvent = (ApplicationEvent) event;
20         }
21         else {
22             applicationEvent = new PayloadApplicationEvent<>(this, event);
23             if (eventType == null) {
24                 eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
25             }
26         }
27 
28         if (this.earlyApplicationEvents != null) {
29             /** 2.1. Nếu các sự kiện cần phát hành sớm chưa được phát hành xong, thì không phát hành ngay lập tức, mà thêm sự kiện vào bộ sưu tập chờ phát hành*/
30             this.earlyApplicationEvents.add(applicationEvent);
31         }
32         else {
33             /** 2.2. Lấy bộ phát sóng sự kiện hiện tại, gọi phương thức multicasterEvent để phát sóng sự kiện*/
34 <strong>            getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
</strong>35         }
36 
37         /** 3. Nếu applicationContext có lớp cha, thì gọi lại phương thức publishEvent của lớp cha*/
38         if (this.parent != null) {
39             if (this.parent instanceof AbstractApplicationContext) {
40                 ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
41             }
42             else {
43                 this.parent.publishEvent(event);
44             }
45         }
46     }

Thực tế logic ở đây cũng khá đơn giản, trước tiên là chuyển sự kiện phát hành thành đối tượng ApplicationEvent, sau đó lấy bộ phát sóng sự kiện, gọi phương thức multicastEvent của bộ phát sóng sự kiện để phát sóng sự kiện, vì vậy logic cốt lõi lại quay về bộ phát sóng sự kiện

 1 /** Phát sóng sự kiện
 2      * @param event: Sự kiện
 3      * @param eventType: Loại sự kiện
 4      * */
 5     @Override
 6     public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
 7         ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
 8         <strong>Executor executor = getTaskExecutor();(Nếu có Executor, thì phát sóng sự kiện là xử lý bất đồng bộ)
</strong>9         /**
10          * 1. Dựa vào sự kiện và loại gọi phương thức getApplicationListeners để lấy tất cả các trình lắng nghe sự kiện lắng nghe sự kiện này
11          * */
12         for (ApplicationListener<?> listener : <strong>getApplicationListeners(event, type)</strong>) {
13             if (executor != null) {
14                 /** 2. Duyệt qua bất đồng bộ thực hiện phương thức invokeListener để đánh thức trình lắng nghe xử lý sự kiện */
15                <strong> executor.execute(() -> invokeListener(listener, event));
</strong>16             }
17             else {
18 <strong>                invokeListener(listener, event);
</strong>19             }
20         }
21     }

Ở đây chủ yếu có hai bước cốt lõi, đầu tiên là dựa vào sự kiện và loại để tìm tất cả các trình lắng nghe sự kiện đã lắng nghe sự kiện này; sau đó duyệt qua để thực hiện logic xử lý của trình lắng nghe. Ngoài ra, nếu đã cấu hình Executor, sẽ sử dụng Executor để phát hành sự kiện bất đồng bộ đến trình lắng nghe.

  1. Dựa vào sự kiện lấy trình lắng nghe sự kiện, mã nguồn như sau:
 1 protected Collection<ApplicationListener<?>> getApplicationListeners(
 2             ApplicationEvent event, ResolvableType eventType) {
 3 
 4         Object source = event.getSource();
 5         Class<?> sourceType = (source != null ? source.getClass() : null);
 6         ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
 7 
 8         // Kiểm tra nhanh cho mục nhập hiện có trên ConcurrentHashMap...
 9         ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
10         if (retriever != null) {
11             return retriever.getApplicationListeners();
12         }
13 
14         if (this.beanClassLoader == null ||
15                 (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
16                         (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
17             // Đồng bộ hoàn toàn xây dựng và lưu trữ một ListenerRetriever
18             synchronized (this.retrievalMutex) {
19                 retriever = this.retrieverCache.get(cacheKey);
20                 if (retriever != null) {
21                     return retriever.getApplicationListeners();
22                 }
23                 retriever = new ListenerRetriever(true);
24                 <strong>Collection<ApplicationListener<?>> listeners =
25                         retrieveApplicationListeners(eventType, sourceType, retriever);
26                 this.retrieverCache.put(cacheKey, retriever);
27                 return listeners;
</strong>28             }
29         }
30         else {
31             // Không lưu trữ ListenerRetriever -> không cần đồng bộ hóa
32             return retrieveApplicationListeners(eventType, sourceType, null);
33         }
34     }

Ở đây phương thức cốt lõi là retrieveApplicationListeners(eventType, sourceType, retriever), mã nguồn như sau:

 1 private Collection<ApplicationListener<?>> retrieveApplicationListeners(
 2             ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {
 3 
 4         List<ApplicationListener<?>> allListeners = new ArrayList<>();
 5         Set<ApplicationListener<?>> listeners;
 6         Set<String> listenerBeans;
 7         synchronized (this.retrievalMutex) {<br></br>               /** Khởi tạo tất cả các trình lắng nghe sự kiện, lưu vào bộ sưu tập*/
 8             listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
 9             listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
10         }
11 
12         // Thêm các trình lắng nghe được đăng ký bằng cách mã hóa, bao gồm cả những cái đến từ
13         // Duyệt qua tất cả các trình lắng nghe sự kiện, gọi supportsEvent để xác định có lắng nghe sự kiện này không
14         for (ApplicationListener<?> listener : listeners) {
15             if (<strong>supportsEvent(listener, eventType, sourceType)</strong>) {
16                 if (retriever != null) {
17                     retriever.applicationListeners.add(listener);
18                 }<br></br>                   /** Nếu trình lắng nghe lắng nghe sự kiện hiện tại, thì thêm vào bộ sưu tập trình lắng nghe*/
19 <strong>                allListeners.add(listener);
</strong>20             }
21         }
22 
23         // Thêm trình lắng nghe theo tên bean, có thể chồng chéo với trình lắng nghe được đăng ký bằng cách mã hóa ở trên
24         // nhưng ở đây có thể có thêm siêu dữ liệu.
25         if (!listenerBeans.isEmpty()) {
26             ConfigurableBeanFactory beanFactory = getBeanFactory();
27                        //
28             for (String listenerBeanName : listenerBeans) {
29                 try {
30                     if (supportsEvent(beanFactory, listenerBeanName, eventType)) {
31                         ApplicationListener<?> listener =
32                                 beanFactory.getBean(listenerBeanName, ApplicationListener.class);
33                         if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
34                             if (retriever != null) {
35                                 if (beanFactory.isSingleton(listenerBeanName)) {
36                                     retriever.applicationListeners.add(listener);
37                                 }
38                                 else {
39                                     retriever.applicationListenerBeans.add(listenerBeanName);
40                                 }
41                             }
42                             allListeners.add(listener);
43                         }
44                     }
45                     else {
46                         // Loại bỏ các trình lắng nghe không khớp ban đầu đến từ
47                         // ApplicationListenerDetector, có thể bị loại bỏ bởi siêu dữ liệu
48                         // BeanDefinition bổ sung (ví dụ: phương thức factory generics) ở trên.
49                         Object listener = beanFactory.getSingleton(listenerBeanName);
50                         if (retriever != null) {
51                             retriever.applicationListeners.remove(listener);
52                         }
53                         allListeners.remove(listener);
54                     }
55                 }
56                 catch (NoSuchBeanDefinitionException ex) {
57                     // Thể hiện trình lắng nghe singleton (không có định nghĩa bean hỗ trợ) đã biến mất -
58                     // có thể đang ở giữa giai đoạn hủy
59                 }
60             }
61         }
62 
63                 /** Sắp xếp tất cả các trình lắng nghe theo Order*/
64 <strong>        AnnotationAwareOrderComparator.sort(allListeners);
</strong>65         if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
66             retriever.applicationListeners.clear();
67             retriever.applicationListeners.addAll(allListeners);
68         }
69         return allListeners;
70     }

Mã khá nhiều, nhưng các bước cốt lõi thực chất chỉ có ba bước,

Bước một: Lấy tất cả các trình lắng nghe sự kiện trong bộ phát sóng sự kiện

Bước hai: Duyệt qua các trình lắng nghe sự kiện, xác định trình lắng nghe này có lắng nghe sự kiện hiện tại không

Bước ba: Sắp xếp tất cả các trình lắng nghe sự kiện lắng nghe sự kiện hiện tại

Trong đó bước hai xác định trình lắng nghe có lắng nghe sự kiện hay không, chủ yếu là thông qua reflection lấy giao diện generic của trình lắng nghe thực hiện, nếu chứa lớp của sự kiện hiện tại thì có nghĩa là lắng nghe, ngược lại thì không có nghĩa là lắng nghe.

  1. Đánh thức trình lắng nghe xử lý sự kiện, mã nguồn như sau:
 1 protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
 2         ErrorHandler errorHandler = getErrorHandler();
 3         if (errorHandler != null) {
 4             try {
 5                 /** Gọi phương thức doInvokeListener*/
 6                 doInvokeListener(listener, event);
 7             }
 8             catch (Throwable err) {
 9                 errorHandler.handleError(err);
10             }
11         }
12         else {
13             /** Gọi phương thức doInvokeListener*/
14             doInvokeListener(listener, event);
15         }
16     }
 1 private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
 2         try {
 3 /** Gọi trực tiếp phương thức onApplicationEvent(event) của ApplicationListener*/
 4 <strong>            listener.onApplicationEvent(event);
</strong> 5         }
 6         catch (ClassCastException ex) {
 7             String msg = ex.getMessage();
 8             if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
 9                 // Có thể là trình lắng nghe được định nghĩa bằng lambda mà chúng ta không thể giải quyết được kiểu sự kiện generic
10                 // -> hãy bỏ qua ngoại lệ và chỉ ghi lại một thông báo debug.
11                 Log logger = LogFactory.getLog(getClass());
12                 if (logger.isTraceEnabled()) {
13                     logger.trace("Non-matching event type for listener: " + listener, ex);
14                 }
15             }
16             else {
17                 throw ex;
18             }
19         }
20     }

Có thể thấy logic đánh thức khá đơn giản, trực tiếp gọi phương thức onApplicationEvent(E event) của trình lắng nghe là được.

Thẻ: Spring Framework applicationevent Observer Pattern Event-Driven Architecture

Đăng vào ngày 6 tháng 6 lúc 22:54