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

Java兩種動(dòng)態(tài)代理JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理詳解

 更新時(shí)間:2023年11月28日 10:08:23   作者:ZhaoJuFei  
這篇文章主要介紹了Java兩種動(dòng)態(tài)代理JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理詳解,代理模式是23種設(shè)計(jì)模式的一種,他是指一個(gè)對(duì)象A通過(guò)持有另一個(gè)對(duì)象B,可以具有B同樣的行為的模式,為了對(duì)外開(kāi)放協(xié)議,B往往實(shí)現(xiàn)了一個(gè)接口,A也會(huì)去實(shí)現(xiàn)接口,需要的朋友可以參考下

代理模式

代理模式是23種設(shè)計(jì)模式的一種,他是指一個(gè)對(duì)象A通過(guò)持有另一個(gè)對(duì)象B,可以具有B同樣的行為的模式。為了對(duì)外開(kāi)放協(xié)議,B往往實(shí)現(xiàn)了一個(gè)接口,A也會(huì)去實(shí)現(xiàn)接口。但是B是“真正”實(shí)現(xiàn)類(lèi),A則比較“虛”,他借用了B的方法去實(shí)現(xiàn)接口的方法。A雖然是“偽軍”,但它可以增強(qiáng)B,在調(diào)用B的方法前后都做些其他的事情。Spring AOP就是使用了動(dòng)態(tài)代理完成了代碼的動(dòng)態(tài)“織入”。

使用代理好處還不止這些,一個(gè)工程如果依賴(lài)另一個(gè)工程給的接口,但是另一個(gè)工程的接口不穩(wěn)定,經(jīng)常變更協(xié)議,就可以使用一個(gè)代理,接口變更時(shí),只需要修改代理,不需要一一修改業(yè)務(wù)代碼。從這個(gè)意義上說(shuō),所有調(diào)外界的接口,我們都可以這么做,不讓外界的代碼對(duì)我們的代碼有侵入,這叫防御式編程。代理其他的應(yīng)用可能還有很多。

上述例子中,類(lèi)A寫(xiě)死持有B,就是B的靜態(tài)代理。如果A代理的對(duì)象是不確定的,就是動(dòng)態(tài)代理。動(dòng)態(tài)代理目前有兩種常見(jiàn)的實(shí)現(xiàn),jdk動(dòng)態(tài)代理和cglib動(dòng)態(tài)代理。

JDK動(dòng)態(tài)代理

jdk動(dòng)態(tài)代理是jre提供給我們的類(lèi)庫(kù),可以直接使用,不依賴(lài)第三方。先看下jdk動(dòng)態(tài)代理的使用代碼,再理解原理。

首先有個(gè)“明星”接口類(lèi),有唱、跳兩個(gè)功能:

package proxy;
 
public interface Star
{
    String sing(String name);
    
    String dance(String name);
}

再有個(gè)明星實(shí)現(xiàn)類(lèi)“劉德華”:

package proxy;
 
public class LiuDeHua implements Star
{   
    @Override
    public String sing(String name)
    {
         System.out.println("給我一杯忘情水");
 
        return "唱完" ;
    }
    
    @Override
    public String dance(String name)
    {
        System.out.println("開(kāi)心的馬騮");
 
        return "跳完" ;
    }
}

明星演出前需要有人收錢(qián),由于要準(zhǔn)備演出,自己不做這個(gè)工作,一般交給一個(gè)經(jīng)紀(jì)人。便于理解,它的名字以Proxy結(jié)尾,但他不是代理類(lèi),原因是它沒(méi)有實(shí)現(xiàn)我們的明星接口,無(wú)法對(duì)外服務(wù),它僅僅是一個(gè)wrapper。

package proxy;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
public class StarProxy implements InvocationHandler
{
    // 目標(biāo)類(lèi),也就是被代理對(duì)象
    private Object target;
    
    public void setTarget(Object target)
    {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        // 這里可以做增強(qiáng)
        System.out.println("收錢(qián)");
        
        Object result = method.invoke(target, args);
        
        return result;
    }
    
    // 生成代理類(lèi)
    public Object CreatProxyedObj()
    {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }  
   
}

上述例子中,方法CreatProxyedObj返回的對(duì)象才是我們的代理類(lèi),它需要三個(gè)參數(shù),前兩個(gè)參數(shù)的意思是在同一個(gè)classloader下通過(guò)接口創(chuàng)建出一個(gè)對(duì)象,該對(duì)象需要一個(gè)屬性,也就是第三個(gè)參數(shù),它是一個(gè)InvocationHandler。需要注意的是這個(gè)CreatProxyedObj方法不一定非得在我們的StarProxy類(lèi)中,往往放在一個(gè)工廠類(lèi)中。上述代理的代碼使用過(guò)程一般如下:

