Java反射工作機制

前言

反射是Spring、mybatis等框架的基礎(chǔ),對于常寫業(yè)務(wù)邏輯的同學(xué)應(yīng)該算是最熟悉的陌生人,今天我們就聊聊Java的反射機制,把不熟悉變成熟悉。

結(jié)論在前:
1:Class對象包含類的所有信息,可以通過該對象獲取到構(gòu)造方法,成員變量,成員方法和接口等信息,這些信息在JVM中同樣以類的形式存在
2:Class對象有三種獲取方法,字面量XXX.class,Object.getClass(),Class.forName()
3:從Class對象中獲取的Method、Field等類的組成元素的時候獲取到的實際上是該Class對象內(nèi)部的一個緩存中存儲的Method、Field的拷貝。
4:調(diào)用Method的invoke方法底層借助了一個叫做MethodAccessor的工具,這個工具又兩種形態(tài),一種是native,一種是字節(jié)碼生成加載后的Magic形態(tài),前者初始化快執(zhí)行慢,后者初始化慢執(zhí)行快,JVM權(quán)衡之后決定前15次用前者,超過15次用后者(inflation機制)。
5:Class對象中的緩存創(chuàng)建使用了CAS+Volatile+死循環(huán)這種無鎖形式

一、Class對象知多少

Class對象可以說是反射的源泉,但是Class對象存放在哪,什么時候存放,存放了哪些信息你真的了解嗎?下面我們就來揭開Class對象的神秘面紗。

1、類生命周期知多少

這個比較簡單,棧、堆、方法區(qū)、其他,棧存放引用,堆存放對象,方法區(qū)存放類信息和靜態(tài)變量以及字面量,實際上Class對象存放在堆中。

1.1、類的生命周期

加載,連接(驗證,準(zhǔn)備,解析),初始化,使用,卸載;解析階段與初始化階段順序不一定。

1.2、類的初始化時機

以下幾種情況虛擬機必須對類進行初始化(加載,驗證,準(zhǔn)備自然必須在此之前)

  • 首先:遇到new,getstatic,putstatic或invokestatic這4條字節(jié)碼指令時,一般為實例化對象,讀取或設(shè)置一個類的靜態(tài)字段(final的除外),調(diào)用一個類的靜態(tài)方法的時候
  • 然后:使用java.lang.reflect包的方法對類進行反射調(diào)用的時候
  • 再次:初始化一個類的時候首先初始化其父類
    然后:虛擬機啟動的時候首先會初始化主類(main方法的類)
  • 最后:java.lang.invoke.MethodHandler實例最后的解析結(jié)果為Ref_getstatic,REF_putStatic,REF_invokeStatic的時候,這個方法對應(yīng)的類需要觸發(fā)初始化
    從上面可以看出,當(dāng)反射調(diào)用類的時候會觸發(fā)類的加載。
1.3、類的加載過程

加載階段需要完成的3件事情

  • 首先:通過一個類的全限定名來獲取定義此類的二進制字節(jié)流,實際上就是獲取你已編譯好的.class字節(jié)碼文件
  • 然后:將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
  • 最后:在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)中這個類的各種數(shù)據(jù)的訪問入口,這個對象存儲在堆內(nèi)存中
    重點就在這,Java的字節(jié)碼Class信息會以Class對象的形式存放在方法區(qū)中!

二、類的結(jié)構(gòu)信息知多少

讓我們好好想想類有哪些信息組成,繼承/實現(xiàn)的接口、全路徑名、類名稱、類訪問限制、構(gòu)造方法、成員變量、成員方法、靜態(tài)變量等等。Class對象包含了所有獲取這些信息的方法。這些信息中,反射最常用的是類的構(gòu)造方法、成員變量、成員方法、成員方法參數(shù)這幾個重要的屬性,而且每個屬性都有對應(yīng)的類實現(xiàn)。

