Cglib invoke為什么會死循環?


Cglib invoke為什么會死循環?

案例分析

package com.demo;

public class UserService {

    public void addUser(){
        System.out.println("添加用戶");
    }
}

簡單介紹下UserService 模擬數據層操作,TxHelper作為一個cglib增強的回調.

package com.demo;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TxHelper implements MethodInterceptor {

    public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
        System.out.println("開啟事務");
        
        Object res=proxy.invokeSuper(obj,args);
//      這里調用invoke方法就會導致死循環從而棧溢出
//      Object res=proxy.invoke(obj,args);
        
        System.out.println("關閉事務");
        return res;
    }

    /*這個方法根據clazz使用空參構造器獲取clazz的cglib子類*/
    public Object getInstance(Class clazz){
        Enhancer enhancer=new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object getInstance2(Class clazz,Class params[],Object[] args){
        Enhancer enhancer=new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create(params,args);
    }
}

TxHelper中為了節省代碼量,將獲取Cglib生成的子類寫在TxHelper中,即getInstance(class)getInstance2(clazz,clazz[],Object[])方法,都是調用的Enhancer.create來獲取Cglib子類.

Cglib依賴添加

<dependencies>
      <dependency>
          <groupId>cglib</groupId>
          <artifactId>cglib</artifactId>
          <version>2.2</version>
      </dependency>

      <dependency>
          <groupId>asm</groupId>
          <artifactId>asm-util</artifactId>
          <version>3.1</version>
      </dependency>
  </dependencies>

依賴說明:cglib2.2版本只依賴於asm3.1,asm-util是asm的相關工具包,這里引入是另有目的.

測試方法

package com.demo;
import net.sf.cglib.core.DebuggingClassWriter;

public class UserServiceTests {

    public static void main(String[] args) {
        //設置cglib.debugLocation屬性指定將動態代理的類指定生成在哪里
        // 以下方式等價於  -Dcglib.debugLocation=E:\\data\\blog
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E:\\data\\blog");
        UserService userService= (UserService) new TxHelper().getInstance(UserService.class);
        userService.addUser();

    }
}

測試方法說明: System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY)是用於設置 cglib動態代理的子類生成的位置,等價於啟動參數 -Dcglib.debugLocation=E:\\data\\blog,這樣就可以將代理子類生成到我們指定目錄。 上面額外引入的依賴 asm-util則是會將動態生成的子類的字節碼展示出來。

public static final String DEBUG_LOCATION_PROPERTY = "cglib.debugLocation";

測試效果

​ 可以看到methodProxy.invokeSuper方法會調用父類的方法 成功添加父類,至於另外的情況最后再分析.

進入之前設置的cglib.debugLocation指定的目錄,查看cglib生成的子類class文件.

​ 在該目錄下的自己的包名文件夾com/demo下會有我們的UserService的cglib子類,額外的還有net/sf/cglib/core以及net/sf/cglib/proxy這兩個cglib額外生成的層級目錄,因為上面引入了 asm-util ,所以伴隨着class文件,還會有一些特殊的 asm 文件,asm 文件使用notepad++等工具就可以查看了,這些asm文件記錄着每一個類生成過程中字節碼.

查看cglib生成的 class文件

​ 可以看到我們的UserService類就已經生成了三個class文件, 其中UserService$$EnhancerByCGLIB$$268385a2可以理解為是 UserService的真實子類,而 UserService$$FastClassByCGLIB$$417ebd8cUserService$$EnhancerByCGLIB$$268385a2$$FastClassByCGLIB$$c6a21d27 則是計算動態代理類調用方法走父類還是本身的方法,這里后面也會發現CGLIB比反射效率高(我的理解,直接調用會比反射效率高).

查看 **asm ** 文件

​ 補充一點:class version 46.0代表 JDK1.2版本編譯的class文件,高版本編譯環境可編譯低版本編譯過來的Class文件,但是低版本的JDK1.7就不能編譯JDK1.8的class文件了,這時候嘗試編譯會報錯:Unsupported major version

CGLIB子類一探究竟

上面測試效果、CGLIB子類我們都獲取到了,甭管怎么生成的CGLIB子類,我們先來看看,CGLIB子類究竟長啥樣?

​ 通常會用 jd-gui 工具查看class文件,但是cglib生成的子類用 jd-gui 查看效果不是很好,不利於閱讀,查看方式:直接將class文件拖到 IDEA 中,就可以查看反編譯后的代碼.