1、new一個(gè)目標(biāo)對(duì)象

2、new一個(gè)InvocationHandler,將目標(biāo)對(duì)象set進(jìn)去

3、通過(guò)CreatProxyedObj創(chuàng)建代理對(duì)象,強(qiáng)轉(zhuǎn)為目標(biāo)對(duì)象的接口類(lèi)型即可使用,實(shí)際上生成的代理對(duì)象實(shí)現(xiàn)了目標(biāo)接口。

        Star ldh = new LiuDeHua();
 
        StarProxy proxy = new StarProxy();
 
        proxy.setTarget(ldh); 
  
        Object obj = proxy.CreatProxyedObj();
        
        Star star = (Star)obj;

Proxy(jdk類(lèi)庫(kù)提供)根據(jù)B的接口生成一個(gè)實(shí)現(xiàn)類(lèi),我們成為C,它就是動(dòng)態(tài)代理類(lèi)(該類(lèi)型是 $Proxy+數(shù)字 的“新的類(lèi)型”)。生成過(guò)程是:由于拿到了接口,便可以獲知接口的所有信息(主要是方法的定義),也就能聲明一個(gè)新的類(lèi)型去實(shí)現(xiàn)該接口的所有方法,這些方法顯然都是“虛”的,它調(diào)用另一個(gè)對(duì)象的方法。當(dāng)然這個(gè)被調(diào)用的對(duì)象不能是對(duì)象B,如果是對(duì)象B,我們就沒(méi)法增強(qiáng)了,等于饒了一圈又回來(lái)了。

所以它調(diào)用的是B的包裝類(lèi),這個(gè)包裝類(lèi)需要我們來(lái)實(shí)現(xiàn),但是jdk給出了約束,它必須實(shí)現(xiàn)InvocationHandler,上述例子中就是StarProxy, 這個(gè)接口里面有個(gè)方法,它是所有Target的所有方法的調(diào)用入口(invoke),調(diào)用之前我們可以加自己的代碼增強(qiáng)。

看下我們的實(shí)現(xiàn),我們?cè)贗nvocationHandler里調(diào)用了對(duì)象B(target)的方法,調(diào)用之前增強(qiáng)了B的方法。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        // 這里增強(qiáng)
        System.out.println("收錢(qián)");
        
        Object result = method.invoke(target, args);
        
        return result;
    }

所以可以這么認(rèn)為C代理了InvocationHandler,InvocationHandler代理了我們的類(lèi)B,兩級(jí)代理。

整個(gè)JDK動(dòng)態(tài)代理的秘密也就這些,簡(jiǎn)單一句話,動(dòng)態(tài)代理就是要生成一個(gè)包裝類(lèi)對(duì)象,由于代理的對(duì)象是動(dòng)態(tài)的,所以叫動(dòng)態(tài)代理。由于我們需要增強(qiáng),這個(gè)增強(qiáng)是需要留給開(kāi)發(fā)人員開(kāi)發(fā)代碼的,因此代理類(lèi)不能直接包含被代理對(duì)象,而是一個(gè)InvocationHandler,該InvocationHandler包含被代理對(duì)象,并負(fù)責(zé)分發(fā)請(qǐng)求給被代理對(duì)象,分發(fā)前后均可以做增強(qiáng)。從原理可以看出,JDK動(dòng)態(tài)代理是“對(duì)象”的代理。

下面看下動(dòng)態(tài)代理類(lèi)到底如何調(diào)用的InvocationHandler的,為什么InvocationHandler的一個(gè)invoke方法能為分發(fā)target的所有方法。C中的部分代碼示例如下,通過(guò)反編譯生成后的代碼查看,摘自鏈接地址。Proxy創(chuàng)造的C是自己(Proxy)的子類(lèi),且實(shí)現(xiàn)了B的接口,一般都是這么修飾的:

public final class XXX extends Proxy implements XXX

