当您重新启动您的 企业 应用程序时,您的客户在打开 Web 浏览器时会看到什么?
-
他们什么也看不到,服务器还没有响应,因此网络浏览器显示
ERR_CONNECTION_REFUSED
。 - 应用程序前面的 Web 代理(如果有)注意到它已关闭并显示“友好”错误消息
-
该网站需要很长时间才能加载 - 它接受套接字连接和 HTTP 请求,但等待响应直到应用程序实际启动。
-
您的应用程序被横向扩展,以便其他节点快速接收请求并且没有人注意到(无论如何都会复制会话)。
-
... 或者应用程序启动速度如此之快,以至于没有人注意到任何中断。 (嘿,一个普通的 Spring Boot
Hello world
应用程序从点击
java -jar ... [Enter]
开始服务请求不到 3 秒。)顺便说一句,查看 SPR-8767: Parallel bean initialization during startup 。
情况 4 和 5 肯定更好,但在本文中,我们将介绍对情况 1 和 3 的更稳健处理。
一个典型的 Spring Boot 应用程序在所有 bean 都加载完毕时(情况 1)在最后启动一个 Web 容器(例如 Tomcat)。这是一个非常合理的默认设置,因为它会阻止客户端在完全配置之前访问我们的端点。但是,这意味着我们无法区分启动几秒钟的应用程序和关闭的应用程序。因此,我们的想法是让应用程序在加载时显示一些有意义的启动页面,类似于显示“ 服务不可用 ”的 Web 代理。然而,由于这样的启动页面是我们应用程序的一部分,它可能更深入地了解启动进度。我们希望在初始化生命周期的早期启动 Tomcat,但在 Spring 完全引导后提供一个特殊用途的启动页面。这个特殊页面应该拦截每一个可能的请求——因此它听起来像一个 servlet 过滤器。
尽早启动 Tomcat
在 Spring Boot servlet 中,容器是通过
EmbeddedServletContainerFactory
初始化的,它创建了
EmbeddedServletContainer
的实例。我们有机会使用
EmbeddedServletContainerCustomizer
拦截这个过程。容器在应用程序生命周期的早期创建,但在整个上下文完成后才
开始
。所以我想我会在我自己的定制器中简单地调用
start()
就是这样。
不幸的是,
ConfigurableEmbeddedServletContainer
没有公开这样的 API,所以我不得不像这样装饰
EmbeddedServletContainerFactory
:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
您可能认为
BeanPostProcessor
太过分了,但它稍后会变得非常有用。我们在这里所做的是,如果我们遇到从应用程序上下文请求的
EmbeddedServletContainerFactory
,我们将返回一个急切启动 Tomcat 的装饰器。这给我们留下了相当不稳定的设置,其中 Tomcat 接受连接到尚未初始化的上下文。所以让我们放置一个 servlet 过滤器拦截所有请求,直到上下文完成。
在启动期间拦截请求
我开始时只是将
FilterRegistrationBean
添加到 Spring 上下文,希望它能拦截传入的请求,直到上下文启动。这是徒劳的:我不得不等待很长时间,直到过滤器被注册并准备就绪,因此从用户的角度来看,应用程序挂起了。后来我什至尝试使用 servlet API (
javax.servlet.ServletContext.addFilter()
) 直接在 Tomcat 中注册过滤器,但显然整个
DispatcherServlet
必须事先引导。请记住,我想要的只是来自即将初始化的应用程序的极快反馈。
所以我最终得到了 Tomcat 的专有 API:
org.apache.catalina.Valve
。
Valve
类似于 servlet filter,但它是 Tomcat 架构的一部分。 Tomcat 自己捆绑了多个阀来处理各种容器功能,如 SSL、会话集群和
X-Forwarded-For
处理。
Logback Access
也使用这个 API,所以我并不感到内疚。阀门看起来像这样:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
阀门通常委托给链中的下一个阀门,但这次我们只是为每个请求返回一个静态
loading.html
页面。注册这样一个阀门非常简单,Spring Boot 有一个 API!
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
定制阀门被证明是一个好主意,它可以立即与 Tomcat 一起启动并且相当容易使用。但是,您可能已经注意到,即使在我们的应用程序启动之后,我们也从未放弃提供
loading.html
服务。那很糟。 Spring 上下文可以通过多种方式发出初始化信号,例如使用
ApplicationListener<ContextRefreshedEvent>
:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
我知道你怎么想,“
static
”?但在
Valve
内部,我根本不想接触 Spring 上下文,因为如果我在错误的时间点从随机线程请求一些 bean,它可能会引入阻塞甚至死锁。当我们完成
promise
时,
Valve
将自行注销:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
这是一个非常干净的解决方案:当不再需要
Valve
时,我们无需为每个请求支付费用,只需将其从处理管道中删除即可。我不打算演示它的工作原理和原因,让我们直接转到目标解决方案。
监控进度
监视 Spring 应用程序上下文启动的进度非常简单。此外,与 EJB 或 JSF 等 API 和规范驱动的框架相比,我对 Spring 的“可破解性”感到惊讶。在 Spring 中,我可以简单地实现
BeanPostProcessor
以通知每个正在创建和初始化的 bean(
完整源代码
):
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
每次初始化一个新 bean 时,我都会将其名称发布到 RxJava 的可观察对象中。整个应用程序初始化后,我完成
Observable
。这个
Observable
以后可以被任何人使用,例如我们的自定义
ProgressValve
(
完整源代码
):
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
ProgressValve
现在更复杂了,我们还没有完成。它可以处理多个不同的请求,例如,我故意在
/health
和
/info
Actuator 端点上返回 503,这样应用程序就好像在启动期间关闭了一样。除了
init.stream
之外的所有其他请求都显示熟悉的
loading.html
。
/init.stream
很特别。这是一个
服务器发送的事件
端点,每次初始化新 bean 时都会推送消息(对不起,代码墙):
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
这意味着我们可以使用简单的 HTTP 接口(!)跟踪 Spring 应用程序上下文启动的进度:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
该端点将实时流式传输(另请参阅:
使用 RxJava 和 SseEmitter 的服务器发送的事件
)每个被初始化的 bean 名称。拥有如此出色的工具,我们将构建更健壮的(
反应式
- 我说的是)
loading.html
页面。
花式进步前端
首先,我们需要确定哪些 Spring bean 代表我们系统中的哪些
子系统
、高级组件(甚至可能是
有界上下文
)。我使用
data-bean
自定义属性将其编码
在 HTML 中
:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
CSS
class="waiting"
表示给定的模块尚未初始化,即给定的 bean 尚未出现在 SSE 流中。最初所有组件都处于
"waiting"
状态。然后我订阅了
init.stream
并更改了 CSS 类以反映模块状态的变化:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
简单吧?显然可以在没有 jQuery 的情况下用纯 JavaScript 编写前端。当所有的 bean 都被加载时,
Observable
在服务器端完成并且 SSE 发出
event: complete
。让我们来处理:
class ProgressBeanPostProcessor implements BeanPostProcessor {
//...
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof EmbeddedServletContainerFactory) {
return wrap((EmbeddedServletContainerFactory) bean);
} else {
return bean;
}
}
private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {
return new EmbeddedServletContainerFactory() {
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);
log.debug("Eagerly starting {}", container);
container.start();
return container;
}
};
}
}
因为在应用程序上下文启动时会通知前端,所以我们可以简单地重新加载当前页面。届时,我们的
ProgressValve
将自行注销,因此重新加载将打开
真正的
应用程序,而不是
loading.html
占位符。我们的工作完成了。此外,我计算启动了多少个 bean 并知道总共有多少个 bean(我用 JavaScript 硬编码了,请原谅),我可以计算启动进度的百分比。一张图片胜过千言万语;让这个截屏视频向您展示我们取得的成果:
后续模块启动良好,我们不再查看浏览器错误。以百分比衡量的进度,让整个启动进度感觉非常顺畅。最后但同样重要的是,当应用程序启动时,我们会自动重定向。希望您喜欢这个概念验证。 GitHub 上提供了整个 工作示例应用程序 。