主頁 > 後端開發 > 不是單例的單例——巧用ClassLoader

不是單例的單例——巧用ClassLoader

2023-05-16 09:40:16 後端開發

本文通過如何將一個單例類實體化兩次的案例,用代碼實踐來引入 Java 類加載器相關的概念與作業機制,理解并熟練掌握相關知識之后可以擴寬解決問題的思路,另辟蹊徑,達到目的,

背景

單例模式是最常用的設計模式之一,其目的是保證一個類在行程中僅有一個實體,并提供一個它的全域訪問方式,那什么場景下一個行程里需要單例類的兩個物件呢?很明顯這破壞了單例模式的設計初衷,

這里舉例一個我司的特殊場景:

RPC 的呼叫規范是每個業務集群里只能有一個呼叫方,如果一個業務節點已經實體化了一個客戶端,就無法再實體化另一個,這個規范的目的是讓一個集群統一個呼叫方,方便服務資料的收集、展示、告警等操作,

一個專案有多個集群,多個專案組維護,各個集群都有一個共同特點,需要呼叫相同的 RPC 服務,如果嚴格按照上述 RPC 規范的話,每一個集群都需要申請一個自己呼叫方,每一個呼叫方都申請相同的 RPC 服務,這樣做完全沒有問題,只是相同的作業會被各個集群都做一遍,并且生成了多個 RPC 的呼叫方,

最終方案是將相同的邏輯代碼打包成一個公用 jar 包,然后其他集群引入這個包就能解決我們上述的問題,這么做的話就碰到了 RPC 規范中的約束問題,jar 包里的公用邏輯會呼叫 RPC 服務,那么勢必會有一個 RPC 的公用呼叫方,我們的業務代碼里也會有自己業務需要呼叫的其他 RPC 服務,這個呼叫方和 jar 包里的呼叫方就沖突了,只能有一個呼叫方會被成功初始化,另一個則會報錯,這個場景是不是就要實體化兩個單例模式的物件呢,

有相關經驗的讀者可能會想到,能不能把各個集群中相同的作業抽取出來,做成一個類似網關的集群,然后各個集群再來呼叫這個公用集群,這樣同一個作業也不會被做多遍,RPC 的呼叫方也被整合成了一個,這個方案也是很好的,考慮到一些客觀因素,最終并沒有選擇這種方式,

實體化兩個單例類

我們假設下述單例類代碼是 RPC 的呼叫 Client:

public class RPCClient {
  	private static BaseClient baseClient;
    private volatile static RPCClient instance;
  
  	static {
        baseClient = BaseClient.getBaseClient();
    }
  
    private RPCClient() {
       System.out.println("構造 Client");
    }
    public String callRpc() {
        return "callRpc success";
    }
    public static RPCClient getClient() {
        if (instance == null) {
            synchronized (RPCClient.class) {
                if (instance == null) {
                    instance = new RPCClient();
                }
            }
        }
        return instance;
    }
}
public class BaseClient {
  ...
  private BaseClient() {
      System.out.println("構造 BaseClient");
  }
  ...
}

這個單例 Client 有一點點不同,就是有一個靜態屬性 baseClient,BaseClient 也是一個簡單的單例類,構造方法里有一些列印操作,方便后續觀察,baseClient 屬性通過靜態代碼塊來賦值,

我們可以想一想,有什么辦法可以將這個單例的 Client 類實體化兩個物件出來?

無所不能的反射大法

最容易想到的就是利用反射獲取構造方法,來規避單例類私有化構造方法的約束來實體化:

Constructor<?> declaredConstructor = RPCClient.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object rpcClient = declaredConstructor.newInstance();
Method sayHi = rpcClient.getClass().getMethod("callRpc");
Object invoke = sayHi.invoke(rpcClient);
//執行輸出
//構造 Client
//callBaseRpc successcallRpc success

