上篇文章中我们讲了Spring Boot的三种创建方式,本章我们来讲讲Spring Boot web项目两种部署方式,第一种是jar包部署;第二种是war包部署,本文将分别介绍两种方式的原理。
1 jar包部署
1.1 创建demo
首先创建Spring Boot项目,在pom.xml中引入WEB依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动类上加入springmvc注解
@SpringBootApplication
@RestController
public class SpringBootLearnDemo1Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootLearnDemo1Application.class, args);
}
@RequestMapping("/hello")
public String hello() {
return "hello springboot";
}
}
好了以上就是一个简单的demo,然后我们开始启动项目。
打成jar包,在jar包目录下通过java -jar 启动项目
启动成功如下,可以看到端口为8080
访问地址:http://127.0.0.1:8080/hello结果如下
1.2 启动原理
为什么通过java -jar 方式可以启动我们Spring Boot web项目呢?
首先我们知道java -jar 方式启动执行jar包执行的是项目根目录下面的/META-INF/MANIFEST.MF文件中配置的主类(Main-Class)的main方法,现在打开jar找到MANIFEST.MF文件如下
可以看到该文件见中有两个配置Main-Class,Start-Class,这些配置都是在我们打包时通过我们在pom.xml文件中配置的插件spring-boot-maven-plugin配置的
现在我们打开配置的Main-Class,打开该类需要引入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
打开JarLauncher找到main方法
main方法中创建了JarLauncher对象,调用了launch方法。
进入launch方法
launch方法里获取了ClassLoader,且通过getMainClass方法获取了Spring Boot启动类。
进入getMainClass方法
可以看出该方法在获取MANIFEST.MF文件中配置的Start-Class,而前文我们已经知道Start-Class为com.jt.springbootlearndemo1.SpringBootLearnDemo1Applicati即是Spring Boot的启动类
返回launch方法中,该方法的最后一行调用了另一个launch重载方法
进入launch重载方法
该方法创建了一个MainMethodRunner对象,调用了其run方法
直接进入MainMethodRunner的run方法
该方法通过反射执行了Spring Boot启动类的main方法
我们知道web项目都是运行在web容器当中,而Spring Boot内置了web容器,如tomcat,那么Spring Boot启动类的main方法是如何启动tomcat?
进入启动类的main方法
可以看到调用了SpringApplication的静态run方法,一路跟进去发现调用了其另一个非静态run方法。
进入run方法
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
/**
*定义了spring容器
*/
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
/**
*创建spring容器AnnotationConfigServletWebServerApplicationContext
*/
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] {
ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
/**
* 初始化容器
*/
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
目前我们只看我注释的地方,即创建了一个SpringBoot实现的spring容器(AnnotationConfigServletWebServerApplicationContext),并初始化容器。
进入初始化容器的方法refreshContext
该方法又调用了refresh方法,一路refresh下去,最终调用了AbstractApplicationContext的refresh,如下
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
/**
* Spring Boot实现了该方法创建了web容器
*/
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
该方法为容器的初始化方法,加载了BeanDefinition,并实例化。Spring Boot实现了该方法调用的onRefresh创建了web容器。
进入onRefresh方法
调用了createWebServer方法
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
/**
* 实例化了创建web容器的工厂类
*/
ServletWebServerFactory factory = getWebServerFactory();
/**
* 通过工程类创建web容器
*/
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
该类通过ServletWebServerFactory 工厂类的getWebServer方法创建了web容器。
进入getWebServer方法
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
/**
* 启动tomcat容器
*/
return getTomcatWebServer(tomcat);
}
在该方法中创建并启动了Tomcat容器;启动tomcat容器在最后一行调用了getTomcatWebServer方法,进入该方法
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
该方法中将tomcat封装成了TomcatWebServer,进入其构造方法
在该方法中最后一行调用initialize方法启动了容器,进入方法
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
/**
* 启动tomcat容器
*/
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
该方法中通过 this.tomcat.start()启动了tomcat,以上就是jar包方式启动的原理。
2 war包部署
前面我们说了jar包部署,Spring Boot也给我们提供了war包部署方式。
2.1 创建demo
修改pom.xml文件,配置packaging为war包方式,显示指定spring-boot-starter-tomcat,并将scope属性配置为provided,表示打包的时候将不会将该依赖打进去。
创建一个WebApplicationInitializer接口的子类,不直接实现该接口Spring Boot已经帮我们实现了一个SpringBootServletInitializer,继承该类并重写configure方法,在SpringApplicationBuilder 中传入启动类。
public class MyServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringBootLearnDemo1Application.class);
}
}
配置完毕,打成war部署到tomcat即可
2.2 启动原理
我们知道使用springmvc的时候我们需要在web.xml文件中配置DispatcherServlet,在初始化方法里spring容器启动,那使用springbooot打成war部署到tomcat是如何启动spring容器的呢?下面让我们一起来看看Spring Boot打成war包是如何启动spring容器的。
首先看我们新创建的类
该类继承了SpringBootServletInitializer,查看其类图
SpringBootServletInitializer实现了WebApplicationInitializer接口,我们知道该接口是spring利用servlet3.0特性做的一个扩展,在tomcat容器启动的时候会调其onStartup方法,下面进入onStartup方法
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
/**
* 这里创建了web容器
*/
WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
if (rootApplicationContext != null) {
servletContext.addListener(new SpringBootContextLoaderListener(rootApplicationContext, servletContext));
}
else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
+ "return an application context");
}
}
可以看到该方法中调用createRootApplicationContext方法创建了spring容器。
进入createRootApplicationContext方法
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
/**
* 创建SpringApplication
*/
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
application.setRegisterShutdownHook(false);
return run(application);
}
该方法创建了一个SpringApplicationBuilder,见名知意,肯定是使用该类创建了一个SpringApplication,这个类大家是否有似曾相识的感觉,上文中通过jar包部署就是调用了该类的run方法启动,下面我们进入该方法的最后一行,调用了run方法。
果然在run方法里面调用SpringApplication的run方法,进入了和上文中jar包方式相同的逻辑。
但是这个逻辑不是会在spring容器初始化的时候,在onRefresh方法中启动内置的tomcat容器吗?
我们再次来看onRefresh方法
进入createWebServer方法
private void createWebServer() {
WebServer webServer = this.webServer;
/**
* 由于我们同tomcat以及启动所以servletContext不为空
* 所以不走内容容器的逻辑
*/
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
该方法在创建Tomcat容器之前判断了ServletContext是否为空,由于tomcat已经先启动所以此处的ServletContext是不等于空的所以,不会走内置tomcat容器。以上就是war部署方式的原理。
3 总结
以上就是jar包和war包部署的启动原理,我们可以看出这是两个相反的逻辑,jar包部署是spring容器带动tomcat容器启动,而war包部署是tomcat容器带动spring容器启动。
4 其他启动方式
除了以上介绍的启动方式,还有:
1.不需要打包直接进入项目的pom.xml文件所在目录,maven插件启动
mvn spring-boot:run
2.进入jar或war目录, loader 启动
java org.springframework.boot.loader.JarLauncher
java org.springframework.boot.loader.WarLauncher