news 2026/9/10 23:03:10

Java学习之SPI、JDBC、SpringFactoriesLoader、Dubbo

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java学习之SPI、JDBC、SpringFactoriesLoader、Dubbo

概述

SPI,Service Provider Interface,一种服务发现机制,指一些提供给你继承、扩展,完成自定义功能的类、接口或方法。

在SPI机制中,服务提供者为某个接口实现具体的类,而在运行时通过SPI机制,查找到对应的实现类,并将其加载进行使用。

JDK 6(参考java.util.ServiceLoader类的注释Since字段)引进的一个特性,可实现动态加载具体实现类的机制,通过它可以具体实现代码的解耦,也可实现类似于IoC的效果。

SPI的实现方式:提供实现的实现类打包成jar文件,jar文件里面必须有个META-INF/services目录,其下有一个文本文件,文件名即为SPI接口的全限定名(完整路径名),文件的内容是该jar包中提供的SPI接口的实现类名。文件编码是UTF-8。

作用有两个:

  • 把标准定义和接口实现分离,在模块化开发中很好地实现解耦
  • 实现功能的扩展,更好地满足定制化需求

SPI的不足之处:不能根据需求去加载扩展实现,每次都会加载扩展接口的所有实现类并进行实例化,实例化会造成性能开销,并且加载一些不需要用到的实现类,会导致内存资源的浪费。

API和SPI

API,提供给他人使用的具备某项功能的类、接口或方法。

SPI用于一些通用的标准中,为标准的实现产商提供扩展点。标准在上层提供API,API内部使用SPI,当API被客户使用时,会动态得从当前运行的classpath中寻找该SPI的实现,然后使用该SPI的实现来完成API的功能。

实例

JDBC

java.sql.Driver数据库驱动接口,JDK中只提供接口的定义,具体的实现类由各个数据库厂商提供的驱动包来完成,程序在运行的时候会根据当前导入的驱动包来完成对应数据库的连接。

如上图,java.sql.Driver文件内容:com.mysql.cj.jdbc.Driver

同理也能在postgresql-42.7.2.jar下找到java.sql.Driver文件,内容为org.postgresql.Driver,截图省略。

com.mysql.cj.jdbc.Driver源码很简单:

