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

深入講解SPI?在?Spring?中的應(yīng)用

 更新時間:2022年06月22日 08:59:20   作者:??vivo互聯(lián)網(wǎng)技術(shù)????  
這篇文章主要介紹了深入講解SPI在Spring中的應(yīng)用,SPI是Java內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機制,可以用來提高框架的擴展性,主要用于框架的開發(fā)中

一、概述

SPI(Service Provider Interface),是Java內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機制,可以用來提高框架的擴展性,主要用于框架的開發(fā)中,比如Dubbo,不同框架中實現(xiàn)略有差異,但核心機制相同,而Java的SPI機制可以為接口尋找服務(wù)實現(xiàn)。SPI機制將服務(wù)的具體實現(xiàn)轉(zhuǎn)移到了程序外,為框架的擴展和解耦提供了極大的便利。

得益于SPI優(yōu)秀的能力,為模塊功能的動態(tài)擴展提供了很好的支撐。

本文會先簡單介紹Java內(nèi)置的SPI和Dubbo中的SPI應(yīng)用,重點介紹分析Spring中的SPI機制,對比Spring SPI和Java內(nèi)置的SPI以及與 Dubbo SPI的異同。

二、Java SPI

Java內(nèi)置的SPI通過java.util.ServiceLoader類解析classPath和jar包的META-INF/services/目錄 下的以接口全限定名命名的文件,并加載該文件中指定的接口實現(xiàn)類,以此完成調(diào)用。

2.1 Java SPI

先通過代碼來了解下Java SPI的實現(xiàn)

① 創(chuàng)建服務(wù)提供接口

package jdk.spi;
// 接口
public interface DataBaseSPI {
    public void dataBaseOperation();
}

② 創(chuàng)建服務(wù)提供接口的實現(xiàn)類

  • MysqlDataBaseSPIImpl

實現(xiàn)類1

package jdk.spi.impl;
import jdk.spi.DataBaseSPI;
 
public class MysqlDataBaseSPIImpl implements DataBaseSPI {
 
    @Override
    public void dataBaseOperation() {
        System.out.println("Operate Mysql database!!!");
    }
}
  • OracleDataBaseSPIImpl

實現(xiàn)類2

package jdk.spi.impl;
import jdk.spi.DataBaseSPI;
public class OracleDataBaseSPIImpl implements DataBaseSPI {
    @Override
    public void dataBaseOperation() {
        System.out.println("Operate Oracle database!!!");
    }
}

③ 在項目META-INF/services/目錄下創(chuàng)建jdk.spi.DataBaseSPI文件

jdk.spi.DataBaseSPI

jdk.spi.impl.MysqlDataBaseSPIImpl
jdk.spi.impl.OracleDataBaseSPIImpl

④ 運行代碼:

JdkSpiTest#main()

package jdk.spi;
import java.util.ServiceLoader;
public class JdkSpiTest {
 
    public static void main(String args[]){
        // 加載jdk.spi.DataBaseSPI文件中DataBaseSPI的實現(xiàn)類(懶加載)
        ServiceLoader<DataBaseSPI> dataBaseSpis = ServiceLoader.load(DataBaseSPI.class);
        // ServiceLoader實現(xiàn)了Iterable,故此處可以使用for循環(huán)遍歷加載到的實現(xiàn)類
        for(DataBaseSPI spi : dataBaseSpis){
            spi.dataBaseOperation();
        }
    }
}

⑤ 運行結(jié)果:

Operate Mysql database!!!
Operate Oracle database!!!

2.2 源碼分析

上述實現(xiàn)即為使用Java內(nèi)置SPI實現(xiàn)的簡單示例,ServiceLoader是Java內(nèi)置的用于查找服務(wù)提供接口的工具類,通過調(diào)用load()方法實現(xiàn)對服務(wù)提供接口的查找(嚴格意義上此步并未真正的開始查找,只做初始化),最后遍歷來逐個訪問服務(wù)提供接口的實現(xiàn)類。

上述訪問服務(wù)實現(xiàn)類的方式很不方便,如:無法直接使用某個服務(wù),需要通過遍歷來訪問服務(wù)提供接口的各個實現(xiàn),到此很多同學會有疑問:

  • Java內(nèi)置的訪問方式只能通過遍歷實現(xiàn)嗎?
  • 服務(wù)提供接口必須放到META-INF/services/目錄下?是否可以放到其他目錄下?

在分析源碼之前先給出答案:兩個都是的;Java內(nèi)置的SPI機制只能通過遍歷的方式訪問服務(wù)提供接口的實現(xiàn)類,而且服務(wù)提供接口的配置文件也只能放在META-INF/services/目錄下。

ServiceLoader部分源碼:

public final class ServiceLoader&lt;S&gt; implements Iterable&lt;S&gt;{
    // 服務(wù)提供接口對應(yīng)文件放置目錄
    private static final String PREFIX = "META-INF/services/";
 
    // The class or interface representing the service being loaded
    private final Class&lt;S&gt; service;
 
    // 類加載器
    private final ClassLoader loader;
 
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
 
    // 按照初始化順序緩存服務(wù)提供接口實例
    private LinkedHashMap&lt;String,S&gt; providers = new LinkedHashMap&lt;&gt;();
 
    // 內(nèi)部類,實現(xiàn)了Iterator接口
    private LazyIterator lookupIterator;
}