上述代碼通過反射來獲取私有化的構造方法,然后通過這個構造方法來實體化物件,這樣確實能生成單例 RPCClient 的第二個物件,觀察代碼執行的輸出能發現,通過反射生成的這個物件 rpcClient 確實是一個新物件,因為輸出里有 RPCClient 構造方法的列印輸出,但是并沒有列印 BaseClient 這個物件的構造方法里的輸出,rpcClient 這個物件里的 baseClient 永遠都是只用一個,因為 baseClient 在靜態代碼塊里賦值的,并且 BaseClient 又是一個單例類,這樣,我們反射生成的物件與非反射生成的物件就不是完全隔離的,

上述的簡單 Demo 里,使用反射好像都不太能夠生成兩個完全隔離的單例客戶端,一個復雜的 RPC Client 類可遠沒有這么簡單,Client 類里還有很多依賴的類,依賴的類里也會依賴其他類,其中不乏各種單例類,通過反射的方法好像行不太通,那還有什么方法能達到目的呢?

自定義類加載器

另一個方法是用一個自定義的類加載器來加載 RPCClient 類并實體化,業務代碼默認使用的是 AppClassLoader 類加載器,這個類加載器來加載 RPCClient 類并實體化第一個 Client 物件,我們自定義的類加載器會加載并實體化第二個 Client 物件,那么在一個 JVM 行程里就存在了兩個 RPCClient 物件了,這兩個物件會不會存在上述反射中沒有完全隔離的問題呢?

答案是不會,類加載是有傳遞性的,當一個類被加載時,這個類依賴的類如果需要加載,使用的類加載器就是當前類的類加載器,我們使用自定義類加載器加載 RPCClient 時,RPCClient 依賴的類也會被自定義加載器加載,這樣依賴類也會被完全隔離,也就沒有在上述反射中存在的 baseClient 屬性還是同一個物件的情況,

自定義類加載器代碼如下:

public class MyClassLoader extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) {
      //通過 findLoadedClass 判斷是否已經被加載 (下文會補充)
      Class<?> loadedClass = findLoadedClass(name);
      //如果已加載回傳已加載的類
      if (loadedClass != null) {
          return loadedClass;
      }
      //通過類名獲取類檔案
      String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
      InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
      //如果查找不到檔案 則委托父類加載器實作 這里的父加載器就是 AppClassLoader 
      if (resourceAsStream == null) {
          return super.loadClass(name);
      }
      //讀取檔案 并加載類
      byte[] bytes = new byte[resourceAsStream.available()];
      resourceAsStream.read(bytes);
      return defineClass(name, bytes, 0, bytes.length);
   }
}

測驗代碼如下:

//實體化自定義類加載器
MyClassLoader myClassLoader = new MyClassLoader();
//獲取當前執行緒的 ContextClassLoader 備用
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//設定當前執行緒的 ContextClassLoader 為實體化的自定義類加載器(這么做的原因下文會補充)
Thread.currentThread().setContextClassLoader(myClassLoader);
//通過自定義類加載器加載 RPCClient
Class<?> rpcClientCls = myClassLoader.loadClass("com.ppphuang.demo.classloader.single.RPCClient");
//將當前執行緒的 ContextClassLoader 還原為初始的 contextClassLoader
Thread.currentThread().setContextClassLoader(contextClassLoader);
//通過反射獲取該類的 getClient 方法
Method getInstance = rpcClientCls.getMethod("getClient");
getInstance.setAccessible(true);
//呼叫 getClient 方法獲取單例物件
Object rpcClient = getInstance.invoke(rpcClientCls);
//獲取 callRpc 方法
Method callRpc = rpcClientCls.getMethod("callRpc");
//呼叫 callRpc 方法
Object callRpcMsg = callRpc.invoke(rpcClient);
System.out.println(callRpcMsg);
//執行輸出
//構造 BaseClient
//構造 Client
//callBaseRpc successcallRpc success

