cglib动态代理实现原理_动态代理的两种方式

cglib动态代理实现原理_动态代理的两种方式CGLib动态代理原理CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法啊,这样也可以保证代理类拥有目标类的同名方法;看一下CGLib的基本结构,下图所示,代理类去继承目标类,每次调用代理类的方法都会被方法拦截器拦截,在拦截器中才是调用目标类的该方法的逻辑,结构还是一目了然的;1.CGLib的基本使用使用一下CGLib,在JDK动态代理中提供一个Proxy类来创建代理类,而在CGLib动态代理中也提供了一个类似的类Enhancer;使用的CGLib版本是2.2.2,我是随便找的,不

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

CGLib动态代理原理

CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法啊,这样也可以保证代理类拥有目标类的同名方法;

看一下CGLib的基本结构,下图所示,代理类去继承目标类,每次调用代理类的方法都会被方法拦截器拦截,在拦截器中才是调用目标类的该方法的逻辑,结构还是一目了然的;
在这里插入图片描述

1.CGLib的基本使用

使用一下CGLib,在JDK动态代理中提供一个Proxy类来创建代理类,而在CGLib动态代理中也提供了一个类似的类Enhancer;

使用的CGLib版本是2.2.2,我是随便找的,不同的版本有点小差异,建议用3.x版本的…我用的maven项目进行测试的,首先要导入cglib的依赖

<dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>2.2.2</version>
</dependency>

目标类(一个公开方法,另外一个用final修饰):

package com.wyq.day527;

public class Dog{ 
   
    
    final public void run(String name) { 
   
        System.out.println("狗"+name+"----run");
    }
    
    public void eat() { 
   
        System.out.println("狗----eat");
    }
}

方法拦截器:

package com.wyq.day527;

import java.lang.reflect.Method;

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

public class MyMethodInterceptor implements MethodInterceptor{ 
   

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { 
   
        System.out.println("这里是对目标类进行增强!!!");
        //注意这里的方法调用,不是用反射哦!!!
        Object object = proxy.invokeSuper(obj, args);
        return object;
    }  
}

测试类:

package com.wyq.day527;

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;

public class CgLibProxy { 
   
    public static void main(String[] args) { 
   
        //在指定目录下生成动态代理类,我们可以反编译看一下里面到底是一些什么东西
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\java\\java_workapace");
        
        //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
        Enhancer enhancer = new Enhancer();
        //设置目标类的字节码文件
        enhancer.setSuperclass(Dog.class);
        //设置回调函数
        enhancer.setCallback(new MyMethodInterceptor());
        
        //这里的creat方法就是正式创建代理类
        Dog proxyDog = (Dog)enhancer.create();
        //调用代理类的eat方法
        proxyDog.eat();       
    }
}

测试结果:

在这里插入图片描述
使用起来还是很容易的,但是其中有很多小细节我们要注意,下面我们就慢慢的看;

2.生成动态代理类

首先到我们指定的目录下面看一下生成的字节码文件,有三个,一个是代理类的FastClass,一个是代理类,一个是目标类的FastClass,我们看看代理类(Dog

EnhancerByCGLIBEnhancerByCGLIB

a063bd58.class),名字略长~后面会仔细介绍什么是FastClass,这里简单说一下,就是给每个方法编号,通过编号找到方法,这样可以避免频繁使用反射导致效率比较低,也可以叫做FastClass机制
在这里插入图片描述  然后我们可以结合生成的动态代理类来简单看看原理,一个反编译工具

我们就打开xxx.java文件,稍微进行整理一下,我们可以看到对于eat方法,在这个代理类中对应会有eat 和CGLIB$eat 0 这 两 个 方 法 ;     − 其 中 前 者 e a t 则 是 我 们 使 用 代 理 类 时 候 调 用 的 方 法 ,     − 后 者 C G L I B 0这两个方法;   - 其中前者eat 则是我们使用代理类时候调用的方法,   - 后者CGLIB 0  eat使  CGLIBeat 0 是 在 方 法 拦 截 器 里 面 调 用 的 ,   换 句 话 来 说 当 我 们 代 码 调 用 代 理 对 象 的 e a t 方 法 , 然 后 会 到 方 法 拦 截 器 中 调 用 i n t e r c e p t 方 法 , 该 方 法 内 则 通 过 p r o x y . i n v o k e S u p e r 调 用 C G L I B 0是在方法拦截器里面调用的,  换句话来说当我们代码调用代理对象的eat方法,然后会到方法拦截器中调用intercept方法,该方法内则通过proxy.invokeSuper调用CGLIB 0 eatinterceptproxy.invokeSuperCGLIBeat$0这个方法,不要因为方法名字太长了就觉得难,其实原理很简单。。。(顺便一提,不知道大家有没有发现代理类中只有eat方法,没有run方法,因为run方法被final修饰了,不可被重写,所以代理类中就没有run方法,这里要符合java规范!!!)