從源碼中可以發(fā)現(xiàn):

  • ServiceLoader類本身實現(xiàn)了Iterable接口并實現(xiàn)了其中的iterator方法,iterator方法的實現(xiàn)中調(diào)用了LazyIterator這個內(nèi)部類中的方法,解析完服務(wù)提供接口文件后最終結(jié)果放在了Iterator中返回,并不支持服務(wù)提供接口實現(xiàn)類的直接訪問。
  • 所有服務(wù)提供接口的對應(yīng)文件都是放置在META-INF/services/目錄下,final類型決定了PREFIX目錄不可變更。

所以Java內(nèi)置的SPI機制思想是非常好的,但其內(nèi)置實現(xiàn)上的不足也很明顯。

三、Dubbo SPI

Dubbo SPI沿用了Java SPI的設(shè)計思想,但在實現(xiàn)上有了很大的改進,不僅可以直接訪問擴展類,而且在訪問的靈活性和擴展的便捷性都做了很大的提升。

3.1 基本概念

① 擴展點:一個Java接口,等同于服務(wù)提供接口,需用@SPI注解修飾。

② 擴展:擴展點的實現(xiàn)類。

③ 擴展類加載器:ExtensionLoader

類似于Java SPI的ServiceLoader,主要用來加載并實例化擴展類。一個擴展點對應(yīng)一個擴展加載器。

④ Dubbo擴展文件加載路徑

Dubbo框架支持從以下三個路徑來加載擴展類:

  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

Dubbo框架針對三個不同路徑下的擴展配置文件對應(yīng)三個策略類:

  • DubboInternalLoadingStrategy
  • DubboLoadingStrategy
  • ServicesLoadingStrategy

三個路徑下的擴展配置文件并沒有特殊之處,一般情況下:

  • META-INF/dubbo對開發(fā)者開放
  • META-INF/dubbo/internal 用來加載Dubbo內(nèi)部的擴展點
  • META-INF/services 兼容Java SPI

⑤ 擴展配置文件

和Java SPI不同,Dubbo的擴展配置文件中擴展類都有一個名稱,便于在應(yīng)用中引用它們。

如:Dubbo SPI擴展配置文件

#擴展實例名稱=擴展點實現(xiàn)類
adaptive=org.apache.dubbo.common.compiler.support.AdaptiveCompiler
jdk=org.apache.dubbo.common.compiler.support.JdkCompiler
javassist=org.apache.dubbo.common.compiler.support.JavassistCompiler

3.2 Dubbo SPI

先通過代碼來演示下 Dubbo SPI 的實現(xiàn)。

① 創(chuàng)建擴展點(即服務(wù)提供接口)

擴展點:

package dubbo.spi;
import org.apache.dubbo.common.extension.SPI;
@SPI  // 注解標記當前接口為擴展點
public interface DataBaseSPI {
    public void dataBaseOperation();
}

② 創(chuàng)建擴展點實現(xiàn)類

  • MysqlDataBaseSPIImpl

擴展類1

package dubbo.spi.impl;
import dubbo.spi.DataBaseSPI;
public class MysqlDataBaseSPIImpl implements DataBaseSPI {
    @Override
    public void dataBaseOperation() {
        System.out.println("Dubbo SPI Operate Mysql database!!!");
    }
}
  • OracleDataBaseSPIImpl

擴展類2

package dubbo.spi.impl;
import dubbo.spi.DataBaseSPI;
public class OracleDataBaseSPIImpl implements DataBaseSPI {
 
    @Override
    public void dataBaseOperation() {
        System.out.println("Dubbo SPI Operate Oracle database!!!");
    }
}

③在項目META-INF/dubbo/目錄下創(chuàng)建dubbo.spi.DataBaseSPI文件:

dubbo.spi.DataBaseSPI

#擴展實例名稱=擴展點實現(xiàn)類
mysql = dubbo.spi.impl.MysqlDataBaseSPIImpl
oracle = dubbo.spi.impl.OracleDataBaseSPIImpl

PS:文件內(nèi)容中,等號左邊為該擴展類對應(yīng)的擴展實例名稱,右邊為擴展類(內(nèi)容格式為一行一個擴展類,多個擴展類分為多行)

④ 運行代碼:

DubboSpiTest#main()

package dubbo.spi;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class DubboSpiTest {
    public static void main(String args[]){
        // 使用擴展類加載器加載指定擴展的實現(xiàn)
        ExtensionLoader<DataBaseSPI> dataBaseSpis = ExtensionLoader.getExtensionLoader(DataBaseSPI.class);
        // 根據(jù)指定的名稱加載擴展實例(與dubbo.spi.DataBaseSPI中一致)
        DataBaseSPI spi = dataBaseSpis.getExtension("mysql");
        spi.dataBaseOperation();

        DataBaseSPI spi2 = dataBaseSpis.getExtension("oracle");
        spi2.dataBaseOperation();
    }
}

⑤ 運行結(jié)果:

Dubbo SPI Operate Mysql database!!!
Dubbo SPI Operate Oracle database!!!

從上面的代碼實現(xiàn)直觀來看,Dubbo SPI在使用上和Java SPI比較類似,但也有差異。

相同:

  • 擴展點即服務(wù)提供接口、擴展即服務(wù)提供接口實現(xiàn)類、擴展配置文件即services目錄下的配置文件 三者相同。
  • 都是先創(chuàng)建加載器然后訪問具體的服務(wù)實現(xiàn)類,包括深層次的在初始化加載器時都未實時解析擴展配置文件來獲取擴展點實現(xiàn),而是在使用時才正式解析并獲取擴展點實現(xiàn)(即懶加載)。

不同:

  • 擴展點必須使用@SPI注解修飾(源碼中解析會對此做校驗)。
  • Dubbo中擴展配置文件每個擴展(服務(wù)提供接口實現(xiàn)類)都指定了一個名稱。
  • Dubbo SPI在獲取擴展類實例時直接通過擴展配置文件中指定的名稱獲取,而非Java SPI的循環(huán)遍歷,在使用上更靈活。