通過測驗代碼的輸出可以看到,RPCClient BaseClient 這兩個類構造方法里的列印都輸出了,那就說明通過自定義類加載器實體化的兩個物件都執行了構造方法,自然就跟直接呼叫 RPCClient.getClient() 生成的物件是完全隔離開的,

你可以通過代碼注釋,來理解一下測驗代碼的執行程序,

如果看到這里你還有一些疑問的話,我們再鞏固一下類加載器相關的知識,

類與類加載器

默認類加載

在 Java 中有三個默認的類加載器:

BootstrapClassLoader

加載 Java 核心庫(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路徑下的內容),用于提供 JVM 自身需要的類,由 C++ 加載,用如下代碼去獲取的話會顯示為 null:

System.out.println(String.class.getClassLoader());
ExtClassLoader

Java 語言撰寫,從 java.ext.dirs 系統屬性所指定的目錄中加載類,或從 JDK 的安裝目錄 jre/lib/ext 子目錄下加載類,如果用戶創建 的 jar 放在此目錄下,也會自動由 ExtClassLoader 加載,

System.out.println(com.sun.crypto.provider.DESedeKeyFactory.class.getClassLoader());
AppClassLoader

它負責加載環境變數 classpath 或系統屬性 java.class.path 指定路徑下的類,應用程式中默認是系統類加載器,

System.out.println(ClassLoader.getSystemClassLoader());

如果我們沒有特殊指定類加載器的話,JVM 行程中所有需要的類都會由上述三個類加載來完成加載,

每個 Class 物件的內部都有一個 classLoader 欄位來標識自己是由哪個 ClassLoader 加載的:

class Class<T> {
  private final ClassLoader classLoader;
}

你可以這樣來獲取某個類的 ClassLoader:

System.out.println(obj.getClass().getClassLoader());

不同類加載器的影響

兩個類相同的前提是類的加載器也相同,不同類加載器加載同一個 Class 也是不一樣的 Class,會影響 equals、instanceof 的運算結果,

下面的代碼展示了不同類加載器對類判等的影響,為了減少代碼篇幅,代碼省略了例外處理:

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) {
                Class<?> loadedClass = findLoadedClass(name);
                if (loadedClass != null) return loadedClass;
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
                if (resourceAsStream == null) {
                    return super.loadClass(name);
                }
                byte[] bytes = new byte[resourceAsStream.available()];
                resourceAsStream.read(bytes);
                return defineClass(name, bytes, 0, bytes.length);
            }
        };
        Object obj = myClassLoader.loadClass("ClassLoaderTest").newInstance();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(com.ppphuang.demo.classloader.ClassLoaderTest.class.getClassLoader());
        System.out.println(obj instanceof ClassLoaderTest);
    }
}
//輸出如下:
//com.ppphuang.demo.classloader.ClassLoaderTest$1@7a07c5b4
//sun.misc.Launcher$AppClassLoader@18b4aac2
//false

上述代碼自定義了一個類加載器 myClassLoader,用 myClassLoader 加載的 ClassLoaderTest 類實體化出的物件與 AppClassLoader 加載的 ClassLoaderTest 類做 instanceof 運算,最終輸出的介面是 false,由此可以判斷出不同加載器加載同一個類,這兩個類也是不相同的,

因為不同類加載器的加載的類是不同的,所以我們可以在一個 JVM 里通過自定義類加載器來將一個單例類實體化兩次,

ClassLoader 傳遞性

程式在運行程序中,遇到了一個未知的類,它會選擇哪個 ClassLoader 來加載它呢?

虛擬機的策略是使用呼叫者 Class 物件的 ClassLoader 來加載當前未知的類,就是在遇到這個未知的類時,虛擬機肯定正在運行一個方法呼叫(靜態方法或者實體方法),這個方法寫在哪個類,那這個類就是呼叫者 Class 物件,前面我們提到每個 Class 物件里面都有一個 classLoader 屬性記錄了當前的類是由誰來加載的,