package com.wyq.day527;
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.*;
//可以看到这个代理类是继承我们的目标类Dog,并且顺便实现了一个Factory接口,这个接口就是一些设置回调函数和返回实例化对象的方法
public class Dog$$EnhancerByCGLIB$$fbca2ec6 extends Dog implements Factory{ 

//这里有很多的属性,仔细看一下就是一个方法对应两个,一个是Method类型,一个是MethodProxy类型
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$eat$0$Method;
private static final MethodProxy CGLIB$eat$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;
//静态代码块,调用下面静态方法,这个静态方法大概做的就是获取目标方法中每个方法的MethodProxy对象
static { 

CGLIB$STATICHOOK1();
}
//无参构造器
public Dog$$EnhancerByCGLIB$$fbca2ec6()
{ 

CGLIB$BIND_CALLBACKS(this);
}
//此方法在上面的静态代码块中被调用
static void CGLIB$STATICHOOK1(){ 

//注意下面这两个Method数组,用于保存反射获取的Method对象,避免每次都用反射去获取Method对象
Method[] amethod;
Method[] amethod1;
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
//获取目标类的字节码文件
Class class1 = Class.forName("com.wyq.day527.Dog$$EnhancerByCGLIB$$fbca2ec6");
//代理类的字节码文件
Class class2;
//ReflectUtils是一个包装各种反射操作的工具类,通过这个工具类来获取各个方法的Method对象,然后保存到上述的Method数组中
amethod = ReflectUtils.findMethods(new String[] { 

"finalize", "()V", "equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"
}, (class2 = Class.forName("java.lang.Object")).getDeclaredMethods());
Method[] _tmp = amethod;
//为目标类的每一个方法都建立索引,可以想象成记录下来目标类中所有方法的地址,需要用调用目标类方法的时候根据地址就能直接找到该方法
//这就是此处CGLIB$xxxxxx$$Proxy的作用。。。
CGLIB$finalize$1$Method = amethod[0];
CGLIB$finalize$1$Proxy = MethodProxy.create(class2, class1, "()V", "finalize", "CGLIB$finalize$1");
CGLIB$equals$2$Method = amethod[1];
CGLIB$equals$2$Proxy = MethodProxy.create(class2, class1, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
CGLIB$toString$3$Method = amethod[2];
CGLIB$toString$3$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
CGLIB$hashCode$4$Method = amethod[3];
CGLIB$hashCode$4$Proxy = MethodProxy.create(class2, class1, "()I", "hashCode", "CGLIB$hashCode$4");
CGLIB$clone$5$Method = amethod[4];
CGLIB$clone$5$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
amethod1 = ReflectUtils.findMethods(new String[] { 

"eat", "()V"
}, (class2 = Class.forName("com.wyq.day527.Dog")).getDeclaredMethods());
Method[] _tmp1 = amethod1;
CGLIB$eat$0$Method = amethod1[0];
CGLIB$eat$0$Proxy = MethodProxy.create(class2, class1, "()V", "eat", "CGLIB$eat$0");
}
//这个方法就是调用目标类的的eat方法
final void CGLIB$eat$0()
{ 

super.eat();
}
//这个方法是我们是我们要调用的,在前面的例子中调用代理对象的eat方法就会到这个方法中
public final void eat(){ 

//CGLIB$CALLBACK_0 = (MethodInterceptor)callback;
CGLIB$CALLBACK_0;
//这里就是判断CGLIB$CALLBACK_0是否为空,也就是我们传入的方法拦截器是否为空,如果不为空就最终到下面的_L4
if(CGLIB$CALLBACK_0 != null) goto _L2; else goto _L1
_L1:
JVM INSTR pop ;
CGLIB$BIND_CALLBACKS(this);
CGLIB$CALLBACK_0;
_L2:
JVM INSTR dup ;
JVM INSTR ifnull 37;
goto _L3 _L4
_L3:
break MISSING_BLOCK_LABEL_21;
_L4:
break MISSING_BLOCK_LABEL_37;
this;
CGLIB$eat$0$Method;
CGLIB$emptyArgs;
CGLIB$eat$0$Proxy;
//这里就是调用方法拦截器的intecept()方法
intercept();
return;
super.eat();
return;
}
//这里省略finalize,equals,toString,hashCode,clone,因为和上面的eat的两个方法差不多
//..........
//...........
//..........
public static MethodProxy CGLIB$findMethodProxy(Signature signature)
{ 

String s = signature.toString();
s;
s.hashCode();
JVM INSTR lookupswitch 6: default 140
// -1574182249: 68
// -1310345955: 80
// -508378822: 92
// 1826985398: 104
// 1913648695: 116
// 1984935277: 128;
goto _L1 _L2 _L3 _L4 _L5 _L6 _L7
_L2:
"finalize()V";
equals();
JVM INSTR ifeq 141;
goto _L8 _L9
_L9:
break MISSING_BLOCK_LABEL_141;
_L8:
return CGLIB$finalize$1$Proxy;
_L3:
"eat()V";
equals();
JVM INSTR ifeq 141;
goto _L10 _L11
_L11:
break MISSING_BLOCK_LABEL_141;
_L10:
return CGLIB$eat$0$Proxy;
_L4:
"clone()Ljava/lang/Object;";
equals();
JVM INSTR ifeq 141;
goto _L12 _L13
_L13:
break MISSING_BLOCK_LABEL_141;
_L12:
return CGLIB$clone$5$Proxy;
_L5:
"equals(Ljava/lang/Object;)Z";
equals();
JVM INSTR ifeq 141;
goto _L14 _L15
_L15:
break MISSING_BLOCK_LABEL_141;
_L14:
return CGLIB$equals$2$Proxy;
_L6:
"toString()Ljava/lang/String;";
equals();
JVM INSTR ifeq 141;
goto _L16 _L17
_L17:
break MISSING_BLOCK_LABEL_141;
_L16:
return CGLIB$toString$3$Proxy;
_L7:
"hashCode()I";
equals();
JVM INSTR ifeq 141;
goto _L18 _L19
_L19:
break MISSING_BLOCK_LABEL_141;
_L18:
return CGLIB$hashCode$4$Proxy;
_L1:
JVM INSTR pop ;
return null;
}
public static void CGLIB$SET_THREAD_CALLBACKS(Callback acallback[])
{ 

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

CGLIB$STATIC_CALLBACKS = acallback;
}
private static final void CGLIB$BIND_CALLBACKS(Object obj)
{ 

Dog$$EnhancerByCGLIB$$fbca2ec6 dog$$enhancerbycglib$$fbca2ec6 = (Dog$$EnhancerByCGLIB$$fbca2ec6)obj;
if(dog$$enhancerbycglib$$fbca2ec6.CGLIB$BOUND) goto _L2; else goto _L1
_L1:
Object obj1;
dog$$enhancerbycglib$$fbca2ec6.CGLIB$BOUND = true;
obj1 = CGLIB$THREAD_CALLBACKS.get();
obj1;
if(obj1 != null) goto _L4; else goto _L3
_L3:
JVM INSTR pop ;
CGLIB$STATIC_CALLBACKS;
if(CGLIB$STATIC_CALLBACKS != null) goto _L4; else goto _L5
_L5:
JVM INSTR pop ;
goto _L2
_L4:
(Callback[]);
dog$$enhancerbycglib$$fbca2ec6;
JVM INSTR swap ;
0;
JVM INSTR aaload ;
(MethodInterceptor);
CGLIB$CALLBACK_0;
_L2:
}
public Object newInstance(Callback acallback[])
{ 

CGLIB$SET_THREAD_CALLBACKS(acallback);
CGLIB$SET_THREAD_CALLBACKS(null);
return new Dog$$EnhancerByCGLIB$$fbca2ec6();
}
public Object newInstance(Callback callback)
{ 

CGLIB$SET_THREAD_CALLBACKS(new Callback[] { 

callback
});
CGLIB$SET_THREAD_CALLBACKS(null);
return new Dog$$EnhancerByCGLIB$$fbca2ec6();
}
public Object newInstance(Class aclass[], Object aobj[], Callback acallback[])
{ 

CGLIB$SET_THREAD_CALLBACKS(acallback);
JVM INSTR new #2   <Class Dog$$EnhancerByCGLIB$$fbca2ec6>;
JVM INSTR dup ;
aclass;
aclass.length;
JVM INSTR tableswitch 0 0: default 35
// 0 28;
goto _L1 _L2
_L2:
JVM INSTR pop ;
Dog$$EnhancerByCGLIB$$fbca2ec6();
goto _L3
_L1:
JVM INSTR pop ;
throw new IllegalArgumentException("Constructor not found");
_L3:
CGLIB$SET_THREAD_CALLBACKS(null);
return;
}
public Callback getCallback(int i)
{ 

CGLIB$BIND_CALLBACKS(this);
this;
i;
JVM INSTR tableswitch 0 0: default 30
// 0 24;
goto _L1 _L2
_L2:
CGLIB$CALLBACK_0;
goto _L3
_L1:
JVM INSTR pop ;
null;
_L3:
return;
}
public void setCallback(int i, Callback callback)
{ 

switch(i)
{ 

case 0: // '\0'
CGLIB$CALLBACK_0 = (MethodInterceptor)callback;
break;
}
}
public Callback[] getCallbacks()
{ 

CGLIB$BIND_CALLBACKS(this);
this;
return (new Callback[] { 

CGLIB$CALLBACK_0
});
}
public void setCallbacks(Callback acallback[])
{ 

this;
acallback;
JVM INSTR dup2 ;
0;
JVM INSTR aaload ;
(MethodInterceptor);
CGLIB$CALLBACK_0;
}
}

根据上面的代码我们可以知道代理类中主要有几部分组成:

