Dubbo中的SPI機制

Dubbo中的SPI機制

概述

Service Provider Interface 即 SPI,是JDK內置的一種服務提供發現機制,可以用來啟用框架擴展和替換組件。可以讓不同的廠商針對統一接口編寫不同的實現

SPI實際上是“接口+策略模式+配置文件”實現的動態加載機制。在系統設計中,模塊之間通常基于接口編程,不直接顯示指定實現類。一旦代碼里指定了實現類,就無法在不修改代碼的情況下替換為另一種實現。為了達到動態可插拔的效果,java提供了SPI以實現服務發現。

SPI機制的應用場景有很多,我們比較常用的就是JDBC,Dubbo等

在談Dubbo的SPI機制前我們需要先了解一下Java原生的SPI機制

Java原生的SPI

  1. 首先我,我們先創建一個接口,接口中定義一個方法

package cuit.epoch.pymjl.spi;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:43**/
public interface Animal {/*** 動物叫*/void call();
}

  1. 然后我們分別寫兩個不同的實現類,實現這個接口

package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:44**/
public class Cat implements Animal {@Overridepublic void call() {System.out.println("貓叫:喵喵喵~");}
}

package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:45**/
public class Dog implements Animal {@Overridepublic void call() {System.out.println("狗叫:汪汪汪~");}
}

  1. 在項目./resources/META-INF/servcie下創建一個文件。文件名為你要動態擴展的接口的全限定名稱,我們這里就是Animaal接口的全限定名稱,如圖所示:

image-20220504201416121

文件里面的內容就是實現類的全限定名稱

cuit.epoch.pymjl.spi.impl.Cat
cuit.epoch.pymjl.spi.impl.Dog

  1. 編寫測試類

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("這是Java原生的SPI機制");ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);for (Animal animal : serviceLoader) {animal.call();}}
}

  1. 運行測試

image-20220504201720318

通過測試代碼可見,我們實現的兩個類成功的被加載并運行了相關的方法。但是我們并沒有在代碼中顯示的創建某個實現類,我們可以通過這種插件化的配置文件的形式實例化我們想要的對象。將程序的決定權解耦到配置文件中

但是Java原生的SPI也有其不足

  1. 通過測試代碼我們可知,我們并不能直接指定加載某一個我們想要的類,只能通過遍歷加載配置文件中所有類來找到我們想要的類,這樣效率是很低的,造成資源的浪費
  2. 獲取類的方式不夠靈活,只能通過Iterator 形式獲取,不能根據某個參數來獲取對應的實現類。
  3. ServiceLoader線程不安全

針對以上不足,我們可以選擇Dubbo的SPI機制,以此來規避不足

Dubbo的SPI

Dubbo 并未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現類。

另外,Dubbo是通過鍵值對的方式進行配置,我們可以直接通過Key獲取我們想要加載的實體類。Dubbo默認的配置文件路徑是在./resources/META-INF/dubbo

編寫測試方法:

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("這是Dubbo的SPI機制");Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog");cat.call();dog.call();}
}

更改配置文件

cat=cuit.epoch.pymjl.spi.impl.Cat
dog=cuit.epoch.pymjl.spi.impl.Dog

運行測試:

image-20220504203927835

通過代碼可知,Dubbo的SPI機制可直接通過Key獲取我們想要的對象實例,比原生的Java SPI更有優勢,除此之外,Dubbo在設計中還用到了大量的全局緩存,提高了我們實例化對象的效率,同時還支持通過注解默認實現,AOP,IOC等功能

下面是仿造Dubbo的源碼自定義實現的SPI機制的代碼,來自于Guide哥開源的RPC項目。雖然和源碼的有一些差別,也沒有Dubbo的功能完善,但核心思想還是一樣的

仿造源碼自定義實現Dubbo SPI機制

自定義注解MySpi

通過上文可知,Dubbo的SPI機制還要根據@spi注解實現,對需要擴展的接口做一個標記,所以我們先自定義一個注解,用來標記我們想要擴展的接口

package cuit.epoch.pymjl.spi;import java.lang.annotation.*;/*** 標記接口,聲明該接口是一個擴展點** @author Pymjl* @version 1.0* @date 2022/5/3 12:05**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MySpi {//TODO value可以提供對默認實現的支持,但是這方面的切面并沒有寫,只是寫在這兒表示有這個東西String value() default "";
}

Holder對象

顧名思義,Holder就是持有通過ExtensionLoader加載來的實例化對象的對象

package cuit.epoch.pymjl.spi;/*** 持有人** @author Pymjl* @version 1.0* @date 2022/5/3 15:11**/
public class Holder<T> {private volatile T value;public T get() {return value;}public void set(T value) {this.value = value;}
}