一個(gè)方法代碼如下:

 
  public final String SayHello(String paramString)
  {
    try
    {
      return (String)this.h.invoke(this, m4, new Object[] { paramString });
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }

可以看到,C中的方法全部通過(guò)調(diào)用h實(shí)現(xiàn),其中h就是InvocationHandler,是我們?cè)谏蒀時(shí)傳遞的第三個(gè)參數(shù)。這里還有個(gè)關(guān)鍵就是SayHello方法(業(yè)務(wù)方法)跟調(diào)用invoke方法時(shí)傳遞的參數(shù)m4一定要是一一對(duì)應(yīng)的,但是這些對(duì)我們來(lái)說(shuō)都是透明的,由Proxy在newProxyInstance時(shí)保證的。留心看到C在invoke時(shí)把自己this傳遞了過(guò)去,InvocationHandler的invoke的第一個(gè)方法也就是我們的動(dòng)態(tài)代理實(shí)例類(lèi),業(yè)務(wù)上有需要就可以使用它。(所以千萬(wàn)不要在invoke方法里把請(qǐng)求分發(fā)給第一個(gè)參數(shù),否則很明顯就死循環(huán)了)

C類(lèi)中有B中所有方法的成員變量

  private static Method m1;
  private static Method m3;
  private static Method m4;
  private static Method m2;
  private static Method m0;

這些變量在static靜態(tài)代碼塊初始化,這些變量是在調(diào)用invocationhander時(shí)必要的入?yún)?,也讓我們依稀看到Proxy在生成C時(shí)留下的痕跡。

static
  {
    try
    {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("jiankunking.Subject").getMethod("SayGoodBye", new Class[0]);
      m4 = Class.forName("jiankunking.Subject").getMethod("SayHello", new Class[] { Class.forName("java.lang.String") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }

從以上分析來(lái)看,要想徹底理解一個(gè)東西,再多的理論不如看源碼,底層的原理非常重要。

jdk動(dòng)態(tài)代理類(lèi)圖如下

cglib動(dòng)態(tài)代理

我們了解到,“代理”的目的是構(gòu)造一個(gè)和被代理的對(duì)象有同樣行為的對(duì)象,一個(gè)對(duì)象的行為是在類(lèi)中定義的,對(duì)象只是類(lèi)的實(shí)例。所以構(gòu)造代理,不一定非得通過(guò)持有、包裝對(duì)象這一種方式。

通過(guò)“繼承”可以繼承父類(lèi)所有的公開(kāi)方法,然后可以重寫(xiě)這些方法,在重寫(xiě)時(shí)對(duì)這些方法增強(qiáng),這就是cglib的思想。根據(jù)里氏代換原則(LSP),父類(lèi)需要出現(xiàn)的地方,子類(lèi)可以出現(xiàn),所以cglib實(shí)現(xiàn)的代理也是可以被正常使用的。

先看下代碼

package proxy;
 
import java.lang.reflect.Method;
 
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
public class CglibProxy implements MethodInterceptor
{
    // 根據(jù)一個(gè)類(lèi)型產(chǎn)生代理類(lèi),此方法不要求一定放在MethodInterceptor中
    public Object CreatProxyedObj(Class<?> clazz)
    {
        Enhancer enhancer = new Enhancer();
        
        enhancer.setSuperclass(clazz);
        
        enhancer.setCallback(this);
        
        return enhancer.create();
    }
    
    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        // 這里增強(qiáng)
        System.out.println("收錢(qián)");
        
        return arg3.invokeSuper(arg0, arg2);
    } 
}

從代碼可以看出,它和jdk動(dòng)態(tài)代理有所不同,對(duì)外表現(xiàn)上看CreatProxyedObj,它只需要一個(gè)類(lèi)型clazz就可以產(chǎn)生一個(gè)代理對(duì)象, 所以說(shuō)是“類(lèi)的代理”,且創(chuàng)造的對(duì)象通過(guò)打印類(lèi)型發(fā)現(xiàn)也是一個(gè)新的類(lèi)型。不同于jdk動(dòng)態(tài)代理,jdk動(dòng)態(tài)代理要求對(duì)象必須實(shí)現(xiàn)接口(三個(gè)參數(shù)的第二個(gè)參數(shù)),cglib對(duì)此沒(méi)有要求。

cglib的原理是這樣,它生成一個(gè)繼承B的類(lèi)型C(代理類(lèi)),這個(gè)代理類(lèi)持有一個(gè)MethodInterceptor,我們setCallback時(shí)傳入的。 C重寫(xiě)所有B中的方法(方法名一致),然后在C中,構(gòu)建名叫“CGLIB”+“$父類(lèi)方法名$”的方法(下面叫cglib方法,所有非private的方法都會(huì)被構(gòu)建),方法體里只有一句話super.方法名(),可以簡(jiǎn)單的認(rèn)為保持了對(duì)父類(lèi)方法的一個(gè)引用,方便調(diào)用。

這樣的話,C中就有了重寫(xiě)方法、cglib方法、父類(lèi)方法(不可見(jiàn)),還有一個(gè)統(tǒng)一的攔截方法(增強(qiáng)方法intercept)。其中重寫(xiě)方法和cglib方法肯定是有映射關(guān)系的。

C的重寫(xiě)方法是外界調(diào)用的入口(LSP原則),它調(diào)用MethodInterceptor的intercept方法,調(diào)用時(shí)會(huì)傳遞四個(gè)參數(shù),第一個(gè)參數(shù)傳遞的是this,代表代理類(lèi)本身,第二個(gè)參數(shù)標(biāo)示攔截的方法,第三個(gè)參數(shù)是入?yún)?,第四個(gè)參數(shù)是cglib方法,intercept方法完成增強(qiáng)后,我們調(diào)用cglib方法間接調(diào)用父類(lèi)方法完成整個(gè)方法鏈的調(diào)用。

這里有個(gè)疑問(wèn)就是intercept的四個(gè)參數(shù),為什么我們使用的是arg3而不是arg1?

    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        System.out.println("收錢(qián)");
        
        return arg3.invokeSuper(arg0, arg2);
    }

 因?yàn)槿绻覀兺ㄟ^(guò)反射 arg1.invoke(arg0, ...)這種方式是無(wú)法調(diào)用到父類(lèi)的方法的,子類(lèi)有方法重寫(xiě),隱藏了父類(lèi)的方法,父類(lèi)的方法已經(jīng)不可見(jiàn),如果硬調(diào)arg1.invoke(arg0, ...)很明顯會(huì)死循環(huán)。