​ 我不太習慣看 反編譯后的 class 文件,於是就將 class文件的內容復制到 com/demo目錄下,並且修改文件名為 .java結尾,但是class文件內容直接復制到java 文件中,會有好多處紅叉報錯:一一解決下. 這里改成java文件只是為了方便自己閱讀,不能用來調試.

動態代理子類的java文件

第一處報錯: final類型變量可能沒初始化,導致編譯不過;其實cglib是初始化了,但是為啥IDEA不認呢?

先說解決方法: 將final 關鍵字都去掉

其實cglib關於 final變量都是初始化了的,一個靜態代碼塊調用 CGLIB$STATICHOOK1,在靜態方法CGLIB$STATICHOOK1中進行了初始化,但是為啥IDEA不認呢?

 static {
        CGLIB$STATICHOOK1();
    }
 
static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("com.demo.UserService$$EnhancerByCGLIB$$268385a2");
        Class var1;
        Method[] var10000 = ReflectUtils.findMethods(new String[]{"addUser", "()V", "getUser", "()V"}, (var1 = Class.forName("com.demo.UserService")).getDeclaredMethods());
        CGLIB$addUser$0$Method = var10000[0];
        CGLIB$addUser$0$Proxy = MethodProxy.create(var1, var0, "()V", "addUser", "CGLIB$addUser$0");
        CGLIB$getUser$1$Method = var10000[1];
        CGLIB$getUser$1$Proxy = MethodProxy.create(var1, var0, "()V", "getUser", "CGLIB$getUser$1");
        var10000 = ReflectUtils.findMethods(new String[]{"finalize", "()V", "equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        CGLIB$finalize$2$Method = var10000[0];
        CGLIB$finalize$2$Proxy = MethodProxy.create(var1, var0, "()V", "finalize", "CGLIB$finalize$2");
        CGLIB$equals$3$Method = var10000[1];
        CGLIB$equals$3$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$3");
        CGLIB$toString$4$Method = var10000[2];
        CGLIB$toString$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$4");
        CGLIB$hashCode$5$Method = var10000[3];
        CGLIB$hashCode$5$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$5");
        CGLIB$clone$6$Method = var10000[4];
        CGLIB$clone$6$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$6");
    } 

第二處報錯:MethodInterceptor.intercept方法異常沒有捕獲

解決方案: 手動Try catch異常吧.

報錯三:new實例化對象缺少括號,下面多了字節碼<init>()

解決方案:上面添上括號,<init>()這行注釋掉

另外兩個 FastClass 類也是類似的操作,細心地發現:只有UserService$$EnhancerByCGLIB$$743464da是繼承了我們的UserService類的並且實現了Factory接口 ,另外兩個類都是繼承自FastClass.

動態代理子類實例化過程

上一步已經得到了cglib生成的動態代理類,動態代理類肯定要初始化一個實例對象,實例化的流程呢:AbstractClassGenerator.create--->Enhancer.firstInstance--->Enhancer.createUsingReflection

private Object createUsingReflection(Class type) {
        setThreadCallbacks(type, callbacks);
        try{
        
        if (argumentTypes != null) {
        	
             return ReflectUtils.newInstance(type, argumentTypes, arguments);
             
        } else {
        	
            return ReflectUtils.newInstance(type);
            
        }
        }finally{
         // clear thread callbacks to allow them to be gc'd
         setThreadCallbacks(type, null);
        }
    }

Enhancer.createUsingReflection首先setThreadCallbacks(type,callbacks)----->ReflectUtils.newInstance

type就是當前這個動態代理子類的class UserService$$EnhancerByCGLIB$$743464dacallbacks 就是我們自己設置到enhancer中的.

setThreadCallbacks就是調用動態代理的類的靜態CGLIB$SET_THREAD_CALLBACKS方法,將callbacks設置進入,查看動態代理類的靜態CGLIB$SET_THREAD_CALLBACKS方法:

 public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) {
        CGLIB$THREAD_CALLBACKS.set(var0);
    }
    
private static  ThreadLocal CGLIB$THREAD_CALLBACKS;

其中 CGLIB$THREAD_CALLBACKS本來是final static 變量,不過被我們強行改為了static,這里可以看到 動態代理的類有個靜態量ThreadLocal,持有回調數組callback,至於ThreadLocal 的初始化在 static 代碼塊中完成了.