  1. 重写的父类方法,
  2. CGLIB$eat$0这种奇怪的方法,
  3. Interceptor()方法,
  4. newInstance和get/setCallback方法

3.FastClass机制分析

为什么要用这种机制呢?直接用反射多好啊,但是我们知道反射虽然很好用,但是和直接new对象相比,效率有点慢,于是就有了这种机制, Jdk动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以cglib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法,下面用一个小例子来说明一下,这样比较直观:

public class test10 { 

public static void main(String[] args){ 

Test tt = new Test();
Test2 fc = new Test2();
int index = fc.getIndex("f()V");
fc.invoke(index, tt, null);
}
}
class Test{ 

public void f(){ 

System.out.println("f method");
}
public void g(){ 

System.out.println("g method");
}
}
class Test2{ 

public Object invoke(int index, Object o, Object[] ol){ 

Test t = (Test) o;
switch(index){ 

case 1:
t.f();
return null;
case 2:
t.g();
return null;
}
return null;
}
public int getIndex(String signature){ 

switch(signature.hashCode()){ 

case 3078479:
return 1;
case 3108270:
return 2;
}
return -1;
}
}

上例中,Test2是Test的Fastclass,在Test2中有两个方法getIndex和invoke。在getIndex方法中对Test的每个方法建立索引,并根据入参(方法名+方法的描述符)来返回相应的索引。Invoke根据指定的索引,以ol为入参调用对象O的方法。这样就避免了反射调用,提高了效率。代理类(Target

EnhancerByCGLIBEnhancerByCGLIB

788444a0)中与生成Fastclass相关的代码如下:

Class localClass1 = Class.forName("net.sf.cglib.test.Target$$EnhancerByCGLIB$$788444a0");
localClass2 = Class.forName("net.sf.cglib.test.Target");
CGLIB$g$0$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "g", "CGLIB$g$0");

