第三章 反射 && 註解 && 依賴注入

反射

簡介 & 功能 & 應用場景

  • Java反射機制定義
    Java 反射機制是在運行狀態中,對於任意一個類,都能夠獲得這個類的所有屬性和方法;對於任意一個對象都能夠調用它的任意一個屬性和方法。這種在運行時動態的獲取類信息以及動態調用對象的方法的功能稱爲Java 的反射機制。
  • Java 反射機制的功能
  1. 在運行時判斷任意一個對象所屬的類。
  2. 在運行時構造任意一個類的對象。
  3. 在運行時判斷任意一個類所具有的成員變量和方法。
  4. 在運行時調用任意一個對象的方法。
  5. 生成動態代理。
  • Java 反射機制的應用場景
  1. 逆向代碼 ,例如反編譯
  2. 與註解相結合的框架 例如Retrofit
  3. 單純的反射機制應用框架 例如EventBus
  4. 動態生成類框架 例如Gson

基本使用

通過Java反射查看類信息

(1)獲取Class類對象——描述.class字節碼文件
每個類被加載之後,系統就會爲該類生成一個對應的Class對象。通過該Class對象就可以訪問到JVM中的這個類。
在Java程序中獲得Class對象通常有如下三種方式:

  1. 使用Class類的forName(String clazzName)靜態方法。該方法需要傳入字符串參數,該字符串參數的值是某個類的全限定名(必須添加完整包名)。
  2. 調用某個類的class屬性來獲取該類對應的Class對象。
  3. 調用某個對象的getClass()方法。該方法是java.lang.Object類中的一個方法。
//第一種方式 通過Class類的靜態方法——forName()來實現
class1 = Class.forName("com.lvr.reflection.Person");
//第二種方式 通過類的class屬性
class1 = Person.class;
//第三種方式 通過對象getClass方法
Person person = new Person();
Class<?> class1 = person.getClass();

(2)類成員變量的反射

  1. 獲取類成員變量:
  • Field[] getFields():獲取所有public 修飾的成員變量
  • Field getField(String name):獲取指定名稱的public修飾的成員變量
  • Field[] getDeclaredFields():獲取所有的成員變量,不考慮修飾符
  • Filed getDeclaredField(String name):獲取指定名稱的成員變量,不考慮修飾符
  1. Filed:成員變量
  • get(Object object) :獲取值
  • void set(Object obj, Object value):設置值
  • setAccessible(true) :忽略訪問權限修飾符的安全檢查,用於暴力反射,修改私有成員變量的值
Field[] allFields = class1.getDeclaredFields();//獲取class對象的所有屬性
Field[] publicFields = class1.getFields();//獲取class對象的public屬性
Field ageField = class1.getDeclaredField("age");//獲取class指定屬性
Field desField = class1.getField("des");//獲取class指定的public屬性

(3)類成員方法的反射

  1. 獲取類成員方法:
  • Method[] getMethods():獲取所有public修飾的成員方法
  • Method getMethod(String name,類<?>… parameterTypes):獲取指定的public修飾的成員方法,name 爲方法名,parameterTypes爲參數列表(重載)
  • Method[] getDeclaredMethods():獲取所有成員方法
  • Method getDeclaredMethod(String name,類<?>… parameterTypes):獲取指定的成員方法,name 爲方法名,parameterTypes爲參數列表(重載)
  1. Method
  • invoke(obj … args):執行方法
  • setAccessible(true) :忽略訪問權限修飾符的安全檢查,用於暴力反射,修改私有成員方法的值
Method[] methods = class1.getDeclaredMethods();//獲取class對象的所有聲明方法
Method[] allMethods = class1.getMethods();//獲取class對象的所有public方法 包括父類的方法
Method method = class1.getMethod("info", String.class);//返回次Class對象對應類的、帶指定形參列表的public方法
Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回次Class對象對應類的、帶指定形參列表的方法