因為 ClassLoader 的傳遞性,所有延遲加載的類都會由初始呼叫 main 方法的這個 ClassLoader 全權負責,它就是 AppClassLoader,

ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest.getClass().getClassLoader());
//sun.misc.Launcher$AppClassLoader@18b4aac2

如果我們使用一個自定義類加載器加載一個類,那么這個類里依賴的類也會由這個類加載來負責加載:

Object obj = myClassLoader.loadClass("com.ppphuang.demo.classloader.ClassLoaderTest").newInstance();

因為類加載器的傳遞性,依賴類的加載器也會使用當前類的加載器,當我們利用自定義類加載器來將一個單例類實體化兩次的時候,能保證兩個單例物件是完全隔離,

雙親委派模型

當一個類加載器需要加載一個類時,自己并不會立即去加載,而是首先委派給父類加載器去加載,父類加載器加載不了再給父類的父類去加載,一層一層向上委托,直到頂層加載器(BootstrapClassLoader),如果父類加載器無法加載那么類加器才會自己去加載,

findLoadedClass

當一個類被父加載器加載了,子加載器再次加載這個類的時候,還需要向父加載器委托嗎?

我們先把問題細化一下:

  1. AClassLoader 的父加載器為 BClassLoader,BClassLoader 的父加載器為 CClassLoader,當 AClassLoader 呼叫 loadClass() 加載類,并最終由 CClassLoader 加載的類,到底算誰加載的?

  2. 后續 AClassLoader 再加載相同類時,是否能直接從 AClassLoader 的 findLoadedClass0() 中找到該類并回傳,還是說再走一次雙親委派最終從 CClassLoader 的 findLoadedClass0() 中找到該類并回傳?

JVM 里有一個資料結構叫做 SystemDictonary,這個結構主要就是用來檢索我們常說的類資訊,其實也就是 private native final Class<?> findLoadedClass0(String name) 方法的邏輯,

這些類資訊對應的結構是 klass,對 SystemDictonary 的理解,可以理解為一個哈希表,key 是類加載器物件 + 類的名字,value是指向 klass 的地址,當我們任意一個類加載器去正常加載類的時候,就會到這個 SystemDictonary 中去查找,看是否有這么一個 klass 可以回傳,如果有就回傳它,否則就會去創建一個新的并放到結構里,

這里面還涉及兩個小概念,初始類加載器、定義類加載器,

上述類加載問題中,AClassLoader 加載類的時候會委托給 BClassLoader 來加載,BClassLoader 加載類的時候會委托給 CClassLoader 來加載,當 AClassLoader 呼叫 loadClass() 加載類,并最終由 CClassLoader 加載,那么我們稱 CClassLoader 為該類的定義類加載器,AClassLoader 和 BClassLoader 為該類的初始類加載器,在這個程序中,AClassLoader、BClassLoader 和 CClassLoader 都會在 SystemDictonary 生成記錄,那么后續 C 的子加載器(AClassLoader 和 BClassLoader)加載相同類時,就能在自己 findLoadedClass0() 中找到該類,不必再向上委托,

雙親委派的目的

  1. 防止重復加載類,在 JVM 中,要唯一確定一個物件,是由類加載器和全類名兩者共同確定的,考慮到各層級的類加載器之間仍然由重疊的類資源加載區域,通過向上拋的方式可以避免一個類被多個不同的類加載器加載,從而形成重復加載,

  2. 防止系統 API 被篡改,例如讀者定義了一個名為 java.lang.Integer 的類,而該類在核心庫中也存在,借用雙親委派的機制,我們就能有效防止該自定義的同名類被加載,從而保護了平臺的安全性,

JDK 1.2 之后引入雙親委派的方式來實作類加載器的層次呼叫,以盡可能保證 JDK 的系統 API 不會被用戶定義的類加載器所破壞,但一些使用場景會打破這個慣例來實作必要的功能,

破壞雙親委派模型

Thread Context ClassLoader

在介紹破壞雙親委派模型之前,我們先了解一下 Thread Context ClassLoader(執行緒背景關系類加載器),

