設計模式 Proxy

什麼是代理

# 增強一個對象的功能
# 買火車票,App 就是一個代理,他代理了火車站,小區當中的代售窗口
# Java 當中如何實現代理

Java 實現的代理的兩種辦法

代理的名詞

代理對象 ===> 增強後的對象
目標對象 ===> 被增強的對象
他們不是絕對的,會根據情況發生變化

靜態代理

1、繼承
代理對象繼承目標對象,重寫需要增強的方法;
缺點:會代理類過多,非常複雜
​
2、聚合
目標對象和代理對象實現同一個接口,代理對象當中要包含目標對象。
缺點:也會產生類爆炸,只不過比繼承少一點點
​
# 總結:如果在不確定的情況下,儘量不要去使用靜態代理。因爲一旦你寫代碼,就會產生類,一旦產生類就爆炸。

動態代理

自己模擬 JDK 動態代理

# 不需要訂閱專欄手動創建類文件(因爲一旦手動創建類文件,就會產生類爆炸),通過接口反射生成一個類文件,然後調用第三方的編譯技術,動態編譯這個產生的類文件成class文件,繼而利用UrlclassLoader(因爲這個動態產生的class不在工程當中所以需要使用UrlclassLoader)把這個動態編譯的類加載到jvm當中,最後通過反射把這個類實例化。
​
缺點:首先要生成文件
缺點:動態編譯class文件
缺點:需要一個 URLclassloader
軟件性能的最終體現在 IO 操作

接口:FutureDao

/**
 * @description: 接口
 * @author: Mr.Li
 * @date: Created in 2020/7/1 15:42
 * @version: 1.0
 * @modified By:
 */
public interface FutureDao {
​
    public void query();
    
    public String returnString();
}

實現類:FutureDaoImpl

/**
 * @description: 實現類
 * @author: Mr.Li
 * @date: Created in 2020/7/1 15:43
 * @version: 1.0
 * @modified By:
 */
public class FutureDaoImpl implements FutureDao {
​
    @Override
    public void query(){
        System.out.println("FutureLL");
​
    }
    
    @Override
    public String returnString() {
        return "Return String";
    }
}

自己實現動態代理:ProxyUtil

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.net.URL;
import java.net.URLClassLoader;
​
/**
 * @description: 自己實現動態代理
 * @author: Mr.Li
 * @date: Created in 2020/7/1 13:19
 * @version: 1.0
 * @modified By:
 */
