浅析Servlet3.0-SCI技术与Spring纯注解式web工程的关系

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框架中,SpringServletContainerInitializerServletContainerInitializer的一个实现,它负责启动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 实现类。具体来说,容器会做以下几步:

  1. 扫描:容器扫描应用的 /META-INF/services/ 目录下的 javax.servlet.ServletContainerInitializer 文件,找到其中指定的实现类的全限定名。

  2. 加载和实例化:容器加载并实例化这些 ServletContainerInitializer 实现类。

  3. 调用 onStartup 方法:容器调用 SpringServletContainerInitializeronStartup 方法,传入感兴趣的类型集合(通过 @HandlesTypes 注解指定)和当前的 ServletContext 对象。对于 Spring,感兴趣的类型是 WebApplicationInitializer

WebApplicationInitializer

WebApplicationInitializer 是 Spring 提供的一个接口,设计用于在 Servlet 3.0+ 环境中替代 web.xml。开发者可以实现这个接口,并通过其 onStartup(ServletContext servletContext) 方法配置 Servlet 上下文,包括注册 DispatcherServlet、各种 Filter 和 Listener。

Spring 提供了几个 WebApplicationInitializer 的具体实现,如 AbstractAnnotationConfigDispatcherServletInitializerAbstractDispatcherServletInitializer,它们提供了一种便捷的方式来配置基于注解的 Spring Web 应用。

通过 SpringServletContainerInitializerWebApplicationInitializer,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 对象作为集合传递给 SpringServletContainerInitializeronStartup 方法,进而触发 Spring Web 应用的初始化过程。

Servlet SCI 技术与 Java SPI 机制的比较

Servlet 的 SCI 技术和 Java 的服务提供接口(Service Provider Interface,简称 SPI)机制都是为了提供一种服务发现和加载的机制,但它们的应用场景和目的有所不同。

相似之处

  1. 服务发现:两者都提供了一种基于接口的服务发现机制,允许服务的使用者不必知道服务提供者的具体实现,从而实现了解耦。
  2. 动态加载:两者都支持动态加载服务实现,不需要在编译时就确定具体的实现类。

不同之处

  1. 应用场景

    • SCI:主要用于 Servlet 容器启动时的初始化工作,特别是在 Web 应用启动阶段,允许框架和库开发者以编程方式对 Servlet 容器进行配置,如注册 Servlet、Filter、Listener 等。
    • SPI:更通用,用于为 Java 应用提供可插拔的服务实现机制。它广泛应用于各种场景,如数据库驱动加载、日志框架绑定等。
  2. 实现方式

    • SCI:通过在 META-INF/services 目录下创建一个名为 javax.servlet.ServletContainerInitializer 的文件,并在文件中指定实现了 ServletContainerInitializer 接口的类的全限定名来实现。
    • SPI:同样通过在 META-INF/services 目录下创建服务接口全限定名命名的文件,但文件内容是该服务接口的一个或多个实现类的全限定名列表。
  3. 目标对象

    • 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)
      • resources/
        • META-INF/services/
          • com.example.spi.MessageService (服务提供者注册文件)

运行程序

当运行Main类时,ServiceLoader会找到并实例化EmailMessageServiceSmsMessageService,然后调用它们的sendMessage方法。输出应该类似于:

Email message: Hello SPI
SMS message: Hello SPI

这个简单的示例展示了Java SPI机制的基本用法,包括定义服务接口、实现服务、注册服务提供者以及如何使用ServiceLoader来发现和加载服务。

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进窄门见微光行远路

如果对你有比较大的帮助

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值