publicclassDriverextendsNonRegisteringDriverimplementsjava.sql.Driver{// Register ourselves with the DriverManager.static{try{DriverManager.registerDriver(newDriver());}catch(SQLExceptionE){thrownewRuntimeException("Can't register driver!");}}publicDriver()throwsSQLException{}}

核心方法就一行:DriverManager.registerDriver(new Driver());,在PG源码org.postgresql.Driver里也能找到DriverManager.registerDriver()

基于JDK 22源码,在DriverManager类里搜索文件名java.sql.Driver,找到方法ensureDriversInitialized:

/* * Load the initial JDBC drivers by checking the System property jdbc.drivers and then use the ServiceLoader mechanism */privatestaticvoidensureDriversInitialized(){// 检查驱动是否已经初始化成功if(driversInitialized){return;}synchronized(lockForInitDrivers){// 优先从环境变量jdbc.drivers获取驱动Stringdrivers;try{drivers=AccessController.doPrivileged(newPrivilegedAction<String>(){publicStringrun(){returnSystem.getProperty("jdbc.drivers");}});}catch(Exceptionex){drivers=null;}// If the driver is packaged as a Service Provider, load it.// Get all the drivers through the classloader exposed as a java.sql.Driver.class service.// ServiceLoader.load() replaces the sun.misc.Providers()AccessController.doPrivileged(newPrivilegedAction<Void>(){publicVoidrun(){// ServiceLoaderServiceLoader<Driver>loadedDrivers=ServiceLoader.load(Driver.class);Iterator<Driver>driversIterator=loadedDrivers.iterator();/* 加载驱动然后初始化,存在一种情况,有java.sql.Driver文件,但是文件内容为空, * 或找不到文件内容指向的Driver类,用Throwable来catch java.util.ServiceConfigurationError */try{// TODO: loadedDrivers并没有赋值到drivers?while(driversIterator.hasNext()){driversIterator.next();}}catch(Throwablet){// Do nothing}returnnull;}});if(drivers!=null&&!drivers.isEmpty()){String[]driversList=drivers.split(":");for(StringaDriver:driversList){try{Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());}catch(Exceptionex){// just logging}}}// 设置驱动初始化成功标志driversInitialized=true;}}

ensureDriversInitialized()方法的调用方有getDriver()getConnection()两个,即获取驱动和获取连接。

Spring

在Spring源码里也有SPI的影子,如spring.factories文件和SpringFactoriesLoader,两者一起构成Spring Boot Starter实现的基础。

以3.2.4版本来讲解,位于spring-boot-actuator-autoconfigure-3.2.4.jar下的spring.factories文件片段:

# Failure Analyzersorg.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer,\ org.springframework.boot.actuate.autoconfigure.health.NoSuchHealthContributorFailureAnalyzer

可见,此文件实际上是.properties文件格式,不同类型的配置项用空行隔开,方便阅读,支持#作为注释字符。配置是键值对形式,Key和Value都是代表类的完整包名,Value允许有多个,用,分隔,多行文本以\分隔;Key是接口、注解、或抽象类,Value是Key的实现类。

而SpringFactoriesLoader自Spring 3.2版本开始就已存在,其属性如下:

// spring.factories文件默认检索位置publicstaticfinalStringFACTORIES_RESOURCE_LOCATION="META-INF/spring.factories";// 默认抛出异常privatestaticfinalFailureHandlerTHROWING_FAILURE_HANDLER=FailureHandler.throwing();// ClassLoader级别的缓存,Value也是Map,K为资源目录,V为全路径类staticfinalMap<ClassLoader,Map<String,SpringFactoriesLoader>>cache=newConcurrentReferenceHashMap<>();// 用于加载类@NullableprivatefinalClassLoaderclassLoader;// 用于保存spring.factories文件里的键值对privatefinalMap<String,List<String>>factories;

核心方法有两个,forResourceLocation是一个静态方法,用于获取实例:

publicstaticSpringFactoriesLoaderforResourceLocation(StringresourceLocation,@NullableClassLoaderclassLoader){Assert.hasText(resourceLocation,"'resourceLocation' must not be empty");ClassLoaderresourceClassLoader=(classLoader!=null?classLoader:SpringFactoriesLoader.class.getClassLoader());Map<String,SpringFactoriesLoader>loaders=cache.computeIfAbsent(resourceClassLoader,key->newConcurrentReferenceHashMap<>());returnloaders.computeIfAbsent(resourceLocation,key->newSpringFactoriesLoader(classLoader,loadFactoriesResource(resourceClassLoader,resourceLocation)));}

参考resourceLocation,可传入指定路径,如果为空,则从META-INF/spring.factories查找。

load方法则用于获取指定类型的实现类集合:

public<T>List<T>load(Class<T>factoryType,@NullableArgumentResolverargumentResolver,@NullableFailureHandlerfailureHandler){Assert.notNull(factoryType,"'factoryType' must not be null");List<String>implementationNames=loadFactoryNames(factoryType);List<T>result=newArrayList<>(implementationNames.size());FailureHandlerfailureHandlerToUse=(failureHandler!=null)?failureHandler:THROWING_FAILURE_HANDLER;for(StringimplementationName:implementationNames){Tfactory=instantiateFactory(implementationName,factoryType,argumentResolver,failureHandlerToUse);if(factory!=null){result.add(factory);}}AnnotationAwareOrderComparator.sort(result);returnresult;}

Dubbo

打开dubbo-3.2.14源码,发现META-INF/services目录下有个org.apache.dubbo.common.extension.LoadingStrategy文本文件,内容如下:

org.apache.dubbo.common.extension.DubboInternalLoadingStrategy org.apache.dubbo.common.extension.DubboLoadingStrategy org.apache.dubbo.common.extension.ServicesLoadingStrategy

DubboInternalLoadingStrategy用于查找META-INF/dubbo/internal/目录下的文件,DubboLoadingStrategy用于查找META-INF/dubbo/目录下的文件,ServicesLoadingStrategy用于查找META-INF/services/目录下的文件。

META-INF/dubbo/internal/目录下有91个文件,所有文件名都是Dubbo下的全路径名指向的接口类,这些接口都添加有注解@SPI(value = "", scope = FRAMEWORK),文件内容则是多行键值对,采用Key=Value形式,Key是一个简称,Value则是Dubbo下接口(文件名对应的)的实现类。

SPI注解源码如下:

@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public@interfaceSPI{Stringvalue()default"";ExtensionScopescope()defaultExtensionScope.APPLICATION;}

ExtensionScope是一个枚举类:

  • FRAMEWORK:
  • APPLICATION:默认值
  • MODULE:
  • SELF:

ExtensionLoader是个泛型类,入口方法是getExtension:

publicTgetExtension(Stringname,booleanwrap){checkDestroyed();if(StringUtils.isEmpty(name)){thrownewIllegalArgumentException("Extension name == null");}if("true".equals(name)){// 获取默认拓展实现类returngetDefaultExtension();}StringcacheKey=name;if(!wrap){cacheKey+="_origin";}// Holder机制包装实现类+缓存finalHolder<Object>holder=getOrCreateHolder(cacheKey);Objectinstance=holder.get();// DCLif(instance==null){synchronized(holder){instance=holder.get();if(instance==null){// 核心方法:创建实现类instance=createExtension(name,wrap);holder.set(instance);}}}return(T)instance;}

接下来看看createExtension方法:

privateTcreateExtension(Stringname,booleanwrap){Class<?>clazz=getExtensionClasses().get(name);if(clazz==null||unacceptableExceptions.contains(name)){throwfindException(name);}try{Tinstance=(T)extensionInstances.get(clazz);if(instance==null){extensionInstances.putIfAbsent(clazz,createExtensionInstance(clazz));instance=(T)extensionInstances.get(clazz);// 实例化前处理instance=postProcessBeforeInitialization(instance,name);// 向实例中注入依赖injectExtension(instance);// 实例化后处理instance=postProcessAfterInitialization(instance,name);}if(wrap){List<Class<?>>wrapperClassesList=newArrayList<>();if(cachedWrapperClasses!=null){wrapperClassesList.addAll(cachedWrapperClasses);wrapperClassesList.sort(WrapperComparator.COMPARATOR);Collections.reverse(wrapperClassesList);}if(CollectionUtils.isNotEmpty(wrapperClassesList)){for(Class<?>wrapperClass:wrapperClassesList){Wrapperwrapper=wrapperClass.getAnnotation(Wrapper.class);booleanmatch=(wrapper==null)||((ArrayUtils.isEmpty(wrapper.matches())||ArrayUtils.contains(wrapper.matches(),name))&&!ArrayUtils.contains(wrapper.mismatches(),name));if(match){// 将当前instance作为参数传给Wrapper的构造方法,并通过反射创建Wrapper实例,向Wrapper实例中注入依赖,最后将Wrapper实例再次赋值给instance变量instance=injectExtension((T)wrapperClass.getConstructor(type).newInstance(instance));instance=postProcessAfterInitialization(instance,name);}}}}// Warning: After an instance of Lifecycle is wrapped by cachedWrapperClasses, it may not still be Lifecycle instance, this application may not invoke the lifecycle.initialize hook.initExtension(instance);returninstance;}catch(Throwablet){// 创建实现类失败thrownewIllegalStateException();}}

核心方法是getExtensionClasses:

privateMap<String,Class<?>>getExtensionClasses(){// 检查缓存Map<String,Class<?>>classes=cachedClasses.get();// DCLif(classes==null){synchronized(cachedClasses){classes=cachedClasses.get();if(classes==null){try{classes=loadExtensionClasses();}catch(InterruptedExceptione){logger.error();thrownewIllegalStateException();}cachedClasses.set(classes);}}}returnclasses;}

继续看loadExtensionClasses:

privateMap<String,Class<?>>loadExtensionClasses()throwsInterruptedException{checkDestroyed();cacheDefaultExtensionName();Map<String,Class<?>>extensionClasses=newHashMap<>();for(LoadingStrategystrategy:strategies){loadDirectory(extensionClasses,strategy,type.getName());// compatible with old ExtensionFactoryif(this.type==ExtensionInjector.class){loadDirectory(extensionClasses,strategy,ExtensionFactory.class.getName());}}returnextensionClasses;}

继续看loadDirectory,两个逻辑,一是调用方法loadDirectoryInternal,二是判断Dubbo 2的向下兼容性,核心方法是loadDirectoryInternal,继续调用方法loadResource,用于读取和解析配置文件,并通过反射加载类,这里面可看到对前面提到的文本文件进行逐行解析,判断=分隔符的逻辑,最后调用 loadClass方法用于操作缓存。loadClass方法调用缓存相关方法,如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等。

参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:06:45

OpenAI曝出最大预训练模型Doug?先看懂预训练与部署再追新

“刚刚&#xff0c;OpenAI最大预训练模型Doug曝光”这个消息传出来之后&#xff0c;很多人第一反应是问“它到底有多大”“能不能超越现在的GPT系列”。但目前关于Doug的参数量、训练数据规模、具体能力评测&#xff0c;公开信息其实非常少。一个更务实的态度是&#xff1a;先别…

作者头像 李华
网站建设 2026/9/9 4:06:48

2013腾讯研发工程师笔试题解析:C/C++数组指针与操作系统高频考点

1. 2013年腾讯研发工程师笔试题到底考什么1.1 这套题的历史背景与考察逻辑聊起腾讯的笔试题&#xff0c;很多人的第一反应是“难、偏、怪”。实际上2013年这套研发工程师笔试题并没那么玄乎&#xff0c;它的命题逻辑非常清晰&#xff1a;在移动互联网刚刚爆发的节点上&#xff…

作者头像 李华
网站建设 2026/9/9 6:49:05

Python NLP必备:.lower()函数在文本预处理中的关键作用

像很多刚接触 NLP 的同学一样&#xff0c;我最初处理文本分类、情感分析任务时&#xff0c;拿到数据第一反应就是先分词、再丢给模型。结果发现同样的词在训练集和测试集里大小写不一致&#xff0c;导致模型效果忽高忽低&#xff0c;后来才明白&#xff1a;很多看似“不起眼”的…

作者头像 李华
网站建设 2026/9/4 9:08:46

铁路机车拍摄与素材管理全流程解析:以DF12单机通过广九线为例

这次我们来看一个具体到车次和机型的铁路机车记录项目&#xff1a;广铁广段 DF12 型 0051 号小运转调度内燃机车&#xff0c;担任 52032 次单机列车&#xff0c;通过广九线小北天桥。这类项目在内容平台上很常见&#xff0c;但真正从拍摄到发布完整跑通的人&#xff0c;会遇到几…

作者头像 李华
网站建设 2026/9/9 20:40:13

Vue生命周期完全指南:钩子函数、工程实践与面试避坑

1. 生命周期到底在解决什么问题1.1 组件不是一锤子买卖我刚带团队的时候&#xff0c;发现很多前端同学对Vue生命周期的理解&#xff0c;停留在"背出那8个钩子函数名字"的层面。你问beforeCreate和created有什么区别&#xff0c;他能答上来"一个拿不到data&#…

作者头像 李华
网站建设 2026/9/5 12:24:57

AI Agent如何用SHA绑定实现从Issue到PR的自动化交付

1. 背景&#xff1a;当 AI Agent 开始接管 Issue 到 PR 的研发闭环1.1 软件开发中最容易被低估的环节先问一个问题&#xff1a;一个功能从“有人提需求”到“代码被合并”&#xff0c;中间到底要经过多少步&#xff1f;如果你在一个中型团队待过&#xff0c;大概率能数出这样一…

作者头像 李华