欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你掌握Java?SPI的原理和實踐

 更新時間:2023年05月30日 09:54:35   作者:守望時空33  
在Java中,我們經(jīng)常會提到面向接口編程,這樣減少了模塊之間的耦合,更加靈活,Java?SPI?(Service?Provider?Interface)就提供了這樣的機制,本文就來講講它的原理與具體使用吧

在Java中,我們經(jīng)常會提到面向接口編程,這樣減少了模塊之間的耦合,更加靈活。

在一個項目中我們也通常將接口和實現(xiàn)類放在一起,但是如果哪天我們要替換其它的實現(xiàn)類,或者是修改實現(xiàn)類,涉及到實現(xiàn)類的代碼也要相應地修改。

能不能這樣:在調用服務的時候,我們只調用接口,不用關心實現(xiàn)類呢?無論我們怎么切換實現(xiàn)類,調用接口的部分代碼都能正常運行?

當然是可以的,Java SPI (Service Provider Interface)就提供了這樣的機制。

Java SPI機制中,我們不再是手動指定接口和實現(xiàn)類的關系,而是讓接口去尋找可用的實現(xiàn)類。

事實上,我們經(jīng)常使用的Spring框架、日志接口等等,都是使用了SPI機制實現(xiàn)了擴展。

1、SPI和API

在說起SPI之前,我們還是先看一下APIAPI我們已經(jīng)很熟悉了,和SPI都可以被稱作接口

只不過API功能的實現(xiàn),以及接口的定義全部是接口的實現(xiàn)者提供的,調用者只需要調用接口即可:

不過SPI就不一樣了,在SPI機制中,調用者仍然是調用接口,但是這個接口是獨立存在的,并且可以由不同的實現(xiàn)者實現(xiàn)

也就是說,這里接口只是一個標準,并且提供接口的那一方并不一定回去實現(xiàn)接口,而是根據(jù)接口的定義,由更多的第三方實現(xiàn)。

這個接口可以由一個甚至是多個實現(xiàn)者去實現(xiàn)。也因此,調用者在調用接口時,可能還需要指定一下使用哪個實現(xiàn)者的實現(xiàn)類。

實現(xiàn)者也叫做服務提供者。

事實上,我們日常生活中經(jīng)常使用的U盤也很類似SPI機制,U盤使用的是USB接口,USB接口僅僅是一個規(guī)范(接口),但是發(fā)明USB接口的公司并沒有去生產(chǎn)U盤,而是由不同的U盤廠商例如金士頓、閃迪(實現(xiàn)者)等等去根據(jù)這個規(guī)范生產(chǎn)U盤,然后我們就可以去選擇自己喜歡的牌子(選擇實現(xiàn)者)購買U盤,不過平時無論使用什么牌子的U盤,我們只需要插入到電腦的USB接口(調用接口)即可使用,而不用關心不同的廠商是怎么實現(xiàn)USB接口的功能的。

可見,SPI機制將實現(xiàn)者和接口再次解耦合了,使得接口更加易于擴展。

事實上,我們常常用的SLF4J就是一個Java的日志接口,但是它也僅僅是一個接口,所以被稱作門面。而它的實現(xiàn)有Logback、Log4j等等,并且在切換實現(xiàn)的時候,我們只需要修改一下依賴配置即可,代碼并不需要任何變動,因為代碼中也僅僅是調用了接口。

2、自己完成一個SPI

那么現(xiàn)在,我們也來以一個最簡單的日志接口為例,實現(xiàn)自己的SPI

(1) 定義SPI接口

先新建一個空的Maven項目log-interface,然后在里面創(chuàng)建一個日志接口,聲明日志接口具備的方法(功能):

package com.gitee.swsk33.loginterface.spi;
/**
 * 定義日志接口
 */
public interface Logger {
	/**
	 * INFO級別日志方法
	 *
	 * @param message 日志打印消息
	 */
	void info(String message);
	/**
	 * DEBUG級別日志方法
	 *
	 * @param message 日志打印消息
	 */
	void debug(String message);
}

這樣,我們便定義了這么一個日志接口,并聲明日志接口需要有infodebug這兩個日志功能。

然后就是編寫服務類,這個服務類是這里最為重要的地方,它的作用是掃描所有實現(xiàn)了Logger接口的實現(xiàn)類并加載進來,然后供調用者去調用。

先看代碼:

package com.gitee.swsk33.loginterface.service;
import com.gitee.swsk33.loginterface.spi.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
/**
 * 服務,用于加載所有服務使用者的實現(xiàn)類,以及供外部調用
 * 該類為一個單例
 */