(4)類構造方法的反射

  1. 獲取類構造方法:
  • Constructor[] getConstructors():獲取public修飾的構造方法
  • Constructor getConstructor(類<?>… parameterTypes):獲取指定的public修飾的構造方法(構造方法的方法名 = 類名),parameterTypes爲參數列表
  • Constructor[] getDeclaredConstructors():獲取所有構造方法
  • Constructor getDeclaredConstructor(類<?>… parameterTypes):獲取指定的構造方法,name爲方法名(構造方法的方法名 = 類名),parameterTypes爲參數列表
  1. Constructor:構造方法
  • T.newInstance(Object… init args):創建對象
  • Class.newInstance():如果使用空參數構造方法創建對象,操作可以簡化爲:Class對象的newInstance方法
  • setAccessible(true) :忽略訪問權限修飾符的安全檢查,用於暴力反射,修改私有構造方法的值
Constructor<?>[] allConstructors = class1.getDeclaredConstructors();//獲取class對象的所有聲明構造函數
Constructor<?>[] publicConstructors = class1.getConstructors();//獲取class對象public構造函數
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//獲取指定聲明構造函數
Constructor publicConstructor = class1.getConstructor(String.class);//獲取指定聲明的public構造函數

通過Java反射生成並操作對象

  • 生成類的實例對象
//第一種方式 Class對象調用newInstance()方法生成
Object obj = class1.newInstance();
//第二種方式 對象獲得對應的Constructor對象,再通過該Constructor對象的newInstance()方法生成
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//獲取指定聲明構造函數
obj = constructor.newInstance("hello");
  • 調用類的方法
// 生成新的對象:用newInstance()方法
Object obj = class1.newInstance();
//首先需要獲得與該方法對應的Method對象
Method method = class1.getDeclaredMethod("setAge", int.class);
//開啓調用該方法的權限
method.setAccessible(true);
//調用指定的函數並傳遞參數
method.invoke(obj, 28);
  • 訪問成員變量值
//生成新的對象:用newInstance()方法 
Object obj = class1.newInstance();
//獲取age成員變量
Field field = class1.getField("age");
//將obj對象的age的值設置爲10
field.setInt(obj, 10);
//獲取obj對象的age的值
field.getInt(obj);

Java反射機制 與 動態代理

第七章 設計模式 —— 動態代理

  • 靜態代理
    在程序運行前就已經存在代理類的.class文件,已經確定代理類和委託類的關係。
  • 動態代理
    通過動態代碼可實現對不同類、不同方法的代理。動態代理的源碼是在程序運行期間由JVM根據反射等機制動態的生成,所以不存在代理類的字節碼文件(.class)。代理類和委託類的關係在程序運行時確定。
    動態代理與靜態代理相比較,最大的好處是接口中聲明的所有方法都被轉移到調用處理器一個集中的方法中處理(InvocationHandler.invoke)。而且動態代理提高了軟件系統的可擴展性,因爲Java 反射機制可以生成任意類型的動態代理類。(而每個靜態代理只能爲一個接口服務)
  • 實現原理 —— 反射
  1. 動態代理類只能代理接口,需創建一個實現接口InvocationHandler的調用處理器,它必須實現invoke方法。invoke方法是調用代理接口所有方法都要調用,返回值是被代理接口的一個實現類(動態代理類)。
public class LogHandler implements InvocationHandler {  
  