2.1、類信息的組成
類繼承體系@2x.png
  • AnnotatedElement接口:該接口提供了獲取注解信息的一些方法,即所有的參數(shù)、方法、成員變量、構(gòu)造方法都能通過反射獲取到注解的相關(guān)信息。
  • package類:對應(yīng)Java包的一些信息
  • Parameter類:對應(yīng)構(gòu)造方法或者成員方法的參數(shù)信息,如參數(shù)訪問限制,參數(shù)類型等等信息
  • AccessibleObject類:該類是成員變量、成員方法、構(gòu)造方法們的父類,提供了一些public、private等修飾符的入口檢查(是否可以訪問、設(shè)置訪問權(quán)限等)
  • GenericDeclaration類:提供泛型的一些信息
  • Field類:這個不用說了,對應(yīng)成員變量
  • Executable類:顧名思義,可執(zhí)行的,由兩個子類,成員方法和構(gòu)造方法,提供了兩種方法公共的必要進行的一些操作,比如獲取方法參數(shù)、獲取方法名稱、獲取方法訪問修飾符等等。
  • Method類:對應(yīng)方法的類,提供了類的一些方法信息
  • Constructor類:對應(yīng)構(gòu)造方法的類,提供了構(gòu)造方法的一些信息。
  • Class類:包含類的所有信息
    其實使用反射關(guān)注最多的就是上面的Field、Method、Constructor三個類。

三、反射

解釋了這么多類的信息,實際上都是為了反射做鋪墊。那么究竟什么是反射呢?反射其實是在JVM運行的時候通過Class對象動態(tài)的獲取類信息,這與我們平時編碼有所差異的地方是反射不需要事先寫好代碼及用編譯器編譯,而是直接在JVM運行過程中拿到類的方法等信息直接執(zhí)行。

3.1、如何獲取Class對象
  • 方法一:通過字面量直接獲取,如XXX.class,值得注意的是這種字面量不會觸發(fā)類的初始化,但此時方法區(qū)中肯定已經(jīng)有了XXX類的Class對象,因此可以推斷XXX類已經(jīng)被加載到方法區(qū),只是沒有完成初始化,實際上初始化就是給類中定義的類變量和成員變量及靜態(tài)代碼塊進行初始化賦值和執(zhí)行操作,初始化完的類程序員才真正可以new出來。
  • 方法二:通過Object類的getClass方法,如xxxObject.getClass(),這種方法會觸發(fā)類的初始化哦
  • 方法三:通過Class的靜態(tài)方法forName(),這種方式也會觸發(fā)類的初始化。
3.2、反射獲取Field、Constructor、Method等

有了Class對象就可以為所欲為嗎?抱歉,有Class對象真的可以為所欲為!