JVM 中經常需要呼叫由其他廠商實作并部署在應用程式的 ClassPath 下的 JNDI 服務提供者介面 (Servicepovider iotertace, SPD) 的代碼,現在問題來了,啟動類加載器是絕不可能認識、加載這些代碼的,那該怎么辦?
為了解決這個困境,Java 的設計團隊只好引入了一個不太優雅的設計:執行緒背景關系類加裁器 ( Thread Context ClassLoader),這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoader 方法進行設定,如果創建執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域范圍內都沒有設定過的話,那這個類加載器默認就是 AppClassLoader,
有了執行緒背景關系類加載器,程式就可以做一些 “舞弊”的事情了,JNDI 服務使用這個執行緒背景關系類加載器去加載所需的 SPI 服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則,但也是無可奈何的事情,Java 中涉及 SPI 的加載基本上都采用這種方式來完成的,

可以通過如下的代碼來獲取當前執行緒的 ContextClassLoader :

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

我們在前面測驗代碼中將 Thread Context ClassLoader 也設定為自定義加載器,目的是避免自定義加載器加載的類里面使用了 Thread Context ClassLoader(默認是 AppClassLoader),導致物件沒有完全完全隔離,這也是自定義加載器的常用原則之一,在自定義加載器加載完成之后也要將 Thread Context ClassLoader 復原:

//實體化自定義類加載器
MyClassLoader myClassLoader = new MyClassLoader();
//獲取當前執行緒的 ContextClassLoader 備用
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//設定當前執行緒的 ContextClassLoader 為實體化的自定義類加載器(這么做的原因下文會補充)
Thread.currentThread().setContextClassLoader(myClassLoader);
//通過自定義類加載器加載 RPCClient
Class<?> rpcClientCls = myClassLoader.loadClass("com.ppphuang.demo.classloader.single.RPCClient");
//將當前執行緒的 ContextClassLoader 還原為初始的 contextClassLoader
Thread.currentThread().setContextClassLoader(contextClassLoader);

Tomcat類加載模型

提到破壞雙親委派模型就必須要提到 Tomcat,部署在一個 Tomcat 中的每個應用程式都會有一個獨一無二的 webapp classloader,他們互相隔離不受彼此的影響,除了互相隔離的類加載器,Tomcat 中還有共享的類加載器,大家可以去查看一下相關的檔案,還是很值得我們借鑒學習的,

看到這里再回頭來理解上文自定義類加載器實體化單例類的代碼,應該就很好理解了,

總結

本文通過如何將一個單例類實體化兩次的案例,用代碼實踐來引入 Java 類加載器相關的概念與作業機制,理解并熟練掌握相關知識之后可以擴寬解決問題的思路,另辟蹊徑,達到目的,

參考

https://blog.csdn.net/qq_43369986/article/details/117048340

https://blog.csdn.net/qq_40378034/article/details/119973663

https://blog.csdn.net/J080624/article/details/84835493

公眾號:DailyHappy 一位后端寫碼師,一位黑暗料理制造者,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552467.html

標籤:Java

上一篇:SpringBoot 使用 Sa-Token 完成注解鑒權功能

下一篇:返回列表