public class ProxyUtil {
​
    /**
     * content --->string
     * .java  io
     * .class
     * <p>
     * .new   反射----》class
     *
     * @return
     */
    public static Object newInstance(Object target) {
​
        // 定義代理對象
        Object proxy = null;
        // getClass(): Returns the runtime class of this {@code Object}.
        // getInterfaces(): Determines the interfaces implemented by the class or interface represented by this object.
        Class targetInf = target.getClass().getInterfaces()[0];
        Method methods[] = targetInf.getDeclaredMethods();
        String line = "\n";
        String tab = "\t";
        // getSimpleName(): Returns the simple name of the underlying class as given in the source code.
        // infName = "FutureDao";
        String infName = targetInf.getSimpleName();
        String content = "";
        String packageContent = "package com.google;" + line;
        // Returns the name of the entity represented by this {@code Class} object, as a {@code String}.
        // targetInf.getName() = "com.futurell.dao.FutureDao"
        String importContent = "import " + targetInf.getName() + ";" + line;
        String clazzFirstLineContent = "public class $Proxy implements " + infName + "{" + line;
        String filedContent = tab + "private " + infName + " target;" + line;
        String constructorContent = tab + "public $Proxy (" + infName + " target){" + line
                + tab + tab + "this.target = target;"
                + line + tab + "}" + line;
        String methodContent = "";
        for (Method method : methods) {
            // getReturnType(): Returns a Class object that represents the formal return type of the method represented by this {@code Method} object.
            String returnTypeName = method.getReturnType().getSimpleName();
            // getName(): Returns the name of the method represented by this {@code Method} object, as a {@code String}.
            String methodName = method.getName();
            // Sting.class String.class
            Class args[] = method.getParameterTypes();
            String argsContent = "";
            String paramsContent = "";
            int flag = 0;
            for (Class arg : args) {
                //getSimpleName(): Returns the simple name of the underlying class as given in the source code.
                String temp = arg.getSimpleName();
                // String
                // String p0,Sting p1,
                argsContent += temp + " p" + flag + ",";
                paramsContent += "p" + flag + ",";
                flag ++;
            }
            if (argsContent.length() > 0) {
                // lastIndexOf(): Returns the index within this string of the last occurrence of the specified substring.
                argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
                paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
            }
​
            methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line
                    + tab + tab + "System.out.println(\" Log \");" + line;
​
            if (returnTypeName.equals("void")) {
                methodContent += tab + tab + "target." + methodName + "(" + paramsContent + ");" + line
                        + tab + "}" + line;
            } else {
                methodContent += tab + tab + "return target." + methodName + "(" + paramsContent + ");" + line
                        + tab + "}" + line;
            }
        }
​
        content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";
​
        File file = new File("e:\\com\\google\\$Proxy.java");
        try {
            if (!file.exists()) {
                file.createNewFile();
            }
​
            FileWriter fw = new FileWriter(file);
            fw.write(content);
            fw.flush();
            fw.close();
​
            /**
             * 編譯過程如下:
             */
            // getSystemJavaCompiler(): Gets the Java; programming language compiler provided with this platform.
            // 得到編譯類,可以動態的編譯一些文件
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
​
            // getStandardFileManager(): Gets a new instance of the standard file manager implementation for this tool.
            // 編譯文件需要文件管理器
            StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
            // getJavaFileObjects(): Gets file objects representing the given files.
            // 把文件放到文件管理器中
            Iterable units = fileMgr.getJavaFileObjects(file);
​
            // getTask(): Creates a future for a compilation task with the given components and arguments.
            // JavaCompiler.CompilationTask: Interface representing a future for a compilation task.
            // 把文件管理器當成一個任務來執行
            JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
            // call(): Performs this compilation task.
            t.call();
            fileMgr.close();
​
            // URL[]: Class {@code URL} represents a Uniform Resource Locator, a pointer to a "resource" on the World Wide Web.
            URL[] urls = new URL[]{new URL("file:E:\\\\")};
            // URLClassLoader: This class loader(類加載器) is used to load classes and resources from a search path of URLs
            //                 referring(引用) to both JAR files(Java Archive, Java歸檔) and directories(目錄).
            // JAR: 通常用於聚合大量的Java類文件、相關的元數據和資源(文本、圖片等)文件到一個文件,以便開發Java平臺應用軟件或庫。
            URLClassLoader urlClassLoader = new URLClassLoader(urls);
            // loadClass(): Loads the class with the specified <a href="#name">binary name</a>.
            Class clazz = urlClassLoader.loadClass("com.google.$Proxy");
            // getConstructor(): Returns a {@code Constructor} object that reflects(反應) the specified
            //                   public constructor of the class represented by this {@code Class} object.
            // clazz這個類它有構造方法,只能以構造方法來new這個類的實例,所以需要先得到該類的構造方法
            Constructor constructor = clazz.getConstructor(targetInf);
​
            // newInstance(): Uses the constructor represented by this {@code Constructor} object to
            //                create and initialize a new instance of the constructor's
            //                declaring class, with the specified initialization parameters.
            // 通過構造方法來創建實例
            proxy = constructor.newInstance(target);
            // clazz.newInstance();
            // Class.forName()
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        /**
         * public UserDaoLog(UserDao target){
         *     this.target = target;
         * }
         */
        return proxy;
    }
}

測試類:Test

import com.futurell.dao.FutureDao;
import com.futurell.dao.FutureDaoImpl;
import com.futurell.proxy.ProxyUtil;
​
/**
 * @description: 測試
 * @author: Mr.Li
 * @date: Created in 2020/7/1 10:56
 * @version: 1.0
 * @modified By:
 */
public class Test {
​
    public static void main(String[] args) {
​
        FutureDao fProxy = (FutureDao) ProxyUtil.newInstance(new FutureDaoImpl());
        fProxy.query();
​
        System.out.println("------------------");
​
        FutureDao fProxyString = (FutureDao) ProxyUtil.newInstance(new FutureDaoImpl());
        System.out.println(fProxyString.returnString());
​
    }
}
/**
 *  輸出: 
 * ------------------
 *  Log 
 * FutureLL
 * ------------------
 *  Log 
 * Return String
 */

JDK 動態代理

接口和實現類與上述自定義相同

代理類:FutureInvocationHandler

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
​
/**
 * @description:
 * @author: Mr.Li
 * @date: Created in 2020/7/2 12:24
 * @version: 1.0
 * @modified By:
 */
public class FutureInvocationHandler implements InvocationHandler {
​
    Object target;
​
    public FutureInvocationHandler(Object target) {
        this.target = target;
    }
​
    /**
     *
     * @param proxy 代理對象
     * @param method 代理對象包含目標對象,目標對象中的方法,就是method
     * @param args 目標方法的參數
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy");
        return method.invoke(target, args);
    }
}

測試類:Test

import com.futurell.JDK.dao.FutureDao;
import com.futurell.JDK.dao.FutureDaoImpl;
import com.futurell.JDK.proxy.FutureInvocationHandler;
​
import java.lang.reflect.Proxy;
​
/**
 * @description: 測試
 * @author: Mr.Li
 * @date: Created in 2020/7/1 10:56
 * @version: 1.0
 * @modified By:
 */
public class Test {
​
    public static void main(String[] args) {
​
        FutureDao jdkProxy = (FutureDao) Proxy.newProxyInstance(
                // getClassLoader(): Returns the class loader for the class.
                // 判斷一個類是否相同,需要根據類加載器是否相同來判斷,爲了保證加載器可用,那麼傳入當前所在類的ClassLoader
                // 在自定義動態代理的代碼中,使用了URLClassLoader,因爲自定義的類不在工程當中
                Test.class.getClassLoader(),
                // Class targetInf = target.getClass().getInterfaces()[0];
                // 自定義使用了上邊的代碼,我們需要得到接口,用來得到接口中的方法,然後對方法進行代理
                new Class[]{FutureDao.class},
                new FutureInvocationHandler(new FutureDaoImpl()));
​
        jdkProxy.query();
        System.out.println("--------------");
        System.out.println(jdkProxy.returnString());
    }
}

CGLIB

如果是要實現 CGLIB,需要先添加 cglib 依賴,然後實現一個接口 MethodInterceptor,重寫 intercept() 方法

依賴

<!-- 使用CGLIB代理需要導入jar包 -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
  <scope>compile</scope>
</dependency>

代理類:MyMethodInterceptor

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
/**
 * 自定義MethodInterceptor
 */
public class MyMethodInterceptor implements MethodInterceptor{
 