public class SingletonTest {
    // 私有構(gòu)造方法
    private SingletonTest(){    
        System.out.println("無參數(shù)---構(gòu)造----");  
    }
    // 私有構(gòu)造方法
    private SingletonTest(String a){      
        System.out.println("有參數(shù)---構(gòu)造----參數(shù)值:" + a);  
    }
    //定義私有類型的變量
    private static volatile  SingletonTest instance;
    //定義一個靜態(tài)共有方法
    public static SingletonTest getInstance(){
        if(instance == null){
            synchronized(SingletonTest.class){
                if(instance == null){
                    return new SingletonTest();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) throws Exception{
        Class clazz = SingletonTest.class;
        /*以下調(diào)用無參的、私有構(gòu)造函數(shù)*/   
        Constructor c0=  clazz.getDeclaredConstructor();   
        c0.setAccessible(true); 
        SingletonTest po=(SingletonTest)c0.newInstance();   
        System.out.println("無參構(gòu)造函數(shù)\t"+po); 
        /*以下調(diào)用帶參的、私有構(gòu)造函數(shù)*/   
        Constructor c1=clazz.getDeclaredConstructor(new Class[]{String.class});   
        c1.setAccessible(true);   
        SingletonTest p1=(SingletonTest)c1.newInstance(new Object[]{"我是參數(shù)值"});   
        System.out.println("有參的構(gòu)造函數(shù)\t"+p1);  
    }
}

執(zhí)行結(jié)果:

無參數(shù)---構(gòu)造----
無參構(gòu)造函數(shù)  com.huo.demos.test.SingletonTest@15db9742
有參數(shù)---構(gòu)造----參數(shù)值:我是參數(shù)值
有參的構(gòu)造函數(shù) com.huo.demos.test.SingletonTest@6d06d69c

具體的API這里就不一一調(diào)用了,自己去參考Java的API吧,上面很詳細。這里要說的是反射技術(shù)可以拿到類的所有信息,并通過這些信息做你想做的事情,無論信息聲明是私有的還是公有的。

四、反射原理

一個Class對象可以同時被多個線程同時反射,這里面有沒有線程安全問題?多個線程同時反射會不會出現(xiàn)性能下降問題?帶著這些疑問我們一起探索一下反射的原理。
首先,我們自己新建一個用于發(fā)射的類,反射獲取該類的成員變量,構(gòu)造方法,成員方法,如下

public class ReflectionMechanism {
    public static void main(String[] args) throws Exception {
        ClassNeedReflect target = new ClassNeedReflect();
        Class<ClassNeedReflect> clazz  = ClassNeedReflect.class;
        Field field = clazz.getDeclaredField("fieldReflected");
        Constructor<ClassNeedReflect> constructor = clazz.getDeclaredConstructor();
        Method method = clazz.getDeclaredMethod("methodReflected");
        method.invoke(target);
    }
    static class ClassNeedReflect {
        private String fieldReflected;
        private ClassNeedReflect(){
            System.out.println("構(gòu)造方法");
        }
        public void methodReflected() {
            System.out.println("反射的方法");
        }
    }
}

然后,無論getXXX()方法(XXX表示成員變量,成員方法或者構(gòu)造方法如getDeclaredMethod方法),內(nèi)部都調(diào)用了privateGetXXX這個方法,該方法內(nèi)部的前三行又不約而同的調(diào)用了下面這行代碼。

 ReflectionData<T> rd = reflectionData();

然后,rd是個緩存對象,緩存的內(nèi)容實際上是反射需要獲取的成員變量、成員方法、構(gòu)造方法、類接口等可以由每個線程獲取到的變量。為什么要這么做呢?緩存嘛,肯定是可以提高效率啊,多線程同時讀取的時候就不用每次都去底層拿這些變量了,具體緩存內(nèi)容如下。

private static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        // Intermediate results for getFields and getMethods
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        volatile Class<?>[] interfaces;
        final int redefinedCount;
        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }

重點來了,敲黑板?。?!,Class用軟引用持有了一個緩存對象,在JVM內(nèi)存吃緊的情況下防止OOM會把這個緩存對象回收,所以其失效時間是內(nèi)存不足的時候。細心的同學(xué)可以發(fā)現(xiàn),這個緩存數(shù)據(jù)是個volatile內(nèi)存可見的變量,結(jié)合CAS操作剛好可以實現(xiàn)線程安全。
我們來分析reflectionData方法,假設(shè)現(xiàn)在多個線程同時調(diào)用了這個方法,那么在Class內(nèi)部已經(jīng)持有該緩存對象的情況下不會出現(xiàn)線程安全問題(因為都是讀操作),但是在Class對象內(nèi)部沒持有該對象的時候多個線程同時創(chuàng)建這個reflectionData可能會出現(xiàn)創(chuàng)建多個對象的問題,我們來看下大師們是如何解決這個問題的。
當(dāng)緩存對象不存在的時候會調(diào)用newReflectionData,注意到該方法實際上用了一個死循環(huán)+CAS操作,由于reflectionData變量是volatile內(nèi)存可見的,死循環(huán)+cas+volatile是保證線程安全的常用手法(可以參見我前面concurrent包的博客),Atomic.casReflectionData保證線程安全的創(chuàng)建了緩存對象。值得注意的是新創(chuàng)建的內(nèi)存內(nèi)部只有成員變量而沒有成員方法和構(gòu)造方法供反射獲?。。。?/p>

ReflectionData<T> rd = reflectionData();
private volatile transient SoftReference<ReflectionData<T>> reflectionData;
private ReflectionData<T> reflectionData() {
        SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
        int classRedefinedCount = this.classRedefinedCount;
        ReflectionData<T> rd;
        if (useCaches &&
            reflectionData != null &&
            (rd = reflectionData.get()) != null &&
            rd.redefinedCount == classRedefinedCount) {
            return rd;
        }
        return newReflectionData(reflectionData, classRedefinedCount);
    }
private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,
                                                int classRedefinedCount) {
        if (!useCaches) return null;
        while (true) {
            ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
            if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
                return rd;
            }
            oldReflectionData = this.reflectionData;
            classRedefinedCount = this.classRedefinedCount;
            if (oldReflectionData != null &&
                (rd = oldReflectionData.get()) != null &&
                rd.redefinedCount == classRedefinedCount) {
                return rd;
            }
        }
    }