MethodProxy中会对localClass1和localClass2进行分析并生成FastClass,然后再使用getIndex来获取方法g 和 CGLIB$g$0的索引,具体的生成过程将在后续进行介绍,这里介绍一个关键的内部类:

private static class FastClassInfo
{ 

FastClass f1; // net.sf.cglib.test.Target的fastclass
FastClass f2; // Target$$EnhancerByCGLIB$$788444a0 的fastclass
int i1; //方法g在f1中的索引
int i2; //方法CGLIB$g$0在f2中的索引
}

MethodProxy 中invokeSuper方法的代码如下:

 FastClassInfo fci = fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);

当调用invokeSuper方法时,实际上是调用代理类的CGLIB$g 0 方 法 , C G L I B 0方法,CGLIB 0CGLIBg$0直接调用了目标类的g方法。所以,在第一节示例代码中我们使用invokeSuper方法来调用被拦截的目标类方法。

4.简单原理

上面我们看了CGLib动态代理的用法、实际生成的代理类以及FastClass机制,下面我们就以最前面的那个例子中调用eat()方法来看看主要的调用步骤;

第一步:是经过一系列操作实例化出了Enhance对象,并设置了所需要的参数然后enhancer.create()成功创建出来了代理对象,这个就不多说了…

第二步:调用代理对象的eat()方法,会进入到方法拦截器的intercept()方法,在这个方法中会调用proxy.invokeSuper(obj, args);方法

