事件和平時所用的回調思想在與GUI(JavaScript,Swing)相關的技術中很是流行。而在Web應用程序的服務器端,咱們不多去直接使用。但這並不意味着咱們沒法在服務端去實現一個面向事件的體系結構。前端
在本文中,咱們將重點介紹Spring框架中的事件處理。首先,會先介紹下事件驅動編程這個概念。接着,咱們會將精力放在專門用於Spring框架中的事件處理之上。而後咱們會看到實現事件調度和監聽的主要方法。最後,咱們將在Spring應用程序中展現如何使用基本的監聽器。java
在開始討論事件驅動編程的編程方面以前,先來講一個場景,用來幫助你們更好地理解event-driven
這個概念。在一個地方只有兩個賣衣服的商店A和B.在A店中,咱們的消費者須要一個一個的接受服務,即,同一時間只有一個客戶能夠購物。在B店裏,能夠容許幾個客戶同時進行購物,當有客戶須要賣家的幫助時,他須要舉起他的右手示意一下。賣家看到後會來找他,幫助其作出更好的選擇。關於事件驅動(event-driven
)編程這個概念經過上述場景描述總結後就是:經過作一些動做來做爲對一些行爲的迴應。web
如上所見,事件驅動的編程(也稱爲基於事件的編程)是基於對接收到的信號的反應的編程形式。這些信號必須以某種形式來傳輸信息。舉一個簡單例子:點擊按鈕
。咱們將這些信號稱爲事件。這些事件能夠經過用戶操做(鼠標點擊,觸摸)或程序條件執行觸發(例如:一個元素的加載結束能夠啓動另外一個操做)來產生。spring
爲了更好地瞭解,請看如下示例,模仿用戶在GUI界面中的操做:數據庫
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();
assertTrue("2 listeners should be invoked but only "+mouse.getListenerCallbacks()+" were", mouse.getListenerCallbacks() == 2);
}
}
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);
}複製代碼
打印輸出以下所示:編程
Clicked !
Listener#1 called
Listener#2 called複製代碼
Spring基於實現org.springframework.context.ApplicationListener接口的bean來進行事件處理。這個接口中只有一個方法,onApplicationEvent用來當一個事件發送過來時這個方法來觸發相應的處理。該接口能夠經過指定須要接收的事件來實現(不懂看源碼咯,源碼裏方法接收一個event
做爲參數)。由此,Spring會自動過濾篩選能夠用來接收給定事件的監聽器(listeners
)。後端
/** * Interface to be implemented by application event listeners. * Based on the standard {@code java.util.EventListener} interface * for the Observer design pattern. * * <p>As of Spring 3.0, an ApplicationListener can generically declare the event type * that it is interested in. When registered with a Spring ApplicationContext, events * will be filtered accordingly, with the listener getting invoked for matching event * objects only. * * @author Rod Johnson * @author Juergen Hoeller * @param <E> the specific ApplicationEvent subclass to listen to * @see org.springframework.context.event.ApplicationEventMulticaster */
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/** * Handle an application event. * @param event the event to respond to */
void onApplicationEvent(E event);
}複製代碼
事件經過org.springframework.context.ApplicationEvent實例來表示。這個抽象類繼承擴展了java.util.EventObject,可使用EventObject中的getSource方法,咱們能夠很容易地得到所發生的給定事件的對象。這裏,事件存在兩種類型: 服務器
ApplicationContext
類型的參數)。這樣,咱們就能夠直接經過應用程序上下文的生命週期來獲得所發生的事件:ContextStartedEvent
在上下文啓動時被啓動,當它中止時啓動ContextStoppedEvent
,當上下文被刷新時產生ContextRefreshedEvent
,最後在上下文關閉時產生ContextClosedEvent
。 /** * Base class for events raised for an {@code ApplicationContext}. * * @author Juergen Hoeller * @since 2.5 */
@SuppressWarnings("serial")
public abstract class ApplicationContextEvent extends ApplicationEvent {
/** * Create a new ContextStartedEvent. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code null}) */
public ApplicationContextEvent(ApplicationContext source) {
super(source);
}
/** * Get the {@code ApplicationContext} that the event was raised for. */
public final ApplicationContext getApplicationContext() {
return (ApplicationContext) getSource();
}
}
/** * Event raised when an {@code ApplicationContext} gets started. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 * @see ContextStoppedEvent */
@SuppressWarnings("serial")
public class ContextStartedEvent extends ApplicationContextEvent {
/** * Create a new ContextStartedEvent. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */
public ContextStartedEvent(ApplicationContext source) {
super(source);
}
}
/** * Event raised when an {@code ApplicationContext} gets stopped. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 * @see ContextStartedEvent */
@SuppressWarnings("serial")
public class ContextStoppedEvent extends ApplicationContextEvent {
/** * Create a new ContextStoppedEvent. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */
public ContextStoppedEvent(ApplicationContext source) {
super(source);
}
}
/** * Event raised when an {@code ApplicationContext} gets initialized or refreshed. * * @author Juergen Hoeller * @since 04.03.2003 * @see ContextClosedEvent */
@SuppressWarnings("serial")
public class ContextRefreshedEvent extends ApplicationContextEvent {
/** * Create a new ContextRefreshedEvent. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */
public ContextRefreshedEvent(ApplicationContext source) {
super(source);
}
}
/** * Event raised when an {@code ApplicationContext} gets closed. * * @author Juergen Hoeller * @since 12.08.2003 * @see ContextRefreshedEvent */
@SuppressWarnings("serial")
public class ContextClosedEvent extends ApplicationContextEvent {
/** * Creates a new ContextClosedEvent. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */
public ContextClosedEvent(ApplicationContext source) {
super(source);
}
}複製代碼
Spring如何將事件分配給專門的監聽器?這個過程由事件廣播器(event multicaster
)來實現,由org.springframework.context.event.ApplicationEventMulticaster接口的實現表示。此接口定義了3種方法,用於: app
依賴查找DL
),而後再將其添加到listener
列表中。 @Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
}
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
return ResolvableType.forInstance(event);
}
/** * Invoke the given listener with the given event. * @param listener the ApplicationListener to invoke * @param event the current event to propagate * @since 4.1 */
@SuppressWarnings({"unchecked", "rawtypes"})
protected void invokeListener(ApplicationListener listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
listener.onApplicationEvent(event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(event.getClass().getName())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}
}複製代碼
咱們來看看event multicaster
在應用程序上下文中所在的位置。在AbstractApplicationContext
中定義的一些方法能夠看到其中包含調用public void publishEvent方法。經過這種方法的註釋可知,它負責向全部監聽器發送給定的事件:框架
/** * Publish the given event to all listeners. * <p>Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. * @param event the event to publish (may be application-specific or a * standard framework event) */
@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}
/** * Publish the given event to all listeners. * <p>Note: Listeners get initialized after the MessageSource, to be able * to access it within listener implementations. Thus, MessageSource * implementations cannot publish events. * @param event the event to publish (may be an {@link ApplicationEvent} * or a payload object to be turned into a {@link PayloadApplicationEvent}) */
@Override
public void publishEvent(Object event) {
publishEvent(event, null);
}
/** * Publish the given event to all listeners. * @param event the event to publish (may be an {@link ApplicationEvent} * or a payload object to be turned into a {@link PayloadApplicationEvent}) * @param eventType the resolved event type, if known * @since 4.2 */
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Publishing event in " + getDisplayName() + ": " + event);
}
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}複製代碼
該方法由如下方法調用:啓動上下文(啓動後發佈ContextStartedEvent
),中止上下文(中止後發佈ContextStoppedEvent
),刷新上下文(刷新後發佈ContextRefreshedEvent
)並關閉上下文(關閉後發佈ContextClosedEvent
):
/** * Finish the refresh of this context, invoking the LifecycleProcessor's * onRefresh() method and publishing the * {@link org.springframework.context.event.ContextRefreshedEvent}. */
protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();
// Initialize lifecycle processor for this context.
initLifecycleProcessor();
// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();
// Publish the final event.生命週期Refreshed事件
publishEvent(new ContextRefreshedEvent(this));
// Participate in LiveBeansView MBean, if active.
LiveBeansView.registerApedplicationContext(this);
}
/** * Actually performs context closing: publishes a ContextClosedEvent and * destroys the singletons in the bean factory of this application context. * <p>Called by both {@code close()} and a JVM shutdown hook, if any. * @see org.springframework.context.event.ContextClosedEvent * @see #destroyBeans() * @see #close() * @see #registerShutdownHook() */
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (logger.isInfoEnabled()) {
logger.info("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// Publish shutdown event. ContextClosed事件
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// Stop all Lifecycle beans, to avoid delays during individual destruction.
try {
getLifecycleProcessor().onClose();
}
...
}
//---------------------------------------------------------------------
// Implementation of Lifecycle interface
//---------------------------------------------------------------------
@Override
public void start() {
getLifecycleProcessor().start();
publishEvent(new ContextStartedEvent(this));
}
@Override
public void stop() {
getLifecycleProcessor().stop();
publishEvent(new ContextStoppedEvent(this));
}複製代碼
使用Spring的Web應用程序也能夠處理與請求相關聯的另外一種類型的事件(以前說到的RequestHandledEvent
)。它的處理方式和麪向上下文的事件相似。首先,咱們能夠找到org.springframework.web.servlet.FrameworkServlet中處理請求的方法processRequest。在這個方法結束的時候,調用了private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, long startTime, @Nullable Throwable failureCause)
方法。如其名稱所表達的,此方法將向全部監聽器發佈給定的RequestHandledEvent
。事件在傳遞給應用程序上下文的publishEvent
方法後,將由event multicaster
發送。這裏沒毛病,由於RequestHandledEvent
擴展了與ApplicationContextEvent
相同的類,即ApplicationEvent
。來看看publishRequestHandledEvent
方法的源碼:
private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response, long startTime, @Nullable Throwable failureCause) {
//不少人問我Spring5和4的代碼有什麼區別,就在不少細微的地方,Spring一直在作不懈的改進和封裝,很少說,沒事可自行 //對比,能學到不少東西
if (this.publishEvents && this.webApplicationContext != null) {
// Whether or not we succeeded, publish an event.
long processingTime = System.currentTimeMillis() - startTime;
this.webApplicationContext.publishEvent(
new ServletRequestHandledEvent(this,
request.getRequestURI(), request.getRemoteAddr(),
request.getMethod(), getServletConfig().getServletName(),
WebUtils.getSessionId(request), getUsernameForRequest(request),
processingTime, failureCause, response.getStatus()));
}
}複製代碼
須要注意的是,你能夠關閉基於請求的事件的調度。FrameworkServlet的setPublishEvents(boolean publishEvents)容許禁用事件分派,例如改進應用程序性能(看代碼註釋,當沒有監聽器來管理相應事件的時候,幹嗎要浪費性能)。默認狀況下,事件調度被激活(默認爲true)。
/** Should we publish a ServletRequestHandledEvent at the end of each request? */
private boolean publishEvents = true;
/** * Set whether this servlet should publish a ServletRequestHandledEvent at the end * of each request. Default is "true"; can be turned off for a slight performance * improvement, provided that no ApplicationListeners rely on such events. * @see org.springframework.web.context.support.ServletRequestHandledEvent */
public void setPublishEvents(boolean publishEvents) {
this.publishEvents = publishEvents;
}複製代碼
假若有思考的話,從上面的代碼中能夠知道,事件在應用程序響應性上的表現會不好(大都是一個調用另外一個)。這是由於默認狀況下,它們是同步調用線程(即便用同一線程去處理事務,處理請求,以及準備視圖的輸出)。所以,若是一個監聽器須要幾秒鐘的時間來響應,整個應用程序可能會受到慢的要死。幸運的是,咱們能夠指定事件處理的異步執行(參考上面的multicastEvent
源碼)。須要注意的是,所處理的事件將沒法與調用者的上下文(類加載器或事務)進行交互。這裏參考multicastEvent
方法源碼便可。默認狀況下,org.springframework.core.task.SyncTaskExecutor用來調用相應監聽器。
public class SyncTaskExecutor implements TaskExecutor, Serializable {
/** * Executes the given {@code task} synchronously, through direct * invocation of it's {@link Runnable#run() run()} method. * @throws IllegalArgumentException if the given {@code task} is {@code null} */
@Override
public void execute(Runnable task) {
Assert.notNull(task, "Runnable must not be null");
task.run();
}
}複製代碼
爲了更好的理解事件監聽器,咱們來寫一個小的測試用例。經過這個例子,咱們要證實默認狀況下,監聽器listeners
在其調用者線程中執行了分發的事件。因此,爲了避免當即獲得結果,咱們在監聽器中休眠5秒(調用Thread.sleep(5000))。測試檢查是否達到3個目的:若是controller 的返回結果和所預期的視圖名稱相匹配,若是事件監聽器花了5秒鐘的時間才響應(Thread.sleep執行沒有任何問題),而且若是controller 的一樣花了5秒鐘來生成視圖(由於監聽器的休眠)。
第二個定義的測試將驗證咱們的監聽器是否在另外一個事件中被捕獲(和以前的類繼承同一個類型)。首先,在配置文件中對bean的定義:
< -- This bean will catch SampleCustomEvent launched in tested controller -->
<bean class="com.migo.event.SampleCustomEventListener">
< -- Thanks to this bean we'll able to get the execution times of tested controller and listener -->
<bean class="com.migo.event.TimeExecutorHolder" id="timeExecutorHolder">
</bean></bean>複製代碼
事件和監聽器的代碼:
public class SampleCustomEvent extends ApplicationContextEvent {
private static final long serialVersionUID = 4236181525834402987L;
public SampleCustomEvent(ApplicationContext source) {
super(source);
}
}
public class OtherCustomEvent extends ApplicationContextEvent {
private static final long serialVersionUID = 5236181525834402987L;
public OtherCustomEvent(ApplicationContext source) {
super(source);
}
}
public class SampleCustomEventListener implements ApplicationListener<samplecustomevent> {
@Override
public void onApplicationEvent(SampleCustomEvent event) {
long start = System.currentTimeMillis();
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
int testTime = Math.round((end - start) / 1000);
((TimeExecutorHolder) event.getApplicationContext().getBean("timeExecutorHolder")).addNewTime("sampleCustomEventListener", new Integer(testTime));
}
}複製代碼
沒什麼複雜的,事件只能被用來初始化。監聽器經過獲取當前時間(以毫秒爲單位)來測試所執行時間,並在轉換後保存(以秒爲單位)。監聽器使用的TimeExecutorHolder
也不復雜:
public class TimeExecutorHolder {
private Map<String, Integer> testTimes = new HashMap();
public void addNewTime(String key, Integer value) {
testTimes.put(key, value);
}
public Integer getTestTime(String key) {
return testTimes.get(key);
}
}複製代碼
此對象只保留測試元素的執行時間一個Map。測試的controller實現看起來相似於監聽器。惟一的區別是它發佈一個事件(接着被已定義的監聽器捕獲)並返回一個名爲「success」的視圖:
@Controller
public class TestController {
@Autowired
private ApplicationContext context;
@RequestMapping(value = "/testEvent")
public String testEvent() {
long start = System.currentTimeMillis();
context.publishEvent(new SampleCustomEvent(context));
long end = System.currentTimeMillis();
int testTime = (int)((end - start) / 1000);
((TimeExecutorHolder) context.getBean("timeExecutorHolder")).addNewTime("testController", new Integer(testTime));
return "success";
}
@RequestMapping(value = "/testOtherEvent")
public String testOtherEvent() {
context.publishEvent(new OtherCustomEvent(context));
return "success";
}
}複製代碼
最後,寫一個測試用例,它調用/testEvent並在TimeExecutorHolder bean
以後檢查以驗證兩個部分的執行時間:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"file:applicationContext-test.xml"})
@WebAppConfiguration
public class SpringEventsTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void test() {
try {
MvcResult result = mockMvc.perform(get("/testEvent")).andReturn();
ModelAndView view = result.getModelAndView();
String expectedView = "success";
assertTrue("View name from /testEvent should be '"+expectedView+"' but was '"+view.getViewName()+"'", view.getViewName().equals(expectedView));
} catch (Exception e) {
e.printStackTrace();
}
TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
int controllerSec = timeHolder.getTestTime("testController").intValue();
int eventSec = timeHolder.getTestTime("sampleCustomEventListener").intValue();
assertTrue("Listener for SampleCustomEvent should take 5 seconds before treating the request but it took "+eventSec+" instead", eventSec == 5);
assertTrue("Because listener took 5 seconds to response, controller should also take 5 seconds before generating the view, but it took "+controllerSec+ " instead", controllerSec == 5);
}
@Test
public void otherTest() {
TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
timeHolder.addNewTime("sampleCustomEventListener", -34);
try {
MvcResult result = mockMvc.perform(get("/testOtherEvent")).andReturn();
ModelAndView view = result.getModelAndView();
String expectedView = "success";
assertTrue("View name from /testEvent should be '"+expectedView+"' but was '"+view.getViewName()+"'", view.getViewName().equals(expectedView));
} catch (Exception e) {
e.printStackTrace();
}
Integer eventSecObject = timeHolder.getTestTime("sampleCustomEventListener");
assertTrue("SampleCustomEventListener shouldn't be trigerred on OtherEvent but it was", eventSecObject.intValue() == -34);
}
}複製代碼
測試經過沒有任何問題。它證實了咱們所設定的許多假設。
首先,咱們看到事件編程包括在信號發送到應用程序時觸發並執行某些操做。這個信號必須有一個監聽器在監聽。在Spring中,因爲監聽器中的泛型定義(void onApplicationEvent(E event);
),事件能夠很容易地被listeners
所捕獲。經過它,若是所觸發的事件對應於監聽器所預期的事件,咱們無須多餘的檢查(說的囉嗦了,就是符合所需求的類型便可,省去不少麻煩,咱們能夠直接根據泛型就能夠實現不少不一樣的處理)。咱們還發現,默認狀況下,監聽器是以同步方式執行的。因此在調用線程同時執行好比視圖生成或數據庫處理的操做是不行的。
最後,要說的是,算是一個先後端通用的思想吧,所謂的事件,其實想來,不過是一個接口而已,把這個接口派發出去(event multicaster),由誰來實現,這是他們的事情,這裏就有一個裝飾類(這麼理解就好),其名字叫listener,拿到這個派發的事件接口,而後調用相應的實現,這裏爲了程序的更加靈活和高可用,咱們會調用相應的adapter適配器,最後調用其相應的Handler實現,而後Handler會調用相應的service,service調用dao。
一樣這個思想用在前端就是組件對外派發一個事件,這個事件由其父組件或者實現,或者繼續向外派發,最後用一個具體的方法將之實現便可
其實對應於咱們的數學來說就是,咱們定義一個數學公式f(x)*p(y)同樣,這個派發出去,不管咱們先實現了f(x)仍是先實現了p(y),仍是一次全實現,仍是分幾回派發出去,終究咱們會在最後去將整個公式徹底解答出來,這也就是所謂的事件機制,難麼?