再次,緩存對象創(chuàng)建完成之后成員方法類型和構(gòu)造方法類型是什么時候加到緩存里面去的呢?這里我們以Method成員方法為例,在某個線程調(diào)用getDeclaredMethod方法獲取/創(chuàng)建了緩存對象之后,接著首先會從緩存中獲取該方法,如果獲取不到才會調(diào)用Reflection.filterMethods方法從VM虛擬機中獲取,然后如果有緩存的話更新緩存。

private Method[] privateGetDeclaredMethods(boolean publicOnly) {
        checkInitted();
        Method[] res;
        ReflectionData<T> rd = reflectionData();
        if (rd != null) {
            res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
            if (res != null) return res;
        }
        // 沒有緩存的條件下從虛擬機中獲取
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
        if (rd != null) {
            if (publicOnly) {
                rd.declaredPublicMethods = res;
            } else {
                rd.declaredMethods = res;
            }
        }
        return res;
    }

然后,上面獲取到的是一個數(shù)組,而要想獲取到指定名稱的那個還經(jīng)歷了一個searchMethods方法。方法代碼如下,該方法在匹配到了指定的Method之后會通過getReflectionFactory().copyMethod(res)底層調(diào)用copy方法,該方法返回的是一個新的Method對象,且對象持有一個root引用指向Method的this對象。所以如果是多線程調(diào)用這個方法實際上是每個線程都new了一個對象,這樣做的好處類似于Spring中的原型,不會出現(xiàn)線程之間同時更改同一個Method對象的線程安全問題。Field、Constructor的方法與Method方法除了名字不同其他一模一樣,連代碼順序都不變!所以原理都是相同的。

private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (m.getName() == internedName
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }

        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }
Method copy() {
        if (this.root != null)
            throw new IllegalArgumentException("Can not copy a non-root Method");
        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        res.root = this;
        // Might as well eagerly propagate this if already present
        res.methodAccessor = methodAccessor;
        return res;
    }

4.1、反射調(diào)用

以上反射獲取對應(yīng)對象過程結(jié)束,下面是對對象的使用,這里同樣以方法為例,獲取到方法之后會調(diào)用invoke方法執(zhí)行方法。方法代碼如下,該方法首先進行了一系列的檢測,然后通過MethodAccessor調(diào)用invoke方法執(zhí)行Method,開始時MethodAccessor為空,需要獲取。

 @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

然后,看下獲取MethodAccessor方法,首先也是從緩存中獲取,如果獲取不到就調(diào)用reflectionFactory.newMethodAccessor方法獲取一個并設(shè)置緩存

private MethodAccessor acquireMethodAccessor() {
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }
        return tmp;
    }

然后,具體看下newMethodAccesor方法,該方法可以使用inflation機制創(chuàng)建accessor,初始的時候noInfalation值為false,所以開始的時候會調(diào)用NativeMethodAccessorImpl的方法invoke。

private static boolean noInflation        = false;
private static int     inflationThreshold = 15;
 public MethodAccessor newMethodAccessor(Method method) {
        checkInitted();
        if (noInflation) {
            return new MethodAccessorGenerator().
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
        } else {
            NativeMethodAccessorImpl acc =
                new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res =
                new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }

再次,看下NativeMethodAccessorImpl的實現(xiàn),這里的++numInvocations就是統(tǒng)計次數(shù)的,有源碼可以看出,前15次會調(diào)用native的invoke0方法,這種方法初始化比較快,但性能不夠好。而過了15次之后都是底層調(diào)用asm給Method生成字節(jié)碼然后加載到內(nèi)存形成對象進行調(diào)用的,這種方式初始化較慢,但性能較好。

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }    
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
        return invoke0(method, obj, args);
    }
    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }
    private static native Object invoke0(Method m, Object obj, Object[] args);
}