public class LoggerService {
	/**
	 * 該類唯一單例
	 */
	private static final LoggerService LOGGER = new LoggerService();
	/**
	 * 默認的Logger實現(xiàn)類
	 */
	private final Logger defaultLogger;
	/**
	 * 所有的Logger實現(xiàn)類列表
	 */
	private final List<Logger> allLoggers = new ArrayList<>();
	/**
	 * 私有化構造器
	 */
	private LoggerService() {
		// 加載全部Logger接口的實現(xiàn)類
		ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
		// 將實現(xiàn)類放入我們的Logger實現(xiàn)類列表
		for (Logger logger : loader) {
			allLoggers.add(logger);
		}
		// 這里取出第一個作為默認實現(xiàn)類
		if (!allLoggers.isEmpty()) {
			defaultLogger = allLoggers.get(0);
		} else {
			defaultLogger = null;
		}
		System.out.println("加載到" + allLoggers.size() + "個服務實現(xiàn)!");
	}
	/**
	 * 獲取該服務類的唯一單例
	 *
	 * @return 該服務類的唯一單例
	 */
	public static LoggerService getInstance() {
		return LOGGER;
	}
	/**
	 * 調用默認的實現(xiàn)類的info日志打印方法
	 *
	 * @param message 消息
	 */
	public void info(String message) {
		if (defaultLogger == null) {
			System.err.println("沒有找到實現(xiàn)了Logger接口的類!");
			return;
		}
		defaultLogger.info(message);
	}
	/**
	 * 調用默認的實現(xiàn)類的debug日志打印方法
	 *
	 * @param message 消息
	 */
	public void debug(String message) {
		if (defaultLogger == null) {
			System.err.println("沒有找到實現(xiàn)了Logger接口的類!");
			return;
		}
		defaultLogger.debug(message);
	}
}

首先這個類是一個單例的類,在構造器中,我們使用ServiceLoader這個類來將實現(xiàn)了Logger接口的所有類都掃描進來,并存入我們的實現(xiàn)類列表,然后我們取出列表中的第一個作為默認實現(xiàn)。

在下面我們定義了infodebug來完成對接口的默認實現(xiàn)類的調用。

最后,在項目目錄下執(zhí)行mvn install命令將其安裝至本地Maven倉庫,以便后續(xù)服務提供者引入并實現(xiàn)。

(2) 完成一個接口的實現(xiàn)

現(xiàn)在再新建一個空的Maven項目logservice-one,并引入上面接口項目為依賴:

然后編寫實現(xiàn)類:

package com.gitee.swsk33.logserviceone.service;
import com.gitee.swsk33.loginterface.spi.Logger;
/**
 * Logger SPI的實現(xiàn)類
 */
public class LogOne implements Logger {
	@Override
	public void info(String s) {
		System.out.println("[LogOne INFO] " + s);
	}
	@Override
	public void debug(String s) {
		System.out.println("[LogOne DEBUG] " + s);
	}
}

然后在resources目錄下創(chuàng)建目錄META-INF/services,這個目錄中是用于聲明該服務實現(xiàn)中有哪些實現(xiàn)類實現(xiàn)了什么接口

在這個目錄下我們新建一個文件名為com.gitee.swsk33.loginterface.spi.Logger,文件中的內容為:

com.gitee.swsk33.logserviceone.service.LogOne

可見,該目錄下文件名要實現(xiàn)的接口的全限定類名(包名 + 類名),而文件中內容實現(xiàn)了該接口的實現(xiàn)類的全限定類名

大家參考這里的文件名及其中的內容,與我們上述的接口全限定類名、實現(xiàn)類全限定類名對比一下就知道了!

如果說這個項目中有多個類實現(xiàn)了Logger接口,那么我們都需要在文件中聲明,一行一個實現(xiàn)類的全限定類名。

最終整個項目結構如下:

同樣地,最后記得在項目目錄下執(zhí)行mvn install命令將其安裝至本地Maven倉庫,以便調用者調用。

(3) 測試接口

這里再新建一個Maven空項目log-test,作為接口的調用者,在依賴中引入實現(xiàn)者

然后創(chuàng)建一個主類調用一下接口試試:

package com.gitee.swsk33.logtest;
import com.gitee.swsk33.loginterface.service.LoggerService;
public class Main {
	private static final LoggerService LOGGER = LoggerService.getInstance();
	public static void main(String[] args) {
		LOGGER.info("測試info消息");
		LOGGER.debug("測試debug消息");
	}
}

結果:

可見,我們成功地調用了Logger接口中的方法。

通常調用者的依賴中可能會同時引入SPI接口依賴和服務提供者(實現(xiàn))的依賴,這樣也沒問題,不過通常服務提供者本身就依賴于SPI接口,因此只引入服務提供者依賴,也會間接地引入SPI接口依賴,不影響我們調用SPI接口。

我們這里只有一個服務提供者logservice-one,如果說還有logservice-two等等多個服務提供者,我們只需要在依賴中更換一下即可,代碼完全不需要改變。

也可見調用者在調用接口的時候,只需要關注接口就行了,不需要關心實現(xiàn)類。