標籤雲
其他(159041) Python(38129) JavaScript(25421) Java(18040) C(15226) 區塊鏈(8265) C#(7972) AI(7469) 爪哇(7425) MySQL(7186) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4572) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1973) 功能(1967) Web開發(1951) HtmlCss(1936) python-3.x(1918) C++(1917) 弹簧靴(1913) xml(1889) PostgreSQL(1876) .NETCore(1860) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 不是單例的單例——巧用ClassLoader

    本文通過如何將一個單例類實體化兩次的案例,用代碼實踐來引入 Java 類加載器相關的概念與作業機制。理解并熟練掌握相關知識之后可以擴寬解決問題的思路,另辟蹊徑,達到目的。 ......

    uj5u.com 2023-05-16 09:40:16 more
  • SpringBoot 使用 Sa-Token 完成注解鑒權功能

    注解鑒權 —— 優雅的將鑒權與業務代碼分離。本篇我們將介紹在 Sa-Token 中如何通過注解完成權限校驗。 Sa-Token 是一個輕量級 java 權限認證框架,主要解決登錄認證、權限認證、單點登錄、OAuth2、微服務網關鑒權 等一系列權限相關問題。 Gitee 開源地址:https://gi ......

    uj5u.com 2023-05-16 09:40:08 more
  • 從3s到25ms!看看京東的介面優化技巧,確實很優雅!!

    大家好,最近看到京東云的一位大佬分享的介面優化方案,感覺挺不錯的,拿來即用。建議收藏一波或者整理到自己的筆記本中,隨時查閱! 來源:https://toutiao.io/posts/0kwkbbt 下面是正文。 一、背景 針對老專案,去年做了許多降本增效的事情,其中發現最多的就是介面耗時過長的問題, ......

    uj5u.com 2023-05-16 09:39:57 more
  • Bigdecimal使用

    ####1.Bigdecimal回傳資料小數后0自動被洗掉的問題 import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fas ......

    uj5u.com 2023-05-16 09:39:49 more
  • java~"與運算"實作保留一個數的低8位

    int型別另外介紹 int型別的封裝型別是Integer型別,它是有符號的型別,即它有負數和正數兩部分,最小為-2^32,最大值是2^32-1。 int型別由32位二進制陣列成,每4位二進制數表示為1位16進制數,每8位2進制數占用存盤空間為1位元組、即每2位16進制也占用1位元組 一個int型別的數字 ......

    uj5u.com 2023-05-16 09:34:03 more
  • 聊一聊模板方法模式

    模板方法模式,又叫模板模式,屬于23種設計模式中的行為型模式。在抽象類中公開定義了執行的方法,子類可以按需重寫其方法,但是要以抽象類中定義的方式呼叫方法。 ......

    uj5u.com 2023-05-16 09:11:15 more
  • 【設計模式】使用 go 語言實作簡單工廠模式

    最近在看《大話設計模式》,這本書通過對話形式講解設計模式的使用場景,有興趣的可以去看一下。 第一篇講的是簡單工廠模式,要求輸入兩個數和運算子號,得到運行結果。 這個需求不難,難就難在類要怎么設計,才能達到可復用、維護性強、可拓展和靈活性高。 運算子可能是加、減、乘、除,未了方便以后可以拓展其它運算子 ......

    uj5u.com 2023-05-16 09:05:56 more
  • Windows平臺下的Go版本切換工具-g

    voidint/g g 是一個 Linux、macOS、Windows 下的命令列工具,可以提供一個便捷的多版本 go 環境的管理和切換。 在這里我們介紹一下在 windows 下的使用,涉及到我們開發所需要用到的 幾個 go 專案層環境變數它們分別是 GOPATH,GOPROXY,GO111MOD ......

    uj5u.com 2023-05-16 09:05:52 more
  • Golang基礎教程

    Golang基礎學習 以下使用goland的IDE演示,包含總計的golang基礎功能共20個章節 一、go語言結構: 二、go基礎語法: 三、變數: 四、常量: 五、運算子: 六、條件陳述句: 七、回圈: 八、函式: 九、變數作用域: 十、陣列: 十一、指標: 十二、結構體: 十三、切片: 十四、范 ......

    uj5u.com 2023-05-16 09:05:48 more
  • 【C++】初始化串列建構式VS普通建構式

    普通建構式VS初始化串列建構式 初始化串列建構式最優先匹配問題 對于一個類而言,只要其中包含有初始化串列的建構式,編譯器在編譯使用{}語法的構造時會最傾向于呼叫初始化串列建構式,哪怕做型別轉換也在所不惜,哪怕有型別最佳匹配的普通建構式或移動建構式也會被劫持 class Widget { ......

    uj5u.com 2023-05-16 09:05:12 more