坐标杭州,平时打打游戏、看看书、为公司写写bug,但有幸为公司招聘新鲜血液,在筛查简历的时候,发现大家必填的一项SpringBoot,包括(掌握SpringBoot了,熟悉SpringBoot了,熟悉使用了,精通等等)什么~~还有精通的吗,看来这小伙子必须得面一面。
所以作为一个老实、沉稳的面试官,SpringBoot的这种面试深海还是需要跟求职人深入探讨一下的,基本上的讨论包括了自动装配原理,什么是内嵌server容器,有哪些方式生成bean呀等等,今天的主题就是一期探讨下内嵌的servlet是如何启动、配置的
- 为 何 使 用 j a v a − j a r 就 能 启 动 w e b 项 目 \color{#00CED1}{为何使用java -jar就能启动web项目} 为何使用java−jar就能启动web项目
- 项 目 里 怎 么 没 有 w e b . x m l 文 件 \color{#00CED1}{项目里怎么没有web.xml文件} 项目里怎么没有web.xml文件
- 有 哪 些 方 式 修 改 绑 定 端 口 \color{#00CED1}{有哪些方式修改绑定端口} 有哪些方式修改绑定端口
- 内 嵌 S e r v e r 如 何 启 动 \color{#00CED1}{内嵌Server如何启动} 内嵌Server如何启动
为何使用java -jar就能启动web项目
引用Spring Boot编程思想第2.2运行SpringBoot应用
我们的项目启动类被 J a r L a u n c h e r \color{#FF7D00}{JarLauncher} JarLauncher装载并执行, M E T A − I N F / M A N I F E S T . M F \color{#FF7D00}{META-INF/MANIFEST.MF} META−INF/MANIFEST.MF资源中的Start-Class属性(也即我们的启动类)被JarLauncher管理项目引导类
私下想要尝试的小伙伴,可以在maven文件添加如下插件,并打包和解压文件查看项目里的MANIFEST.MF文件
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.wang.springboottest.SpringBootTestApplication</mainClass> </manifest> </archive> </configuration> </plugin>
项目里怎么没有web.xml文件
还记得我们Servlet 3.0以前的版本都是含有web.xml 配置上下文信息,过滤器、监听器啥的信息是吧,但是当我们接触springBoot后,神奇的发现项目内并没有此文件,那么springboot是如何启动servlet并且如何配置过滤器、监听器呢
- s p r i n g . f a c t o r i e s 添 加 l i s t e n e r 、 f i l t e r 、 s e r v l e t 的 动 态 配 置 b e a n \color{#00CED1}{spring.factories添加listener、filter、servlet的动态配置bean} spring.factories添加listener、filter、servlet的动态配置bean
- J a v a 代 码 R e g i s t r a t i o n B e a n 动 态 添 加 s e r v l e t f i l t e r l i s t e n e r \color{#00CED1}{Java代码RegistrationBean动态添加servlet filter listener} Java代码RegistrationBean动态添加servletfilterlistener
- S e r v l e t C o n t e x t I n i t i a l i z e r \color{#00CED1}{ServletContextInitializer} ServletContextInitializer
经过以上三个新特性,我们就可以抛弃web.xml文件了,具体在SpringBoot如何使用,在这里卖个关子,稍后源码一见分晓
有哪些方式修改绑定的端口号
大家都知道SpringBoot项目创建的时候有个配置文件,里面可以配置各种属性,包括绑定端口号,那么除了配置文件还知道其他方式吗。并且为何在配置文件写内容,数据就会被同步到内嵌Server的中呢
先如下举例下绑定方式
- 配 置 文 件 ( 最 直 白 的 ) \color{#00CED1}{配置文件(最直白的)} 配置文件(最直白的)
- 启 动 参 数 ( j a v a − j a r − D s e r v e r . p o r t = 8081 ) \color{#00CED1}{启动参数(java -jar -Dserver.port=8081)} 启动参数(java−jar−Dserver.port=8081)
- 环 境 变 量 \color{#00CED1}{环境变量} 环境变量(-. = 但是本人测试失败无法使用server.port的环境变量)
内嵌Servlet如何启动
- 首 先 什 么 是 内 嵌 s e r v l e t \color{#00CED1}{首先什么是内嵌servlet} 首先什么是内嵌servlet
- 如 何 查 看 默 认 的 s e r v l e t \color{#00CED1}{如何查看默认的servlet} 如何查看默认的servlet
- 该 s e r v l e t 是 如 何 启 动 的 \color{#00CED1}{该servlet是如何启动的} 该servlet是如何启动的
什么是内嵌servlet
先举例正常项目启动的操作流程
1、
项
目
打
包
成
w
a
r
包
\color{#FF7D00}{项目打包成war包}
项目打包成war包
2、
w
a
r
包
复
制
到
t
o
m
c
a
t
目
录
下
的
w
e
b
a
p
p
s
\color{#FF7D00}{war包复制到tomcat目录下的webapps}
war包复制到tomcat目录下的webapps
3、
启
动
t
o
m
c
a
t
\color{#FF7D00}{启动tomcat}
启动tomcat
4、
项
目
启
动
\color{#FF7D00}{项目启动}
项目启动
但SpringBoot不需要我们自己下载tomcat,Spring已经在我们项目中的导入必要的第三方jar包,如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
包含
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.2.5.RELEASE</version>
<scope>compile</scope>
</dependency>
此jar包包含各种tomcat核心jar包
启动图我们可以清晰看到tomcat的打印日志
当然springBoot并非只支持Tomcat内嵌,我们可以替换pom文件替换内嵌servlet
查看官方文档 我们可以看到
Spring Boot includes support for embedded Tomcat, Jetty, and Undertow servers. Most developers use the appropriate “Starter” to obtain a fully configured instance. By default, the embedded server listens for HTTP requests on port 8080.
当然替换很简单
如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
相当于Spring在启动main文件后,会直接启动内嵌的jetty,以供服务于servlet容器
照例、直接上图:
可以清晰的看到 j e t t y W e b S e r v e r \color{#FF7D00}{jettyWebServer} jettyWebServer
接下来就是详细而无聊的源码讲解,到底springBoot是如何启动内嵌的容器(一下栗子采取的都是内嵌Tomcat服务器)
- 首先我们查看启动类的 S p r i n g A p p l i c a t i o n \color{#FF7D00}{SpringApplication} SpringApplication的run方法,会先创建应用上下文
context = createApplicationContext();
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
// 我们项目包含了webjar包,属于servlet项目,并生成了AnnotationConfigServletWebServerApplicationContext对象
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
- 接下来 A n n o t a t i o n C o n f i g S e r v l e t W e b S e r v e r A p p l i c a t i o n C o n t e x t \color{#FF7D00}{AnnotationConfigServletWebServerApplicationContext} AnnotationConfigServletWebServerApplicationContext就是我们的重头戏,他会在刷新上下文的时候创建server,如下
@Override
protected void onRefresh() {
super.onRefresh();
try {
// 创建tomcat服务器 初始化servlet
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
- 进入createWebServer
private void createWebServer() {
// 还未初始化Server 所以以下2项都为空
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 不是重点,稍微介绍下
// 获取当前项目的web服务工厂,比如web默认的TomcatWeb服务 或者替换的jetty、undertow
// 并且注册为Spring的bean
ServletWebServerFactory factory = getWebServerFactory();
// 继续深入分析,这里是我们的重点,也是内嵌Tomcat的重点
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
- 继续深入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);
return getTomcatWebServer(tomcat);
}
此处因与本文联系不大,后续章节会出一版<<深入了解SpringBoot的Tomcat详解>>
现粗略讲下大致流程初始化tomcat对象 -> 初始化连接器 -> 初始化Service组件 -> 初始化Host组件 -> 初始化engine组件 -> 初始化context组件 -> 初始化servlet容器
相信大家都熟悉了这张图,在此再贴一次 毕竟Tomcat就像咱的女朋友不仅每天使用,百看不厌嘛
- 可能大家对Tomcat不太熟悉的人对这种跳跃性难以接受(其实是我自己也看不下去了,但作为第一次写博,希望大家稍微谅解下哈),在此给大家画个时序图&贴栈信息的情况供大家粗略了解下
- 栈信息如下:
- 配合时序图观看效果更佳哦
- 栈信息如下:
- 咱继续深入初始化Servlet容器初始化
private void selfInitialize(ServletContext servletContext) throws ServletException {
// spring 上下文 设置 servletContext
prepareWebApplicationContext(servletContext);
// 注册应用上下文属性的servlet包装类
registerApplicationScope(servletContext);
// 注册servlet各属性到Spring容器的单例bean(包括:servletContext,servletConfig,contextAttributes ...)
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
// 重点来咯,getServletContextInitializerBeans的时候会初始化ServletContextInitializerBeans对象
// 这个对象做了许多bean初始化的操作,待我后续慢慢讲来
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
// 调用start方法 配置具体的servlet filter listener等
beans.onStartup(servletContext);
}
}
-
- 初始化Servlet上下文Bean对象
@SafeVarargs public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) { // 初始化数组 this.initializers = new LinkedMultiValueMap<>(); // a this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class); // b addServletContextInitializerBeans(beanFactory); // c addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream() .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this.sortedList = Collections.unmodifiableList(sortedInitializers); logMappings(this.initializers); }
-
a: 因参数initializerTypes传过来为null,所以会新增ServletContextInitializer,大家还记得上面讲过的消失的web.xml吗,是的,我们开始收尾呼应了有木有,(老铁们,双击666,点个关注不迷路)
-
b:重点 初始化dispatcherServlet到spring上下文的容器内
-
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) { for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) { for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory, initializerType)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } } }
-
重 点 代 码 g e t O r d e r e d B e a n s O f T y p e ( b e a n F a c t o r y , i n i t i a l i z e r T y p e ) ) , 生 成 d i s p a t c h e r S e r v l e t 的 B e a n \color{#FF7D00}{重点代码getOrderedBeansOfType(beanFactory,initializerType)),生成dispatcherServlet的Bean} 重点代码getOrderedBeansOfType(beanFactory,initializerType)),生成dispatcherServlet的Bean
-
这时候就会有同学提问了,为什么这里加载的就是dispatcherServlet呢,ServletContextInitializer有很多的子类呀,不知大家是否还记得自动配置原理(这里先不讲述,如果懂的人不是很多,可以下一期讲解下)我们在spring-boot-autoconfigure的jar包下的spring.factories看到org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration这行代码,点进发现这是一个自动配置类,并且我们的项目已经符合了他的condition(如下),所以在项目启动初期的时候该类下的Bean,包括(DispatcherServlet、MultipartResolver、DispatcherServletRegistrationBean)会被beanFactory扫描到,加载class到bean工厂内
-
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(DispatcherServlet.class) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration
-
至此,我们的servlet已经初始化成功
-
-
c:重点,注册上传文件配置、servlet注册器、filter注册器、listener注册器到spring容器
-
protected void addAdaptableBeans(ListableBeanFactory beanFactory) { // A MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory); // B addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig)); // C addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter()); for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) { // D addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType, new ServletListenerRegistrationBeanAdapter()); } }
-
A:上传文件配置bean的注册
-
B:注册spring.factories包含的servlet(除dispatcherServlet)
-
C:注册spring.factories包含的Filter到spring的容器内
-
D:注册spring.factories包含的listener到spring容器内
-
总结
SpringBoot的内嵌容器展现了其灵活的可扩展性、并且灵活运用了其自动装配和IOC容器等,我们可以在其项目启动期间做一些我们个人的操作等。
当然文章中还有很多的遗漏点,其实那些大家抽空的时间稍晚看下源码也能猜到大概的意思,期间还是离不开我们老生常谈的了Spring的ApplicationContext和BeanFactory
唠叨
工作三年有余,思来想去,在工作之时并没有记录那些映象深刻的问题、以及Bug,但多在平时工作中经常遇到,致遗忘的比较快,特此以写文的方式,与大家一起探讨、检索工作中遇到的难解的问题。
如遇文章有错误的点,请您动动小手,帮忙指点一二,让我们一同成长,一起成为面试中的霸者。offer拿到堆不下,数钱数到手抽筋
参考资料:https://www.cnkirito.moe/servlet-explore/ (寻找遗失的web.xml)
小马哥的Spring Boot编程思想