Spring Boot与Servlet

Spring Boot与Servlet

1、配置嵌入式Servlet容器

SpringBoot默认使用Tomcat作为嵌入式的Servlet容器;

tomcat

问题?

1.1 如何定制和修改Servlet容器的相关配置;

  • 修改和server有关的配置(ServerProperties{也是EmbeddedServletContainerCustomizer});
1
2
3
4
5
6
7
8
9
server.port=8088
server.context-path=/

server.tomcat.uri-encoding=UTF-8

//通用的Servlet容器设置
server.xxx
//Tomcat的设置
server.tomcat.xxx
  • 编写一个EmbeddedServletContainerCustomizer:嵌入式的Servlet容器的定制器;来修改Servlet容器的配置
1
2
3
4
5
6
7
8
9
10
11
12
@Bean  //一定要将这个定制器加入到容器中
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){

return new EmbeddedServletContainerCustomizer() {

//定制嵌入式的Servlet容器相关的规则
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(8083);
}
};
}

1.2 注册Servlet三大组件(Servlet、Filter、Listener)

由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。

注册三大组件用以下方式

  • ServletRegistrationBean
1
2
3
4
5
6
7
8
9
10
11
//注册三大组件
@Bean
public ServletRegistrationBean myServlet(){

ServletRegistrationBean registrationBean = new ServletRegistrationBean(
new CustomServlet(),
"/myServlet"
);

return registrationBean;
}
  • FilterRegistrationBean
1
2
3
4
5
6
7
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new CustomFilter());
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}
  • ServletListenerRegistrationBean
1
2
3
4
5
6
@Bean
public ServletListenerRegistrationBean myListener(){
ServletListenerRegistrationBean<CustomListener> registrationBean =
new ServletListenerRegistrationBean<>(new CustomListener());
return registrationBean;
}

SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet;

DispatcherServletAutoConfiguration中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bean(
name = {"dispatcherServletRegistration"}
)
@ConditionalOnBean(
value = {DispatcherServlet.class},
name = {"dispatcherServlet"}
)
public ServletRegistrationBean dispatcherServletRegistration(DispatcherServlet
dispatcherServlet) {

ServletRegistrationBean registration = new ServletRegistrationBean(
dispatcherServlet,
new String[]{this.serverProperties.getServletMapping()}
);

//默认拦截: / 所有请求;包静态资源,但是不拦截jsp请求; /*会拦截jsp
//可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
registration.setName("dispatcherServlet");
registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}

return registration;
}

2、SpringBoot能不能支持其他的Servlet容器?

在之前写定制嵌入式的Servlet容器相关的规则配置类的时候

ServletConfig.class中用的ConfigurableEmbeddedServletContainer声明的变量,选中,点击Ctrl+h,会在右侧显示它的继承树:

可配置的嵌入式容器工程

other servlet

3、替换为其他嵌入式Servlet容器

默认支持:

3.1 Tomcat(默认使用)

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器-->
</dependency>

3.2 Jetty(长链接,例如聊天)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<!-- 引入除Tomcat外的其他Servlet: jetty -->
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

3.3 Undertow(不支持JSP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<!-- 引入除Tomcat外的其他Servlet: undertow -->
<dependency>
<artifactId>spring-boot-starter-undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

3.4 嵌入式Servlet容器自动配置原理

在自动配置包里的EmbeddedServletContainerAutoConfiguration.class:嵌入式的Servlet容器自动配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@AutoConfigureOrder(-2147483648)
@Configuration
@ConditionalOnWebApplication
@Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
// 导入BeanPostProcessorsRegistrar; 给容器中导入一些组件
// 导入了EmbeddedServletContainerCustomizerBeanPostProcessor
// 后置处理器: bean初始化前后(创建完对象,还没赋值赋值)执行初始化工作
public class EmbeddedServletContainerAutoConfiguration {
...
@Configuration
@ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedUndertow {
public EmbeddedUndertow() {
}

@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory(){
return new UndertowEmbeddedServletContainerFactory();
}
}

@Configuration
@ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedJetty {
public EmbeddedJetty() {
}

@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}

@Configuration
// 判断当前是否引入了Tomcat依赖
@ConditionalOnClass({Servlet.class, Tomcat.class})
// 判断当前容器没有用户自己定义EmbeddedServletContainerFactory: 嵌入式的Servlet容器工厂
// 作用: 创建嵌入式的Servlet容器
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedTomcat {
public EmbeddedTomcat() {
}

@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
}
  • EmbeddedServletContainerFactory(嵌入式Servlet容器工厂)
1
2
3
4
public interface EmbeddedServletContainerFactory {
// 获取嵌入式的Servlet容器
EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... var1);
}

EmbeddedServletContainerFactory

  • EmbeddedServletContainer(嵌入式的Servlet容器)

EmbeddedServletContainer

  • TomcatEmbeddedServletContainerFactory为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers) {

// 创建一个Tomcat
Tomcat tomcat = new Tomcat();
// 配置Tomcat的基本环镜
File baseDir =
this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();

while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}

