在 spring boot 中,很吸引人的一个特性是可以直接把应用打包称为一个 jar/war,这个 jar/war 是可以直接启动的,不需要额外配置 web server。
疑问
- Spring boot 如何启动?
- Spring boot 内置的 embed tomcat 是如何工作的?静态文件,jsp,网页模板是如何加载到的?
打包为单个 jar 包时,spring boot 的启动方式
maven 打包之后,会生成两个 jar 文件:
demo-0.0.1-SNAPSHOT.jar
demo-0.0.1-SNAPSHOT.jar.original
其中 demo-0.0.1-SNAPSHOT.jar.original 是默认的 maven-jar-plugin 生成的包。demo-0.0.1-SNAPSHOT.jar 是 spring boot maven 插件生成的 jar 包,里面包含了应用的依赖,以及 spring boot 相关的类。
先来看看 spring boot 打包好的目录结构:
├── META-INF
│ ├── MANIFEST.MF
├── application.properties
├── com
│ └── example
│ └── SpringBootDemoApplication.class
├── lib
│ ├── aopalliance-1.0.jar
│ ├── spring-beans-4.2.3.RELEASE.jar
│ ├── ...
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── JavaAgentDetector.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── ...
MANIFEST.MF
Manifest-Version: 1.0
Start-Class: com.example.SpringBootDemoApplication
Implementation-Vendor-Id: com.example
Spring-Boot-Version: 1.3.0.RELEASE
Created-By: Apache Maven 3.3.3
Build-Jdk: 1.8.0_60
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
- 可以看到有 Main-Class 是 org.springframework.boot.loader.JarLauncher,这个是 jar 启动的 Main 函数。
- 还有一个 Start-Class 是 com.example.SpringBootDemoApplication,这个是我们应用自己的 Main 函数。
com/example 目录
这里面放的是应用的 .class 文件。
lib 目录
这里存放的是应用的 Maven 依赖的 jar 包文件。比如 spring-beans,spring-mvc 等 jar 包。
org.springframework.boot.loader
这里面存放的是 spring boot loader 的 .class 文件。
Archive 的概念
- archive 是归档文件,这个概念在 linux 下比较常见。
- 通常就是一个 tar/zip 格式的压缩包。
- jar 是 zip 格式。
在 spring boot 中,抽象除了 Archive 的概念。一个 archive 可以是一个 jar(JarFileArchive),也可以是一个文件目录(ExploadedArchive)。可以理解为 spring boot 抽象出来的统一访问资源的层。
上面的 demo-0.0.1-SNAPSHOT.jar 是要给 Archive,demo-0.0.1-SNAPSHOT.jar 里面的 /lib 目录下的每一个 jar 包都是一个 Archive。
public abstract URL getUrl();
public String getMainClass();
public abstract Collection<Entry> getEntries();
public abstract List<Archive> getNestedArchives(EntryFilter filter);
每个 Archive 都有一个自己的 URL,比如:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
还有一个 getNestedArchives 函数,这个实际返回的是 demo-0.0.1-SNAPSHOT.jar/lib 下面的 jar 的 Archive 列表。它们的 URL 是:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jar
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar
JarLauncher
从 MANIFEST.MF 可以看到 Jar 启动的 Main 函数是 JarLauncher,下面来分析它的工作流程。
class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher
-
以 demo-0.01-SNAPSHOT.jar 创建一个 Archive:
JarLauncher 先找到自己所在的 jar,即 demo-0.01-SNAPSHOT.jar 的路径,然后创建一个 Archive。
-
获取 /lib 下面的 jar,并创建一个 LaunchedURLClassLoader:
JarLauncher 创建好 Archive 之后,通过 getNestedArchives 函数来获取 demo-0.0.1-SNAPSHOT.jar/lib 下面所有的 jar 文件,并创建为 List。获取到这些 Archive 的 URL 之后,也就获得了 URL[] 的数组,用来构造一个自定义的 ClassLoader: LaunchedURLClassLoader。
创建好 ClassLoader 之后,再从 MANIFEST.MF 里读取到 start-class,即 com.example.SpringBootDemoApplication,然后创建一个新的线程来启动 Main 函数。
/** * Launch the application given the archive file and a fully configured classloader. */ protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { Runnable runner = createMainMethodRunner(mainClass, args, classLoader); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start(); } /** * Create the {@code MainMethodRunner} used to launch the application. */ protected Runnable createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) throws Exception { Class<?> runnerClass = classLoader.loadClass(RUNNER_CLASS); Constructor<?> constructor = runnerClass.getConstructor(String.class, String[].class); return (Runnable) constructor.newInstance(mainClass, args); }
-
LaunchedURLClassLoader
LaunchedURLClassLoader 和普通的 URLClassLoader 的不同之处在于,它提供了从 Archive 加载 .class 的能力。
Spring boot 应用启动流程总结
- spring boot 应用打包之后,生成了一个 jar 包,里面包含了应用依赖的 jar 包,还有 spring boot loader 相关的类。
- Jar 包启动的 Main 函数是 JarLauncher,它负责创建一个 LaunchedURLClassLoader 来加载 /lib 下面的 jar,并以一个新线程启动应用的 Main 函数。
Embed Tomcat 的启动流程
-
判断是否在 web 环境
spring boot 启动时,先通过一个简单的查找 Servlet 类的方式来判断是不是在 web 环境:
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }; private boolean deduceWebEnvironment() { for (String className : WEB_ENVIRONMENT_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return false; } } return true; }
-
如果是 web 环境,则会创建 AnnotationConfigEmbeddedWebApplicationContext,否则 spring context 就是 AnnotationConfigApplicationContext。
//org.springframework.boot.SpringApplication protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS); } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass); }
-
获取 EmbeddedServletContainerFactory 的实现类
spring boot 通过获取 EmbeddedServletContainerFactory 来启动对应的 web 服务器。常用的两个实现类是 TomcatEmbeddedServletContainerFactory 和 JettyEmbeddedServletContainerFactory。
//TomcatEmbeddedServletContainerFactory @Override public EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null ? this.baseDirectory : createTempDir("tomcat")); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); tomcat.getEngine().setBackgroundProcessorDelay(-1); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatEmbeddedServletContainer(tomcat); }
会为 tomcat 创建一个临时文件目录,如:/tmp/tomcat.2233614112516545210,作为 tomcat 的 basedir。里面会存放 tomcat 的临时文件,比如 work 目录。
还会初始化 Tomcat 的一些 Servlet,比如比较中要的 default/jsp servlet:
private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set defaultServlet.setOverridable(true); context.addChild(defaultServlet); context.addServletMapping("/", "default"); } private void addJspServlet(Context context) { Wrapper jspServlet = context.createWrapper(); jspServlet.setName("jsp"); jspServlet.setServletClass(getJspServletClassName()); jspServlet.addInitParameter("fork", "false"); jspServlet.setLoadOnStartup(3); context.addChild(jspServlet); context.addServletMapping("*.jsp", "jsp"); context.addServletMapping("*.jspx", "jsp"); }
Spring Boot 的 web 应用访问 Resource
当 spring boot 应用被打包为一个 jar 时,是如何访问到 web resource 的?
实际上是通过 Archive 提供的 URL,然后通过 Classloader 提供的访问 classpath resource 的能力来实现的。
index.html
比如需要配置一个 index.html,这个可以直接放在代码里的 src/main/resources/static 目录下。
对于 index.html 欢迎页,spring boot 在初始化时,就会创建一个 ViewController 来处理。
//ResourceProperties
public class ResourceProperties implements ResourceLoaderAware {
private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
//WebMvcAutoConfigurationAdapter
@Override
public void addViewControllers(ViewControllerRegistry registry) {
Resource page = this.resourceProperties.getWelcomePage();
if (page != null) {
logger.info("Adding welcome page: " + page);
registry.addViewController("/").setViewName("forward:index.html");
}
}
template
像页面模板文件可以放在 src/main/resources/template 目录下,这个是需要模板实现类自己处理的,比如 ThymeleafProperties 类里的:
public static final String DEFAULT_PREFIX = "classpath:/templates/";
jsp
jsp 页面和 template 类似。实际上是通过 springmvc 内置的 JstlView 来处理的。
可以通过配置 spring.view.prefix 来设定 jsp 页面的目录。
spring.view.prefix: /WEB-INF/jsp/
统一错误页面处理
对于错误页面,Spring boot 也是通过创建一个 BasicErrorController 来统一处理的。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController
对应的页面是一个简单的 html。
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
Spring boot 应用的 maven 打包过程
先通过 maven-shade-plugin 生成一个包含依赖的 jar,再通过 spring-boot-maven-plugin 插件把 spring boot loader 相关的类,还有 MANIFEST.MF 打包到 jar 里。