ExtensionLoader

Dubbo SPI機制的核心類,使用它從文件中讀取配置,并通過反射實例化對象

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 模仿Dubbo自定義擴展機制** @author Pymjl* @version 1.0* @date 2022/5/3 15:12**/
@Slf4j
public class ExtensionLoader<T> {/*** SPI配置文件根目錄*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地緩存,Dubbo會先通過getExtensionLoader方法從緩存中獲取一個ExtensionLoader* 若緩存未命中,則會生成一個新的實例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();/*** 目標擴展類的字節碼和實例對象*/private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();/*** 需要加載的擴展類類別*/private final Class<?> type;/*** 本地緩存*/private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();/*** 擴展類實例對象,key為配置文件中的key,value為實例對象的全限定名稱*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();public ExtensionLoader(Class<?> type) {this.type = type;}/*** 得到擴展加載程序** @param type 要擴展的接口,必須被MySpi標記* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判斷type是否為nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判斷是否被MySpi標記if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先從緩存中獲取擴展加載器,如果未命中,則創建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中則創建,并放入緩存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}/*** 得到擴展類對象實例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//檢查參數if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先從緩存中獲取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder還未持有目標對象,則為其創建一個單例對象Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {instance = createExtension(name);holder.set(instance);}}}return (T) instance;}/*** 通過擴展類字節碼創建實例對象** @param name 名字* @return {@code T}*/private T createExtension(String name) {//從文件中加載所有類型為 T 的擴展類并按名稱獲取特定的擴展類Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw new RuntimeException("No such extension of name " + name);}T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {try {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 獲取所有擴展類** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//從緩存中獲取已經加載的擴展類Map<String, Class<?>> classes = cachedClasses.get();//雙重檢查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//從配置文件中加載所有擴展類loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 從配置目錄中加載所有擴展類** @param extensionsClasses 擴展類的K,V鍵值對*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();try {//獲取配置文件的資源路徑Enumeration<URL> urls;ClassLoader classLoader = ExtensionLoader.class.getClassLoader();urls = classLoader.getResources(fileName);if (urls != null) {while (urls.hasMoreElements()) {URL resourceUrl = urls.nextElement();loadResource(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通過Url加載資源** @param extensionClasses 擴展類,key為配置文件中的key,Value為實現類的全限定名稱* @param classLoader      類加載器* @param resourceUrl      資源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//讀取文件中的每一行數據while ((line = reader.readLine()) != null) {//先排除配置文件中的注釋final int noteIndex = line.indexOf('#');//我們應該忽略掉注釋后的內容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}
}

  1. 先聲明了一些常量,例如配置文件的根目錄,本地緩存等

