SpringBoot系列(7):SpringBoot启动流程源码分析

前言

经过前面《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;
    ... ...
}
发布了93 篇原创文章 · 获赞 136 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_27706119/article/details/100751279