ReflectUtils.newInstance方法會根據動態代理類可用的構造方法調用 反射來實例化 動態代理子類UserService$$EnhancerByCGLIB$$743464da . 而實例化完成之后,仍然調用CGLIB$THREAD_CALLBACKSThreadLocal變量中的callback清除.

UserService$$EnhancerByCGLIB$$743464da的實例化

public UserService$$EnhancerByCGLIB$$743464da() {
        CGLIB$BIND_CALLBACKS(this);
    }
    
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
        UserService$$EnhancerByCGLIB$$743464da var1 = (UserService$$EnhancerByCGLIB$$743464da)var0;
        if (!var1.CGLIB$BOUND) {
            var1.CGLIB$BOUND = true;
            Object var10000 = CGLIB$THREAD_CALLBACKS.get();
            if (var10000 == null) {
                var10000 = CGLIB$STATIC_CALLBACKS;
                if (CGLIB$STATIC_CALLBACKS == null) {
                    return;
                }
            }

            var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
        }

    }    

可以看到 動態代理類初始化調用了 CGLIB$BIND_CALLBACKS(this),先判斷CGLIB$BOUND標志位,第一次綁定之后就為true了,綁定過程就是 從 ThreadLocal 中提取出來callback,並賦給動態代理對象的CGLIB$CALLBACK_0屬性,回調屬性都是以CGLIB$CALLBACK_開頭,假如回調數組元素為多個,那就有多少個屬性,分別是CGLIB$CALLBACK_0CGLIB$CALLBACK_1.

總結下來:回調數組callback賦值給了動態代理類的每一個屬性,有幾個回調元素,就有幾個回調屬性;賦值過程是在實例化動態代理類時候完成的,為了防止線程不安全,用的是ThreadLocal來保存callback

動態代理類調用過程

現在UserService$$EnhancerByCGLIB$$743464da實例已經有了,調用addUser方法,會先進入子類的方法.

 public final void addUser() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }
        if (var10000 != null) {
            try {
                var10000.intercept(this, CGLIB$addUser$0$Method, CGLIB$emptyArgs, CGLIB$addUser$0$Proxy);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            super.addUser();
        }
    }

嘗試獲取CGLIB$CALLBACK_0屬性,為什么說嘗試呢?如果CGLIB$CALLBACK_0為空,還回去從 ThreadLocal中取,當獲取到CGLIB$CALLBACK_0不為空,調用CGLIB$CALLBACK_0.intercept方法.

四個入參:

  1. this,代表當前代理類的對象,沒啥好說的;
  2. CGLIB$addUser$0$Method 靜態代碼塊中初始化,獲取的是父類void UserService.addUser()方法,也就是父類方法的Method.
private final static  Method CGLIB$addUser$0$Method;