所以調(diào)用的是cglib開(kāi)頭的方法,但是,我們使用arg3也不是簡(jiǎn)單的invoke,而是用的invokeSuper方法,這是因?yàn)閏glib采用了fastclass機(jī)制,不僅巧妙的避開(kāi)了調(diào)不到父類(lèi)方法的問(wèn)題,還加速了方法的調(diào)用。

fastclass基本原理是,給每個(gè)方法編號(hào),通過(guò)編號(hào)找到方法執(zhí)行避免了通過(guò)反射調(diào)用。

對(duì)比JDK動(dòng)態(tài)代理,cglib依然需要一個(gè)第三者分發(fā)請(qǐng)求,只不過(guò)jdk動(dòng)態(tài)代理分發(fā)給了目標(biāo)對(duì)象,cglib最終分發(fā)給了自己,通過(guò)給method編號(hào)完成調(diào)用。cglib是繼承的極致發(fā)揮,本身還是很簡(jiǎn)單的,只是fastclass需要另行理解。

測(cè)試

   public static void main(String[] args)
    {
        int times = 1000000;
        
        Star ldh = new LiuDeHua();
        StarProxy proxy = new StarProxy();
        proxy.setTarget(ldh);
        
        long time1 = System.currentTimeMillis();
        Star star = (Star)proxy.CreatProxyedObj();
        long time2 = System.currentTimeMillis();
        System.out.println("jdk創(chuàng)建時(shí)間:" + (time2 - time1));
        
        CglibProxy proxy2 = new CglibProxy();
        long time5 = System.currentTimeMillis();
        Star star2 = (Star)proxy2.CreatProxyedObj(LiuDeHua.class);
        long time6 = System.currentTimeMillis();
        System.out.println("cglib創(chuàng)建時(shí)間:" + (time6 - time5));
        
        long time3 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            star.sing("ss");
            
            star.dance("ss");
        }
        long time4 = System.currentTimeMillis();
        System.out.println("jdk執(zhí)行時(shí)間" + (time4 - time3));
        
        long time7 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            star2.sing("ss");
            
            star2.dance("ss");
        }
        
        long time8 = System.currentTimeMillis();
        
        System.out.println("cglib執(zhí)行時(shí)間" + (time8 - time7));   
    }

經(jīng)測(cè)試,jdk創(chuàng)建對(duì)象的速度遠(yuǎn)大于cglib,這是由于cglib創(chuàng)建對(duì)象時(shí)需要操作字節(jié)碼。cglib執(zhí)行速度略大于jdk,所以比較適合單例模式。另外由于CGLIB的大部分類(lèi)是直接對(duì)Java字節(jié)碼進(jìn)行操作,這樣生成的類(lèi)會(huì)在Java的永久堆中。如果動(dòng)態(tài)代理操作過(guò)多,容易造成永久堆滿,觸發(fā)OutOfMemory異常。spring默認(rèn)使用jdk動(dòng)態(tài)代理,如果類(lèi)沒(méi)有接口,則使用cglib。