3.3 源碼分析

以上述的代碼實現(xiàn)作為源碼分析入口,了解下Dubbo SPI是如何實現(xiàn)的。

ExtensionLoader

① 通過ExtensionLoader.getExtensionLoader(Classtype)創(chuàng)建對應(yīng)擴展類型的擴展加載器。

ExtensionLoader#getExtensionLoader()

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    // 校驗當前類型是否為接口
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    // 接口上是否使用了@SPI注解
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
                ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }
    // 從內(nèi)存中讀取該擴展點的擴展類加載器
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    // 內(nèi)存中不存在則直接new一個擴展
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

getExtensionLoader()方法中有三點比較重要的邏輯:

判斷當前type類型是否為接口類型。

當前擴展點是否使用了@SPI注解修飾。

EXTENSION_LOADERS為ConcurrentMap類型的內(nèi)存緩存,內(nèi)存中存在該類型的擴展加載器則直接使用,不存在就new一個并放入內(nèi)存緩存中。

再看下new ExtensionLoader(type)源碼

ExtensionLoader#ExtensionLoader()

// 私有構(gòu)造器
private ExtensionLoader(Class<?> type) {
     this.type = type;
     // 創(chuàng)建ExtensionFactory自適應(yīng)擴展
     objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
 }

**重點:**構(gòu)造方法為私有類型,即外部無法直接使用構(gòu)造方法創(chuàng)建ExtensionLoader實例。

每次初始化ExtensionLoader實例都會初始化type和objectFactory ,type為擴展點類型;objectFactory 為ExtensionFactory類型。

② 使用getExtension()獲取指定名稱的擴展類實例getExtension為重載方法,分別為getExtension(String name)和getExtension(String name, boolean wrap),getExtension(String name)方法最終調(diào)用的還是getExtension(String name, boolean wrap)方法。

ExtensionLoader#getExtension()

public T getExtension(String name) {
     // 調(diào)用兩個參數(shù)的getExtension方法,默認true表示需要對擴展實例做包裝
     return getExtension(name, true);
 }
 
 public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // 獲取Holder實例,先從ConcurrentMap類型的內(nèi)存緩存中取,沒值會new一個并存放到內(nèi)存緩存中
    // Holder用來存放一個類型的值,這里用于存放擴展實例
    final Holder<Object> holder = getOrCreateHolder(name);
    // 從Holder讀取該name對應(yīng)的實例
    Object instance = holder.get();
    if (instance == null) {
       // 同步控制
       synchronized (holder) {
          instance = holder.get();
          // double check
          if (instance == null) {
             // 不存在擴展實例則解析擴展配置文件,實時創(chuàng)建
             instance = createExtension(name, wrap);
             holder.set(instance);
          }
        }
     }
     return (T) instance;
}

Holder類:這里用來存放指定擴展實例

③ 使用createExtension()創(chuàng)建擴展實例

ExtensionLoader#createExtension()