static{
     ..........
     Class var1=null;
        try {
            CGLIB$addUser$0$Method = ReflectUtils.findMethods(new String[]{"addUser", "()V"}, (var1 = Class.forName("com.demo.UserService")).getDeclaredMethods())[0];
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
      ............
}
  1. CGLIB$emptyArgs 一個大小為0的Object數組

  2. CGLIB$addUser$0$Proxy 通過MethodProxy.create(superClass,cglibSonClass,methodDescriptor,superMethodName,sonMethodName)來獲取MethodProxy對象.

    private static final MethodProxy CGLIB$addUser$0$Proxy;
    
    static{
         Class var0 = null;
            try {
                var0 = Class.forName("com.demo.UserService$$EnhancerByCGLIB$$743464da");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Class var1=null;
            try {
                CGLIB$addUser$0$Method = ReflectUtils.findMethods(new String[]{"addUser", "()V"}, (var1 = Class.forName("com.demo.UserService")).getDeclaredMethods())[0];
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            CGLIB$addUser$0$Proxy = MethodProxy.create(var1, var0, "()V", "addUser", "CGLIB$addUser$0");
    }
    

MethodProxy.create過程

public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
        MethodProxy proxy = new MethodProxy();
        proxy.sig1 = new Signature(name1, desc);
        proxy.sig2 = new Signature(name2, desc);
        proxy.createInfo = new CreateInfo(c1, c2);
        return proxy;
    }

解釋說明: c1是父類class,c2是cglib子類class,desc就是方法修飾符,比如()V代表無如參void類型,(Ljava/lang/String;)I代表String入參int返回值的方法,對象類型都是 L全限定名; 這種,基本數據類型int對應 I 這種,數組對應 [

name1是父類中方法的名稱,name2是子類中方法的名稱 ,比如addUser現在cglib子類想要直接調用父類的方法,不需要增強,那我調用CGLIB$addUser$0方法就等價於 直接調用父類的方法,調用方式如下:

意味着:cglib子類中的addUser方法就是增強的方法,而CGLIB$addUser$0就是未增強的方法,方法名的生成規則后續再記錄.

public static void main(String[] args) throws Exception {
        //設置cglib.debugLocation屬性指定將動態代理的類指定生成在哪里
        // 以下方式等價於  -Dcglib.debugLocation=E:\\data\\blog
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E:\\data\\blog");
        UserService userService= (UserService) new TxHelper().getInstance(UserService.class);
//        userService.addUser();
        for (final Method declaredMethod : userService.getClass().getDeclaredMethods()) {
            System.out.println(declaredMethod.getName());
        }
        Method m = userService.getClass().getDeclaredMethod("CGLIB$addUser$0", null);
        m.invoke(userService,null);

    }

回到MethodProxy.create中,生成了一個MethodProxy對象,sig1、sig2都是Signature類型的,代表方法簽名,sig1是父類方法的簽名,sig2是子類方法的簽名;而 createInfo屬性最主要的是持有 c1(父類class)、c2(cglib子類class).

至此我們已經分析完畢MethodProxy.create的邏輯.

MethodInterceptor.intercept調用邏輯

**MethodInterceptor ** 怎么來的、有什么屬性我們已經分析完畢,intercept方法就進入了我們自定義的回調類中,離我們分析的目標更近了.

​ 我們自定義回調邏輯中,知道proxy.invokeSuper方法才是正確的,而proxy.invoke會導致棧溢出. 現在我們已經知道這個intercept方法的幾個入參: obj 就是當前cglib子類實例,method就是父類UserService.adUser()Method,args就是方法的入參,proxy就是上面創建的MethodProxy對象。 cglib子類中每一個繼承父類的方法都會生成一個MethodProxy對應,額外還有Object類的toStringhashCodeequalsfinalizeclone方法

public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
        System.out.println("開啟事務");
        Object res=proxy.invokeSuper(obj,args);
//      這里invoke方法就會導致死循環從而棧溢出
//      Object res=proxy.invoke(obj,args);
        System.out.println("關閉事務");
        return res;
    }

動態代理之MethodProxy.invokeSuper

MethodProxy的 invokeSuper方法如下.

說明文檔上介紹:調用增強類(cglib子類)的父類的方法,obj對象就是增強的cglib子類.

第一步 init()方法

private void init()
    {
        if (fastClassInfo == null)
        {
            synchronized (initLock)
            {
                if (fastClassInfo == null)
                {
                    CreateInfo ci = createInfo;

                    FastClassInfo fci = new FastClassInfo();
                    fci.f1 = helper(ci, ci.c1);
                    fci.f2 = helper(ci, ci.c2);
                    fci.i1 = fci.f1.getIndex(sig1);
                    fci.i2 = fci.f2.getIndex(sig2);
                    fastClassInfo = fci;
                }
            }
        }
    }
  1. 雙重檢查鎖機制,一直以為是安全的,但是cglib注釋提到 這段代碼在JDK1.5之前可能會導致fastClassInfo實例化多個?所以fastClassInfo對象用了volatile關鍵字來修飾,咱也沒用過volatile關鍵字,這里就暫留作疑問吧.

  2. createInfo對象之前提到了,是在實例化MethodProxy中實例化的,持有兩個重要屬性父類c1、cglib子類c2helper方法另外生成了兩個FastClass的實例,f1就是UserService這種被UserService$$FastClassByCGLIB$$xxxxf2就是UserService$$EnhancerByCGLIB$$xxx$$FastClassByCGLIB$$xxx,生成過程采用ASM生成字節碼較為復雜就忽略,如果需要查看字節碼,引入asm-util並且指定cglib.debugLocation參數即可生成asm文件.

  3. 這里的fastClassInfo是單例的,又有點緩存的意思,很多初始化邏輯在第一次實例化過程中完成,后續進入就不需要init.

FAQ

init()方法中volatile修飾fastClassInfo的作用是啥?

CGLIB的FastClass實現類

​ 參考上面獲取cglib java類,我們可以看到指定的cglib.debugLocation指定包目錄下另外兩個class就是這里的FastClass

​ 以UserService的FastClass來看,主要實現了三類方法getIndexinvokenewInstance,先來看第一類方法getIndex

getIndex

Signature是cglib包中的類,代表一個方法的簽名. 根據Signature來獲取索引,索引代表要執行哪個方法. 首先判斷 方法名 + 方法返回值 的hash值,hash值一致的情況下,還需要再用 equals 方法再次比較。 因為兩個不同的字符串是有可能hash值相等的. 這樣確保獲取到的索引正確,索引用呢是在invoke方法中. 至於這個索引則是在 生成字節碼中判斷的.

invoke

​ 上面獲取的方法索引在這里就可以體現用處了,就是var1參數。 switch中的序號和上面 getIndex一定是一一對應的.
同理,cglib子類的getIndexinvoke方法都是類似的,只不過cglib子類的 FastClassgetIndex以及 invoke的選項會多一倍,因為一個是 繼承自父類,也就是增強的方法,還一個是重命名的父類方法,單純調用父類方法,比如CGLIB$toString$3()

init方法繼續

if (fastClassInfo == null)
        {
            synchronized (initLock)
            {
                if (fastClassInfo == null)
                {
                    CreateInfo ci = createInfo;

                    FastClassInfo fci = new FastClassInfo();
                    fci.f1 = helper(ci, ci.c1);
                    fci.f2 = helper(ci, ci.c2);
                    fci.i1 = fci.f1.getIndex(sig1);
                    fci.i2 = fci.f2.getIndex(sig2);
                    fastClassInfo = fci;
                }
            }
        }

FastClassInfo通過ASM生成了兩個FastClass子類之后,f1就是 UserService$$FastClassByCGLIB的實例,f2就是UserService$$EnhancerByCGLIB$$FastClass..的實例,sig1代表被增強方法的簽名,addUser()V這種,sig2代表別增強方法在cglib子類中的簽名,如CGLIB$addUser$0.

invokeSuper方法調用

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }

fci.i2就是CGLIB$addUser$0在其中的索引,比如我這里是15. obj對象是cglib的子類,invokeSuper就是調用這個cglib增強類的索引為15的方法,而繞回來這個索引又是通過 getIndex 獲取CGLIB$addUser$0獲取的,等於直接調用了 cglib增強子類的CGLIB$addUser$0方法,就是調用了父類的addUser方法. 這樣invokeSuper就實現了動態調用父類的方法,比如UserService有很多種方法,我現在是A方法,那里面的MethodProxy對象又是一個全新的關聯了A以及CGLIB$A$...方法,而我們只需要使用proxy.invokeSuper就能自動調用父類方法,這里繞開了反射,相應的在動態代理類、每個方法第一次調用會帶來一定的性能損耗,但是后續使用起來與普通調用無差別,會優於后續反射來調用方法.

final void CGLIB$addUser$0() {
        super.addUser();
    }

invoke方法調用

​ 相信這里我們就能大概明白,為啥invoke會帶來方法死循環.

public Object invoke(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            return fci.f1.invoke(fci.i1, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (IllegalArgumentException e) {
            if (fastClassInfo.i1 < 0)
                throw new IllegalArgumentException("Protected method: " + sig1);
            throw e;
        }
    }

​ 這里與invokeSuper的區別大概是,f2換成了f1,索引也換成了對應的i1,我們不饒一圈,就是直接調用了動態代理類的addUser方法. 嗯? 我們都增強了addUser方法,你還調用增強的方法,那不是又饒了一圈嘛,難怪會死循環,從而棧溢出.

CGLIB實現類如何不調用增強方法

​ 我常常在想假如我就不想增強某個方法,咋調用呢? 想了個笨的方法,反射找到那個直接調用父類的方法,反射執行行不?

public static void main(String[] args) throws Exception {
        //設置cglib.debugLocation屬性指定將動態代理的類指定生成在哪里
        // 以下方式等價於  -Dcglib.debugLocation=E:\\data\\blog
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E:\\data\\blog");
        UserService userService= (UserService) new TxHelper().getInstance(UserService.class);
//        userService.addUser();
        for (final Method declaredMethod : userService.getClass().getDeclaredMethods()) {
            System.out.println(declaredMethod.getName());
        }
        Method m = userService.getClass().getDeclaredMethod("CGLIB$addUser$0", null);
        m.setAccessible(true);
        m.invoke(userService,null);

    }

​ 查看輸出.......

想了想,知識有限,我們查看他所有的方法,還好對於CGLIB有一點點了解,找那個方法名類似CGLIB$XXX$數字的方法,反射調用試試唄, 雖然這種方法肯定存在很多弊端,奈何能耐有限呢?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM