日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

掌握Spring事件監聽器的內部邏輯與實現

來源: 責編: 時間:2023-11-01 09:18:58 319觀看
導讀1. 事件的層次傳播 在Spring中,ApplicationContext可以形成一個層次結構,通常由主容器和多個子容器組成。一個常見的疑問是:當一個事件在其中一個容器中發布時,這個事件會如何在這個層次結構中傳播? 為了探討這個問題,我

1. 事件的層次傳播

 在Spring中,ApplicationContext可以形成一個層次結構,通常由主容器和多個子容器組成。一個常見的疑問是:當一個事件在其中一個容器中發布時,這個事件會如何在這個層次結構中傳播?G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 為了探討這個問題,我們創建了一個名為HierarchicalEventPropagationEvent的事件類和一個對應的監聽器HierarchicalEventPropagationListener。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

全部代碼如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo.event;import org.springframework.context.ApplicationEvent;// 事件類public class HierarchicalEventPropagationEvent extends ApplicationEvent {    private String message;    public HierarchicalEventPropagationEvent(Object source, String message) {        super(source);        this.message = message;    }    public String getMessage() {        return message;    }}

相應地,為 HierarchicalEventPropagationEvent 定義一個監聽器HierarchicalEventPropagationListener :G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo.listener;import com.example.demo.event.HierarchicalEventPropagationEvent;import org.springframework.context.ApplicationListener;// 監聽器類public class HierarchicalEventPropagationListener implements ApplicationListener<HierarchicalEventPropagationEvent> {    private String listenerId;    public HierarchicalEventPropagationListener(String listenerId) {        this.listenerId = listenerId;    }    @Override    public void onApplicationEvent(HierarchicalEventPropagationEvent event) {        System.out.println(listenerId + " received event - " + event.getMessage());    }}

 為了測試繼承機制,我們需要構建主容器和子容器,并為每個容器注冊了一個監聽器。初始化容器后,我們在兩個容器中分別發布事件。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 請注意,首先需要刷新主容器,然后刷新子容器。否則會出現異常:Exception in thread "main" java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: org.springframework.context.annotation.AnnotationConfigApplicationContext@somehashcodeG8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

主程序如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