// 部分createExtension代碼
private T createExtension(String name, boolean wrap) {
   // 先調(diào)用getExtensionClasses()解析擴展配置文件,并生成內(nèi)存緩存,
   // 然后根據(jù)擴展實例名稱獲取對應(yīng)的擴展類
   Class<?> clazz = getExtensionClasses().get(name);
   if (clazz == null) {
       throw findException(name);
   }
   try {
       // 根據(jù)擴展類生成實例并對實例做包裝(主要是進行依賴注入和初始化)
       // 優(yōu)先從內(nèi)存中獲取該class類型的實例
       T instance = (T) EXTENSION_INSTANCES.get(clazz);
       if (instance == null) {
           // 內(nèi)存中不存在則直接初始化然后放到內(nèi)存中
           EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
           instance = (T) EXTENSION_INSTANCES.get(clazz);
       }
       // 主要是注入instance中的依賴
       injectExtension(instance);
       ......
}

createExtension()方法:創(chuàng)建擴展實例,方法中EXTENSION_INSTANCES為ConcurrentMap類型的內(nèi)存緩存,先從內(nèi)存中取,內(nèi)存中不存在重新創(chuàng)建;其中一個核心方法是getExtensionClasses():

ExtensionLoader#getExtensionClasses()

private Map<String, Class<?>> getExtensionClasses() {
   // 優(yōu)先從內(nèi)存緩存中讀
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        // 采用同步手段解析配置文件
        synchronized (cachedClasses) {
            // double check
            classes = cachedClasses.get();
            if (classes == null) {
                // 正式開始解析配置文件
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

cachedClasses為Holder<map<string, class>>類型的內(nèi)存緩存,getExtensionClasses中會優(yōu)先讀內(nèi)存緩存,內(nèi)存中不存在則采用同步的方式解析配置文件,最終在loadExtensionClasses方法中解析配置文件,完成從擴展配置文件中讀出擴展類:

ExtensionLoader#loadExtensionClasses()

// 在getExtensionClasses方法中是以同步的方式調(diào)用,是線程安全
private Map<String, Class<?>> loadExtensionClasses() {
   // 緩存默認擴展名稱
   cacheDefaultExtensionName();
   Map<String, Class<?>> extensionClasses = new HashMap<>();
   // strategies策略類集合,分別對應(yīng)dubbo的三個配置文件目錄
   for (LoadingStrategy strategy : strategies) {
      loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
      loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(),
           strategy.excludedPackages());
   }
 
   return extensionClasses;
}

源碼中的strategies即static volatile LoadingStrategy[] strategies數(shù)組,通過Java SPI從META-INF/services/目錄下加載配置文件完成初始化,默認包含三個類:

  • DubboInternalLoadingStrategy
  • DubboLoadingStrategy
  • ServicesLoadingStrategy

分別對應(yīng)dubbo的三個目錄:

  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

上述的源碼分析只是對Dubbo SPI做了簡要的介紹,Dubbo中對SPI的應(yīng)用很廣泛,如:序列化組件、負載均衡等都應(yīng)用了SPI技術(shù),還有很多SPI功能未做分析,比如:自適應(yīng)擴展、Activate活性擴展等 等,感興趣的同學可以更深入的研究。

四、Spring SPI

Spring SPI沿用了Java SPI的設(shè)計思想,但在實現(xiàn)上和Java SPI及Dubbo SPI也存在差異,Spring通過spring.handlers和spring.factories兩種方式實現(xiàn)SPI機制,可以在不修改Spring源碼的前提下,做到對Spring框架的擴展開發(fā)。

4.1 基本概念

  • DefaultNamespaceHandlerResolver

類似于Java SPI的ServiceLoader,負責解析spring.handlers配置文件,生成namespaceUri和NamespaceHandler名稱的映射,并實例化NamespaceHandler。

  • spring.handlers

自定義標簽配置文件;Spring在2.0時便引入了spring.handlers,通過配置spring.handlers文件實現(xiàn)自定義標簽并使用自定義標簽解析類進行解析實現(xiàn)動態(tài)擴,內(nèi)容配置如:

http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler
spring.handlers實現(xiàn)的SPI是以namespaceUri作為key,NamespaceHandler作為value,建立映射關(guān)系,在解析標簽時通過namespaceUri獲取相應(yīng)的NamespaceHandler來解析
  • SpringFactoriesLoader

類似于Java SPI的ServiceLoader,負責解析spring.factories,并將指定接口的所有實現(xiàn)類實例化后返回。

  • spring.factories

Spring在3.2時引入spring.factories,加強版的SPI配置文件,為Spring的SPI機制的實現(xiàn)提供支撐,內(nèi)容配置如:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
 
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\org.springframework.boot.context.event.EventPublishingRunListener
 
spring.factories實現(xiàn)的SPI是以接口的全限定名作為key,接口實現(xiàn)類作為value,多個實現(xiàn)類用逗號隔開,最終返回的結(jié)果是該接口所有實現(xiàn)類的實例集合
  • 加載路徑

Java SPI從/META-INF/services目錄加載服務(wù)提供接口配置,而Spring默認從META-INF/spring.handlers和META-INF/spring.factories目錄加載配置,其中META-INF/spring.handlers的路徑可以通過創(chuàng)建實例時重新指定,而META-INF/spring.factories固定不可變。

4.2 spring.handlers

首先通過代碼初步介紹下spring.handlers實現(xiàn)。

4.2.1 spring.handlers SPI

① 創(chuàng)建NameSpaceHandler

MysqlDataBaseHandler

package spring.spi.handlers;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Element;

// 繼承抽象類
public class MysqlDataBaseHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
    }

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        System.out.println("MysqlDataBaseHandler!!!");
        return null;
    }
}

OracleDataBaseHandler

package spring.spi.handlers;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Element;
public class OracleDataBaseHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
    }
 
    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        System.out.println("OracleDataBaseHandler!!!");
        return null;
    }
}

② 在項目META-INF/目錄下創(chuàng)建spring.handlers文件:

文件內(nèi)容:

spring.handlers

#一個namespaceUri對應(yīng)一個handler
http\://www.mysql.org/schema/mysql=spring.spi.handlers.MysqlDataBaseHandler
http\://www.oracle.org/schema/oracle=spring.spi.handlers.OracleDataBaseHandler

③ 運行代碼:

SpringSpiTest#main()

package spring.spi;
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.NamespaceHandler;
public class SpringSpiTest {
    public static void main(String args[]){
        // spring中提供的默認namespace URI解析器
        DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();
        // 此處假設(shè)nameSpaceUri已從xml文件中解析出來,正常流程是在項目啟動的時候會解析xml文件,獲取到對應(yīng)的自定義標簽
        // 然后根據(jù)自定義標簽取得對應(yīng)的nameSpaceUri
        String mysqlNameSpaceUri = "http://www.mysql.org/schema/mysql";
        NamespaceHandler  handler = resolver.resolve(mysqlNameSpaceUri);
        // 驗證自定義NamespaceHandler,這里參數(shù)傳null,實際使用中傳具體的Element
        handler.parse(null, null);
         
        String oracleNameSpaceUri = "http://www.oracle.org/schema/oracle";
        handler = resolver.resolve(oracleNameSpaceUri);
        handler.parse(null, null);
    }
}

④ 運行結(jié)果:

MysqlDataBaseHandler!!!
OracleDataBaseHandler!!!

上述代碼通過解析spring.handlers實現(xiàn)對自定義標簽的動態(tài)解析,以NameSpaceURI作為key獲取具體的NameSpaceHandler實現(xiàn)類,這里有別于Java SPI,其中:

DefaultNamespaceHandlerResolver是NamespaceHandlerResolver接口的默認實現(xiàn)類,用于解析自定義標簽。

  • DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法以namespaceUri作為參數(shù),默認加載各jar包中的META-INF/spring.handlers配置文件,通過解析spring.handlers文件建立NameSpaceURI和NameSpaceHandler的映射。
  • 加載配置文件的默認路徑是META-INF/spring.handlers,但可以使用DefaultNamespaceHandlerResolver(ClassLoader, String)構(gòu)造方法修改,DefaultNamespaceHandlerResolver有多個重載方法。
  • DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法主要被BeanDefinitionParserDelegate的parseCustomElement()和decorateIfRequired()方法中調(diào)用,所以spring.handlers SPI機制主要用在bean的掃描和解析過程中。

