• CGLib原理学习


    一、使用示例:

    public class UserDaoImpl implements UserDao {

    @Override
    public void addUser(User user) {
    System.out.println("connect to mySQL dataBase.......");
    System.out.println("添加用户信息成功...");
    }
    }
    public class LogInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    System.out.println("before inteceptor: " + method.getName());

    Object obj = methodProxy.invokeSuper(o, objects);

    System.out.println("after inteceptor: " + method.getName());

    return obj;
    }
    }
    public static void main(String[] args) {
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "e:\001\");
    
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(UserDaoImpl.class);
            enhancer.setCallback(new LogInterceptor());
            UserDaoImpl userDao = (UserDaoImpl) enhancer.create();
    
            User user = new User();        
            userDao.addUser(user);
    }

    二、原理分析:

    CGLib生成的代理类继承了委托类,注意如果委托类为final或者方法为final,则该委托类不能被代理;代理类会为委托方法生成两个方法,一个是重写的addUser方法,另一个是CGLIB$addUser$0方法;

    当执行代理对象的addUser方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,则将调用MethodInterceptor中的intercept方法:

    
    
    public class UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c extends UserDaoImpl implements Factory {
    private boolean CGLIB$BOUND;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static final Method CGLIB$addUser$0$Method;
    private static final MethodProxy CGLIB$addUser$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$finalize$1$Method;
    private static final MethodProxy CGLIB$finalize$1$Proxy;
    private static final Method CGLIB$equals$2$Method;
    private static final MethodProxy CGLIB$equals$2$Proxy;
    private static final Method CGLIB$toString$3$Method;
    private static final MethodProxy CGLIB$toString$3$Proxy;
    private static final Method CGLIB$hashCode$4$Method;
    private static final MethodProxy CGLIB$hashCode$4$Proxy;
    private static final Method CGLIB$clone$5$Method;
    private static final MethodProxy CGLIB$clone$5$Proxy;

    static void CGLIB$STATICHOOK2() {
    CGLIB$THREAD_CALLBACKS = new ThreadLocal();
    CGLIB$emptyArgs = new Object[0];
    Class var0 = Class.forName("com.ucar.test.service.Impl.UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c");
    Class var1;
    Method[] 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$1$Method = var10000[0];
    CGLIB$finalize$1$Proxy = MethodProxy.create(var1, var0, "()V", "finalize", "CGLIB$finalize$1");
    CGLIB$equals$2$Method = var10000[1];
    CGLIB$equals$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
    CGLIB$toString$3$Method = var10000[2];
    CGLIB$toString$3$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
    CGLIB$hashCode$4$Method = var10000[3];
    CGLIB$hashCode$4$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$4");
    CGLIB$clone$5$Method = var10000[4];
    CGLIB$clone$5$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
    CGLIB$addUser$0$Method = ReflectUtils.findMethods(new String[]{"addUser", "(Lcom/ucar/test/dto/User;)V"}, (var1 = Class.forName("com.ucar.test.service.Impl.UserDaoImpl")).getDeclaredMethods())[0];
    CGLIB$addUser$0$Proxy = MethodProxy.create(var1, var0, "(Lcom/ucar/test/dto/User;)V", "addUser", "CGLIB$addUser$0");
    }

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

    public final void addUser(User var1) {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) {
    var10000.intercept(this, CGLIB$addUser$0$Method, new Object[]{var1}, CGLIB$addUser$0$Proxy);
    } else {
    super.addUser(var1);
    }
    }

    final void CGLIB$finalize$1() throws Throwable {
    super.finalize();
    }

    protected final void finalize() throws Throwable {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) {
    var10000.intercept(this, CGLIB$finalize$1$Method, CGLIB$emptyArgs, CGLIB$finalize$1$Proxy);
    } else {
    super.finalize();
    }
    }

    final boolean CGLIB$equals$2(Object var1) {
    return super.equals(var1);
    }

    public final boolean equals(Object var1) {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) {
    Object var2 = var10000.intercept(this, CGLIB$equals$2$Method, new Object[]{var1}, CGLIB$equals$2$Proxy);
    return var2 == null ? false : (Boolean)var2;
    } else {
    return super.equals(var1);
    }
    }

    final String CGLIB$toString$3() {
    return super.toString();
    }

    public final String toString() {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    return var10000 != null ? (String)var10000.intercept(this, CGLIB$toString$3$Method, CGLIB$emptyArgs, CGLIB$toString$3$Proxy) : super.toString();
    }

    final int CGLIB$hashCode$4() {
    return super.hashCode();
    }

    public final int hashCode() {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) {
    Object var1 = var10000.intercept(this, CGLIB$hashCode$4$Method, CGLIB$emptyArgs, CGLIB$hashCode$4$Proxy);
    return var1 == null ? 0 : ((Number)var1).intValue();
    } else {
    return super.hashCode();
    }
    }

    final Object CGLIB$clone$5() throws CloneNotSupportedException {
    return super.clone();
    }

    protected final Object clone() throws CloneNotSupportedException {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
    CGLIB$BIND_CALLBACKS(this);
    var10000 = this.CGLIB$CALLBACK_0;
    }

    return var10000 != null ? var10000.intercept(this, CGLIB$clone$5$Method, CGLIB$emptyArgs, CGLIB$clone$5$Proxy) : super.clone();
    }

    public static MethodProxy CGLIB$findMethodProxy(Signature var0) {
    String var10000 = var0.toString();
    switch(var10000.hashCode()) {
    case -1574182249:
    if (var10000.equals("finalize()V")) {
    return CGLIB$finalize$1$Proxy;
    }
    break;
    case -508378822:
    if (var10000.equals("clone()Ljava/lang/Object;")) {
    return CGLIB$clone$5$Proxy;
    }
    break;
    case 1147247458:
    if (var10000.equals("addUser(Lcom/ucar/test/dto/User;)V")) {
    return CGLIB$addUser$0$Proxy;
    }
    break;
    case 1826985398:
    if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
    return CGLIB$equals$2$Proxy;
    }
    break;
    case 1913648695:
    if (var10000.equals("toString()Ljava/lang/String;")) {
    return CGLIB$toString$3$Proxy;
    }
    break;
    case 1984935277:
    if (var10000.equals("hashCode()I")) {
    return CGLIB$hashCode$4$Proxy;
    }
    }

    return null;
    }

    public UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c() {
    CGLIB$BIND_CALLBACKS(this);
    }

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

    public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) {
    CGLIB$STATIC_CALLBACKS = var0;
    }

    private static final void CGLIB$BIND_CALLBACKS(Object var0) {
    UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c var1 = (UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c)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];
    }

    }

    public Object newInstance(Callback[] var1) {
    CGLIB$SET_THREAD_CALLBACKS(var1);
    UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c var10000 = new UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c();
    CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
    return var10000;
    }

    public Object newInstance(Callback var1) {
    CGLIB$SET_THREAD_CALLBACKS(new Callback[]{var1});
    UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c var10000 = new UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c();
    CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
    return var10000;
    }

    public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) {
    CGLIB$SET_THREAD_CALLBACKS(var3);
    UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c var10000 = new UserDaoImpl$$EnhancerByCGLIB$$d79a8f3c;
    switch(var1.length) {
    case 0:
    var10000.<init>();
    CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
    return var10000;
    default:
    throw new IllegalArgumentException("Constructor not found");
    }
    }

    public Callback getCallback(int var1) {
    CGLIB$BIND_CALLBACKS(this);
    MethodInterceptor var10000;
    switch(var1) {
    case 0:
    var10000 = this.CGLIB$CALLBACK_0;
    break;
    default:
    var10000 = null;
    }

    return var10000;
    }

    public void setCallback(int var1, Callback var2) {
    switch(var1) {
    case 0:
    this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2;
    default:
    }
    }

    public Callback[] getCallbacks() {
    CGLIB$BIND_CALLBACKS(this);
    return new Callback[]{this.CGLIB$CALLBACK_0};
    }

    public void setCallbacks(Callback[] var1) {
    this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0];
    }

    static {
    CGLIB$STATICHOOK2();
    }
    }
    
    

    在我们的代理类中除了做些增强处理外,还会调用methodProxy.invokeSuper方法来调用委托类的方法;下面看看invokeSuper的代码:

    1 public Object invokeSuper(Object obj, Object[] args) throws Throwable {
    2         try {
    3             init();
    4             FastClassInfo fci = fastClassInfo;
    5             return fci.f2.invoke(fci.i2, obj, args);
    6         } catch (InvocationTargetException e) {
    7             throw e.getTargetException();
    8         }
    9     }

    在JDK动态代理中方法的调用是通过反射来完成的。但是在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:FastClass对Class对象进行特别的处理,比如将会用数组保存method的引用,每次调用方法的时候都是通过一个index下标来保持对方法的引用。比如下面的getIndex方法就是通过方法签名来获得方法在存储了Class信息的数组中的下标。

    1 private static class FastClassInfo
    2 {
    3         FastClass f1;
    4         FastClass f2;
    5         int i1;
    6         int i2;
    7 }

    其中,f1指向委托类对象,f2指向代理类对象,i1和i2分别代表着addUser方法以及CGLIB$addUser$0方法在对象信息数组中的下标。因此对委托对象的调用invoke实际上就是对FastClass的getIndex和invoke的调用,但是在FastClass中的这两个方法都是抽象方法,并没有实现,因此对其调用是在动态生成的FastClass的子类中实现的:

    1 abstract public int getIndex(String name, Class[] parameterTypes);
    2 abstract public Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException;
    
    
      1 public class DelegateClass$$FastClassByCGLIB$$4af5b667 extends FastClass {
      2     
      3     /**
      4      * 动态子类构造方法
      5      */
      6     public DelegateClass$$FastClassByCGLIB$$4af5b667(Class delegateClass) {
      7         super(delegateClass);
      8     }
      9 
     10     /**
     11      * 根据方法签名得到方法索引
     12      *
     13      * @param name 方法名
     14      * @param parameterTypes 方法参数类型
     15      */
     16     public int getIndex(String methodName, Class[] parameterTypes) {
     17         switch(methodName.hashCode()) {
     18             
     19             // 委托类方法add索引:0
     20             case 96417:
     21                 if (methodName.equals("add")) {
     22                     switch(parameterTypes.length) {
     23                         case 2:
     24                             if (parameterTypes[0].getName().equals("java.lang.String") && 
     25                                 parameterTypes[1].getName().equals("int")) {
     26                                 return 0;
     27                             }
     28                     }
     29                 }
     30                 break;
     31             
     32             // 委托类方法addUser索引:1
     33             case -838846263:
     34                 if (methodName.equals("addUser")) {
     35                     switch(parameterTypes.length) {
     36                         case 0:
     37                             return 1;
     38                     }
     39                 }
     40                 break;
     41                 
     42             // Object方法equals索引:2
     43             case -1295482945:
     44                 if (methodName.equals("equals")) {
     45                     switch(parameterTypes.length) {
     46                         case 1:
     47                             if (parameterTypes[0].getName().equals("java.lang.Object")) {
     48                                 return 2;
     49                             }
     50                     }
     51                 }
     52                 break;
     53             
     54             // Object方法toString索引:3
     55             case -1776922004:
     56                 if (methodName.equals("toString")) {
     57                     switch(parameterTypes.length) {
     58                         case 0: return 3;
     59                     }
     60                 }
     61                 break;
     62             
     63             // Object方法hashCode索引:4
     64             case 147696667:
     65                 if (methodName.equals("hashCode")) {
     66                     switch(parameterTypes.length) {
     67                         case 0:
     68                             return 4;
     69                     }
     70                 }
     71         }
     72 
     73         return -1;
     74     }
     75     
     76     /**
     77      * 根据方法索引调用委托类方法
     78      *
     79      * @param methodIndex 方法索引
     80      * @param delegateInstance 委托类实例
     81      * @param parameterValues 方法参数对象
     82      */
     83     public Object invoke(int methodIndex, Object delegateInstance, Object[] parameterValues) {
     84         DelegateClass instance = (DelegateClass) delegateInstance;
     85         int index = methodIndex;
     86         try {
     87             switch(index) {
     88                 case 0:
     89                     // 委托类实例直接调用方法语句
     90                     return new Boolean(instance.add((String)parameterValues[0], 
     91                             ((Number)parameterValues[1]).intValue()));
     92                 case 1:
     93                     instance.addUser();
     94                     return null;
     95                 case 2:
     96                     return new Boolean(instance.equals(parameterValues[0]));
     97                 case 3:
     98                     return instance.toString();
     99                 case 4:
    100                     return new Integer(instance.hashCode());
    101             }
    102         } catch (Throwable t) {
    103             throw new InvocationTargetException(t);
    104         }
    105 
    106         throw new IllegalArgumentException("Cannot find matching method/constructor");
    107     }
    108 
    109     /**
    110      * 根据构造方法描述符(参数类型)找到构造方法索引
    111      *
    112      * @param parameterTypes 构造方法参数类型
    113      */
    114     public int getIndex(Class[] parameterTypes) {
    115         switch(parameterTypes.length) {
    116             // 无参构造方法索引:0
    117             case 0:
    118                 return 0;
    119             
    120             // 有参构造方法索引:1
    121             case 1:
    122                 if (parameterTypes[0].getName().equals("java.lang.String")) {
    123                     return 1;
    124                 }
    125             default:
    126                 return -1;
    127         }
    128     }
    129     
    130     /**
    131      * 根据构造方法索引调用委托类构造方法
    132      *
    133      * @param methodIndex 构造方法索引
    134      * @param parameterValues 构造方法参数对象
    135      */
    136     public Object newInstance(int methodIndex, Object[] parameterValues) {
    137         // 创建委托类实例
    138         DelegateClass newInstance = new DelegateClass;
    139         DelegateClass newObject = newInstance;
    140         int index = methodIndex;
    141         try {
    142             switch(index) {
    143                 // 调用构造方法(<init>)
    144                 case 0:
    145                     newObject.<init>();
    146                     return newInstance;
    147                 case 1:
    148                     newObject.<init>((String)parameterValues[0]);
    149                     return newInstance;
    150             }
    151         } catch (Throwable t) {
    152             throw new InvocationTargetException(t);
    153         }
    154 
    155         throw new IllegalArgumentException("Cannot find matching method/constructor");
    156     }
    157 
    158     public int getMaxIndex() {
    159         return 4;
    160     }
    161 }

    三、FastClass原理分析:

    FastClass使用示例:

     1 public class DelegateClass {
     2 
     3     public DelegateClass() {
     4     }
     5 
     6     public DelegateClass(String string) {
     7     }
     8 
     9     public boolean add(String string, int i) {
    10         System.out.println("This is add method: " + string + ", " + i);
    11         return true;
    12     }
    13 
    14     public void addUser() {
    15         System.out.println("This is addUser method");
    16     }
    17 }
     1 public static void main(String[] args) throws Exception {
     2     // 保留生成的FastClass类文件
     3     System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\Temp\CGLib\FastClass");
     4 
     5     Class delegateClass = DelegateClass.class;
     6 
     7     // Java Reflect
     8 
     9     // 反射构造类
    10     Constructor delegateConstructor = delegateClass.getConstructor(String.class);
    11     // 创建委托类实例
    12     DelegateClass delegateInstance = (DelegateClass) delegateConstructor.newInstance("Tom");
    13 
    14     // 反射方法类
    15     Method addMethod = delegateClass.getMethod("add", String.class, int.class);
    16     // 调用方法
    17     addMethod.invoke(delegateInstance, "Tom", 30);
    18 
    19     Method addUserMethod = delegateClass.getMethod("addUser");
    20     addUserMethod.invoke(delegateInstance);
    21 
    22     // CGLib FastClass
    23 
    24     // FastClass动态子类实例
    25     FastClass fastClass = FastClass.create(DelegateClass.class);
    26 
    27     // 创建委托类实例
    28     DelegateClass fastInstance = (DelegateClass) fastClass.newInstance(
    29         new Class[] {String.class}, new Object[]{"Jack"});
    30 
    31     // 调用委托类方法
    32     fastClass.invoke("add", new Class[]{ String.class, int.class}, fastInstance, 
    33         new Object[]{ "Jack", 25});
    34 
    35     fastClass.invoke("addUser", new Class[]{}, fastInstance, new Object[]{});
    36 }

    FastClass原理分析:

    FastClass不使用反射类(Constructor或Method)来调用委托类方法,而是动态生成一个新的类(继承FastClass),向类中写入委托类实例直接调用方法的语句,用模板方式解决Java语法不支持问题,同时改善Java反射性能。

    动态类为委托类方法调用语句建立索引,使用者根据方法签名(方法名+参数类型)得到索引值,再通过索引值进入相应的方法调用语句,得到调用结果。

     1 public abstract class FastClass{
     2 
     3     // 委托类
     4     private Class type;
     5     
     6     // 子类访问构造方法
     7     protected FastClass() {}
     8     protected FastClass(Class type) {
     9         this.type = type;
    10     }
    11     
    12     // 创建动态FastClass子类
    13     public static FastClass create(Class type) {
    14         // Generator:子类生成器,继承AbstractClassGenerator
    15         Generator gen = new Generator();
    16         gen.setType(type);
    17         gen.setClassLoader(type.getClassLoader());
    18         return gen.create();
    19     }
    20     
    21     /**
    22      * 调用委托类方法
    23      *
    24      * @param name 方法名
    25      * @param parameterTypes 方法参数类型
    26      * @param obj 委托类实例
    27      * @param args 方法参数对象
    28      */
    29     public Object invoke(String name, Class[] parameterTypes, Object obj, Object[] args) {
    30         return invoke(getIndex(name, parameterTypes), obj, args);
    31     }
    32     
    33     /**
    34      * 根据方法描述符找到方法索引
    35      *
    36      * @param name 方法名
    37      * @param parameterTypes 方法参数类型
    38      */
    39     public abstract int getIndex(String name, Class[] parameterTypes);
    40     
    41     
    42     /**
    43      * 根据方法索引调用委托类方法
    44      *
    45      * @param index 方法索引
    46      * @param obj 委托类实例
    47      * @param args 方法参数对象
    48      */
    49     public abstract Object invoke(int index, Object obj, Object[] args);
    50     
    51     /**
    52      * 调用委托类构造方法
    53      * 
    54      * @param parameterTypes 构造方法参数类型
    55      * @param args 构造方法参数对象
    56      */
    57     public Object newInstance(Class[] parameterTypes, Object[] args) throws {
    58         return newInstance(getIndex(parameterTypes), args);
    59     }
    60     
    61     /**
    62      * 根据构造方法描述符(参数类型)找到构造方法索引
    63      *
    64      * @param parameterTypes 构造方法参数类型
    65      */
    66     public abstract int getIndex(Class[] parameterTypes);
    67     
    68     /**
    69      * 根据构造方法索引调用委托类构造方法
    70      *
    71      * @param index 构造方法索引
    72      * @param args 构造方法参数对象
    73      */
    74     public abstract Object newInstance(int index, Object[] args);
    75     
    76 }

    三、参考资料:

    https://blog.csdn.net/gyshun/article/details/81000997

    https://www.runoob.com/w3cnote/cglibcode-generation-library-intro.html

    https://www.jianshu.com/p/9a61af393e41?from=timeline&isappinstalled=0

    https://www.cnblogs.com/cruze/category/593899.html

    https://www.cnblogs.com/cruze/p/3865180.html

  • 相关阅读:
    safari 扩展配置访问指定站点 allows domains 记录
    tomcat windows启动错误,错误代码4
    iOS实现组件录屏视频不可见,用户肉眼可见(类似系统键盘效果)
    YogaKit.modulemap not found react iOS Xcode
    smoothsignature 使用经验
    CHM文件的打开问题
    如何根据基本类型(值类型)动态生成对象
    怎么根据基本类型的名字字符串,判断一个值是否在一个范围内
    使用腾讯云 EKS 部署 WordPress
    Fastdfs 部署与使用
  • 原文地址:https://www.cnblogs.com/laoxia/p/11127011.html
Copyright © 2020-2023  润新知