ackage com.example.demo;import com.example.demo.event.HierarchicalEventPropagationEvent;import com.example.demo.listener.HierarchicalEventPropagationListener;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class DemoApplication {    public static void main(String[] args) {        // 創建父容器,注冊監聽器        AnnotationConfigApplicationContext parentCtx = new AnnotationConfigApplicationContext();        parentCtx.addApplicationListener(new HierarchicalEventPropagationListener("Parent Listener"));        parentCtx.refresh();        // 創建子容器,注冊監聽器        AnnotationConfigApplicationContext childCtx = new AnnotationConfigApplicationContext();        childCtx.setParent(parentCtx);        childCtx.addApplicationListener(new HierarchicalEventPropagationListener("Child Listener"));        childCtx.refresh();        // 發布事件        HierarchicalEventPropagationEvent event1 = new HierarchicalEventPropagationEvent(parentCtx, "Event from parent");        parentCtx.publishEvent(event1);        HierarchicalEventPropagationEvent event2 = new HierarchicalEventPropagationEvent(childCtx, "Event from child");        childCtx.publishEvent(event2);    }}

運行結果

G8528資訊網——每日最新資訊28at.com


G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 主容器發布的事件只觸發了一次監聽,而子容器發布的事件觸發了兩次監聽。父容器和子容器都監聽到了來自子容器的事件,而只有父容器監聽到了來自父容器的事件。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 所以得出結論:在Spring的父子容器結構中,事件會從子容器向上傳播至其父容器,但父容器中發布的事件不會向下傳播至子容器。 這種設計可以幫助開發者在父容器中集中處理所有的事件,而不必擔心事件在多個子容器之間的傳播。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

2. PayloadApplicationEvent的使用

 PayloadApplicationEvent是Spring提供的一種特殊事件,用于傳遞數據(稱為"payload")。所以不需要自定義事件,PayloadApplicationEvent可以直接傳遞任何類型的數據,只需要指定它的類型即可。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

全部代碼如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

  • 定義監聽器

首先,我們來看怎樣定義一個監聽器來接收這個事件:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

通用監聽器 - 會監聽到所有種類的PayloadApplicationEvent:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo.listener;import org.springframework.context.ApplicationListener;import org.springframework.context.PayloadApplicationEvent;/** * 通用監聽器,能監聽到所有類型的PayloadApplicationEvent */public class CustomObjectApplicationListener implements ApplicationListener<PayloadApplicationEvent> {        @Override    public void onApplicationEvent(PayloadApplicationEvent event) {        System.out.println("收到PayloadApplicationEvent,數據是:" + event.getPayload());    }}

特定數據類型的監聽器 - 只會監聽指定類型的數據。例如,如果我們只對字符串數據感興趣,我們可以如此定義:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo.listener;import org.springframework.context.ApplicationListener;import org.springframework.context.PayloadApplicationEvent;/** * 特定數據類型的監聽器。這個監聽器專門監聽String類型的PayloadApplicationEvent */public class CustomStringApplicationListener implements ApplicationListener<PayloadApplicationEvent<String>> {        @Override    public void onApplicationEvent(PayloadApplicationEvent<String> event) {        System.out.println("收到了字符串數據:" + event.getPayload());    }}
  • 測試示例

要看這兩種監聽器如何工作,我們來寫一個測試。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo;import com.example.demo.listener.CustomObjectApplicationListener;import com.example.demo.listener.CustomStringApplicationListener;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import java.util.Date;public class DemoApplication {    public static void main(String[] args) {        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();        // 注冊監聽器        ctx.addApplicationListener(new CustomObjectApplicationListener());        ctx.addApplicationListener(new CustomStringApplicationListener());        ctx.refresh();        // 發送事件        ctx.publishEvent("Hello, World!");  // 發送一個字符串        ctx.publishEvent(2023);              // 發送一個整數        ctx.publishEvent(new Date());        // 發送一個日期對象    }}

在這個測試中,我們發送了三種類型的數據:一個字符串、一個整數和一個日期。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

執行結果如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

從輸出可以看出:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

第一種監聽器(通用的)接收到了所有三個事件,因為它不關心數據的具體類型。G8528資訊網——每日最新資訊28at.com

第二種監聽器(字符串專用的)只接收到了字符串類型的事件。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

3. 為什么選擇自定義事件?

 雖然PayloadApplicationEvent提供了簡化事件監聽的能力,但其可能不足以滿足特定的業務需求,尤其是當需要更多上下文和數據時。下面是一個使用自定義事件ArticlePublishedEvent的例子。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

全部代碼如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

自定義事件: ArticlePublishedEventG8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

這個事件代表了“新文章發布”,附帶有文章的標題、作者和發布日期等信息。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

package com.example.demo.event;import org.springframework.context.ApplicationEvent;public class ArticlePublishedEvent extends ApplicationEvent {    private String title;    private String author;    private String publishedDate;    public ArticlePublishedEvent(Object source, String title, String author, String publishedDate) {        super(source);        this.title = title;        this.author = author;        this.publishedDate = publishedDate;    }    public String getTitle() {        return title;    }    public String getAuthor() {        return author;    }    public String getPublishedDate() {        return publishedDate;    }}

自定義監聽器: ArticlePublishedListener

G8528資訊網——每日最新資訊28at.com

這個監聽器專門響應ArticlePublishedEvent,執行特定的業務邏輯,例如通知訂閱者、更新搜索索引等。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

import org.springframework.context.ApplicationListener;public class ArticlePublishedListener implements ApplicationListener<ArticlePublishedEvent> {    @Override    public void onApplicationEvent(ArticlePublishedEvent event) {        System.out.println("A new article has been published!");        System.out.println("Title: " + event.getTitle());        System.out.println("Author: " + event.getAuthor());        System.out.println("Published Date: " + event.getPublishedDate());        // Notify subscribers about the new article        notifySubscribers(event);        // Update search engine index with new article details        updateSearchIndex(event);        // Update statistical data about articles        updateStatistics(event);    }    private void notifySubscribers(ArticlePublishedEvent event) {        // Logic to notify subscribers (dummy logic for demonstration)        System.out.println("Notifying subscribers about the new article titled: " + event.getTitle());    }    private void updateSearchIndex(ArticlePublishedEvent event) {        // Logic to update search engine index (dummy logic for demonstration)        System.out.println("Updating search index with the new article titled: " + event.getTitle());    }    private void updateStatistics(ArticlePublishedEvent event) {        // Logic to update statistical data (dummy logic for demonstration)        System.out.println("Updating statistics with the new article titled: " + event.getTitle());    }}-----------------------------------?著作權歸作者所有:來自51CTO博客作者華為云開發者聯盟的原創作品,請聯系作者獲取轉載授權,否則將追究法律責任掌握Spring事件監聽器的內部邏輯與實現https://blog.51cto.com/u_15214399/8112689

在接收到新文章發布的事件后,監聽器ArticlePublishedListener需要執行以下業務邏輯:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

  • 通知所有訂閱新文章通知的用戶。
  • 將新文章的標題、作者和發布日期添加到搜索引擎的索引中,以便用戶可以搜索到這篇新文章。
  • 更新統計信息,例如總文章數、最近發布的文章等。

 這樣,每次新文章發布的事件被觸發時,訂閱者都會被通知,搜索引擎的索引將會得到更新,同時相關的統計數據也會得到更新。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

主程序:

G8528資訊網——每日最新資訊28at.com

模擬文章發布的場景G8528資訊網——每日最新資訊28at.com

package com.example.demo;import com.example.demo.event.ArticlePublishedEvent;import com.example.demo.listener.ArticlePublishedListener;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class DemoApplication {    public static void main(String[] args) {        // Initialize Spring ApplicationContext        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();        // Register listener        ctx.addApplicationListener(new ArticlePublishedListener());        ctx.refresh();        // Simulate publishing an article        ctx.publishEvent(new ArticlePublishedEvent(ctx, "Spring Events", "John Doe", "2023-09-15"));    }}-----------------------------------?著作權歸作者所有:來自51CTO博客作者華為云開發者聯盟的原創作品,請聯系作者獲取轉載授權,否則將追究法律責任掌握Spring事件監聽器的內部邏輯與實現https://blog.51cto.com/u_15214399/8112689

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

運行結果如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 我們可以看到ArticlePublishedEvent比PayloadApplicationEvent具有更多的業務含義和上下文。這樣的設計使我們能夠更具體地響應和處理特定的業務事件。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 實際上,在企業級應用中,文章發布可能會觸發多種不同的后續動作,使用Spring的事件監聽器模式可以帶來如下優勢:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

解耦:事件發布者(即新文章發布功能)不必關心具體的后續處理步驟。它只需發布事件,然后其他感興趣的監聽器會相應地做出響應。這種設計有助于各個功能之間的解耦。G8528資訊網——每日最新資訊28at.com

可擴展性:如果未來需要為新文章發布添加更多的后續處理,只需添加更多的監聽器即可,無需修改原有的業務邏輯。G8528資訊網——每日最新資訊28at.com

維護性:由于功能之間的解耦,每個功能模塊都可以獨立維護,這有助于提高代碼的可維護性。G8528資訊網——每日最新資訊28at.com

 Spring為開發者提供了強大的事件監聽機制,無論是使用自定義事件還是利用PayloadApplicationEvent進行快速開發,都使我們能夠構建一個高度解耦、可擴展且易于維護的系統。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

4. 事件廣播原理

4.1 Spring 5.x的事件模型概述

核心概念:

ApplicationEvent:這是所有Spring事件的超類。用戶可以通過繼承此類來創建自定義事件。G8528資訊網——每日最新資訊28at.com

ApplicationListener:這是所有事件監聽器的接口。它定義了一個onApplicationEvent方法,用于處理特定類型的事件。G8528資訊網——每日最新資訊28at.com

ApplicationEventPublisher:這是一個接口,定義了發布事件的方法。ApplicationContext繼承了這個接口,因此任何Spring bean都可以發布事件。G8528資訊網——每日最新資訊28at.com

ApplicationEventMulticaster:這個組件負責將事件廣播到所有匹配的監聽器。G8528資訊網——每日最新資訊28at.com

事件發布:

 用戶可以通過ApplicationEventPublisher接口或ApplicationContext來發布事件。通常情況下,當我們在Spring bean中需要發布事件時,可以讓這個bean實現ApplicationEventPublisherAware接口,這樣Spring容器會注入一個事件發布器。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

異步事件:

 從Spring 4.2開始,我們可以輕松地使事件監聽器異步化。在Spring 5中,這一功能仍然得到支持。只需要在監聽器方法上添加@Async注解并確保啟用了異步支持。這使得事件處理可以在單獨的線程中執行,不阻塞發布者。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

泛型事件:

 Spring 4.2引入了對泛型事件的支持,這在Spring 5中得到了維護。這意味著監聽器現在可以根據事件的泛型類型進行過濾。例如,一個ApplicationListener<ApplicationEvent<String>>將只接收到攜帶String負載的事件。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

事件的排序:

 監聽器可以實現Ordered接口或使用@Order注解來指定事件的執行順序。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

新的事件類型:

 Spring 5引入了新的事件類型,如ServletRequestHandledEvent,為web請求處理提供更多的鉤子。而像ContextRefreshedEvent這樣的事件,雖然不是Spring 5新引入的,但它為特定的生命周期回調提供了鉤子。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

Reactive事件模型:

 與Spring 5引入的WebFlux一起,還引入了對反應式編程模型的事件監聽和發布的支持。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

總結:

 在Spring 5.x中,事件模型得到了進一步的增強和優化,增加了對異步、泛型和反應式編程的支持,提供了更強大、靈活和高效的機制來處理應用程序事件。對于開發者來說,這為在解耦的同時實現復雜的業務邏輯提供了便利。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

4.2 發布事件publishEvent源碼分析

上圖,這里是Spring 5.3.7的源碼,下面講單獨抽出來分析G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com


G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

public void publishEvent(ApplicationEvent event) {    this.publishEvent(event, (ResolvableType)null);}

分析:G8528資訊網——每日最新資訊28at.com

 該方法接受一個ApplicationEvent對象并調用其重載版本publishEvent(Object event, @Nullable ResolvableType eventType),為其傳遞null作為事件類型。這是為了簡化用戶使用,用戶可以直接傳遞一個ApplicationEvent對象而無需考慮其具體的類型。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

public void publishEvent(Object event) {    this.publishEvent(event, (ResolvableType)null);}

分析:G8528資訊網——每日最新資訊28at.com

 與上一個方法類似,但它接受任何Object作為事件,并將其與null的eventType一起傳遞給核心方法。這增加了靈活性,用戶可以發送任何對象作為事件,而不僅僅是ApplicationEvent對象。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {    // 檢查事件對象是否為空,確保發布的事件是有意義的    Assert.notNull(event, "Event must not be null");    // 判斷傳入的事件是否已經是ApplicationEvent類型,如果是,則無需再進行包裝    Object applicationEvent;    if (event instanceof ApplicationEvent) {        applicationEvent = (ApplicationEvent)event;    } else {        // 如果傳入的事件不是ApplicationEvent類型,則將其包裝為PayloadApplicationEvent        applicationEvent = new PayloadApplicationEvent(this, event);        // 如果未指定事件類型,那么從包裝后的事件中獲取其真實類型        if (eventType == null) {            eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();        }    }    // 判斷當前是否存在earlyApplicationEvents列表    if (this.earlyApplicationEvents != null) {        // 如果存在,說明ApplicationContext還未完全初始化,將事件添加到此列表中,稍后再進行處理        this.earlyApplicationEvents.add(applicationEvent);    } else {        // 如果ApplicationContext已經初始化,那么直接通過事件多播器廣播事件        this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);    }    // 如果存在父ApplicationContext,則也將事件發布到父容器中    if (this.parent != null) {        if (this.parent instanceof AbstractApplicationContext) {            // 如果父容器是AbstractApplicationContext類型,則帶上事件類型進行發布            ((AbstractApplicationContext)this.parent).publishEvent(event, eventType);        } else {            // 否則,只傳遞事件對象進行發布            this.parent.publishEvent(event);        }    }}-----------------------------------?著作權歸作者所有:來自51CTO博客作者華為云開發者聯盟的原創作品,請聯系作者獲取轉載授權,否則將追究法律責任掌握Spring事件監聽器的內部邏輯與實現https://blog.51cto.com/u_15214399/8112689

這個方法究竟做了什么?G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

事件非空檢查:為了確保事件對象不為空,進行了初步的斷言檢查。這是一個常見的做法,以防止無效的事件被廣播。G8528資訊網——每日最新資訊28at.com

事件類型檢查與封裝:Spring允許使用任意類型的對象作為事件。如果傳入的不是ApplicationEvent的實例,它會使用PayloadApplicationEvent來進行封裝。這種設計提供了更大的靈活性。G8528資訊網——每日最新資訊28at.com

早期事件的處理:在Spring的生命周期中,ApplicationContext可能還沒有完全初始化,這時會有一些早期的事件。如果earlyApplicationEvents不為空,這些事件會被添加到此列表中,稍后再廣播。G8528資訊網——每日最新資訊28at.com

事件廣播:如果ApplicationContext已初始化,事件會被廣播給所有的監聽器。這是通過ApplicationEventMulticaster完成的,它是Spring中負責事件廣播的核心組件。G8528資訊網——每日最新資訊28at.com

處理父ApplicationContext:在有些應用中,可以存在父子ApplicationContext。當子容器廣播一個事件時,也可以考慮在父容器中廣播這個事件。這是為了確保在整個上下文層次結構中的所有感興趣的監聽器都能收到事件。G8528資訊網——每日最新資訊28at.com

通過這種方式,Spring的事件發布機制確保了事件在不同的上下文和生命周期階段都能被正確處理和廣播。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

上面說到事件廣播是ApplicationEventMulticaster完成的,這個是什么?下面來看看G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

4.3 Spring事件廣播:從ApplicationEventMulticaster開始

 當我們在Spring中討論事件,我們實際上是在討論兩件事:事件(即發生的事情)和監聽器(即對這些事件感興趣并作出反應的實體)。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 ApplicationEventMulticaster的主要職責是管理事件監聽器并廣播事件給這些監聽器。我們主要關注SimpleApplicationEventMulticaster,因為這是默認的實現,但請注意,Spring允許替換為自定義的ApplicationEventMulticaster。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

以下是SimpleApplicationEventMulticaster中的相關代碼與分析,有興趣的小伙伴可以自行查看:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {    // 可選的任務執行器,用于異步調用事件監聽器。    @Nullable    private Executor taskExecutor;    // 可選的錯誤處理器,用于處理在廣播事件過程中出現的錯誤。    @Nullable    private ErrorHandler errorHandler;    // 用于記錄日志的logger,它是延遲初始化的。    @Nullable    private volatile Log lazyLogger;    // 默認構造函數。    public SimpleApplicationEventMulticaster() {    }    // 帶有BeanFactory參數的構造函數,通常用于更復雜的應用上下文配置中。    public SimpleApplicationEventMulticaster(BeanFactory beanFactory) {        this.setBeanFactory(beanFactory);    }    // 設置任務執行器。可以是任何Java Executor,比如Spring的SimpleAsyncTaskExecutor或Java的FixedThreadPool。    public void setTaskExecutor(@Nullable Executor taskExecutor) {        this.taskExecutor = taskExecutor;    }    // 獲取當前設置的任務執行器。    @Nullable    protected Executor getTaskExecutor() {        return this.taskExecutor;    }    // 設置錯誤處理器。    public void setErrorHandler(@Nullable ErrorHandler errorHandler) {        this.errorHandler = errorHandler;    }    // 獲取當前設置的錯誤處理器。    @Nullable    protected ErrorHandler getErrorHandler() {        return this.errorHandler;    }    // 這是廣播事件的主要方法。它首先解析事件的類型,然后調用具有額外參數的重載方法。    public void multicastEvent(ApplicationEvent event) {        this.multicastEvent(event, this.resolveDefaultEventType(event));    }    // 這個方法是真正執行廣播操作的方法。    public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {        // 確定事件類型。        ResolvableType type = (eventType != null) ? eventType : this.resolveDefaultEventType(event);        // 獲取任務執行器。        Executor executor = this.getTaskExecutor();        // 獲取匹配此事件類型的所有監聽器。        Iterator<ApplicationListener<?>> listeners = this.getApplicationListeners(event, type).iterator();        // 遍歷每個監聽器并調用它。        while(listeners.hasNext()) {            ApplicationListener<?> listener = listeners.next();            // 如果有設置任務執行器,則異步調用監聽器。            if (executor != null) {                executor.execute(() -> this.invokeListener(listener, event));            } else {                // 如果沒有設置任務執行器,則同步調用監聽器。                this.invokeListener(listener, event);            }        }    }    // 為給定的事件解析默認的事件類型。    private ResolvableType resolveDefaultEventType(ApplicationEvent event) {        return ResolvableType.forInstance(event);    }    // 調用指定的監聽器來處理給定的事件,并根據需要處理錯誤。    protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {        // 獲取當前的錯誤處理器。        ErrorHandler errorHandler = this.getErrorHandler();        if (errorHandler != null) {            try {                // 嘗試調用監聽器。                this.doInvokeListener(listener, event);            } catch (Throwable ex) {                // 如果出現錯誤,使用錯誤處理器處理。                errorHandler.handleError(ex);            }        } else {            // 如果沒有設置錯誤處理器,則直接調用監聽器。            this.doInvokeListener(listener, event);        }    }    // 直接調用監聽器,捕獲任何類型不匹配的異常。    private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {        try {            listener.onApplicationEvent(event);        } catch (ClassCastException ex) {            // 捕獲類型轉換異常,并根據需要進行處理。            // 這可以確保如果監聽器不能處理特定類型的事件,不會導致整個廣播操作失敗。            String msg = ex.getMessage();            if (msg != null && !this.matchesClassCastMessage(msg, event.getClass()) && (!(event instanceof PayloadApplicationEvent) || !this.matchesClassCastMessage(msg, ((PayloadApplicationEvent)event).getPayload().getClass()))) {                throw ex;            }                        // 在預期情況下捕獲并記錄異常,而不是拋出它。            Log loggerToUse = this.lazyLogger;            if (loggerToUse == null) {                loggerToUse = LogFactory.getLog(this.getClass());                this.lazyLogger = loggerToUse;            }            if (loggerToUse.isTraceEnabled()) {                loggerToUse.trace("Non-matching event type for listener: " + listener, ex);            }        }    }    // 根據給定的類型錯誤消息和事件類來檢查ClassCastException是否是預期的。    private boolean matchesClassCastMessage(String classCastMessage, Class<?> eventClass) {        if (classCastMessage.startsWith(eventClass.getName())) {            return true;        } else if (classCastMessage.startsWith(eventClass.toString())) {            return true;        } else {            int moduleSeparatorIndex = classCastMessage.indexOf(47);            return moduleSeparatorIndex != -1 && classCastMessage.startsWith(eventClass.getName(), moduleSeparatorIndex + 1);        }    }}

關于核心方法multicastEvent需要作出特別說明:G8528資訊網——每日最新資訊28at.com

// 這個方法是真正執行廣播操作的方法。    public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {        // 確定事件類型。        ResolvableType type = (eventType != null) ? eventType : this.resolveDefaultEventType(event);		......		// 獲取匹配此事件類型的所有監聽器。        Iterator<ApplicationListener<?>> listeners = this.getApplicationListeners(event, type).iterator();        ......    }-----------------------------------?著作權歸作者所有:來自51CTO博客作者華為云開發者聯盟的原創作品,請聯系作者獲取轉載授權,否則將追究法律責任掌握Spring事件監聽器的內部邏輯與實現https://blog.51cto.com/u_15214399/8112689

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

方法第一行得到一個ResolvableType類型的對象,為什么 Spring 選擇使用 ResolvableType 而不是直接使用 Java 類型?最主要的原因是 Java 的泛型擦除。 在 Java 中,泛型只存在于編譯時,一旦代碼被編譯,泛型信息就會被擦除,運行時就不能直接獲取到泛型的實際類型。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

 為了解決這個問題,Spring 引入了 ResolvableType,一個能夠解析泛型類型信息的工具類。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

舉個例子:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

假設有如下的類定義:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

public class Sample {    private List<String> names;}

我們可以這樣獲取 names 字段的泛型類型:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

ResolvableType t = ResolvableType.forField(Sample.class.getDeclaredField("names"));Class<?> genericType = t.getGeneric(0).resolve(); // 得到 String.class

在 Spring 事件中的使用

G8528資訊網——每日最新資訊28at.com

 ResolvableType 在 Spring 事件中的應用主要是確定事件的類型和監聽器監聽的事件類型。當我們發布一個事件:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

ApplicationEvent event = new MyCustomEvent(this, "data");applicationContext.publishEvent(event);

 Spring 內部會使用 ResolvableType.forInstance(event) 來獲取這個事件的類型。然后,它會找到所有注冊的監聽器,查看它們監聽的事件類型是否與此事件匹配(通過比較 ResolvableType)。匹配的監聽器會被調用。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

對于一個監聽器:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

public class MyListener implements ApplicationListener<MyCustomEvent> {    @Override    public void onApplicationEvent(MyCustomEvent event) {        // handle the event    }}

Spring 內部會使用 ResolvableType 來解析這個監聽器監聽的事件類型(在這個例子中是 MyCustomEvent)。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

總之,ResolvableType 在 Spring 中的主要用途是提供了一種方式來解析和操作運行時的泛型類型信息,特別是在事件發布和監聽中。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

4.4 Spring事件發布與處理流程圖

如果看不清,建議在新標簽頁中打開圖片后放大看G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com


G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

4.5 監聽器內部邏輯

再來看看監聽器內部邏輯,我們來分析在multicastEvent方法中調用的getApplicationListeners(event, type)來分析下G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event, ResolvableType eventType) {    // 獲取事件來源對象    Object source = event.getSource();    // 判斷事件來源對象是否為null,是則返回null,否則返回事件來源對象的類    Class<?> sourceType = source != null ? source.getClass() : null;    // 使用事件類型和源類型作為緩存鍵    AbstractApplicationEventMulticaster.ListenerCacheKey cacheKey = new AbstractApplicationEventMulticaster.ListenerCacheKey(eventType, sourceType);    // 初始化一個新的監聽器檢索器為null    AbstractApplicationEventMulticaster.CachedListenerRetriever newRetriever = null;    // 嘗試從緩存中使用鍵取得一個已存在的檢索器    AbstractApplicationEventMulticaster.CachedListenerRetriever existingRetriever = (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.get(cacheKey);    // 如果沒有從緩存中獲取到檢索器,并且滿足緩存安全性條件    if (existingRetriever == null && (this.beanClassLoader == null || ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) && (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {        // 創建一個新的檢索器        newRetriever = new AbstractApplicationEventMulticaster.CachedListenerRetriever();        // 嘗試將新檢索器添加到緩存中        existingRetriever = (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.putIfAbsent(cacheKey, newRetriever);        // 如果緩存中已經有了一個值(由于并發的原因),則將新檢索器設回null        if (existingRetriever != null) {            newRetriever = null;        }    }    // 如果有現有的檢索器    if (existingRetriever != null) {        // 嘗試從檢索器中獲取監聽器集合        Collection<ApplicationListener<?>> result = existingRetriever.getApplicationListeners();        // 如果結果不為null,則直接返回        if (result != null) {            return result;        }    }    // 如果上述步驟都沒有返回,調用retrieveApplicationListeners進行實際的監聽器檢索    return this.retrieveApplicationListeners(eventType, sourceType, newRetriever);}private Collection<ApplicationListener<?>> retrieveApplicationListeners(ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable AbstractApplicationEventMulticaster.CachedListenerRetriever retriever) {    // 初始化一個空的監聽器列表    List<ApplicationListener<?>> allListeners = new ArrayList();    // 若retriever非null,則初始化集合來保存過濾出來的監聽器和Bean名    Set<ApplicationListener<?>> filteredListeners = retriever != null ? new LinkedHashSet() : null;    Set<String> filteredListenerBeans = retriever != null ? new LinkedHashSet() : null;    LinkedHashSet listeners;    LinkedHashSet listenerBeans;    // 同步從defaultRetriever中獲取已注冊的監聽器和其Bean名稱    synchronized(this.defaultRetriever) {        listeners = new LinkedHashSet(this.defaultRetriever.applicationListeners);        listenerBeans = new LinkedHashSet(this.defaultRetriever.applicationListenerBeans);    }    // 遍歷所有的監聽器    for (ApplicationListener<?> listener : listeners) {        // 檢查該監聽器是否支持此事件類型和源類型        if (this.supportsEvent(listener, eventType, sourceType)) {            if (retriever != null) {                // 如果支持并且retriever非null,添加到過濾監聽器集合                filteredListeners.add(listener);            }            // 將支持的監聽器添加到allListeners列表            allListeners.add(listener);        }    }    // 如果存在監聽器Bean名稱    if (!listenerBeans.isEmpty()) {        ConfigurableBeanFactory beanFactory = this.getBeanFactory();        for (String listenerBeanName : listenerBeans) {            try {                // 檢查Bean工廠中的Bean是否支持該事件                if (this.supportsEvent(beanFactory, listenerBeanName, eventType)) {                    ApplicationListener<?> listener = (ApplicationListener)beanFactory.getBean(listenerBeanName, ApplicationListener.class);                    // 再次檢查確保Bean實例支持事件,并且它還沒有被加入allListeners列表                    if (!allListeners.contains(listener) && this.supportsEvent(listener, eventType, sourceType)) {                        if (retriever != null) {                            // 若該Bean是單例并且retriever非null,添加到過濾監聽器集合                            if (beanFactory.isSingleton(listenerBeanName)) {                                filteredListeners.add(listener);                            } else {                                filteredListenerBeans.add(listenerBeanName);                            }                        }                        // 添加到allListeners列表                        allListeners.add(listener);                    }                } else {                    // 若不支持該事件,從allListeners中移除該Bean                    Object listener = beanFactory.getSingleton(listenerBeanName);                    if (retriever != null) {                        filteredListeners.remove(listener);                    }                    allListeners.remove(listener);                }            } catch (NoSuchBeanDefinitionException e) {                // 若Bean不存在,直接繼續下一個            }        }    }    // 對allListeners列表進行排序,確保監聽器的執行順序    AnnotationAwareOrderComparator.sort(allListeners);    // 如果retriever非null,更新其內部集合以后續使用    if (retriever != null) {        if (filteredListenerBeans.isEmpty()) {            retriever.applicationListeners = new LinkedHashSet(allListeners);            retriever.applicationListenerBeans = filteredListenerBeans;        } else {            retriever.applicationListeners = filteredListeners;            retriever.applicationListenerBeans = filteredListenerBeans;        }    }    // 返回allListeners作為結果    return allListeners;}-----------------------------------?著作權歸作者所有:來自51CTO博客作者華為云開發者聯盟的原創作品,請聯系作者獲取轉載授權,否則將追究法律責任掌握Spring事件監聽器的內部邏輯與實現https://blog.51cto.com/u_15214399/8112689

監聽器內部做了什么?G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

在getApplicationListeners方法中,采用了一種優化檢索的緩存機制來提高性能并確保線程安全性。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

具體分析如下:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

首次檢查:

G8528資訊網——每日最新資訊28at.com

AbstractApplicationEventMulticaster.CachedListenerRetriever existingRetriever =     (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.get(cacheKey);

這里,我們首先從retrieverCache中檢索existingRetriever。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

判斷是否需要進入同步代碼塊:

if (existingRetriever == null && (this.beanClassLoader == null || ...)) {    ...}

G8528資訊網——每日最新資訊28at.com

如果existingRetriever為空,那么我們可能需要創建一個新的CachedListenerRetriever并放入緩存中。但是,為了確保線程安全性,我們必須在這之前進行進一步的檢查。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

雙重檢查:

在創建新的CachedListenerRetriever之前,我們使用了putIfAbsent方法。這個方法會嘗試添加一個新值,但如果該值已存在,它只會返回現有的值。該機制采用了一種緩存優化策略:通過ConcurrentMap的putIfAbsent方法,即使多個線程同時到達這個代碼段,也確保只有一個線程能夠成功地放入新的值,從而保證線程安全性。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

newRetriever = new AbstractApplicationEventMulticaster.CachedListenerRetriever();existingRetriever = (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.putIfAbsent(cacheKey, newRetriever);

這里的邏輯使用了ConcurrentMap的putIfAbsent方法來確保線程安全性,而沒有使用傳統的synchronized塊。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

所以,我們可以說getApplicationListeners中的這部分邏輯采用了一種優化檢索的緩存機制。它利用了并發容器的原子性操作putIfAbsent來保證線程安全,而不是依賴于傳統的雙重檢查鎖定模式。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

總體概括一下,對于getApplicationListeners和retrieveApplicationListeners兩個方法的功能可以總結為以下三個步驟:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

從默認檢索器篩選監聽器:

這部分代碼直接從defaultRetriever中獲取監聽器,并檢查它們是否支持當前事件。在retrieveApplicationListeners方法中,代碼首先從defaultRetriever中獲取已經編程式注入的監聽器,并檢查每個監聽器是否支持當前的事件類型。G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

listeners = new LinkedHashSet(this.defaultRetriever.applicationListeners);for (ApplicationListener<?> listener : listeners) {    if (this.supportsEvent(listener, eventType, sourceType)) {        ... // 添加到篩選出來的監聽器列表    }}

從IOC容器中篩選監聽器:

在retrieveApplicationListeners方法中,除了從defaultRetriever中獲取已經編程式注入的監聽器,代碼還會嘗試從IOC容器(通過bean名稱)獲取監聽器,并檢查它們是否支持當前的事件。G8528資訊網——每日最新資訊28at.com

if (!listenerBeans.isEmpty()) {    ConfigurableBeanFactory beanFactory = this.getBeanFactory();    for (String listenerBeanName : listenerBeans) {        ... // 檢查并添加到篩選出來的監聽器列表    }}

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

監聽器排序:

最后,為確保監聽器按照預定的順序響應事件,篩選出的所有監聽器會經過排序。排序基于Spring的@Order注解或Ordered接口,如AnnotationAwareOrderComparator.sort(allListeners)所示G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

AnnotationAwareOrderComparator.sort(allListeners);

4.6 Spring事件監聽器檢索流程圖

G8528資訊網——每日最新資訊28at.com


G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

5. Spring事件傳播、異步處理等機制的詳細圖示

G8528資訊網——每日最新資訊28at.com


G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

說明:G8528資訊網——每日最新資訊28at.com

G8528資訊網——每日最新資訊28at.com

容器和事件廣播:

ApplicationContext 是Spring的應用上下文容器。在圖中,我們有一個主容器和一個子容器。G8528資訊網——每日最新資訊28at.com

當我們想發布一個事件時,我們調用 publishEvent 方法。G8528資訊網——每日最新資訊28at.com

ApplicationEventMulticaster 負責實際地將事件廣播到各個監聽器。G8528資訊網——每日最新資訊28at.com

主容器和子容器關系:

在Spring中,可以有多個容器,其中一個是主容器,其他的則是子容器。G8528資訊網——每日最新資訊28at.com

通常,子容器可以訪問主容器中的bean,但反之則不行。但在事件傳播的上下文中,子容器發布的事件默認不會在主容器中傳播。這一點由 Note1 注釋標明。G8528資訊網——每日最新資訊28at.com

異步處理:

當事件被發布時,它可以被異步地傳播到監聽器,這取決于是否配置了異步執行器。G8528資訊網——每日最新資訊28at.com

是否使用異步執行器? 這個決策點說明了基于配置,事件可以同步或異步地傳播到監聽器。G8528資訊網——每日最新資訊28at.com

事件生命周期:

在Spring容器的生命周期中,有些事件在容器初始化前觸發,這些被稱為 early events。這些事件會被緩存起來,直到容器初始化完成。G8528資訊網——每日最新資訊28at.com

一旦容器初始化完成,這些早期的事件會被處理,并開始處理常規事件。G8528資訊網——每日最新資訊28at.com

在容器銷毀時,也可能觸發事件。G8528資訊網——每日最新資訊28at.com

注意事項 (Note1):G8528資訊網——每日最新資訊28at.com

這個部分強調了一個特定的行為,即在某些配置下,子容器發布的事件可能也會在主容器中傳播,但這并不是默認行為。G8528資訊網——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-16286-0.html掌握Spring事件監聽器的內部邏輯與實現

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 深入剖析Netflix Ribbon:分布式微服務架構的負載均衡神器

下一篇: WebGPU 入門:繪制一個三角形

標簽:
  • 熱門焦點
  • MIX Fold3包裝盒泄露 新機本月登場

    小米的全新折疊屏旗艦MIX Fold3將于本月發布,近日該機的真機包裝盒在網上泄露。從圖上來看,新的MIX Fold3包裝盒在外觀設計方面延續了之前的方案,變化不大,這也是目前小米旗艦
  • 掘力計劃第 20 期:Flutter 混合開發的混亂之治

    在掘力計劃系列活動第20場,《Flutter 開發實戰詳解》作者,掘金優秀作者,Github GSY 系列目負責人戀貓的小郭分享了Flutter 混合開發的混亂之治。Flutter 基于自研的 Skia 引擎
  • Golang 中的 io 包詳解:組合接口

    io.ReadWriter// ReadWriter is the interface that groups the basic Read and Write methods.type ReadWriter interface { Reader Writer}是對Reader和Writer接口的組合,
  • K8S | Service服務發現

    一、背景在微服務架構中,這里以開發環境「Dev」為基礎來描述,在K8S集群中通常會開放:路由網關、注冊中心、配置中心等相關服務,可以被集群外部訪問;圖片對于測試「Tes」環境或者
  • JavaScript學習 -AES加密算法

    引言在當今數字化時代,前端應用程序扮演著重要角色,用戶的敏感數據經常在前端進行加密和解密操作。然而,這樣的操作在網絡傳輸和存儲中可能會受到惡意攻擊的威脅。為了確保數據
  • Python異步IO編程的進程/線程通信實現

    這篇文章再講3種方式,同時講4中進程間通信的方式一、 Python 中線程間通信的實現方式共享變量共享變量是多個線程可以共同訪問的變量。在Python中,可以使用threading模塊中的L
  • 梁柱接棒兩年,騰訊音樂闖出新路子

    文丨田靜 出品丨牛刀財經(niudaocaijing)7月5日,企鵝FM發布官方公告稱由于業務調整,將于9月6日正式停止運營,這意味著騰訊音樂長音頻業務走向消亡。騰訊在長音頻領域還在摸索。為
  • 拼多多APP上線本地生活入口,群雄逐鹿萬億市場

    Tech星球(微信ID:tech618)文 | 陳橋輝 Tech星球獨家獲悉,拼多多在其APP內上線了&ldquo;本地生活&rdquo;入口,位置較深,位于首頁的&ldquo;充值中心&rdquo;內,目前主要售賣美食相關的
  • 蘋果、三星、惠普等暫停向印度出口筆記本和平板電腦

    集微網消息,據彭博社報道,在8月3日印度突然禁止在沒有許可證的情況下向印度進口電腦/平板及顯示器等產品后,蘋果、三星電子和惠普等大公司暫停向印度
Top 主站蜘蛛池模板: 中西区| 巴青县| 鹤山市| 镇雄县| 赣州市| 隆林| 聂荣县| 新郑市| 乌拉特前旗| 兴山县| 华蓥市| 顺昌县| 辛集市| 赣榆县| 平乐县| 梁河县| 乌海市| 天峨县| 谢通门县| 乌拉特后旗| 怀远县| 滁州市| 阿荣旗| 阿鲁科尔沁旗| 开原市| 泰州市| 合作市| 米泉市| 白银市| 金湖县| 安国市| 平凉市| 千阳县| 云霄县| 安泽县| 从化市| 东至县| 潜江市| 双鸭山市| 常宁市| 垣曲县|