     /*** SPI配置文件根目錄*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地緩存,Dubbo會先通過getExtensionLoader方法從緩存中獲取一個ExtensionLoader* 若緩存未命中,則會生成一個新的實例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();/*** 目標擴展類的字節碼和實例對象*/private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();/*** 需要加載的擴展類類別*/private final Class<?> type;/*** 本地緩存*/private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();/*** 擴展類實例對象,key為配置文件中的key,value為實例對象的全限定名稱*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    
  2. ExtensionLoader 的getExtensionLoader 方法先從緩存中獲取一個ExtensionLoader 實例,若緩存未命中,則new一個。

    /*** 得到擴展加載程序** @param type 要擴展的接口,必須被MySpi標記* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判斷type是否為nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判斷是否被MySpi標記if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先從緩存中獲取擴展加載器,如果未命中,則創建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中則創建,并放入緩存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}
    
  3. 然后再通過ExtensionLoader 的getExtension 方法獲取拓展類對象。跟上面的邏輯一樣,先從緩存中查找,若緩存未命中則新建一個

   /*** 得到擴展類對象實例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//檢查參數if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先從緩存中獲取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder還未持有目標對象,則為其創建一個單例對象Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {instance = createExtension(name);holder.set(instance);}}}return (T) instance;}/*** 通過擴展類字節碼創建實例對象** @param name 名字* @return {@code T}*/private T createExtension(String name) {//從文件中加載所有類型為 T 的擴展類并按名稱獲取特定的擴展類Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw new RuntimeException("No such extension of name " + name);}T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {try {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 獲取所有擴展類** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//從緩存中獲取已經加載的擴展類Map<String, Class<?>> classes = cachedClasses.get();//雙重檢查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//從配置文件中加載所有擴展類loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 從配置文件中加載所有擴展實現類** @param extensionsClasses 擴展類的K,V鍵值對*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();try {//獲取配置文件的資源路徑Enumeration<URL> urls;ClassLoader classLoader = ExtensionLoader.class.getClassLoader();urls = classLoader.getResources(fileName);if (urls != null) {while (urls.hasMoreElements()) {URL resourceUrl = urls.nextElement();loadResource(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通過Url加載資源** @param extensionClasses 擴展類,key為配置文件中的key,Value為實現類的全限定名稱* @param classLoader      類加載器* @param resourceUrl      資源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//讀取文件中的每一行數據while ((line = reader.readLine()) != null) {//先排除配置文件中的注釋final int noteIndex = line.indexOf('#');//我們應該忽略掉注釋后的內容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}


---------------------
作者:Pymj
來源:CSDN
原文:https://blog.csdn.net/apple_52109766/article/details/124577928
版權聲明:本文為作者原創文章,轉載請附上博文鏈接!
內容解析By:CSDN,CNBLOG博客文章一鍵轉載插件

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:
http://www.pswp.cn/news/283890.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/283890.shtml
英文地址,請注明出處:http://en.pswp.cn/news/283890.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JWT:擁有我,即擁有權力

Hi&#xff0c;這里是桑小榆。上篇文章中&#xff0c;我們一起探討了 OAuth 協議的原理以及授權認證流程&#xff0c;本次我們一起探討 jwt 令牌作為授權協議的傳輸介質。OAuth協議規范了幾個參與角色的授權標準&#xff0c;安全可控的授予第三方應用&#xff0c;第三方應用獲取…

雙十一到來之前,阿里AI設計師“魯班”1天能做4000萬張海報

相比較去年&#xff0c;“魯班”的設計技藝有所提升。 人工智能很大程度上便利了我們的生活&#xff0c;現在他們甚至還能取代了一些設計師的工作&#xff0c;在雙十一正式到來之前&#xff0c;淘寶的宣傳已經鋪天蓋地&#xff0c;然而很多人都沒想到&#xff0c;我們打開淘寶…

Appium移動自動化測試之獲取appPackage和appActivity

方法一&#xff1a;直接打開Appium,點擊左上角機器人圖標 選擇apk所在位置&#xff0c;如圖所示&#xff0c;這里以ContactManager.apk為例 方法二&#xff1a;利用dex2jar和jd-gui這兩個工具反編譯apk文件 這里仍以ContactManager.apk為例 (1)重命名ContactManager.apk為Conta…

CAD轉WPF: 關于CAD圖紙文件轉換為WPF矢量代碼文件(xaml文件)的技巧

前言&#xff1a;下面的文章&#xff0c;我將會以幾個很簡單的步驟&#xff0c;來演示一下通過CAD圖紙轉換為XAML代碼文件的方法&#xff0c;供大佬們參考。一、為了演示一個簡單的操作&#xff0c;我此處先打開一個空白的CAD&#xff0c;等下用來進行繪制點內容使用。二、自定…

python之新式類與經典類

經典類與新式類經典類:P 或 P()--深度查找&#xff0c;向上查父節點新式類 :P(object)---廣度查找&#xff0c;繼承object&#xff0c;新式類的方法較多轉載于:https://www.cnblogs.com/zyy98877/p/8574983.html

Flowportal-BPM——環境配置

環境配置&#xff1a; 一、控制面板→程序和功能→打開或不關閉Window功能→選擇選項 二、控制面板→管理工具→Internet信息服務&#xff08;IIS&#xff09;管理器→左側第一個→ISAPI和CGI限制→全部選為【允許】 三、控制面板→管理工具→Internet信息服務&#xff08;IIS&…

一篇文章帶你搞懂什么是DevOps?

DevOps DevOps 它的英文發音是 /de’v?ps/&#xff0c;類似于“迪沃普斯”&#xff0c;一詞本身是對于 development 以及 operation 兩個詞的混合&#xff0c;其目的在于縮短系統開發的生命周期&#xff0c;在這過程中發布特性、修復bug以及更新均被緊密的結合。 簡化的含義為…

iOS 時間戳的轉換

在開發iOS程序時&#xff0c;有時候需要將時間格式調整成自己希望的格式&#xff0c;這個時候我們可以用NSDateFormatter類來處理。例如&#xff1a; //實例化一個NSDateFormatter對象 NSDateFormatter *dateFormatter [[NSDateFormatter alloc] init]; //設定時間格式,這里可…

微服務架構下分布式事務解決方案 —— 阿里GTS

1 微服務的發展 微服務倡導將復雜的單體應用拆分為若干個功能簡單、松耦合的服務&#xff0c;這樣可以降低開發難度、增強擴展性、便于敏捷開發。當前被越來越多的開發者推崇&#xff0c;很多互聯網行業巨頭、開源社區等都開始了微服務的討論和實踐。Hailo有160個不同服務構成&…

重要消息丨.NET Core 3.1 將于今年12月13日結束支持

點擊上方藍字關注我們&#xff08;本文閱讀時間&#xff1a;5分鐘).NET Core 3.1 將于 2022 年 12 月 13 日結束支持。此后&#xff0c;Microsoft 將不再為 .NET Core 3.1 提供服務更新或技術支持。我們建議盡快遷移到 .NET 6。如果您在支持日期結束后仍在使用 .NET Core 3.1&a…

產品設計的三大原則

1.它有用嗎? 如果我們必須從這三個特性中選擇一個作為最重要的&#xff0c;那就是有用性。 首要的是&#xff0c;一個產品必須有用。如果它無用&#xff0c;其它任何東西都是不相關的&#xff0c;因為沒有人會需要它。很明顯&#xff0c;有用性和可享用性看上去一樣重要&#…

常用的17個運維監控系統

1. Zabbix Zabbix 作為企業級的網絡監控工具&#xff0c;通過從服務器&#xff0c;虛擬機和網絡設備收集的數據提供實時監控&#xff0c;自動發現&#xff0c;映射和可擴展等功能。 Zabbix的企業級監控軟件為用戶提供內置的Java應用服務器監控&#xff0c;硬件監控&#xff0c…

關于html-三角的制作

因為最近看到別人寫的不錯的樣式&#xff0c;所以就想自己實現&#xff0c;但是呢用到了一個三角形&#xff0c;所以稍微研究一下。效果是這樣的&#xff1a;注意是下邊那個淺色三角&#xff0c;感覺書簽的效果有木有。看著很有層次感。接下來就是實現了&#xff0c;利用border…

ABP中的數據過濾器

本文首先介紹了ABP內置的軟刪除過濾器(ISoftDelete)和多租戶過濾器(IMultiTenant)&#xff0c;然后介紹了如何實現一個自定義過濾器&#xff0c;最后介紹了在軟件開發過程中遇到的實際問題&#xff0c;同時給出了解決問題的一個未必最優的思路。一.預定義過濾器ABP中的數據過濾…

ActiveMQ與spring整合

2019獨角獸企業重金招聘Python工程師標準>>> 1 生產者 第一步&#xff1a;引用相關的jar包。 <dependency> <groupId>org.springframework</groupId><artifactId>spring-jms</artifactId> </dependency> <dependency><…

最新遠程部署運維工具匯總

一&#xff0e;Puppet 轉載https://baike.baidu.com/item/puppet/5109503?fraladdin puppet是一種Linux、Unix、windows平臺的集中配置管理系統&#xff0c;使用自有的puppet描述語言&#xff0c;可管理配置文件、用戶、cron任務、軟件包、系統服務等。puppet把這些系統實體…

Kali Linux 2016.2初體驗使用總結

Kali Linux 2016.2初體驗使用總結Kali Linux官方于8月30日發布Kali Linux 2016的第二個版本Kali Linux 2016.2。該版本距離Kali Linux 2016.1版本發布&#xff0c;已經有7個月。在這期間&#xff0c;在Kali Linux 2016.2版本發布的這段時間&#xff0c;Kali Linux官方增補了94個…

Kafka入門教程:學習總結目錄索引

【Kafka】| 總結/Edison ZhouEdison總結了Kafka的學習征途系列&#xff0c;特意整理了一份目錄索引&#xff0c;希望對你有幫助。0Kafka學習路徑在學習Kafka的途中&#xff0c;我總結了一個系列的Kafka學習征途系列教程&#xff0c;它只選取了我認為最實用的部分整理出來&#…

javaweb學習中的路徑問題

1. 項目結構 2. 客戶端路徑 1. 超鏈接 <% page language"java" contentType"text/html; charsetUTF-8"pageEncoding"UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/…

步步為營-11-ListT泛型的簡單練習

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace 集合簡單練習 {class Program{static void Main(string[] args){}private static void Test3(){//奇偶分揀,奇數在前偶數在后List<int>…