前言
經過前面《SpringBoot2.1.x源碼閱讀環境搭建詳解》,本節主要內容----SpringBoot啓動流程源碼分析。
首先看下環境準備:
項目/工具 | 版本 |
SpringBoot | v2.1.x |
spring | v5.1.x |
maven | v3.5.4 |
SpringBoot框架減少的大量的文件配置,框架集成便捷,給項目開發帶來了很多便利。SpringBoot項目一般都會有註解*Application標註的入口類,入口類會有一個main方法,main方法是一個標準的Java應用程序的入口,可以直接啓動。
OK,廢話不多說,進入正題。
1、項目啓動類
在入口程序,我們可以看到其引入了@SpringBootApplication這個註解,它是SpringBoot的核心註解,用此註解標註的入口類是應用的啓動類,通常會在啓動類的在main()方法中創建了SpringApplication類的實例,然後調用該類的run()方法來啓動SpringBoot項目。
@SpringBootApplication
public class SpringBootAnalysisApplication {
private static final Logger logger = LoggerFactory.getLogger(SpringStudyPractise.class);
public static void main(String[] args) {
logger.debug("================正在啓動==============");
SpringApplication app = new SpringApplication(SpringStudyPractise.class);
app.run(args);
logger.debug("================啓動成功==============");
}
}
@SpringBootApplication其實是一個組合註解。源碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
這個註解主要組合了以下註解:
【1】@SpringBootConfiguration:它是SpringBoot項目的配置註解,也是一個組合註解,源碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
在SpringBoot項目中推薦使用@SpringBootConfiguration註解來替代@Configuration註解。
【2】@EnableAutoConfiguration:啓動自動配置,該註解會讓SpringBoot根據當前項目所依賴的jar包自動配置項目的相關配置項。
【3】@ComponentScan:掃描配置,SpringBoot默認會掃描@SpringBootApplication所在類的同級包以及它的子包,所以建議將@SpringBootApplication修飾的入口類放置在項目包下(Group Id + Artifact Id),這樣做的好處是,可以保證SpringBoot項目自動掃描到項目所有的包。
而main方法中這個SpringApplication實例所提供的run()方法只應用主程序開始的運行,SpringApplication這個類可用於從Java主方法引導和啓動Spring應用程序。
OK,繼續往下主程序啓動流程。
啓動主程序main方法,初始化SpringApplication實例對象:
/**
* 創建一個新的{@link SpringApplication}實例。
* 應用程序上下文將從指定的主要源加載bean。可以在調用{@link #run(String…)}之前定製實例。
* @param resourceLoader 資源加載器使用
* @param primarySources bean對象
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//【1】設置servlet環境
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//【2】獲取ApplicationContextInitializer,也是在這裏開始首次加載spring.factories文件
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//【3】獲取監聽器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
首先,來看一下判斷Web環境的deduceFromClassoath()
方法:
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
【1】這裏主要是通過判斷REACTIVE
相關的字節碼是否存在,如果不存在,則web環境即爲SERVLET
類型。這裏設置好web環境類型,在後面會根據類型初始化對應環境。
繼續往下看【2】getSpringFactoriesInstances()方法:它以ApplicationContextInitializer接口類爲入參,是
spring組件spring-context
組件中的一個接口,主要是spring ioc容器刷新之前的一個回調接口,用於處於自定義邏輯。
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
//在這裏,將加載sprin.factories
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
這裏看一下names集合的入參的loadFactoryNames()方法,這裏第一次加載META-INF/spring.factories文件,
在加載了spring.factories文件後,會將配置文件中的各個屬性設置加入緩存中。同時,這裏也加載了ApplicationListener監聽器,這10個監聽器會貫穿springBoot整個生命週期。
關於SpringApplication的實例化流程,限於篇幅,將於後面內容進行分析。隨後,SpringBoot將進入自動化啓動流程。
這裏,進入SpringApplication的run()方法:
/**
* 運行這個Spring應用,創建併產生一個新的應用上下文
* {@link ApplicationContext}.
* @param args 來自於Java程序main方法中的參數
* @return a running
*/
public ConfigurableApplicationContext run(String... args) {
//【1】初始化時間監控器
StopWatch stopWatch = new StopWatch();
//開始記錄啓動時間,啓動一個未命名的任務。如果在不調用該方法的情況下調用{@link #stop()}或計時方法,則結果是未定義的。
stopWatch.start();
ConfigurableApplicationContext context = null;
//【2】初始化Spring異常報告集合
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//【3】java.awt.headless是J2SE的一種模式用於在缺少顯示屏、鍵盤或者鼠標時的系統配置
// 很多監控工具如jconsole 需要將該值設置爲true,系統變量默認爲true
configureHeadlessProperty();
//【4】獲取spring.factories中的監聽器變量
// 參數args:爲指定的參數數組,默認爲當前類SpringApplication
//獲取並啓動監聽器,即初始化監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
//在run方法第一次啓動時立即調用。可以用於非常早期的初始化。
listeners.starting();
try {
//【5】裝配參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//【6】初始化容器環境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
//【7】設置需要忽略的Bean信息
configureIgnoreBeanInfo(environment);
//【7.1】打印banner,啓動的Banner就是在這一步打印出來的。
Banner printedBanner = printBanner(environment);
//【8】創建容器
context = createApplicationContext();
//【9】實例化SpringBootExceptionReporter.class,用來支持報告關於啓動的錯誤
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//【10】準備容器,裝配參數:容器屬性、容器環境、監聽器屬性、應用對象
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//【11】刷新容器
refreshContext(context);
//【12】刷新容器後的拓展接口,在容器被刷新之後調用。
afterRefresh(context, applicationArguments);
//【13】停止監聽器
stopWatch.stop();
//【14】在啓動時記錄應用程序日誌信息。
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//【15】使用廣播和回調機制通知監聽器springboot容器啓動成功(容器已經被刷新,應用程序已經啓動)
listeners.started(context);
【16】容器bean的回調。
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
【17】使用廣播和回調機制通知監聽器springboot容器已成功running
listeners.running(context);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
【18】返回容器
return context;
}
Spring中加載一個應用,主要是通過一些複雜的配置實現,這裏看來,SpringBoot幫我們將這些配置工作提前實現了。
從上面SpringApplication類中run()方法的源碼,我們基本上知道SpringBoot在主程序執行run()方法後,啓動如下關鍵流程:
- 【1】初始化時間監聽器,開始記錄項目的啓動時間
- 【2】初始化Spring異常報告集合
- 【3】系統監控工具設置
- 【4】獲取並啓動監聽器,即初始化監聽器。(獲取spring.factories中的監聽器變量)
- 【5】裝配參數
- 【6】初始化容器環境
- 【7】設置需要忽略的Bean信息,包括打印Banner,啓動的Banner就是在這一步打印出來的。
- 【8】創建容器
- 【9】實例化SpringBootExceptionReporter.class,用來支持報告關於啓動的錯誤
- 【10】準備容器,裝配參數
- 【11】刷新容器
- 【12】刷新容器後的擴展接口,在容器刷新後調用
- 【13】停止監聽器
- 【14】在啓動時記錄應用程序日誌信息。
- 【15】使用廣播和回調機制通知監聽器springboot容器啓動成功(容器已經被刷新,應用程序已經啓動)
- 【16】容器bean的回調。
- 【17】使用廣播和回調機制通知監聽器springboot容器已成功running。
- 【18】返回容器
接下來,逐步分析SpringBoot在主程序執行run()方法後的關鍵流程:
第一步:初始化時間監聽器(開始記錄項目的啓動時間)
初始化時間監聽器,開始記錄項目啓動時間:
StopWatch stopWatch = new StopWatch();
stopWatch.start();
這裏,初始化構造一個時間監聽器對象,進入start()方法,開始記錄啓動時間,並啓動一個未命名的任務:
/**
* 開始記錄啓動時間,啓動一個未命名的任務。
* 如果在不調用該方法的情況下調用{@link #stop()}或計時方法,則結果是未定義的。
* @param taskName 要啓動的任務的名稱
*/
public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
this.currentTaskName = taskName;
this.startTimeMillis = System.currentTimeMillis();
}
第二步:初始化Spring異常報告集合
初始化Spring異常報告集合,這裏將構造出一個SpringBootExceptionReporter接口類的集合對象:
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
OK,進入SpringBootExceptionReporter看一下:
/**
* 該類是一個用於支持自定義報告{@link SpringApplication}啓動錯誤的回調接口類。
* {@link SpringBootExceptionReporter}是通過{@link SpringFactoriesLoader}加載的,
* 並且必須聲明一個帶有單個{@link ConfigurableApplicationContext}參數的公共構造函數。
*/
@FunctionalInterface
public interface SpringBootExceptionReporter {
/**
* 啓動失敗,則上報失敗信息。
*
* 如果報告失敗,返回true;
* 如果發生默認報告,則返回false
*/
boolean reportException(Throwable failure);
}
第三步: 系統監控工具設置
java.awt.headless是J2SE的一種模式用於在缺少顯示屏、鍵盤或者鼠標時的系統配置。
configureHeadlessProperty();
很多監控工具如jconsole 需要將該值設置爲true,系統變量默認爲true。看下源碼:
private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
Boolean.toString(this.headless)));
}
第四步:獲取並啓動監聽器,即初始化監聽器(獲取spring.factories中的監聽器變量)
//【4】獲取spring.factories中的監聽器變量
// 參數args:爲指定的參數數組,默認爲當前類SpringApplication
//獲取並啓動監聽器,即初始化監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
//在run方法第一次啓動時立即調用。可以用於非常早期的初始化。
listeners.starting();
看上邊程序,首先獲取監聽器getRunListeners(),然後啓動監聽器starting()。逐步分析:
【1】獲取監聽器
//第一步:獲取並啓動監聽器,即初始化監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
進入getRunListeners()方法,
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
分析:
- 返回一個容器監聽器對象,
- 入參agrs是一個默認爲空的字符串數組;
- getSpringFactoriesInstances()方法將args作爲入參,獲取Spring工廠實例。在啓動時最終將獲取spring.factories對應的監聽器:
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
進入getSpringFactoriesInstances(),最終將返回一個工廠實例。
/***
* 獲取Spring工廠實例
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
//獲取類加載器
ClassLoader classLoader = getClassLoader();
//使用給定的類加載器,從{@value #FACTORIES_RESOURCE_LOCATION}("META-INF/spring.factories")裝入給定類型的工廠實現的完全限定類名。
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//根據啓動類的類型、參數類型和類加載器創建工廠實例集合
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
loadFactoryNames()方法,將取到工廠實例name的集合:
進入createSpringFactoriesInstances()方法,它展示了整個SpringBoot框架獲取factories的方式:
@SuppressWarnings("unchecked")
private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,ClassLoader classLoader, Object[] args, Set<String> names) {
List<T> instances = new ArrayList<>(names.size());
for (String name : names) {
try {
//加載class類文件到內存中
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
Assert.isAssignable(type, instanceClass);
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
//通過反射創建實例
T instance = (T) BeanUtils.instantiateClass(constructor, args);
instances.add(instance);
} catch (Throwable ex) {
throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
}
}
return instances;
}
通過反射獲取實例,將觸發EventPublishingRunListener的構造方法:
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
private final SpringApplication application;
private final String[] args;
/**
* 將所有事件廣播給所有已註冊的監聽器,讓監聽器來忽略它們不感興趣的事件。
* 監聽器通常會對傳入的事件對象執行相應的{@code instanceof}檢查。
*
* 默認情況下,在調用線程中調用所有監聽器。
* 這允許流氓監聽器阻塞整個應用程序的危險,但只增加了最小的開銷。
* 指定一個可選的任務執行器,以便在不同的線程中(例如從線程池中)執行監聽器。
*/
private final SimpleApplicationEventMulticaster initialMulticaster;
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<?> listener : application.getListeners()) {
//加載監聽器
this.initialMulticaster.addApplicationListener(listener);
}
}
... ...
}
這裏加載所有監聽器,進入addApplicationListener()方法看一下:
@Override
public void addApplicationListener(ApplicationListener<?> listener) {
synchronized (this.retrievalMutex) {
//如果已經註冊,則顯式刪除代理的目標
//爲了避免對同一個監聽器的重複調用。
Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
if (singletonTarget instanceof ApplicationListener) {
this.defaultRetriever.applicationListeners.remove(singletonTarget);
}
/**
* 在靜態類AbstractApplicationEventMulticaster中定義了一個目標監聽器實例
* private final ListenerRetriever defaultRetriever = new ListenerRetriever(false);
*
* 默認目標監聽器實例調用了一個AbstractApplicationEventMulticaster類的內部類
* ListenerRetriever實例對象
*/
this.defaultRetriever.applicationListeners.add(listener);
this.retrieverCache.clear();
}
}
OK,進入applicationListeners()方法,也就進入這個內部類:
/**
* ListenerRetriever 是一個Helper類,它封裝了一組特定的目標監聽器偵聽器,允許有效地檢索預先過
* 濾的監聽器。並且此幫助器的實例按事件類型和源類型緩存。
*/
private class ListenerRetriever {
public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
public final Set<String> applicationListenerBeans = new LinkedHashSet<>();
private final boolean preFiltered;
public ListenerRetriever(boolean preFiltered) {
this.preFiltered = preFiltered;
}
public Collection<ApplicationListener<?>> getApplicationListeners() {
List<ApplicationListener<?>> allListeners = new ArrayList<>(
this.applicationListeners.size() + this.applicationListenerBeans.size());
allListeners.addAll(this.applicationListeners);
if (!this.applicationListenerBeans.isEmpty()) {
//Bean工廠實例化
BeanFactory beanFactory = getBeanFactory();
//遍歷當前的監聽器Bean實例集合
for (String listenerBeanName : this.applicationListenerBeans) {
try {
//從監聽器bean集合中獲取監聽器的實例Bean對象
ApplicationListener<?> listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
if (this.preFiltered || !allListeners.contains(listener)) {
//裝載監聽器實例bean
allListeners.add(listener);
}
} catch (NoSuchBeanDefinitionException ex) {
// Singleton listener instance (without backing bean definition) disappeared -
// probably in the middle of the destruction phase
}
}
}
if (!this.preFiltered || !this.applicationListenerBeans.isEmpty()) {
//對監聽器排序
AnnotationAwareOrderComparator.sort(allListeners);
}
return allListeners;
}
}
通過this.defaultRetriever.applicationListeners.add(listener),將監聽器spring.factories中的監聽器傳遞給SimpleApplicationEventMulticaster中。觸發EventPublishingRunListener的構造方法,然後獲取到所有的監聽器實例。
內部類SimpleApplicationEventMulticaster繼承了AbstractApplicationEventMulticaster,然後由AbstractApplicationEventMulticaster實現三個接口。繼承關係如下:
【2】啓動監聽器
下一步啓動監聽器,繼續看SpringApplication.java類:
//在run方法第一次啓動時立即調用。可以用於非常早期的初始化。
listeners.starting();
從獲取監聽器分析,可知這裏啓動EventPublishingRunListener監聽器,即啓動時間發佈監聽器,用來發布啓動事件。
EventPublishingRunListener作爲早期的監聽器,執行後邊的started()方法,將發佈監聽事件。這裏,我們進入該類的starting()
@Override
public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}
@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
}
啓動監聽器時,調用starting()方法,這裏我們進入starting()方法,調用multicastEvent()方法:
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
//解析事件類型
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//獲取線程池
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
//如果爲空則同步處理
if (executor != null) {
//異步發送監聽事件
executor.execute(() -> invokeListener(listener, event));
} else {
//同步發送監聽事件
invokeListener(listener, event);
}
}
}
以日誌監聽器爲例:
/**
* 繼承自ApplicationListener接口的方法
*
* @param event
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
//Springboot啓動時
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
//環境準備完成時
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
//容器環境配置完成後
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
//容器關閉時
else if (event instanceof ContextClosedEvent
&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
//容器啓動失敗時
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
第五步:裝配參數
裝配參數,構建一個默認的應用對象:
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
進入DefaultApplicationArguments()方法:
public DefaultApplicationArguments(String[] args) {
Assert.notNull(args, "Args must not be null");
this.source = new Source(args);
this.args = args;
}
第六步:初始化容器環境
//初始化容器環境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
查看prepareEnvironment()方法:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
//獲取相應的配置環境
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
//監聽環境已準備事件,併發布
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
這裏,環境信息配置,我們先進入getOrCreateEnvironment()方法:
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
//根據web環境類型判斷,返回對應的環境類型配置
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
看下枚舉類WebApplicationType:
public enum WebApplicationType {
/**
*應用程序不應作爲web應用程序運行,也不應啓動嵌入式web服務器。
*/
NONE,
/**
* 應用程序應該作爲基於servlet的web應用程序運行,並且應該啓動嵌入式servlet web服務器。
*/
SERVLET,
/**
*應用程序應該作爲反應性web應用程序運行,並應該啓動嵌入式反應性web服務器。
*/
REACTIVE;
... ...
}