【小家Spring】探讨注解驱动Spring应用的机制,详解ServiceLoader、SpringFactoriesLoader的使用(以JDBC、spring.factories为例介绍SPI)(中)

简介: 【小家Spring】探讨注解驱动Spring应用的机制,详解ServiceLoader、SpringFactoriesLoader的使用(以JDBC、spring.factories为例介绍SPI)(中)

ServiceLoader的应用


一、此处以Hadoop的FileSystem为例,它的原理有这么一段:


private static void loadFileSystems() {  
  synchronized(FileSystem.class){  
    if(!FILE_SYSTEMS_LOADED) {  
    // 此处通过ServiceLoader把FileSystem所有的服务提供者都拿出来并且缓存起来了
    // 这个概念,特别特别像通过配置文件配置Bean一样,类比Spring吧  只是没那么强大而已
      ServiceLoader<FileSystem> serviceLoader = ServiceLoader.load(FileSystem.class); 
      for(FileSystem fs : serviceLoader) {  
        SERVICE_FILE_SYSTEMS.put(fs.getScheme(),fs.getClass());  
      } 
      FILE_SYSTEMS_LOADED= true; 
    } 
  } 
}


可以看到FileSystem会把所有的FileSystem的实现都以scheme和class来cache,之后就从这个cache中取相应的值。


因此,以后可以通过ServiceLoader来实现一些类似的功能,而不用依赖像Spring这样的第三方框架(你让Hadoop去强一来Spring显然是非常不合适的)。


二、另外一个应用是责任链设计模式

责任链模式的定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。


其实Spring底层很多都使用了此模式,但今天主要讲讲平时java中的实现。

其中我们熟悉的JDBC驱动加载就是这个例子。java.sql.DriverManager是用来管理负责加载JDBC驱动的,部分源码展示如下:

public class DriverManager {
  ...
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
      ,,,
      ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
      Iterator<Driver> driversIterator = loadedDrivers.iterator();
       try{
          // 只要导入了对应的Jar包,项目启动时候都会加载进来,用户其实并不需要再手动去加载了
           while(driversIterator.hasNext()) {
               driversIterator.next();
           }
       } catch(Throwable t) {
       // Do nothing
       }
    }
    ...


然后我们此处以导入MySql驱动的Jar为例,它有一个如下文件:


image.png


这也就顺便解释了我们前边的一个疑问,其实JDBC驱动我们自己并不需要手动的去Class.forName()去加载了,JDK6以后,只要你导入了包,就自动给你加载进去了


另外我还看到有个哥们写的一个例子,也是典型应用,也分享给大家:消灭成堆的分支语句之类责任链模式


其实JDK的实现方案有的时候并不能满足我们的要求,比如我们希望这个配置文件在任意地方怎么办呢? 这里介绍一个方案:借助google开源的AutoService去自助实现(只不过一般都不这么干,Android应用这么用的可能性会大一点) 这样我们的配置文件就可以像Spring配置文件一下,放在几乎任何地方了



ServletContainerInitializer:和web容器相关的启动器


在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。


使用方式:每个框架要使用ServletContainerInitializer就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作


咋一看,是不是似曾相识呢?对的,思想同JDK的一毛一样,所以说天下技术也是一大抄嘛~~


一般伴随着ServletContainerInitializer一起使用的还有HandlesTypes注解,通过HandlesTypes可以将感兴趣的一些类注入到ServletContainerInitializerde的onStartup方法作为参数传入


这里需要说明一点:即使你类上不标注HandlesTypes注解也是ok的。onStartup方法还是会执行,只不过是public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException { ... }它的c这个属性值为null而已。


Servlet3.0规范的类


// 全类名:javax.servlet.ServletContainerInitializer
// @since Servlet 3.0
public interface ServletContainerInitializer {
  ...
}


ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架.


以SpringMVC举例, servlet3.0之前我们需要在web.xml中依据Spring的规范新建一堆配置。这样就相当于将框架和容器紧耦合了。而在3.x后注册的功能内聚到Spring里,Spring-web就变成一个纯粹的即插即用的组件,不用依据应用环境定义一套新的配置。


原理


  1. ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider
  2. 容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.(参考类:ContextConfig监听器)
  3. 另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为null
  4. 这一类实现了SCI(全名:ServletContainerInitializer)的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册


SCI(全称 ServletContainerInitializer)则是根据约定的标准,扫描META-INF中包含注册信息的 class 并在启动阶段调用其onStartup


以Tomcat为例,源码解释原理


为了一探究竟,我下载了Tomcat9的源码并且编译运行,然后做如下记录。


首先我们看ContextConfig,它是个LifecycleListener,在Tomcat容器启动的时候会执行~

