成都创新互联网站制作重庆分公司

代理模式——JDK动态代理与CGLib原理及对比分析

1.前言

首先回顾下代理模式(Proxy Pattern)的定义:代理模式指为其他对象提供一种代理,以控制这个对象的访问,属于结构型设计模式。其适用于在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端于目标对象之间起到中介的作用。

成都创新互联公司服务项目包括额济纳网站建设、额济纳网站制作、额济纳网页制作以及额济纳网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,额济纳网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到额济纳省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

代理模式主要分为静态代理和动态代理两种方式,静态代理需要手动创建代理类,代理的目标对象是固定的;动态代理使用反射机制,代理的目标对象是活动的,不需要创建代理类即可给不同的目标随时创建代理。本篇重点探究动态代理的实现。

2.JDK动态代理

JDK动态代理采用字节重组,重新生成对象来替代原始对象,以达到动态代理的目的。JDK动态代理生成对象的步骤如下:

  1. 获取被代理对象的引用,并且获取它的所有接口,反射获取。
  2. JDK动态代理类重新生成一个新的类,同时新的类要实现被代理类实现的所有接口。
  3. 动态生成Java代码,新加的业务逻辑方法由一定的逻辑代码调用(在代码中体现)。
  4. 编译新生成的Java代码.class文件。
  5. 重新加载到JVM中运行。

2.1 JDK动态代理实现及原理源码解析

实现一个JDK动态代理,方式为实现java.lang.reflect.InvocationHandler接口,并使用java.lang.reflect.Proxy.newProxyInstance()方法生成代理对象。

/**
* 要代理的接口
*/
public interface IPerson {
    void learn();
}

/**
* 真实调用类
*/
public class Zhangsan implements IPerson {
    public void learn() {
        System.out.println("==张三学习中间件==");
    }
}

/**
* JDK代理类生成
*/
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkInvocationHandler implements InvocationHandler {
    private IPerson target;
    public IPerson getInstance(IPerson target){
        this.target = target;
        Class clazz =  target.getClass();
        return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target,args);
        after();
        return result;
    }
    private void before() {
        System.out.println("事前做好计划");
    }
    
    private void after() {
        System.out.println("事后回顾梳理");
    }
}