this.prepareContext(tomcat.getHost(), initializers);
// 将配置好的Tomcat传入进去,返回一个EmbeddedServletContainer;并且启动Tomcat服务器
return this.getTomcatEmbeddedServletContainer(tomcat);
}
  • 我们对嵌入式容器的配置修改是怎么生效?
1
2
# 配置文件
ServerProperties

1
2
// 配置类
EmbeddedServletContainerCustomizer()

EmbeddedServletContainerCustomizer:定制器帮我们修改了Servlet容器的配置?

怎么修改的原理?

  • 容器中导入了EmbeddedServletContainerCustomizerBeanPostProcessor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 初始化之前
public Object postProcessBeforeInitialization(Object bean, String beanName) throws
BeansException {

// 如果当前初始化的是一个ConfigurableEmbeddedServletContainer类型的组件
if (bean instanceof ConfigurableEmbeddedServletContainer) {
this.postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer)bean);
}

return bean;
}
...
private void postProcessBeforeInitialization(ConfigurableEmbeddedServletContainer bean) {
Iterator var2 = this.getCustomizers().iterator();

//获取所有的定制器,调用每一个定制器的customize方法来给Servlet容器进行属性赋值
while(var2.hasNext()) {
EmbeddedServletContainerCustomizer customizer =
(EmbeddedServletContainerCustomizer)var2.next();

customizer.customize(bean);
}

private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
if (this.customizers == null) {
// 从容器中获取所有这葛类型的组件: EmbeddedServletContainerCustomizer
// 定制Servlet容器,给容器中可以添加一个EmbeddedServletContainerCustomizer类型的组件
this.customizers =
new ArrayList(
this.beanFactory.getBeansOfType(
EmbeddedServletContainerCustomizer.class, false, false).values()
);

Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}

return this.customizers;
}

}
  • ServerProperties也是定制器,步骤:
    • SpringBoot根据导入的依赖情况,给容器中添加相应的嵌入式容器工厂,EmbeddedServletContainerFactory[TomcatEmbeddedServletContainerFactory]
    • 容器中某个组件要创建对象就会惊动后置处理器;EmbeddedServletContainerCustomizerBeanPostProcessor;只要是嵌入式的Servlet容器工厂,后置处理器就工作;
    • 后置处理器,从容器中获取所有的EmbeddedServletContainerCustomizer,调用定制器的定制方法

4、嵌入式Servlet容器启动原理

什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat?

获取嵌入式的Servlet容器工厂:

  • SpringBoot应用启动运行run方法
  • refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每一个组件】;如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext,否则:AnnotationConfigApplicationContext
  • refresh(context),刷新刚才创建好的ioc容器;查看AbstractApplicationContext.class源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);

try {
this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn(
"Exception encountered during context initialization - cancelling refresh attempt: "
+ var9
);
}

this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
  • onRefresh(),web的ioc容器重写了onRefresh方法

  • webioc容器会创建嵌入式的Servlet容器;createEmbeddedServletContainer();

  • 获取嵌入式的Servlet容器工厂:**
1
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();

从ioc容器中获取EmbeddedServletContainerFactory 组件;TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;

  • 使用容器工厂获取嵌入式的Servlet容器
1
2
this.embeddedServletContainer = 
containerFactory.getEmbeddedServletContainer(getSelfInitializer());
  • 嵌入式的Servlet容器创建对象并启动Servlet容器;

先启动嵌入式的Servlet容器,再将ioc容器中剩下没有创建出的对象获取出来;