    /**
     * sub:cglib生成的代理對象
     * method:被代理對象方法
     * objects:方法入參
     * methodProxy: 代理方法
     */
    @Override
    public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(sub, objects);
        System.out.println("======插入後者通知======");
        return object;
    }
}

JDK 底層原理

測試類:Test

FutureDao jdkProxy = (FutureDao) Proxy.newProxyInstance(
    // getClassLoader(): Returns the class loader for the class.
    // 判斷一個類是否相同,需要根據類加載器是否相同來判斷,爲了保證加載器可用,那麼傳入當前所在類的ClassLoader
    // 在自定義動態代理的代碼中,使用了URLClassLoader,因爲自定義的類不在工程當中
    Test.class.getClassLoader(),
    // Class targetInf = target.getClass().getInterfaces()[0];
    // 自定義使用了上邊的代碼,我們需要得到接口,用來得到接口中的方法,然後對方法進行代理
    new Class[]{FutureDao.class},
    new FutureInvocationHandler(new FutureDaoImpl())
);

測試類調用 Proxy 類的 newProxyInstance() 方法

/**
 * Returns an instance of a proxy class for the specified interfaces
 * that dispatches method invocations to the specified invocation
 * handler.
 */
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) throws IllegalArgumentException {
    
    Objects.requireNonNull(h);
​
    // 拿到實現類的接口,使用接口可以對接口中的方法進行處理
    final Class<?>[] intfs = interfaces.clone();
    // 系統做的安全驗證,這個不需要我們去管
    // getSecurityManager(): Gets the system security interface.
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }
​
    /*
     * Look up or generate the designated proxy class.
     * 找到或生成指定的代理類
     */
    Class<?> cl = getProxyClass0(loader, intfs);
