Servlet 3.0 引入了许多重要的新特性,其中之一就是Servlet容器初始化(Servlet Container Initialization,简称SCI)技术,这项技术允许开发者通过编程方式而非声明方式(例如,使用web.xml
文件)来配置Servlet容器。这为基于注解的配置和编程式的Web应用开发打开了大门。
Servlet 3.0 SCI技术
在Servlet 3.0及更高版本中,可以通过@WebServlet
、@WebFilter
和@WebListener
等注解直接在类上声明Servlets、Filters和Listeners,而无需在web.xml
文件中进行配置。此外,Servlet 3.0引入了ServletContainerInitializer
接口,该接口允许第三方库在容器启动时动态地注册Servlets、Filters和Listeners等组件。
ServletContainerInitializer
的实现类可以通过在META-INF/services/javax.servlet.ServletContainerInitializer
文件中指定来自动被Servlet容器发现。当Web应用启动时,Servlet容器会加载并执行这些ServletContainerInitializer
实现,从而允许它们在运行时向容器注册组件。
Spring的纯注解式Web工程实现
在Tomcat中,Servlet 3.0 规范中加载 ServletContainerInitializer 实现类的逻辑主要在 org.apache.catalina.startup.ContextConfig
类中。在 Tomcat 中,ContextConfig
类负责处理 Servlet 容器初始化时的配置和初始化工作,其中包括扫描 WAR 文件的 META-INF 目录下的配置文件,找到实现了 javax.servlet.ServletContainerInitializer
接口的类,并调用其 onStartup
方法来完成初始化工作。
具体来说,ContextConfig
类会在 Context 初始化的过程中,通过调用 processServletContainerInitializers
方法来加载并执行 ServletContainerInitializer 实现类,从而触发 Servlet 容器的初始化流程。这个过程涉及到 Tomcat 的内部实现细节,但整体遵循 Servlet 3.0 规范的要求,通过 SPI 机制加载并执行 ServletContainerInitializer 实现类。
在Spring框架中,SpringServletContainerInitializer
是ServletContainerInitializer
的一个实现,它负责启动Spring的Web应用上下文。Spring利用这一机制,通过其WebApplicationInitializer
接口允许开发者以编程方式配置Servlet容器,而无需任何web.xml
配置文件。
开发者可以创建WebApplicationInitializer
的实现类,Spring在容器启动时会自动检测到这些实现,并调用它们的onStartup
方法。在onStartup
方法中,开发者可以注册和配置DispatcherServlet、Filters、Listeners等组件,实现完全无web.xml
的Web应用配置。
替换web.xml配置
通过实现WebApplicationInitializer
,可以完全替代web.xml
中的配置。以下是一个简单的示例,展示了如何使用Spring的WebApplicationInitializer
来配置DispatcherServlet
和一个简单的ContextLoaderListener
:
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 创建Spring应用上下文
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class); // 注册配置类
context.setServletContext(servletContext);
// 注册DispatcherServlet
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(context));
servlet.setLoadOnStartup(1);
servlet.addMapping("/");
// 添加监听器
servletContext.addListener(new ContextLoaderListener(context));
}
}
在这个示例中,AppConfig
类将是一个使用@Configuration
注解的Spring配置类。这种方式使得Web应用的配置完全通过Java代码完成,提高了配置的灵活性和可维护性。
Spring Framework 通过 SpringServletContainerInitializer
类实现了 ServletContainerInitializer
接口,这是 Servlet 3.0+ 规范的一部分。此实现是 Spring Web 应用程序启动过程中的关键一环,允许开发者以编程方式配置 Servlet 容器,而无需依赖 web.xml
文件。
SpringServletContainerInitializer 实现
在 Spring 的上下文中,SpringServletContainerInitializer
主要负责桥接 Servlet API 和 Spring 的 Web 应用初始化过程。它不直接注册 Servlet、Filter 或 Listener,而是查找实现了 WebApplicationInitializer
接口的类,并委托这些类来完成实际的注册工作。
当 Servlet 容器(如 Tomcat)启动并准备部署 Web 应用时,它会扫描应用的类路径,寻找 META-INF/services/javax.servlet.ServletContainerInitializer
文件中指定的 ServletContainerInitializer
实现。对于 Spring 来说,这个实现就是 SpringServletContainerInitializer
。
触发机制
Servlet 容器在启动时会自动加载并实例化 ServletContainerInitializer
实现类。具体来说,容器会做以下几步:
-
扫描:容器扫描应用的
/META-INF/services/
目录下的javax.servlet.ServletContainerInitializer
文件,找到其中指定的实现类的全限定名。 -
加载和实例化:容器加载并实例化这些
ServletContainerInitializer
实现类。 -
调用
onStartup
方法:容器调用SpringServletContainerInitializer
的onStartup
方法,传入感兴趣的类型集合(通过@HandlesTypes
注解指定)和当前的ServletContext
对象。对于 Spring,感兴趣的类型是WebApplicationInitializer
。
WebApplicationInitializer
WebApplicationInitializer
是 Spring 提供的一个接口,设计用于在 Servlet 3.0+ 环境中替代 web.xml
。开发者可以实现这个接口,并通过其 onStartup(ServletContext servletContext)
方法配置 Servlet 上下文,包括注册 DispatcherServlet、各种 Filter 和 Listener。
Spring 提供了几个 WebApplicationInitializer
的具体实现,如 AbstractAnnotationConfigDispatcherServletInitializer
和 AbstractDispatcherServletInitializer
,它们提供了一种便捷的方式来配置基于注解的 Spring Web 应用。
通过 SpringServletContainerInitializer
和 WebApplicationInitializer
,Spring 实现了与 Servlet 3.0+ 规范的集成,使得开发者能够以完全基于 Java 的方式配置 Web 应用,无需 web.xml
文件。这种机制大大增强了 Spring Web 应用的灵活性和可维护性,简化了 Web 应用的配置过程。
@HandlesTypes(WebApplicationInitializer.class)的意义是什么?servlet SCI技术 跟java的 SPI 机制是否类似?有什么异同?
@HandlesTypes
注解的意义
@HandlesTypes
是 Servlet 3.0 规范中引入的一个注解,用于指定 ServletContainerInitializer
(SCI) 实现类感兴趣的类型。当 Servlet 容器启动时,它会扫描应用中所有的类,寻找那些实现了或扩展了通过 @HandlesTypes
指定的类型的类。然后,这些类会作为一个集合传递给 ServletContainerInitializer
实现的 onStartup
方法。
对于 @HandlesTypes(WebApplicationInitializer.class)
来说,它的意义在于告诉 Servlet 容器:SpringServletContainerInitializer
对实现了 WebApplicationInitializer
接口的类感兴趣。因此,在 Web 应用启动过程中,容器会查找所有实现了 WebApplicationInitializer
的类,并将这些类的 Class
对象作为集合传递给 SpringServletContainerInitializer
的 onStartup
方法,进而触发 Spring Web 应用的初始化过程。
Servlet SCI 技术与 Java SPI 机制的比较
Servlet 的 SCI 技术和 Java 的服务提供接口(Service Provider Interface,简称 SPI)机制都是为了提供一种服务发现和加载的机制,但它们的应用场景和目的有所不同。
相似之处:
- 服务发现:两者都提供了一种基于接口的服务发现机制,允许服务的使用者不必知道服务提供者的具体实现,从而实现了解耦。
- 动态加载:两者都支持动态加载服务实现,不需要在编译时就确定具体的实现类。
不同之处:
-
应用场景:
- SCI:主要用于 Servlet 容器启动时的初始化工作,特别是在 Web 应用启动阶段,允许框架和库开发者以编程方式对 Servlet 容器进行配置,如注册 Servlet、Filter、Listener 等。
- SPI:更通用,用于为 Java 应用提供可插拔的服务实现机制。它广泛应用于各种场景,如数据库驱动加载、日志框架绑定等。
-
实现方式:
- SCI:通过在
META-INF/services
目录下创建一个名为javax.servlet.ServletContainerInitializer
的文件,并在文件中指定实现了ServletContainerInitializer
接口的类的全限定名来实现。 - SPI:同样通过在
META-INF/services
目录下创建服务接口全限定名命名的文件,但文件内容是该服务接口的一个或多个实现类的全限定名列表。
- SCI:通过在
-
目标对象:
- SCI:主要针对 Servlet 容器和 Web 应用。
- SPI:面向所有 Java 应用和服务。
总的来说,虽然 Servlet 的 SCI 技术和 Java 的 SPI 机制在概念上有相似之处,但它们被设计来解决的问题和应用的上下文有所不同。SPI 提供了一种更为通用的服务加载机制,而 SCI 特别针对 Servlet 容器的初始化和配置。
Java 的 SPI demo
Java的SPI(Service Provider Interface)机制允许服务提供者为接口提供多种实现。以下是一个简单的SPI示例,包括定义一个服务接口、创建两个服务提供者实现、使用ServiceLoader
加载服务以及一个模块化项目结构的简单说明。
步骤 1: 定义服务接口
首先,我们定义一个服务接口。在这个例子中,我们创建一个简单的MessageService
接口,它有一个方法sendMessage
用于发送消息。
// 文件路径: src/main/java/com/example/spi/MessageService.java
package com.example.spi;
public interface MessageService {
void sendMessage(String message);
}
步骤 2: 创建服务提供者实现
接下来,我们为MessageService
接口创建两个实现。每个实现类都会以自己的方式发送消息。
实现类 1:
// 文件路径: src/main/java/com/example/spi/impl/EmailMessageService.java
package com.example.spi.impl;
import com.example.spi.MessageService;
public class EmailMessageService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Email message: " + message);
}
}
实现类 2:
// 文件路径: src/main/java/com/example/spi/impl/SmsMessageService.java
package com.example.spi.impl;
import com.example.spi.MessageService;
public class SmsMessageService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("SMS message: " + message);
}
}
步骤 3: 注册服务提供者
在src/main/resources/META-INF/services
目录下创建一个文件,文件名为接口的全限定名com.example.spi.MessageService
。在该文件中列出所有服务提供者的全限定名,每个名称占一行。
com.example.spi.impl.EmailMessageService
com.example.spi.impl.SmsMessageService
步骤 4: 使用ServiceLoader加载和使用服务
最后,我们使用ServiceLoader
来加载并使用这些服务。
// 文件路径: src/main/java/com/example/spi/Main.java
package com.example.spi;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
ServiceLoader<MessageService> services = ServiceLoader.load(MessageService.class);
for (MessageService service : services) {
service.sendMessage("Hello SPI");
}
}
}
模块化项目结构
- src/
- main/
- java/
- com/example/spi/
MessageService.java
(服务接口)
- com/example/spi/impl/
EmailMessageService.java
(服务实现1)SmsMessageService.java
(服务实现2)
- com/example/spi/
- resources/
- META-INF/services/
com.example.spi.MessageService
(服务提供者注册文件)
- META-INF/services/
- java/
- main/
运行程序
当运行Main
类时,ServiceLoader
会找到并实例化EmailMessageService
和SmsMessageService
,然后调用它们的sendMessage
方法。输出应该类似于:
Email message: Hello SPI
SMS message: Hello SPI
这个简单的示例展示了Java SPI机制的基本用法,包括定义服务接口、实现服务、注册服务提供者以及如何使用ServiceLoader
来发现和加载服务。