/**
* 测试
*/
public class TestProxy {
    public static void main(String[] args) {
        try {
            //把生成的字节码保存到本地磁盘,动态生成的类会保存在工程根目录下的 com/sun/proxy 目录里面
            System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
            IPerson obj = (IPerson) new JdkInvocationHandler().getInstance(new Zhangsan());
            obj.learn();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

看下 Proxy.newProxyInstance 里面究竟发生了什么?

结合流程图,在生成字节码的那个地方,也就是 ProxyGenerator.generateProxyClass() 方法里面,通过代码可以看到(自行查阅,篇幅原因,这里不贴代码),里面是用参数 saveGeneratedFiles 来控制是否把生成的字节码保存到本地磁盘。代码中已经设置保存到本地,现在找到刚才生成的 $Proxy0.class,反编译打开如下:

import com.zang.jdkproxy.IPerson;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements IPerson {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void learn() throws  {
        try {
           // super.h 对应的是父类的h变量,也就是Proxy.newProxyInstance方法中的InvocationHandler参数
           // 所以这里实际上就是使用了我们自己写的InvocationHandler实现类的invoke方法
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.zang.jdkproxy.IPerson").getMethod("learn");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到 $Proxy0类继承了Proxy类,里面有一个跟IPerson一样签名的 learn 方法,方法实现中的super.h.invoke(this, m3, (Object[])null);,super.h 对应的是父类的h变量,也就是Proxy.newProxyInstance方法中的InvocationHandler参数:

package java.lang.reflect;
//import略

public class Proxy implements java.io.Serializable {

    protected InvocationHandler h;

    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }

    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class[] intfs = interfaces.clone();
        //

所以这里实际上就是使用了我自己写的InvocationHandler实现类JdkInvocationHandlerinvoke方法,当调用 IPerson.learn的时候,其实它是被转发到了 JdkInvocationHandler.invoke。至此,整个魔术过程就透明了。

2.2 手写JDK动态代理

使用JDK动态代理的类名和方法名定义以及执行思路,下面来进行手写实现。

创建MyInvocationHandler接口:

import java.lang.reflect.Method;

public interface MyInvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}

创建MyProxy类:

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 自己实现的代理类,用来生成字节码文件,并动态加载到JVM中
 */
public class MyProxy {

    public static final String ln = "\r\n";

    /**
     * 生成代理对象
     * @param classLoader 类加载器,用于加载被代理类的类文件
     * @param interfaces 被代理类的接口
     * @param h 自定义的InvocationHandler接口,用于具体代理方法的执行
     * @return 返回被代理后的代理对象
     */
    public static Object newProxyInstance(MyClassLoader classLoader, Class[] interfaces, MyInvocationHandler h) {
        try {
        //1、动态生成源代码.java文件
            String src = generateSrc(interfaces);
        //2、Java文件输出磁盘
            String filePath = MyProxy.class.getResource("").getPath();

            File f = new File(filePath + "$Proxy0.java");
            FileWriter fw = new FileWriter(f);
            fw.write(src);
            fw.flush();
            fw.close();

        //3、把生成的.java文件编译成.class文件
            //获取Java编译器
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            //标注Java文件管理器,用来获取Java字节码文件
            StandardJavaFileManager manage = compiler.getStandardFileManager(null, null, null);
            Iterable iterable = manage.getJavaFileObjects(f);
            //创建task,通过java字节码文件将类信息加载到JVM中
            JavaCompiler.CompilationTask task = compiler.getTask(null, manage, null, null, null, iterable);
            //开始执行task
            task.call();
            //关闭管理器
            manage.close();

        //4、编译生成的.class文件加载到JVM中来
            Class proxyClass = classLoader.findClass("$Proxy0");
            Constructor c = proxyClass.getConstructor(MyInvocationHandler.class);
            f.delete();

        //5、返回字节码重组以后的新的代理对象
            return c.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 生成代理类的源代码
     */
    private static String generateSrc(Class[] interfaces) {
        StringBuffer sb = new StringBuffer();
        sb.append(MyProxy.class.getPackage() + ";" + ln);
        sb.append("import " + interfaces[0].getName() + ";" + ln);
        sb.append("import java.lang.reflect.*;" + ln);
        sb.append("public class $Proxy0 implements " + interfaces[0].getName() + "{" + ln);
        sb.append("GPInvocationHandler h;" + ln);
        sb.append("public $Proxy0(GPInvocationHandler h) { " + ln);
        sb.append("this.h = h;");
        sb.append("}" + ln);
        for (Method m : interfaces[0].getMethods()) {
            Class[] params = m.getParameterTypes();

            StringBuffer paramNames = new StringBuffer();
            StringBuffer paramValues = new StringBuffer();
            StringBuffer paramClasses = new StringBuffer();

            for (int i = 0; i < params.length; i++) {
                Class clazz = params[i];
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName());
                paramNames.append(type + " " + paramName);
                paramValues.append(paramName);
                paramClasses.append(clazz.getName() + ".class");
                if (i > 0 && i < params.length - 1) {
                    paramNames.append(",");
                    paramClasses.append(",");
                    paramValues.append(",");
                }
            }

            sb.append("public " + m.getReturnType().getName() + " " + m.getName() + "(" + paramNames.toString() + ") {" + ln);
            sb.append("try{" + ln);
            sb.append("Method m = " + interfaces[0].getName() + ".class.getMethod(\"" + m.getName() + "\",new Class[]{" + paramClasses.toString() + "});" + ln);
            sb.append((hasReturnValue(m.getReturnType()) ? "return " : "") + getCaseCode("this.h.invoke(this,m,new Object[]{" + paramValues + "})", m.getReturnType()) + ";" + ln);
            sb.append("}catch(Error _ex) { }");
            sb.append("catch(Throwable e){" + ln);
            sb.append("throw new UndeclaredThrowableException(e);" + ln);
            sb.append("}");
            sb.append(getReturnEmptyCode(m.getReturnType()));
            sb.append("}");
        }
        sb.append("}" + ln);
        return sb.toString();
    }


    private static Map mappings = new HashMap();

    static {
        mappings.put(int.class, Integer.class);
    }

    private static String getReturnEmptyCode(Class returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "return 0;";
        } else if (returnClass == void.class) {
            return "";
        } else {
            return "return null;";
        }
    }

    private static String getCaseCode(String code, Class returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "((" + mappings.get(returnClass).getName() + ")" + code + ")." + returnClass.getSimpleName() + "Value()";
        }
        return code;
    }

    private static boolean hasReturnValue(Class clazz) {
        return clazz != void.class;
    }

    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

}

创建类加载器MyClassLoader:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader {

    private File classPathFile;
    public MyClassLoader(){
        String classPath = MyClassLoader.class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }

    /**
     * 通过类名称加载类字节码文件到JVM中
     * @param name 类名
     * @return 类的Class独享
     * @throws ClassNotFoundException
     */
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        //获取类名
        String className = MyClassLoader.class.getPackage().getName() + "." + name;
        if(classPathFile  != null){
            //获取类文件
            File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class");
            if(classFile.exists()){
                //将类文件转化为字节数组
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try{
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte [] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1){
                        out.write(buff,0,len);
                    }
                    //调用父类方法生成class实例
                    return defineClass(className,out.toByteArray(),0,out.size());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

实现并测试

/**
* 要代理的接口
*/
public interface IPerson {
    void learn();
}

/**
* 真实调用类
*/
public class Zhangsan implements IPerson {
    public void learn() {
        System.out.println("==张三学习中间件==");
    }
}

/**
* JDK代理类生成
*/
public class CustomInvocationHandler implements MyInvocationHandler {
    private IPerson target;
    public IPerson getInstance(IPerson target){
        this.target = target;
        Class clazz =  target.getClass();
        return (IPerson) MyProxy.newProxyInstance(new MyClassLoader(),clazz.getInterfaces(),this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target,args);
        after();
        return result;
    }

    private void before() {
        System.out.println("事前做好计划");
    }
    
    private void after() {
        System.out.println("事后回顾梳理");
    }

}

/**
* 测试
*/
public class Test {
    public static void main(String[] args) {
        CustomInvocationHandler custom = new CustomInvocationHandler();
        IPerson zhangsan = custom.getInstance(new Zhangsan());
        zhangsan.learn();
    }
}

至此,手写完成,读者也可自行参照实现。

3.CGLib动态代理API原理分析

3.1 CGLib动态代理的使用

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 CustomCGlib implements MethodInterceptor {

    public Object getInstance(Class clazz) throws Exception{
        //相当于Proxy,代理的工具类
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o,objects);
        after();
        return obj;
    }

    private void before() {
        System.out.println("事前做好计划");

    }

    private void after() {
        System.out.println("事后回顾梳理");
    }
}

这里有一个小细节,CGLib动态代理的目标对象不需要实现任何接口,它是通过动态继承目标对象实现动态代理的,客户端测试代码如下:

public class CglibTest {
    public static void main(String[] args) {
        try {
            Zhangsan obj = (Zhangsan) new CustomCGlib().getInstance(Zhangsan.class);
            obj.learn();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.2 CGLib动态代理的实现原理

CGLib动态代理的实现原理又是怎样的呢?可以在客户端测试代码中加上一句代码,将CGLib动态代理后的.class文件写入磁盘,然后反编译来一探究竟,代码如下:

//import net.sf.cglib.core.DebuggingClassWriter;
//使用CGLib的代理类可以将内存中的.class文件写入本地磁盘
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E://cglib_proxy_classes");
Zhangsan obj = ···
//···

重新执行代码,再输出目录下会出现三个.class文件,一个是目标(被代理)类的FastClass,一个是代理类,一个是代理类的FastClass。如图:

其中,Zhangsan$$EnhancerByCGLIB$$3d23e0ea.class就是CGLib动态代理生成的代理类,继承了Zhangsan类。

package com.zang.cglibproxy;

import java.lang.reflect.Method;
import net.sf.cglib.*;

public class Zhangsan$$EnhancerByCGLIB$$3d23e0ea extends Zhangsan implements Factory {
	//···
   //传入的MethodInterceptor对象      
   private MethodInterceptor CGLIB$CALLBACK_0;
   //目标类的learn方法对象  
   private static final Method CGLIB$learn$0$Method;
   //代理类的learn方法对象  
   private static final MethodProxy CGLIB$learn$0$Proxy;
   private static final Object[] CGLIB$emptyArgs;

   //初始化方法,其中部分代码略  
    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("com.zang.cglibproxy.Zhangsan$$EnhancerByCGLIB$$78b");
        Class var1;
        Method[] var = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        //···
        //初始化目标类的learn方法对象
        CGLIB$learn$0$Method = ReflectUtils.findMethods(new String[]{"learn", "()V"}, (var1 = Class.forName("com.zang.cglibproxy.Zhangsan")).getDeclaredMethods())[0];
        //初始化代理类的learn方法对象
        CGLIB$learn$0$Proxy = MethodProxy.create(var1, var0, "()V", "learn", "CGLIB$learn$0");
    }
    
    //这里直接调用Zhangsan#learn
    final void CGLIB$learn$0() {
        super.learn();
    }

    public final void learn() {
        MethodInterceptor var = this.CGLIB$CALLBACK_0;
        if (var == null) {
            CGLIB$BIND_CALLBACKS(this);
            var = this.CGLIB$CALLBACK_0;
        }

        if (var != null) {
            //这里执行拦截器定义逻辑
            var.intercept(this, CGLIB$learn$0$Method, CGLIB$emptyArgs, CGLIB$learn$0$Proxy);
        } else {
            super.learn();
        }
    }
  //···
}

调用过程为:代理对象调用this.learn方法→调用拦截器→methodProxy.invokeSuper()CGLIB$learn$0→被代理对象learn方法。

package net.sf.cglib.proxy;

import java.lang.reflect.Method;

public interface MethodInterceptor extends Callback {
    Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}
public class CustomCGlib implements MethodInterceptor {
	//···
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o,objects);
        after();
        return obj;
    }
    //···
}

MethodInterceptor拦截器就是由MethodProxyinvokeSuper方法调用代理方法的,因此,MethodProxy类中的代码非常关键,下面分析它具体做了什么:

package net.sf.cglib.proxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import net.sf.cglib.*;

public class MethodProxy {
    private Signature sig1;
    private Signature sig2;
    private MethodProxy.CreateInfo createInfo;
    private final Object initLock = new Object();
    private volatile MethodProxy.FastClassInfo fastClassInfo;

   private void init() {
        if (this.fastClassInfo == null) {
            synchronized(this.initLock) {
                if (this.fastClassInfo == null) {
                    MethodProxy.CreateInfo ci = this.createInfo;
                    MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
                    //创建目标类的FastClass对象(在缓存中,则取出;没在,则重新生成)
                    fci.f1 = helper(ci, ci.c1);
                    //创建代理类的FastClass对象
                    fci.f2 = helper(ci, ci.c2);
                    //获取learn方法的索引
                    fci.i1 = fci.f1.getIndex(this.sig1);
                    //获取CGLIB$learn$0方法的索引
                    fci.i2 = fci.f2.getIndex(this.sig2);
                    this.fastClassInfo = fci;
                }
            }
        }

    }
    
    public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            //初始化,创建了两个FastClass类对象
            this.init();
            MethodProxy.FastClassInfo fci = this.fastClassInfo;
            //这里将直接调用代理类的CGLIB$learn$0方法,而不是通过反射调用
            //fci.f2:代理类的FastClass对象,fci.i2为CGLIB$learn$0方法对应的索引,obj为当前的代理类对象,args为learn方法的参数列表
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException var4) {
            throw var4.getTargetException();
        }
    }

上面代码调用获取代理类对应的FastClass,并执行代理方法。还记得之前生成的三个.class文件吗?Zhangsan$$EnhancerByCGLIB$$78b$$FastClassByCGLIB$$a8f9873c.class就是代理类的FastClass,Zhangsan$$FastClassByCGLIB$$bcf7b1f4.class就是目标类的FastClass。

CGLib动态代理执行代理方法的效率之所以比JDK高,是因为CGlib采用了FastClass机制,它的原理简单来说就是:为代理类和被代理类各生成一个类,这个类会为代理类或被代理类的方法分配一个index(int类型);这个index被当作一个入参,FastClass可以直接定位要调用的方法并直接进行调用,省去了反射调用,因此调用效率比JDK动态代理通过反射调用高(并不绝对,还需参考JDK版本及使用场景来说)。下面来反编译一个FastClass。

public class Zhangsan$$FastClassByCGLIB$$bcf7b1f4 extends FastClass {
    public Zhangsan$$FastClassByCGLIB$$bcf7b1f4(Class var1) {
        super(var1);
    }

    public int getIndex(Signature var1) {
        String var = var1.toString();
        switch(var.hashCode()) {
        case :
            if (var.equals("learn()V")) {
                //learn方法返回0
                return 0;
            }
            break;
        case :
            if (var.equals("equals(Ljava/lang/Object;)Z")) {
   //···
            }
        }
    }
    
    //根据index获取方法
    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        Zhangsan var = (Zhangsan)var2;
        int var = var1;

        try {
            switch(var) {
            case 0:
                //传入index为0则执行learn方法
                var.learn();
                return null;
            case 1:
                return new Boolean(var.equals(var3[0]));
   //···           
                    

FastClass并不是跟代理类一起生成的,而是在第一次执行MethodProxyinvokeinvokeSuper方法时生成的,并被放在了缓存中。

4.总结

通过上面的分析,相信会对两种动态代理的实现原理有一个深入的认识,总结性比较两者的区别如下:

  1. JDK动态代理实现了被代理对象的接口,CGLib动态代理继承了被代理对象。
  2. JDK动态代理和CGLib动态代理都在运行期生成字节码,JDK动态代理直接写Class字节码,CGLib动态代理使用ASM框架写Class字节码。CGLib动态代理实现更复杂,生成代理类比JDK动态代理效率低。
  3. JDK动态代理调用代理方法是通过反射机制调用的,CGLib动态代理是通过FastClass机制直接调用方法的,CGLib动态代理的执行效率更高。

当前题目:代理模式——JDK动态代理与CGLib原理及对比分析
本文地址:http://cxhlcq.com/article/dscgisj.html

其他资讯

在线咨询

微信咨询

电话咨询

028-86922220(工作日)

18980820575(7×24)

提交需求

返回顶部