​
    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
​
        // 通過 cl.getConstructor() 得到一個構造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        // 這裏將得到的構造方法 new 成一個對象
        // 所以我們需要看這裏 new 出來的對象是不是我們需要的代理對象
        // 首先複製 cons.newInstance(new Object[]{h}),鼠標右鍵點擊 Evaluate Expression,將複製的代碼粘貼回車,看到 result 的值就是我們的代理對象
        // 得到 cl 代理類,代理對象就輕易被 new 出來了,在我們的傳入的是 new FutureInvocationHandler(new FutureDaoImpl())
        // 所以最重要的是我們怎麼得到的 cl 代理類,進入 getProxyClass0() 這個方法
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

進入 getProxyClass0() 我們要知道如何得到的 cl 代理類

/**
 * Generate a proxy class.  Must call the checkProxyAccess method
 * to perform permission checks before calling this.
 */
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
​
    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    // 如果實現給定接口的給定加載器定義的代理類存在,這將簡單地返回緩存的副本;否則,它將通過 ProxyClassFactory 創建代理類
    // 這個方法只有一行代碼,我們進入 get() 方法
    return proxyClassCache.get(loader, interfaces);
}

進入 get() 方法

/**
 * Look-up the value through the cache. This always evaluates the
 * {@code subKeyFactory} function and optionally evaluates
 * {@code valueFactory} function if there is no entry in the cache for given
 * pair of (key, subKey) or the entry has already been cleared.
 */
public V get(K key, P parameter) {
    Objects.requireNonNull(parameter);
​
    expungeStaleEntries();
​
    Object cacheKey = CacheKey.valueOf(key, refQueue);
​
    // lazily install the 2nd level valuesMap for the particular cacheKey
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    if (valuesMap == null) {
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
            = map.putIfAbsent(cacheKey,
                              valuesMap = new ConcurrentHashMap<>());
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }
​
    // create subKey and retrieve the possible Supplier<V> stored by that
    // subKey from valuesMap
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;
​
    while (true) {
        if (supplier != null) {
            // supplier might be a Factory or a CacheValue<V> instance
            // 進入這個 get() 方法
            V value = supplier.get();
            if (value != null) {
                // 最終返回的是這個 value,我們不知道從哪裏看的時候,就找到結果從下往上反推即可
                // 向上看有一個 supplier.get() 它將返回值賦給了 value
                return value;
            }
        }
        // else no supplier in cache
        // or a supplier that returned null (could be a cleared CacheValue
        // or a Factory that wasn't successful in installing the CacheValue)
​
        // lazily construct a Factory
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }
​
        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                // successfully installed Factory
                supplier = factory;
            }
            // else retry with winning supplier
        } else {
            if (valuesMap.replace(subKey, supplier, factory)) {
                // successfully replaced
                // cleared CacheEntry / unsuccessful Factory
                // with our Factory
                supplier = factory;
            } else {
                // retry with current supplier
                supplier = valuesMap.get(subKey);
            }
        }
    }
}

進入另一個 get() 方法

@Override
public synchronized V get() { // serialize access
    // re-check
    Supplier<V> supplier = valuesMap.get(subKey);
    if (supplier != this) {
        // something changed while we were waiting:
        // might be that we were replaced by a CacheValue
        // or were removed because of failure ->
        // return null to signal WeakCache.get() to retry
        // the loop
        return null;
    }
    // else still us (supplier == this)
​
    // create new value
    // 這個方法我們可以看出,結果需要返回 value,而 value 最終的取值是在下邊的 try-finally 中取值
    V value = null;
    try {
        // 首先我們進入 apply() 方法
        value = Objects.requireNonNull(valueFactory.apply(key, parameter));
    } finally {
        if (value == null) { // remove us on failure
            valuesMap.remove(subKey, this);
        }
    }
    // the only path to reach here is with non-null value
    assert value != null;
​
    // wrap value with CacheValue (WeakReference)
    CacheValue<V> cacheValue = new CacheValue<>(value);
​
    // put into reverseMap
    reverseMap.put(cacheValue, Boolean.TRUE);
​
    // try replacing us with CacheValue (this should always succeed)
    if (!valuesMap.replace(subKey, this, cacheValue)) {
        throw new AssertionError("Should not reach here");
    }
​
    // successfully replaced us with new CacheValue -> return the value
    // wrapped by it
    // 返回 value
    return value;
}