    // 目標對象  
    private Object targetObject;  
    //綁定關係,也就是關聯到哪個接口(與具體的實現類綁定)的哪些方法將被調用時,執行invoke方法。              
    public Object newProxyInstance(Object targetObject){  
        this.targetObject=targetObject;  
        //該方法用於爲指定類裝載器、一組接口及調用處理器生成動態代理類實例    
        //第一個參數指定產生代理對象的類加載器,需要將其指定爲和目標對象同一個類加載器  
        //第二個參數要實現和目標對象一樣的接口,所以只需要拿到目標對象的實現接口  
        //第三個參數表明這些被攔截的方法在被攔截時需要執行哪個InvocationHandler的invoke方法  
        //根據傳入的目標返回一個代理對象  
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),  
                targetObject.getClass().getInterfaces(),this);  
    }  
    @Override  
    //關聯的這個實現類的方法被調用時將被執行  
    /*InvocationHandler接口的方法,proxy表示代理,method表示原對象被調用的方法,args表示方法的參數*/  
    public Object invoke(Object proxy, Method method, Object[] args)  
            throws Throwable {  
        System.out.println("start-->>");  
        for(int i=0;i<args.length;i++){  
            System.out.println(args[i]);  
        }  
        Object ret=null;  
        try{  
            /*原對象方法調用前處理日誌信息*/  
            System.out.println("satrt-->>");  
              
            //調用目標方法  
            ret=method.invoke(targetObject, args);  
            /*原對象方法調用後處理日誌信息*/  
            System.out.println("success-->>");  
        }catch(Exception e){  
            e.printStackTrace();  
            System.out.println("error-->>");  
            throw e;  
        }  
        return ret;  
    }  
}  
  1. 創建接口及具體實現類(委託類)
// 接口
public interface UserManager{
	public void addUser(String userId, String userName);
	public void delUser(String userId) ;
	public String findUser(String userId) ;
	public void modifyUser(String userId, String userName) ;
}
// 實現類(委託類)
public class UserManagerImpl implements UserManager {  
  
    @Override  
    public void addUser(String userId, String userName) {  
        System.out.println("UserManagerImpl.addUser");  
    }  
  
    @Override  
    public void delUser(String userId) {  
        System.out.println("UserManagerImpl.delUser");  
    }  
  
    @Override  
    public String findUser(String userId) {  
        System.out.println("UserManagerImpl.findUser");  
        return "張三";  
    }  
  
    @Override  
    public void modifyUser(String userId, String userName) {  
        System.out.println("UserManagerImpl.modifyUser");  
  
    }  
}  
  1. 通過動態代理類調用Proxy的靜態方法newProxyInstance,提供ClassLoader和代理接口類型數組動態創建一個代理類,並通過代理調用方法
public class Client {  
  
    public static void main(String[] args){  
        LogHandler logHandler=new LogHandler();  
        UserManager userManager=(UserManager)logHandler.newProxyInstance(new UserManagerImpl());  
        // 獲取動態代理對象
        userManager.addUser("1111", "張三");  // 調用動態代理的addUser方法,該調用會轉發到logHandler的invoke上,從而達到動態代理的效果
    }  
}  

可以看到,我們可以通過LogHandler代理不同類型的對象,如果我們把對外的接口都通過動態代理來實現,那麼所有的函數調用最終都會經過invoke函數的轉發,因此我們就可以在這裏做一些自己想做的操作,比如日誌系統、事務、攔截器、權限控制等。這也就是AOP(面向切面編程)的基本原理。

AOP(AspectOrientedProgramming):將日誌記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行爲的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行爲的時候不影響業務邏輯的代碼—解耦。

Java反射機制 與 泛型

反射在項目中的應用

應用一:簡單工廠創建對象

  • 需求
    通過一個工廠類創建不同類型的實例。
  • 解決方案
  1. 在配置文件中配置需要創建對象的信息
  2. 通過類加載器加載配置文件
  3. 獲取類對象,通過類的全限定名創建實例對象
  • 代碼
public class BasicFactory {
    private BasicFactory(){}

    private static BasicFactory bf = new BasicFactory();
    private static Properties pro = null;