最后,來看下asm生成字節(jié)碼方法的實現(xiàn),這個方法的最后返回了一個MagicAccesorImpl(開始是沒有實現(xiàn)的空類),其中的AccessController.doPrivileged(new PrivilegeAction)相當(dāng)于執(zhí)行一個任務(wù),該任務(wù)調(diào)用ClassDefiner的defineClass方法,該方法內(nèi)部同樣也是執(zhí)行任務(wù),但內(nèi)部的方法每次執(zhí)行的時候都會返回一個類加DelegatingClassLoader類加載器,然后把類的一些字節(jié)碼信息連同這個類加載器一起交給unsafe讓他去把字節(jié)碼通過類加載器加載到內(nèi)存里面來以MagicAccessorImpl的類形式存在與內(nèi)存中。

return (MagicAccessorImpl) AccessController.doPrivileged(new PrivilegedAction() {
                public MagicAccessorImpl run() {
                    try {
                        return (MagicAccessorImpl) ClassDefiner
                                .defineClass(arg12, arg16, 0, arg16.length, arg0.getClassLoader()).newInstance();
                    } catch (IllegalAccessException | InstantiationException arg1) {
                        throw new InternalError(arg1);
                    }
                }
            });
class MagicAccessorImpl {
}
class ClassDefiner {
    static final Unsafe unsafe = Unsafe.getUnsafe();
    static Class<?> defineClass(String arg, byte[] arg0, int arg1, int arg2, final ClassLoader arg3) {
        ClassLoader arg4 = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
            public ClassLoader run() {
                return new DelegatingClassLoader(arg3);
            }
        });
        return unsafe.defineClass(arg, arg0, arg1, arg2, arg4, (ProtectionDomain) null);
    }
}
4.2、多線程情況下的反射

我們回到最開始的方法執(zhí)行,假如有多個線程同時執(zhí)行反射方法的調(diào)用,上面有說過每個線程持有一個自己的Method對象,當(dāng)調(diào)用invoke方法后,每個線程都調(diào)用了acquireMethodAccessor()方法。

Method method = clazz.getDeclaredMethod("methodReflected");
method.invoke(target);
@CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

看下面兩個圖片,reflectionFactory是Method、Field、Constructor等類的父類AccessibleObject中的靜態(tài)變量,在AccessibleObject初始化的時候就已經(jīng)存在了,是由多線程共享的一個變量,所以在下圖執(zhí)行到reflectionFactory.newMethodAccessor(this)的時候可能會出現(xiàn)多線程同時執(zhí)行的該方法,好在的是該方法我們上面分析過,內(nèi)部都是無狀態(tài)的變量?,F(xiàn)在假如有1000個線程同時調(diào)用了該方法,線程執(zhí)行到該方法的時候就會在內(nèi)存中通過asm生成1000份字節(jié)碼和1000個MagicAccessorImpl的類及類加載器。這些類會占用方法區(qū)哦,不同的虛擬機回收策略可能會有所不同,所以這塊可能會有性能問題。這里不要被1000個線程誤導(dǎo),如果緩存的是NativeAccessorImpl即使是一個線程執(zhí)行1000次也會出現(xiàn)同樣的結(jié)果!


image.png

image.png

總結(jié):

  • 1、反射獲取的Field、Method和Constructor等除了第一次都是從一個軟引用的緩存中獲取的拷貝
  • 2、invoke方法內(nèi)部也使用了緩存技術(shù)緩存執(zhí)行invoke方法的Accessor對象,而且內(nèi)部有種機制叫inflation,即在前15次執(zhí)行invoke的時候會調(diào)用native,之后通過字節(jié)碼技術(shù)創(chuàng)建MagicAccessorImpl對象執(zhí)行invoke。inflation的執(zhí)行次數(shù)閾值是可以設(shè)置的。
  • 3、以上!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容