4.2.2 源碼分析

下面從上述代碼開始深入源碼了解spring handlers方式實現(xiàn)的SPI是如何工作的。

  • DefaultNamespaceHandlerResolver

① DefaultNamespaceHandlerResolver.resolve()方法本身是根據(jù)namespaceUri獲取對應(yīng)的namespaceHandler對標簽進行解析,核心源碼:

DefaultNamespaceHandlerResolver#resolve()

public NamespaceHandler resolve(String namespaceUri) {
    // 1、核心邏輯之一:獲取namespaceUri和namespaceHandler映射關(guān)系
    Map<String, Object> handlerMappings = getHandlerMappings();
    // 根據(jù)namespaceUri參數(shù)取對應(yīng)的namespaceHandler全限定類名or NamespaceHandler實例
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
        return null;
    }
    // 2、handlerOrClassName是已初始化過的實例則直接返回
    else if (handlerOrClassName instanceof NamespaceHandler) {
        return (NamespaceHandler) handlerOrClassName;
    }else {
        String className = (String) handlerOrClassName;
        try {
            ///3、使用反射根據(jù)namespaceHandler全限定類名加載實現(xiàn)類
            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                        "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
            }
            // 3.1、初始化namespaceHandler實例
            NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            // 3.2、 初始化,不同的namespaceHandler實現(xiàn)類初始化方法邏輯有差異
            namespaceHandler.init();
            // 4、將初始化好的實例放入內(nèi)存緩存中,下次解析到相同namespaceUri標簽時直接返回,避免再次初始化
            handlerMappings.put(namespaceUri, namespaceHandler);
            return namespaceHandler;
        }catch (ClassNotFoundException ex) {
            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "] not found", ex);
        }catch (LinkageError err) {
            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "]: problem with handler class file or dependent class", err);
        }
    }
}

**第1步:**源碼中g(shù)etHandlerMappings()是比較核心的一個方法,通過懶加載的方式解析spring.handlers并返回namespaceUri和NamespaceHandler的映射關(guān)系。

**第2步:**根據(jù)namespaceUri返回對應(yīng)的NamespaceHandler全限定名或者具體的實例(是名稱還是實例取決于是否被初始化過,若是初始化過的實例會直接返回)

**第3步:**是NamespaceHandler實現(xiàn)類的全限定名,通過上述源碼中的第3步,使用反射進行初始化。

**第4步:**將初始化后的實例放到handlerMappings內(nèi)存緩存中,這也是第2步為什么可能是NamespaceHandler類型的原因。

看完resolve方法的源碼,再看下resolve方法在Spring中調(diào)用場景,大致可以了解spring.handlers的使用場景:

可以看到resolve()主要用在標簽解析過程中,主要被在BeanDefinitionParserDelegate的parseCustomElement和decorateIfRequired方法中調(diào)用。

② resolve()源碼中核心邏輯之一便是調(diào)用的getHandlerMappings(),在getHandlerMappings()中實現(xiàn)對各個jar包中的META-INF/spring.handlers文件的解析,如:

DefaultNamespaceHandlerResolver#getHandlerMappings()

private Map<String, Object> getHandlerMappings() {
    Map<String, Object> handlerMappings = this.handlerMappings;
    // 使用線程安全的解析邏輯,避免在并發(fā)場景下重復的解析,沒必要重復解析
    // 這里在同步代碼塊的內(nèi)外對handlerMappings == null作兩次判斷很有必要,采用懶漢式初始化
    if (handlerMappings == null) {
        synchronized (this) {
            handlerMappings = this.handlerMappings;
            // duble check
            if (handlerMappings == null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                }
                try {
                    // 加載handlerMappingsLocation目錄文件,handlerMappingsLocation路徑值可變,默認是META-INF/spring.handlers
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                    }
                    // 初始化內(nèi)存緩存
                    handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
                    // 將加載到的屬性合并到handlerMappings中
                    CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                    // 賦值內(nèi)存緩存
                    this.handlerMappings = handlerMappings;
                }catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                }
            }
        }
    }
    return handlerMappings;
}

源碼中this.handlerMappings是一個Map類型的內(nèi)存緩存,存放解析到的namespaceUri以及NameSpaceHandler實例。

getHandlerMappings()方法體中的實現(xiàn)使用了線程安全方式,增加了同步邏輯。

通過閱讀源碼可以了解到Spring基于spring.handlers實現(xiàn)SPI邏輯相對比較簡單,但應(yīng)用卻比較靈活,對自定義標簽的支持很方便,在不修改Spring源碼的前提下輕松實現(xiàn)接入,如Dubbo中定義的各種Dubbo標簽便是很好的利用了spring.handlers。

Spring提供如此靈活的功能,那是如何應(yīng)用的呢?下面簡單了解下parseCustomElement()。

  • BeanDefinitionParserDelegate.parseCustomElement()

resolve作為工具類型的方法,被使用的地方比較多,這里僅簡單介紹在BeanDefinitionParserDelegate.parseCustomElement()中的應(yīng)用。