第三步:invokeSuper中,通过FastClass机制调用目标类的方法

方法拦截器中只有一个invoke方法,这个方法有四个参数,obj表示代理对象,method表示目标类中的方法,args表示方法参数,proxy表示代理方法的MethodProxy对象
在这里插入图片描述
在这个方法内部会调用proxy.invokeSuper(obj, args)方法,我们进入.invokeSuper方法内部看看:

在这里插入图片描述

简单看看init()方法:

在这里插入图片描述
 FastClassInfo内部如下图,由此可以看出prxy.invokeSuper()方法中fci.f2.invoke(fci.i2, obj, args),其实就是调用CGLIB e a t eat eat这个方法

在这里插入图片描述
 invoke方法是个抽象方法,我们反编译一下代理类的FastClass(也就是生成的那三个字节码文件名称最长的那个)就可以看到,由于代码比较长,就不复制了…
在这里插入图片描述

5.总结

CGLib动态代理是将继承用到了极致
  这里随便画一个简单的图看看整个过程,当我们去调用方法一的时候,在代理类中会先判断是否实现了方法拦截的接口,没实现的话直接调用目标类的方法一;如果实现了那就会被方法拦截器拦截,在方法拦截器中会对目标类中所有的方法建立索引,其实大概就是将每个方法的引用保存在数组中,我们就可以根据数组的下标直接调用方法,而不是用反射;索引建立完成之后,方法拦截器内部就会调用invoke方法(这个方法在生成的FastClass中实现),在invoke方法内就是调用CGLIB方 法 一 方法一方法一这种方法,也就是调用对应的目标类的方法一;

一般我们要添加自己的逻辑就是在方法拦截器那里。。。。

在这里插入图片描述

学习参考记录:
https://www.cnblogs.com/wyq1995/p/10945034.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/179117.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • linux下 ls -l 命令显示结果每一列代表什么意思

    linux下 ls -l 命令显示结果每一列代表什么意思

    2021年10月15日
  • https,httpClient 绕过证书验证的两种写法「建议收藏」

    https,httpClient 绕过证书验证的两种写法「建议收藏」https,httpClient 绕过证书验证的两种写法

  • charles乱码怎么解决_抓包精灵乱码

    charles乱码怎么解决_抓包精灵乱码前言当使用Charles抓包时,发现数据都是乱码,这时需要安装证书解决办法1.点击charles窗口,点击左上角Help->SSLProxying→InstallCharles

  • 学了一天java,我总结了这些知识点

    学了一天java,我总结了这些知识点大家好,我是KookNut39也是Tommy,在CSDN写文,写Java时候我是Tommy,分享一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。以前一直更新C/C++方面的知识,今天是我第一次更新Java方面的知识,以后会持续更新,感兴趣的欢迎关注博主,和博主一起从0学习Java知识。大家可以去专栏查看之前的文章,希望未来能和大家共同探讨技术。文章目录1.注释(1)单行注释(2)多行注释(3)文档注释2.关键字3.保留字4.标识符5.Java数据类型(1)基本数据类型.

  • vod_cache_data是什么?

    vod_cache_data是什么?这个其实是迅雷看看的缓存文件夹,并不是病毒。迅雷相对于flashget来说,速度非常快。主要是因为迅雷的p2sp技术,但是这个也通常被人们认为是盗链技术。anyway,如果只是用“不管白猫

  • rpm 的卸载

    rpm 的卸载把clickhouse的目录不小心误删了,结果得重新安装clickhouse但是rpm安装和解压不同,要把安装来源卸载掉[root@qianfeng01etc]#rpm-qa|grepclickhouseclickhouse-server-common-20.3.12.112-1.el7.x86_64clickhouse-common-static-20.3.12.112-1.el7.x86_64clickhouse-server-20.3.12.112-1.el7.x86_6

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号