    static{
        pro = new Properties();
        try{    
            //通過類加載器加載配置文件
            pro.load(new FileReader(BasicFactory.class.getClassLoader().
                    getResource("config.properties").getPath()));
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static BasicFactory getFactory(){
        return bf;
    }

    //使用泛型獲得通用的對象
    public  <T> T newInstance(Class<T> clazz){
        String cName = clazz.getSimpleName();   //獲得字節碼對象的類名
        String clmplName = pro.getProperty(cName);   //根據字節碼對象的類名通過配置文件獲得類的全限定名

        try{
            return (T)Class.forName(clmplName).newInstance();   //根據類的全限定名創建實例對象
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

應用二:過濾符合輸入關鍵字的數據

  • 需求
    輸入關鍵字檢索通過網絡請求加載好的列表數據,過濾出符合過濾條件(包含關鍵字)的數據項。
  • 爲何使用反射
    一般解決方案爲,遍歷存放數據的List,然後每項對比關鍵字,將過濾後的數據存入一個新的List中,返回給RecyclerView的Adapter進行數據刷新。
    有2個問題:
  1. List獲取的數據由於業務不同,其泛型也不同,且泛型沒有關聯性
  2. 每一個泛型都是一個Bean,要對Bean中的屬性盡興過濾,屬性是不確定的
    由於列表數據過濾在項目中很常用,幾乎每個列表項都有過濾功能,如果對於每種列表都採用硬編碼進行封裝,代碼的複用性很低,且項目不易維護。
  • 解決方案
    採用反射機制
    主要是getClassInfo獲取某個類需要過濾的數據項,並與關鍵字對比並過濾。
  • 代碼
public class ListFilter<T> {

    /**
     * 過濾ArrayList中的關鍵字數據
     * @param models 網絡獲取到的數據列表
     * @param query 過濾關鍵字
     * @param propertyName 泛型的數據過濾項
     * @return 返回過濾後的數據
     */
    public ArrayList<T> filter(ArrayList<T> models, String query, String propertyName) {

        ArrayList<T> filteredModelList = null;
        //實例化這個類賦給o
        try {
            query = query.toLowerCase();
            filteredModelList = new ArrayList<>();
            for (T model : models) {
                final String text = getClassInfo(model,model.getClass().getName(),propertyName).toLowerCase();
                if (text.contains(query)) {
                    filteredModelList.add(model);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return filteredModelList;
    }

    /**
     *  獲取類的屬性值
     * @param obj 類實例
     * @param classNameString 類名
     * @param propertyNameString 獲取的屬性名
     * @return  屬性值
     */
    private  String getClassInfo(Object obj,String classNameString,String propertyNameString) {
        String returnString="";
        try{
            Class classInfo = Class.forName(classNameString);
            if(!(classInfo.isInstance(obj))){
                L.e("傳入的java實例與配置的java對象類型不符!");
                return returnString;
            }
            Field field = classInfo.getDeclaredField(propertyNameString);
            field.setAccessible(true);
            returnString=field.get(obj).toString();
        }catch(Exception e){
            e.printStackTrace();
        }
        return returnString;
    }
}

反射效率低 原因 & 解決

導致反射效率慢的因素及解決:

  1. 獲取Class對象時使用Class.forName效率低,耗時。把Class.forName返回的Class對象緩存起來,下一次使用的時候直接從緩存裏面獲取,這樣就極大的提高了獲取Class的效率。
  2. 同理,在項目啓動時將反射需要的相關配置和數據(Field、Method、Constructor等)加載進內存,在運行時直接從緩存中取這些元數據進行反射操作。
  3. 調用method.setAccessible(true)
    jdk在設置獲取字段,調用方法的時候會執行安全訪問檢查,而此類操作會比較耗時,所以通過setAccessible(true)的方式可以關閉安全檢查,從而提升反射效率。
  4. 採用高性能的反射工具包,如ReflectASM。
  5. 使用高版本JDK,提高反射性能。
  6. 反射效率慢,但速度也是可以接受的。所以對於反射不應該因爲速度慢而對其"望而卻步"。

註解(Annotation)

簡介

註解(元數據)是一種代碼級別的說明。是JDK 5.0 後引入的新特性。註解作爲程序的元數據嵌入到程序中,聲明在類、成員變量、成員方法等前面,用來對這些元素進行說明,註釋。註解可以被解析工具/編譯工具解析。
Annotation的作用可分爲3類:

  1. 編寫文檔:通過代碼裏標識的註解生成文檔
  2. 代碼分析:通過反射獲取註解信息並對代碼進行分析
Class<ReflectTest> reflectTestClass = ReflectTest.class;	// 1.通過反射獲取字節碼文件對象
Pro an = reflectTestClass.getAnnotation(Pro.class);	// 2.調用getAnnotation(class)獲取註解對象
String className = an.className();
String methodName = an.methodName();	// 3.調用註解對象中定義的抽象方法,獲取返回值(返回值即註解信息)
  1. 編譯檢查:如@override 用於檢測被該註解標註的方法是否是繼承自父類(接口)

工作機制

註解本質是一個繼承了Annotation 的特殊接口,其具體實現類是Java 運行時生成的動態代理類。而我們通過反射獲取註解時,返回的是Java 運行時生成的動態代理對象$Proxy1。通過代理對象調用自定義註解(接口)的方法,會最終調用AnnotationInvocationHandler 的invoke方法。該方法會從memberValues 這個Map 中索引出對應的值。而memberValues 的來源是Java 常量池。

JDK註解 / 自定義註解

  • JDK —— 編譯檢查
  1. @Override
    用於檢測被該註解標註的方法是否是繼承自父類(接口)的。
    如果某個方法帶有該註解但並沒有覆寫超類相應的方法,則編譯器會生成一條錯誤信息。如果父類沒有這個要覆寫的方法,則編譯器也會生成一條錯誤信息。
    @Override可適用元素爲方法,僅僅保留在java源文件中。
  2. @Deprecated
    用於標註已經過時的方法。
    用於告知編譯器,某一程序元素(比如方法,成員變量)不建議使用了(即過時了)。編譯器會出現警告。
    @Deprecated可適合用於除註解類型聲明之外的所有元素,保留時長爲運行時。
  3. @SuppressWarnnings
    用於通知java編譯器禁止特定的編譯警告。
    @SuppressWarnings可適合用於除註解類型聲明和包名之外的所有元素,僅僅保留在java源文件中。
  • 自定義註解 —— 代碼分析/編寫文檔
  • 自定義註解的格式
元註解
public @interface 註解名稱{
	... 屬性列表
}

// 舉例
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotataion{
    String name();
    String website() default "hello";
    int revision() default 1;
}

public class AnnotationDemo {
// 當註解中有成員變量時,若沒有默認值,需要在使用註解時,指定成員變量的值。
	@MyAnnotataion(name="lvr", website="hello", revision=2)
    public void demo(){
        System.out.println("I am demo method");
    }
}
  • 元註解
    通過元註解註解其他註解。Java提供了4個標準元註解:
    (1)Target:描述註解作用位置,如CONSTRUCTOR、FIELD、METHOD、TYPE等等
    (2)Retention:描述註解保留的階段,包括SOURCE、CLASS、RUNTIME
    (3)Documented:描述註解是否被抽取到生成的API文檔中
    (4)Inherited:描述註解是否被子類繼承
  • 屬性列表
    接口中的抽象方法被稱爲註解的屬性。每一個抽象方法實際上是聲明瞭一個配置參數。方法的名稱就是參數的名稱,返回值類型就是參數類型。

如何解析註解

  • 通過反射技術解析自定義註解
    關於反射類位於包java.lang.reflect,其中有一個接口AnnotatedElement。該接口定義了註釋相關的幾個核心方法,如下:
    在這裏插入圖片描述
    因此,當獲取了某個類的Class對象,然後獲取其Field,Method等對象,通過上述4個方法提取其中的註解,然後獲得註解的詳細信息。
public class AnnotationParser {
    public static void main(String[] args) throws SecurityException, ClassNotFoundException {
        String clazz = "com.lvr.annotation.AnnotationDemo";
        Method[]  demoMethod = AnnotationParser.class
                .getClassLoader().loadClass(clazz).getMethods();

        for (Method method : demoMethod) {
            if (method.isAnnotationPresent(MyAnnotataion.class)) {
                 MyAnnotataion annotationInfo = method.getAnnotation(MyAnnotataion.class);
                 System.out.println("method: "+ method);
                 System.out.println("name= "+ annotationInfo.name() +
                         " , website= "+ annotationInfo.website()
                        + " , revision= "+annotationInfo.revision());
            }
        }
    }
}
  • 註解解析案例:實現反射動態加載類
  1. 通過反射獲取註解定義位置的對象(Class,Method,Field)
  2. 獲取指定的註解:getAnnotation(Class)其實就是在內存中生成了一個該註解接口的子類實現對象
  3. 調用註解中的抽象方法獲取配置的屬性值

Pro.java


//註解配置文件:描述需要執行的類名和方法名
@Target ({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro{
	String className();
	String methodName();
}

ReflectTest.java

public class ReflectTest{
		public static void main(String[] args) throws Exception{
		// 前提:不能改變該類的任何代碼,可以創建任意類的對象,可以執行任意方法
		// 1. 解析註解
		// 1.1 獲取該類的字節碼文件對象
		Class<ReflectTest> reflectTestClass = ReflectTest.class;
		// 2.獲取上邊的註解對象,起始就是在內存中生成了一個註解接口的子類實現對象
		// public class ProImpl implements Pro{
		//	public String className(){
		//		return "cn.itcast.annotation.Demo1";
		// 	}
		// 	public String methodName(){
		//		return "show";
		//	}
		// }
		Pro an = reflectTestClass.getAnnotation(Pro.class);
		// 3.調用註解對象中定義的抽象方法,獲取返回值
		String className = an.className();
		String methodName = an.methodName();
		// 4. 加載該類進內存
		Class cls = Class.forName(className);
		// 5. 創建對象
		Object obj = cls.newInstance();
		// 6. 獲取方法對象
		Method method = cls.getMethod(methodName);
		// 7. 執行方法
		method.invoke(obj);
}

控制反轉(IOC)/依賴注入(DI)

  • 依賴(Dependency)
    依賴是類與類之間的相互聯繫,如人(Person)出行可以使用自行車Bike、轎車Car或火車(Train),因此Person類可以依賴Bike類、Car類和Train類。
    每次出行都需要修改Person類代碼。
public class Person {

    private Bike mBike;
    private Car mCar;
    private Train mTrain;

    public Person(){
        mBike = new Bike();
        // mCar = new Car();
        // mTrain = new Train();
    }

    public void goOut(){
        System.out.println("出遊");
        mBike.drive();
        // mCar.drive();
        // mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
  • 依賴反轉(IOC)
    IOC對上層模塊與底層模塊進行了進一步的解耦。控制反轉的意思是反轉了上層模塊對於底層模塊的依賴控制。
public class Person {

    private Driveable mDriveable;

    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    public void goOut(){
        System.out.println("出門啦");
        mDriveable.drive();
        // mCar.drive();
        // mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
  • 依賴注入(DI)
    DI是指在外部將依賴實例化並賦值給IOC容器。依賴注入有3種方式:構造函數注入、setter方式注入、接口注入。
public interface DepedencySetter {
    void set(Driveable driveable);
}
public class Person2  implements DepedencySetter {

    //接口方式注入
    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }

    private Driveable mDriveable;

    //構造函數注入
    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    //setter 方式注入
    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }

    public void goOut(){
        System.out.println("出門啦");
        mDriveable.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}

ButterKnife用法 & 原理?

ButterKnife:是視圖注入中相對簡單易懂的開源框架,其優勢在於:

  1. 強大的View綁定和Click事件處理功能,簡單代碼,提高開發效率
  2. 方便的處理Adapter和ViewHolder綁定問題
  3. 提高APP運行效率,使用配置方便
  4. 代碼清晰,可讀性強
@InjectView(R.id.listview)
ListView mListview;
@OnItemClick(R.id.listview)
public void onItemClick(int position){
     Toast.makeText(getBaseContext(), "item"+position, Toast.LENGTH_SHORT).show();
}
發佈了37 篇原創文章 · 獲贊 7 · 訪問量 5254
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章