BeanDefinitionParserDelegate#parseCustomElement()

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
     // 獲取標簽的namespaceUri
     String namespaceUri = getNamespaceURI(ele);
     // 首先獲得DefaultNamespaceHandlerResolver實例在再以namespaceUri作為參數(shù)調(diào)用resolve方法解析取得NamespaceHandler
     NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
     if (handler == null) {
         error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
         return null;
     }
     // 調(diào)用NamespaceHandler中的parse方法開始解析標簽
     return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
 }

parseCustomElement作為解析標簽的中間方法,再看下parseCustomElement的調(diào)用情況:

在parseBeanDefinitions()中被調(diào)用,再看下parseBeanDefinitions的源碼

DefaultBeanDefinitionDocumentReader#parseBeanDefinitions()

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // spring內(nèi)部定義的標簽為默認標簽,即非spring內(nèi)部定義的標簽都不是默認的namespace
    if (delegate.isDefaultNamespace(root)) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                // root子標簽也做此判斷
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }else{
                    // 子標簽非spring默認標簽(即自定義標簽)也走parseCustomElement來解析
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }else {
        // 非spring的默認標簽(即自定義的標簽)走parseCustomElement來解析
        delegate.parseCustomElement(root);
    }
}

到此就很清晰了,調(diào)用前判斷是否為Spring默認標簽,不是默認標簽調(diào)用parseCustomElement來解析,最后調(diào)用resolve方法。

4.2.3 小節(jié)

Spring自2.0引入spring.handlers以后,為Spring的動態(tài)擴展提供更多的入口和手段,為自定義標簽的實現(xiàn)提供了強力支撐。

很多文章在介紹Spring SPI時都重點介紹spring.factories實現(xiàn),很少提及很早就引入的spring.handlers,但通過個人的分析及與Java SPI的對比,spring.handlers也是一種SPI的實現(xiàn),只是基于xml實現(xiàn)。

相比于Java SPI,基于spring.handlers實現(xiàn)的SPI更加的靈活,無需遍歷,直接映射,更類似于Dubbo SPI的實現(xiàn)思想,每個類指定一個名稱(只是spring.handlers中是以namespaceUri作為key,Dubbo配置中是指定的名稱作為key)。

4.3 spring.factories

同樣先以測試代碼來介紹spring.factories實現(xiàn)SPI的邏輯。

4.3.1 spring.factories SPI

① 創(chuàng)建DataBaseSPI接口

接口

package spring.spi.factories;
public interface DataBaseSPI {
    public void dataBaseOperation();
}

② 創(chuàng)建DataBaseSPI接口的實現(xiàn)類

MysqlDataBaseImpl

#實現(xiàn)類1
package spring.spi.factories.impl;
import spring.spi.factories.DataBaseSPI;
public class MysqlDataBaseImpl implements DataBaseSPI {
 
    @Override
    public void dataBaseOperation() {
        System.out.println("Mysql database test!!!!");
    }
}

MysqlDataBaseImpl

#實現(xiàn)類2
package spring.spi.factories.impl;
import spring.spi.factories.DataBaseSPI;
public class OracleDataBaseImpl implements DataBaseSPI {
    @Override
    public void dataBaseOperation() {
        System.out.println("Oracle database test!!!!");
    }
}

③ 在項目META-INF/目錄下創(chuàng)建spring.factories文件:

文件內(nèi)容:

spring.factories

#key是接口的全限定名,value是接口的實現(xiàn)類
spring.spi.factories.DataBaseSPI = spring.spi.factories.impl.MysqlDataBaseImpl,spring.spi.factories.impl.OracleDataBaseImpl

④ 運行代碼

SpringSpiTest#main()

package spring.spi.factories;
import java.util.List;
import org.springframework.core.io.support.SpringFactoriesLoader;
public class SpringSpiTest {
    public static void main(String args[]){
         
        // 調(diào)用SpringFactoriesLoader.loadFactories方法加載DataBaseSPI接口所有實現(xiàn)類的實例
        List<DataBaseSPI> spis= SpringFactoriesLoader.loadFactories(DataBaseSPI.class, Thread.currentThread().getContextClassLoader());
         
        // 遍歷DataBaseSPI接口實現(xiàn)類實例
        for(DataBaseSPI spi : spis){
            spi.dataBaseOperation();
        }
    }
}

⑤ 運行結(jié)果

Mysql database test!!!!
Oracle database test!!!!

從上述的示例代碼中可以看出spring.facotries方式實現(xiàn)的SPI和Java SPI很相似,都是先獲取指定接口類型的實現(xiàn)類,然后遍歷訪問所有的實現(xiàn)。但也存在一定的差異:

(1)配置上:

Java SPI是一個服務(wù)提供接口對應(yīng)一個配置文件,配置文件中存放當前接口的所有實現(xiàn)類,多個服務(wù)提供接口對應(yīng)多個配置文件,所有配置都在services目錄下;

Spring factories SPI是一個spring.factories配置文件存放多個接口及對應(yīng)的實現(xiàn)類,以接口全限定名作為key,實現(xiàn)類作為value來配置,多個實現(xiàn)類用逗號隔開,僅spring.factories一個配置文件。

(2)實現(xiàn)上

Java SPI使用了懶加載模式,即在調(diào)用ServiceLoader.load()時僅是返回了ServiceLoader實例,尚未解析接口對應(yīng)的配置文件,在使用時即循環(huán)遍歷時才正式解析返回服務(wù)提供接口的實現(xiàn)類實例;

Spring factories SPI在調(diào)用SpringFactoriesLoader.loadFactories()時便已解析spring.facotries文件返回接口實現(xiàn)類的實例(實現(xiàn)細節(jié)在源碼分析中詳解)。

4.3.2 源碼分析