到此這篇關(guān)于Java兩種動(dòng)態(tài)代理JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理詳解的文章就介紹到這了,更多相關(guān)Java動(dòng)態(tài)代理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java編程中使用lambda表達(dá)式的奇技淫巧

    Java編程中使用lambda表達(dá)式的奇技淫巧

    這篇文章主要介紹了Java編程中使用lambda表達(dá)式的奇技淫巧,使用Lambda表達(dá)式可以替代只有一個(gè)函數(shù)的接口實(shí)現(xiàn),告別匿名內(nèi)部類(lèi),代碼看起來(lái)更簡(jiǎn)潔易懂,是Java8開(kāi)始推出的人們期待已久的功能,需要的朋友可以參考下
    2016-03-03
  • 關(guān)于分布式鎖(Redisson)的原理分析

    關(guān)于分布式鎖(Redisson)的原理分析

    這篇文章主要介紹了關(guān)于分布式鎖(Redisson)的原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • IDEA無(wú)法識(shí)別SpringBoot項(xiàng)目的簡(jiǎn)單解決辦法

    IDEA無(wú)法識(shí)別SpringBoot項(xiàng)目的簡(jiǎn)單解決辦法

    今天使用idea的時(shí)候,遇到idea無(wú)法啟動(dòng)springboot,所以這篇文章主要給大家介紹了關(guān)于IDEA無(wú)法識(shí)別SpringBoot項(xiàng)目的簡(jiǎn)單解決辦法,需要的朋友可以參考下
    2023-08-08
  • SpringBoot如何配置CROS Filter

    SpringBoot如何配置CROS Filter

    這篇文章主要介紹了SpringBoot如何配置CROS Filter問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • 關(guān)于Java并發(fā)編程中線程間協(xié)作的兩種方式

    關(guān)于Java并發(fā)編程中線程間協(xié)作的兩種方式

    這篇文章主要介紹了關(guān)于Java并發(fā)編程中線程間協(xié)作的兩種方式,當(dāng)隊(duì)列滿時(shí),生產(chǎn)者需要等待隊(duì)列有空間才能繼續(xù)往里面放入商品,而在等待的期間內(nèi),生產(chǎn)者必須釋放對(duì)臨界資源的占用權(quán),這是消費(fèi)者模式,需要的朋友可以參考下
    2023-07-07
  • @Configuration與@Component作為配置類(lèi)的區(qū)別詳解

    @Configuration與@Component作為配置類(lèi)的區(qū)別詳解

    這篇文章主要介紹了@Configuration與@Component作為配置類(lèi)的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-06-06
  • java求整數(shù)的位數(shù)方式

    java求整數(shù)的位數(shù)方式

    這篇文章主要介紹了java求整數(shù)的位數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-01-01
  • SpringBoot @Scope與@RefreshScope注解使用詳解

    SpringBoot @Scope與@RefreshScope注解使用詳解

    spring的bean管理中,每個(gè)bean都有對(duì)應(yīng)的scope。在BeanDefinition中就已經(jīng)指定scope,默認(rèn)的RootBeanDefinition的scope是prototype類(lèi)型,使用@ComponentScan掃描出的BeanDefinition會(huì)指定是singleton,最常使用的也是singleton
    2022-11-11
  • Java并發(fā)編程之關(guān)鍵字volatile知識(shí)總結(jié)

    Java并發(fā)編程之關(guān)鍵字volatile知識(shí)總結(jié)

    今天帶大家學(xué)習(xí)java的相關(guān)知識(shí),文章圍繞著Java關(guān)鍵字volatile展開(kāi),文中有非常詳細(xì)的知識(shí)總結(jié),需要的朋友可以參考下
    2021-06-06
  • 為Java應(yīng)用創(chuàng)建Docker鏡像的3種方式總結(jié)

    為Java應(yīng)用創(chuàng)建Docker鏡像的3種方式總結(jié)

    Docker的使用可以將應(yīng)用程序做成鏡像,這樣可以將鏡像發(fā)布到私有或者公有倉(cāng)庫(kù)中,在其他主機(jī)上也可以pull鏡像,并且運(yùn)行容器,運(yùn)行程,下面這篇文章主要給大家總結(jié)介紹了關(guān)于為Java應(yīng)用創(chuàng)建Docker鏡像的3種方式,需要的朋友可以參考下
    2023-06-06

最新評(píng)論