IOC容器启动创建嵌入式的Servlet容器

5、使用外置的Servlet容器

嵌入式Servlet容器:应用打成可执行的jar

优点:简单、便携;

缺点:默认不支持JSP、优化定制比较复杂(使用定制器{ServerProperties、自定义EmbeddedServletContainerCustomizer},自己编写嵌入式Servlet容器的创建工厂{EmbeddedServletContainerFactory});

外置的Servlet容器:外面安装Tomcat—-应用war包的方式打包;

5.1 创建JSPweb项目教程:

  • 使用Spring Initializr新建项目:

jsp web

  • 剩下都是Next,得到项目结构:

Structure

  • 生成webapp文件夹(可手动创建,也可用以下方法):

点击编译器右上的Project Structure按钮—>Modules—>项目展开—>Web—>Web Resource Directories

webapp

双击Web Resource Directory下的路径,弹出对话框:

webapp dialog

点击OK,然后在弹出对话框点Yes即可;

  • 部署描述图生成XML文件:

web.xml

将默认的路径...\WEB-INF\web.xml改为...\src\main\webapp\WEB-INF\web.xml,点击OK即可,最后Apply—>OK

如何启动?

  • 服务器整合进IDEA

编译器上方Edit Configurations…

tomcat

添加本地Tomcat配置即可:

tomcat

配置完后添加部署项目:

deploy

出现对话框,选择第二个暴露的就行

select artifacts to deploy

最后Apply—>OK。运行Tomcat即可

5.2 步骤

  • 必须创建一个war项目;(利用IDEA创建好目录结构)
  • 将嵌入式的Tomcat指定为provided;
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
  • 必须编写一个SpringBootServletInitializer的子类,并调用configure方法
1
2
3
4
5
6
7
8
9
public class ServletInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
// 传入SpringBoot应用的主程序
return application.sources(SpringBootWebJspApplication.class);
}

}
  • 启动服务器就可以使用。

5.3 原理

  • jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的Servlet容器;

  • war包:启动服务器,服务器启动SpringBoot应用{SpringBootServletInitializer},启动ioc容器;

servlet3.0(规范文档)

5.4 查看8.2.4 Shared libraries / runtimes pluggability条内容:

规则:

  • 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例;
  • ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名
  • 还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类

5.5 流程

  • 启动Tomcat

  • org\springframework\spring-web\4.3.14.RELEASE\spring-web-4.3.14.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer:

    Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer

  • SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set>;为这些WebApplicationInitializer类型的类创建实例;

  • 每一个WebApplicationInitializer都调用自己的onStartup;

WebApplicationInitializer

  • 相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法
  • SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext;创建容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// SpringBootServletInitializer.class
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// 1、创建SpringApplicationBuilder
SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
StandardServletEnvironment environment = new StandardServletEnvironment();
environment.initPropertySources(servletContext, (ServletConfig)null);
builder.environment(environment);
builder.main(this.getClass());
ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
(Object)null
);

builder.initializers(
new ApplicationContextInitializer[]{
new ParentContextApplicationContextInitializer(parent)
}
);
}

builder.initializers(
new ApplicationContextInitializer[]{
new ServletContextApplicationContextInitializer(servletContext)
}
);

builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
// 调用configure方法,子类重写了这个方法,将SpringBoot的主程序类传入了进来
builder = this.configure(builder);
// 使用builder创建一个Spring应用
SpringApplication application = builder.build();
if (application.getSources().isEmpty() &&
AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
application.getSources().add(this.getClass());
}

Assert.state(
!application.getSources().isEmpty(),
"No SpringApplication sources have been defined.
Either override the configure method or add an @Configuration annotation"
);

if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
//启动Spring应用
return this.run(application);
}
  • Spring的应用就启动并且创建IOC容器

查看上面run方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();

try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment =
this.prepareEnvironment(listeners, applicationArguments);

Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
new FailureAnalyzers(context);
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新IOC容器
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
listeners.finished(context, (Throwable)null);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(
this.getApplicationLog(),
stopWatch
);
}

return context;
} catch (Throwable var9) {
this.handleRunFailure(context, listeners, (FailureAnalyzers)analyzers, var9);
throw new IllegalStateException(var9);
}
}

启动Servlet容器,再启动SpringBoot应用

评论