package org.apache.catalina.startup
public class ContextConfig implements LifecycleListener {
  ...
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
      ...
      // 容器启动前,会执行configureStart配置工作
        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
        }
        ... 
    }
    protected synchronized void configureStart() {
      ...
        webConfig();
    }
    protected void webConfig() {
      ...
        // Step 3. Look for ServletContainerInitializer implementations
        if (ok) {
            processServletContainerInitializers();
        }
    }
  // 这一步就是Tomcat专门寻找,并且执行Servlet规范中的`ServletContainerInitializer`的所有的实现类的
    /**
     * Scan JARs for ServletContainerInitializer implementations.
     */
    protected void processServletContainerInitializers() {
        List<ServletContainerInitializer> detectedScis;
        try {
      // 此处使用的是Tomcat自己实现的 WebappServiceLoader,它使用方式完全同java.util.ServiceLoader,并且相关规范也一样的
      // 具体使用方式,请参见ServiceLoader的使用步骤
            WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
      // 找到所有的SCI
            detectedScis = loader.load(ServletContainerInitializer.class);
        } catch (IOException e) {
            ok = false;
            return;
        }
    // 遍历,逐个处理@HandlesTypes注解~~~
        for (ServletContainerInitializer sci : detectedScis) {
            initializerClassMap.put(sci, new HashSet<Class<?>>());
            HandlesTypes ht;
            try {
              // 拿到实现类上面标注的@HandlesTypes注解~~~
                ht = sci.getClass().getAnnotation(HandlesTypes.class);
            } catch (Exception e) {
                continue;
            }
      // 如过没有标注此注解,就继续处理下一个
            if (ht == null) {
                continue;
            }
      // 标注了注解,但是没有写value值,也继续处理下一个
            Class<?>[] types = ht.value();
            if (types == null) {
                continue;
            }
      // types:注解里面所关心的types,
            for (Class<?> type : types) {
                if (type.isAnnotation()) {
                    handlesTypesAnnotations = true;
                } else {
                    handlesTypesNonAnnotations = true;
                }
        // 以该type为key,对应上对应的SCI  最终onStartup()方法里会把对应Class类型放进去的
                Set<ServletContainerInitializer> scis = typeInitializerMap.get(type);
                if (scis == null) {
                    scis = new HashSet<>();
                    typeInitializerMap.put(type, scis);
                }
                scis.add(sci);
            }
        }
    }
}


它的执行在StandardContext#startInternal()这个方法,启动的时候会统一调用。


org.apache.jasper.servlet.JasperInitializer就是Tomcat内部的一个初始化器,用于处理支持JSP页面的。我编译源码运行,发现并 报错:并不支持JSP,所以我只能自己手动添加了此注册器。


    protected synchronized void configureStart() {
      ...
        webConfig();
        // 手动添加的
        context.addServletContainerInitializer(new JasperInitializer(), null);
    ...     
    }


目前本人并不确定是不是Tomcat9默认是不予支持JSP页面的,因为不写页面好久了。希望有知道的可以留言告知我~~~~


Tomcat调用SCI的时机


ServletContainerInitializer的调用时机,可能在绝大部分情况下我们都不必要去了解,只需要知道它会调用就成。但是如果我们了解了回调的先后顺序,可以帮助我们快速定位和理解某些奇怪的问题(因为servlet容器除了会回调SCI之外, 还有回调诸如servlet#init方法, listener等)


这里肯定就以最常用的servlet容器:tomcat举例,回调处理步骤总结如下:


  1. 解析web.xml(第一还是先解析web.xml,因为必须要保持向下兼容嘛)
  2. 往ServletContext实例中注入<context-param> 参数
  3. 回调Servlet3.0的ServletContainerInitializers接口实现类
  4. 触发 Listener 事件(beforeContextInitialized, afterContextInitialized); 这里只会触发 ServletContextListener 类型的
  5. 初始化 Filter, 调用其init方法
  6. 加载 启动时即加载的servlet(servlet最靠后加载~~~)


SCI在Spring中的使用,热插拔的实现基石


SpringBoot 也是这样被点燃的(关于Spring Boot和Tomcat嵌入式容器的关系,后面章节很定还会详细分析)

首先看看Spring Boot中有哪些实现类:


image.png


这里面最熟悉的莫过于:SpringServletContainerInitializer。SpringServletContainerInitializer类实现了 ServletContainerInitializer接口。这意味着servlet(3.0以上版本)容器启动时,该类被容器自动加载并执行其onStart方法,这样我们就可以启动我们的Spring容器了


这其中还有有一个LogbackServletContainerInitializer我们看起来非常眼熟,Logback是通过该类进行Web环境初始化的。

相关文章
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
162 1
|
2月前
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
3月前
|
安全 Java 网络安全
当网络安全成为数字生活的守护者:Spring Security,为您的应用筑起坚不可摧的防线
【9月更文挑战第2天】在数字化时代,网络安全至关重要。本文通过在线银行应用案例,详细介绍了Spring Security这一Java核心安全框架的核心功能及其配置方法。从身份验证、授权控制到防御常见攻击,Spring Security提供了全面的解决方案,确保应用安全。通过示例代码展示了如何配置`WebSecurityConfigurerAdapter`及`HttpSecurity`,帮助开发者有效保护应用免受安全威胁。
68 4
|
29天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
102 62
|
27天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
27天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
51 2
|
2月前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。
|
2月前
|
Java 关系型数据库 MySQL
mysql5.7 jdbc驱动
遵循上述步骤,即可在Java项目中高效地集成MySQL 5.7 JDBC驱动,实现数据库的访问与管理。
215 1
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第8天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建并配置 Spring Boot 项目,实现后端 API 和安全配置。接着,使用 Ant Design Pro Vue 脚手架创建前端项目,配置动态路由和菜单,并创建相应的页面组件。最后,通过具体实践心得,分享了版本兼容性、安全性、性能调优等注意事项,帮助读者快速搭建高效且易维护的应用框架。
45 3