我們還是從測試代碼開始,了解下spring.factories的SPI實現(xiàn)源碼,細品spring.factories的實現(xiàn)方式。

  • SpringFactoriesLoader測試代碼入口直接調(diào)用SpringFactoriesLoader.loadFactories()靜態(tài)方法開始解析spring.factories文件,并返回方法參數(shù)中指定的接口類型,如測試代碼里的DataBaseSPI接口的實現(xiàn)類實例。

SpringFactoriesLoader#loadFactories()

public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
    Assert.notNull(factoryClass, "'factoryClass' must not be null");
    ClassLoader classLoaderToUse = classLoader;
    // 1.確定類加載器
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    // 2.核心邏輯之一:解析各jar包中META-INF/spring.factories文件中factoryClass的實現(xiàn)類全限定名
    List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
    if (logger.isTraceEnabled()) {
        logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
    }
    List<T> result = new ArrayList<T>(factoryNames.size());
    // 3.遍歷實現(xiàn)類的全限定名并進行實例化
    for (String factoryName : factoryNames) {
        result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
    }
    // 排序
    AnnotationAwareOrderComparator.sort(result);
    // 4.返回實例化后的結(jié)果集
    return result;
}

源碼中l(wèi)oadFactoryNames() 是另外一個比較核心的方法,解析spring.factories文件中指定接口的實現(xiàn)類的全限定名,實現(xiàn)邏輯見后續(xù)的源碼。

經(jīng)過源碼中第2步解析得到實現(xiàn)類的全限定名后,在第3步通過instantiateFactory()方法逐個實例化實現(xiàn)類。

再看loadFactoryNames()源碼是如何解析得到實現(xiàn)類全限定名的:

SpringFactoriesLoader#loadFactoryNames()

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    // 1.接口全限定名
    String factoryClassName = factoryClass.getName();
    try {
        // 2.加載META-INF/spring.factories文件路徑(分布在各個不同jar包里,所以這里會是多個文件路徑,枚舉返回)
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        // 3.遍歷枚舉集合,逐個解析spring.factories文件
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String propertyValue = properties.getProperty(factoryClassName);
            // 4.spring.factories文件中一個接口的實現(xiàn)類有多個時會用逗號隔開,這里拆開獲取實現(xiàn)類全限定名
            for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
                result.add(factoryName.trim());
            }
        }
        return result;
    }catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

源碼中第2步獲取所有jar包中META-INF/spring.factories文件路徑,以枚舉值返回。

源碼中第3步開始遍歷spring.factories文件路徑,逐個加載解析,整合factoryClass類型的實現(xiàn)類名稱。

獲取到實現(xiàn)類的全限定名集合后,便根據(jù)實現(xiàn)類的名稱逐個實例化,繼續(xù)看下instantiateFactory()方法的源碼:

SpringFactoriesLoader#instantiateFactory()

private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
    try {
        // 1.使用classLoader類加載器加載instanceClassName類
        Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
        if (!factoryClass.isAssignableFrom(instanceClass)) {
            throw new IllegalArgumentException(
                    "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
        }
        // 2.instanceClassName類中的構(gòu)造方法
        Constructor<?> constructor = instanceClass.getDeclaredConstructor();
        ReflectionUtils.makeAccessible(constructor);
        // 3.實例化
        return (T) constructor.newInstance();
    }
    catch (Throwable ex) {
        throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
    }
}

實例化方法是私有型(private)靜態(tài)方法,這個有別于loadFactories和loadFactoryNames。

實例化邏輯整體使用了反射實現(xiàn),比較通用的實現(xiàn)方式。

通過對源碼的分析,Spring factories方式實現(xiàn)的SPI邏輯不是很復雜,整體上的實現(xiàn)容易理解。

Spring在3.2便已引入spring.factories,那spring.factories在Spring框架中又是如何使用的呢?先看下loadFactories方法的調(diào)用情況:

從調(diào)用情況看Spring自3.2引入spring.factories SPI后并沒有真正的利用起來,使用的地方比較少,然而真正把spring.factories發(fā)揚光大的,是在Spring Boot中, 簡單了解下SpringBoot中的調(diào)用。

  • getSpringFactoriesInstances()getSpringFactoriesInstances()并不是Spring框架中的方法,而是SpringBoot中SpringApplication類里定義的私有型(private)方法,很多地方都有調(diào)用,源碼如下:

SpringApplication#getSpringFactoriesInstance()

// 單個參數(shù)getSpringFactoriesInstances方法
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    // 默認調(diào)用多參的重載方法
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}
// 多個參數(shù)的getSpringFactoriesInstances方法
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // 調(diào)用SpringFactoriesLoader中的loadFactoryNames方法加載接口實現(xiàn)類的全限定名
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 實例化
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

在getSpringFactoriesInstances()中調(diào)用了SpringFactoriesLoader.loadFactoryNames()來加載接口實現(xiàn)類的全限定名集合,然后進行初始化。

SpringBoot中除了getSpringFactoriesInstances()方法有調(diào)用,在其他邏輯中也廣泛運用著SpringFactoriesLoader中的方法來實現(xiàn)動態(tài)擴展,這里就不在一一列舉了,有興趣的同學可以自己去發(fā)掘。

4.3.3 小節(jié)

Spring框架在3.2引入spring.factories后并沒有有效的利用起來,但給框架的使用者提供了又一個動態(tài)擴展的能力和入口,為開發(fā)人員提供了很大的自由發(fā)揮的空間,尤其是在SpringBoot中廣泛運用就足以證明spring.factories的地位。spring.factories引入在 提升Spring框架能力的同時也暴露出其中的不足:

首先,spring.factories的實現(xiàn)類似Java SPI,在加載到服務(wù)提供接口的實現(xiàn)類后需要循環(huán)遍歷才能訪問,不是很方便。

其次,Spring在5.0.x版本以前SpringFactoriesLoader類定義為抽象類,但在5.1.0版本之后Sping官方將SpringFactoriesLoader改為final類,類型變化對前后版本的兼容不友好。

五、應(yīng)用實踐

介紹完Spring中SPI機制相關(guān)的核心源碼,再來看看項目中自己開發(fā)的輕量版的分庫分表SDK是如何利用Spring的SPI機制實現(xiàn)分庫分表策略動態(tài)擴展的。

基于項目的特殊性并沒有使用目前行業(yè)中成熟的分庫分表組件,而是基于Mybatis的插件原理自己開發(fā)的一套輕量版分庫分表組件。為滿足不同場景分庫分表要求,將其中分庫分表的相關(guān)邏輯以策略模式進行抽取分離,每種分庫分表的實現(xiàn)對應(yīng)一條策略,支持使用方對分庫分表策略的動態(tài)擴展,而這里的動態(tài)擴展就利用了spring.factories。

首先給出輕量版分庫分表組件流程圖,然后我們針對流程圖中使用到Spring SPI的地方進行詳細分析。

說明:

上述流程圖中項目啟動過程中生成數(shù)據(jù)源和分庫分表策略的初始化,策略初始化完成后緩存到內(nèi)存中。

發(fā)起數(shù)據(jù)庫操作指令時,解析是否需要分庫分表(流程中只給出了需要分庫分表的流程),需要則通過提取到的策略key獲取對應(yīng)的分庫分表策略并進行分庫分表,完成數(shù)據(jù)庫操作。

通過上述的流程圖可以看到,分庫分表SDK通過spring.factories支持動態(tài)加載分庫分表策略以兼容不同項目的不同使用場景。

其中分庫分表部分的策略類圖:

其中:ShardingStrategy和DBTableShardingStrategy為接口;BaseShardingStrategy為默認實現(xiàn)類;DefaultStrategy和CountryDbSwitchStrategy為SDK中基于不同場景默認實現(xiàn)的分庫分表策略。

在項目實際使用時,動態(tài)擴展的分庫分表策略只需要繼承BaseShardingStrategy即可,SDK中初始化分庫分表策略時通過SpringFactoriesLoader.loadFactories()實現(xiàn)動態(tài)加載。

六、總結(jié)

SPI技術(shù)將服務(wù)接口與服務(wù)實現(xiàn)分離以達到解耦,極大的提升程序的可擴展性。

本文重點介紹了Java內(nèi)置SPI和Dubbo SPI以及Spring SPI三者的原理和相關(guān)源碼;首先演示了三種SPI技術(shù)的實現(xiàn),然后通過演示代碼深入閱讀了三種SPI的實現(xiàn)源碼;其中重點介紹了Spring SPI的兩種實現(xiàn)方式:spring.handlers和spring.factories,以及使用spring.factories實現(xiàn)的分庫分表策略加載。希望通過閱讀本文可以讓讀者對SPI有更深入的了解。

到此這篇關(guān)于深入講解SPI 在 Spring 中的應(yīng)用的文章就介紹到這了,更多相關(guān)Spring SPI內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Swing圖形界面實現(xiàn)可動態(tài)刷新的驗證碼

    Swing圖形界面實現(xiàn)可動態(tài)刷新的驗證碼

    這篇文章主要為大家詳細介紹了Swing圖形界面實現(xiàn)可動態(tài)刷新的驗證碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • 使用SpringSecurity 進行自定義Token校驗

    使用SpringSecurity 進行自定義Token校驗

    這篇文章主要介紹了使用SpringSecurity 進行自定義Token校驗操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-06-06
  • Mybatis-Plus BaseMapper的用法詳解

    Mybatis-Plus BaseMapper的用法詳解

    這篇文章主要介紹了Mybatis-Plus BaseMapper的用法詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-08-08
  • Spring AOP實現(xiàn)記錄操作日志

    Spring AOP實現(xiàn)記錄操作日志

    這篇文章主要為大家詳細介紹了Spring AOP實現(xiàn)記錄操作日志,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-09-09
  • 如何在java中使用SFTP協(xié)議安全的傳輸文件

    如何在java中使用SFTP協(xié)議安全的傳輸文件

    這篇文章主要介紹了如何在java中使用SFTP協(xié)議安全的傳輸文件,幫助大家更好的理解和使用JSch,感興趣的朋友可以了解下
    2020-10-10
  • 基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解

    基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解

    這篇文章主要為大家介紹了基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • Java中實現(xiàn)簡單的Excel導出

    Java中實現(xiàn)簡單的Excel導出

    今天小編就為大家分享一篇關(guān)于Java中實現(xiàn)簡單的Excel導出,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • SpringBoot淺析緩存機制之Redis單機緩存應(yīng)用

    SpringBoot淺析緩存機制之Redis單機緩存應(yīng)用

    在上文中我介紹了Spring Boot使用EhCache 2.x來作為緩存的實現(xiàn),本文接著介紹使用單機版的Redis作為緩存的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-08-08
  • java實現(xiàn)批量生成二維碼

    java實現(xiàn)批量生成二維碼

    這篇文章主要為大家詳細介紹了java實現(xiàn)批量生成二維碼的相關(guān)代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • Android中Parcelable的作用實例解析

    Android中Parcelable的作用實例解析

    這篇文章主要介紹了Android中Parcelable的作用,對于Android初學者有一定的參考學習價值,需要的朋友可以參考下
    2014-08-08

最新評論