進入 apply() 方法

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
​
    Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
    // 這裏的 interfaces 的所有接口只有一個 FutureDao,循環所有接口,將接口賦給 intf
    for (Class<?> intf : interfaces) {
        /*
         * Verify that the class loader resolves the name of this
         * interface to the same Class object.
         */
        Class<?> interfaceClass = null;
        try {
            // 這個有個點需要注意, intf 本身就是一個 Class,爲什麼這裏還要再次得到一個 Class
            // 判斷對象是否相同的一個前提,是否是同一個類加載器
            // 原因: 這裏要判斷兩個接口是不是同一個接口,
            interfaceClass = Class.forName(intf.getName(), false, loader);
        } catch (ClassNotFoundException e) {
            
        }
        // 如果兩個不相同那麼拋出異常: IllegalArgumentException,相等繼續執行後面的代碼
        if (interfaceClass != intf) {
            throw new IllegalArgumentException(
                intf + " is not visible from class loader");
        }
        /*
         * Verify that the Class object actually represents an
         * interface.
         */
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException(
                interfaceClass.getName() + " is not an interface");
        }
        /*
         * Verify that this interface is not a duplicate.
         */
        if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
            throw new IllegalArgumentException(
                "repeated interface: " + interfaceClass.getName());
        }
    }
​
    String proxyPkg = null;     // package to define proxy class in
    int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
​
    /*
     * Record the package of a non-public proxy interface so that the
     * proxy class will be defined in the same package.  Verify that
     * all non-public proxy interfaces are in the same package.
     */
    for (Class<?> intf : interfaces) {
        int flags = intf.getModifiers();
        // 如果這個接口不是 public,那麼引用的時候會有問題,這裏需要做一個轉換
        if (!Modifier.isPublic(flags)) {
            accessFlags = Modifier.FINAL;
            String name = intf.getName();
            int n = name.lastIndexOf('.');
            String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
            if (proxyPkg == null) {
                proxyPkg = pkg;
            } else if (!pkg.equals(proxyPkg)) {
                throw new IllegalArgumentException(
                    "non-public interfaces from different packages");
            }
        }
    }
​
    if (proxyPkg == null) {
        // if no non-public proxy interfaces, use com.sun.proxy package
        proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
    }
​
    /*
     * Choose a name for the proxy class to generate.
     */
    long num = nextUniqueNumber.getAndIncrement();
    // 這裏給 proxyPkg 加一個標識,防止併發的情況下類名相同
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
​
    /*
     * Generate the specified proxy class.
     * 生成指定的代理類的二進制文件,
     */
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
    
    try {
        // 我們知道一個對象的產生順序: 
        // .java ---> .class ---ClassLoader---> JVM ---byte[]---> object
        // 而這裏需要返回一個 Class,那麼將二進制文件轉換成對象就在 defineClass0() 方法中進行
        return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        /*
         * A ClassFormatError here means that (barring bugs in the
         * proxy class generation code) there was some other
         * invalid aspect of the arguments supplied to the proxy
         * class creation (such as virtual machine limitations
         * exceeded).
         */
        throw new IllegalArgumentException(e.toString());
    }
}

進入 defineClass0() 方法

// 這個方法是一個被 native 修飾的方法是一個本地方法,也就是一個 Java 調用非 Java 代碼的接口,再開發 JDK 的時候底層使用了 C 或 C++ 開發(可以去看一下 OpenJDK),也就是說 C 和 C++ 負責把 byte[] 變成 Class 對象
private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

總結:

# 通過接口反射得到字節碼,然後把字節碼轉換成 Class,就得到了我們想要的對象

有興趣的同學可以關注我的個人公衆號,期待我們共同進步!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章