3、再看ServiceLoader

可見在SPI接口中,我們使用ServiceLoader完成了對所有實現(xiàn)了Logger接口的類的掃描和加載,那么具體的過程是什么樣的呢?

如果大家去查看這個類的源碼,可以發(fā)現(xiàn)它實現(xiàn)了Iterable接口,這也說明我們可以通過迭代的方式去完成多個實現(xiàn)類的切換。

然后在其源碼中,有這么一個常量定義:

static final String PREFIX = "META-INF/services/";

這就說明,ServiceLoader會去掃描服務提供者的classpath路徑下的META-INF/services目錄,來掃描哪些類實現(xiàn)了指定接口,而其靜態(tài)方法load的參數(shù),正是指定了被實現(xiàn)的接口。也因此我們要在服務提供者的項目的resources目錄下創(chuàng)建這個目錄并申明接口和對應實現(xiàn)類的全限定類名。

在Maven項目中,resources目錄就對應的是classpath的根目錄。

簡而言之,ServiceLoader加載實現(xiàn)類的過程如下:

  • 先是調用load方法并指定要掃描的接口
  • 然后掃描項目中META-INF/services目錄,這包括調用者項目以及它所引入的所有依賴包中的META-INF/services目錄下的聲明
  • 掃描到所有實現(xiàn)類后,根據(jù)其類名,先判斷是否跟SPI接口為同一類型,如果是則利用反射的方式將所有實現(xiàn)類實例化,加載進內存,并返回所有實現(xiàn)類的實例列表

可見,這就是JDK中SPI機制加載服務的大致過程,事實上,現(xiàn)在很多框架也利用SPI機制實現(xiàn)了靈活地擴展。

示例倉庫地址:傳送門

以上就是一文帶你掌握Java SPI的原理和實踐的詳細內容,更多關于Java SPI的資料請關注腳本之家其它相關文章!

相關文章

  • Mybatis-plus如何開啟二級緩存

    Mybatis-plus如何開啟二級緩存

    這篇文章主要介紹了Mybatis-plus如何開啟二級緩存問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • 解決@RequestMapping和@FeignClient放在同一個接口上遇到的坑

    解決@RequestMapping和@FeignClient放在同一個接口上遇到的坑

    這篇文章主要介紹了解決@RequestMapping和@FeignClient放在同一個接口上遇到的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • 自定義注解+Spel實現(xiàn)分布式鎖方式

    自定義注解+Spel實現(xiàn)分布式鎖方式

    這篇文章主要介紹了自定義注解+Spel實現(xiàn)分布式鎖方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • java?jar包后臺運行的兩種方式詳解

    java?jar包后臺運行的兩種方式詳解

    后臺運行jar的方法有多種方法可以實現(xiàn)Java后臺運行jar文件,下面介紹其中兩種常見的方法,下面這篇文章主要給大家介紹了關于java?jar包后臺運行的兩種方式,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2024-07-07
  • 解析Java和Eclipse中加載本地庫(.dll文件)的詳細說明

    解析Java和Eclipse中加載本地庫(.dll文件)的詳細說明

    本篇文章是對Java和Eclipse中加載本地庫(.dll文件)進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05
  • Java中父類和子類之間的轉換操作示例

    Java中父類和子類之間的轉換操作示例

    這篇文章主要介紹了Java中父類和子類之間的轉換操作,結合實例形式分析了Java中父類和子類之間的轉換相關原理、操作技巧與使用注意事項,需要的朋友可以參考下
    2020-05-05
  • idea創(chuàng)建properties文件,解決亂碼問題

    idea創(chuàng)建properties文件,解決亂碼問題

    這篇文章主要介紹了idea創(chuàng)建properties文件,解決亂碼問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • elasticsearch集群查詢超10000的解決方案

    elasticsearch集群查詢超10000的解決方案

    ES為了避免用戶的過大分頁請求造成ES服務所在機器內存溢出,默認對深度分頁的條數(shù)進行了限制,默認的最大條數(shù)是10000條,這篇文章主要給大家介紹了關于elasticsearch集群查詢超10000的解決方案,需要的朋友可以參考下
    2024-08-08
  • Spring Boot項目中如何對接口請求參數(shù)打印日志

    Spring Boot項目中如何對接口請求參數(shù)打印日志

    在SpringBoot項目中,打印接口請求參數(shù)有多種方法,如使用AOP、控制器建議、攔截器、@ModelAttribute、SpringBootActuator、日志框架的MDC、自定義過濾器和SpringWebflux,這些方法有助于API調試和監(jiān)控,但需注意隱私和敏感信息安全
    2024-10-10
  • Java日期工具類DateUtils實例詳解

    Java日期工具類DateUtils實例詳解

    這篇文章主要為大家詳細介紹了Java日期工具類DateUtils實例,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-12-12

最新評論