spring源碼學習(一)——簡單認識一下bean的註冊

源碼的導入

源碼的導入其實比較簡單,但是,就是有點費時間,spring的代碼本身又是gradle構建的,因此有些小坑。之前寫過一個構建spring源碼的博客,大致步驟不差——傳送門:spring 源碼的控制檯構建 當時利用控制檯構建的時候,也踩了不少坑,這裏不再使用這種方式,而是直接使用idea構建。

下載源碼的操作和原來一樣,只是下載完之後,不再dos窗口中構建源碼了,直接在idea中構建gradle的依賴。這裏就不再贅述了。

構建花費時間很長(我的網速巨垃圾,構建了很多次,每次構建一部分,後續接着構建)。構建完成之後,自己就在源碼的模塊下建立一個新的gradle模塊,然後加入下面的實例(開始的實例部分)之後,我們就可以開始我們的閱讀了。不過這裏有幾個坑

1、提示kotlin版本不支持,由於我用的idea版本時2018的,而spring源碼的版本時5.1.X的,運行的時候會提示不支持這個kotlin插件的版本,這就需要我們update一下kotlin插件的版本(可以直接去idea的插件官網下載,然後再導入idea安裝,也可直接在idea中更新前提是如果你能忍受這個網速的話,個人建議直接去idea的插件官網下載,下載地址:idea插件官網中關於kotlin版本的下載列表

2、提示什*instrument無法解析的問題,這個需要在spring-context模塊下將instrument的optional類依賴,改成compile的類型,如下所示:

在這裏插入圖片描述

之後纔可以開始我們的實例(這些坑都親自踩過,給出的解決方法親測有效,但是針對第二個問題好像還有些不同的解決方法,這裏就不深入列出了)。

開始的實例

1、先就在構建的spring源碼中建一個模塊
在這裏插入圖片描述
主要的測試類,easy到爆的東西

/**
 * autor:liman
 * createtime:2020/3/6
 * comment:
 */
public class TestConfig {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext annotationConfigApplicationContext =
				new AnnotationConfigApplicationContext(AppConfigTest.class);

		TestBean bean = annotationConfigApplicationContext.getBean(TestBean.class);
		bean.test();
	}
}

我們自己定義的一個簡單的bean

@Service
public class TestBean {

	public void test(){
		System.out.println("test");
	}

}

直接運行一下實例,如果順利輸出,表示我們spring的源碼構建順利,可以開始浪了。

基於註解的bean註冊

有很多源碼大多都是基於ClassPathXmlApplicationContext來進行源碼解析,其實都差不多,這個和我們上面實例中用的AnnotationConfigApplicationContext一樣都是ApplicationContext的一個具體的實現類。但是在真正開始源碼的過程之前,我們還是梳理一下各個類的關係。

ApplicationContext各個類的關係

先簡單上個圖(請點擊圖片觀看,採用idea的快捷鍵Ctrl+Shift+Alt+U自動生成)。當我看到這個圖的時候,我的內心是崩潰的,實在不知道該從何處下手,spring源碼想要總結清楚確實不容易。各個類之間的依賴關係確實比較複雜,有時候不理清楚,看源碼就會陷入”我是誰,我在哪兒,我爲什麼要看這玩意“的哲學思考。

在這裏插入圖片描述

針對idea生成的類關係圖,各種亂七八糟的箭頭給出如下一個圖例:

在這裏插入圖片描述

從AnnotationConfigApplicationContext入手

先直接給出類圖,如下所示。這裏只是需要指出的是所謂的ApplicationContext容器其實繼承了BeanDefinitionRegistry(可以將bean註冊到容器中),也就是說容器本身也就是個註冊器。

在這裏插入圖片描述

AnnotationConfigApplicationContext中有如下兩個屬性

/**
 * 一個bean定義的讀取器,ClassPathBeanDefinitionScanner的替代方法,與註解的方式相同,但是可以用於顯示的註冊類
 */
private final AnnotatedBeanDefinitionReader reader;

/**
 * 一個用於掃描bean定義的掃描器
 */
private final ClassPathBeanDefinitionScanner scanner;

在構造函數構造了這兩個屬性,這兩個屬性一個用戶讀取配置,一個用於掃描bean。

//AnnotationConfigApplicationContext 94行
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
	this();//這裏構建了一個類的掃描器和一個BeanDefinitionReader的讀取器
	register(componentClasses); //這裏就是註冊一個bean,這裏是根據class進行註冊 --> 進入到AnnotaionConfigApplicationContext的第170行
	refresh();
}

//其中的this就會調用如下的構造方法
//AnnotationConfigApplicationContext 71行 這裏面的邏輯相當複雜,涉及到IOC工廠容器的初始化,這篇博客暫時不展開說
public AnnotationConfigApplicationContext() {
	//創建一個beanDefinition的讀取器——BeanDefinitionReader
	this.reader = new AnnotatedBeanDefinitionReader(this);
	//實例化了一個掃描器
	this.scanner = new ClassPathBeanDefinitionScanner(this);
}

註冊方法如下:

/**
 * AnnotationConfigApplicationContext 170行
 * 註冊單個bean給容器,比如有新加的類可以用這個方法,但是註冊之後需要手動調用refresh方法去觸發容器解析註解。
 * 這個方法可以註冊一個配置類,也可以註冊一個普通的bean
 */
@Override
public void register(Class<?>... componentClasses) {
	Assert.notEmpty(componentClasses, "At least one component class must be specified");

	//TODO:通過reader中的register方法去註冊類-->進入到AnnotationBeanDefinitionReader中的第137行
	this.reader.register(componentClasses);
}

通過this.reader.register(componentClasses)進入到AnnotationBeanDefinitionReader中這個類在AnnotationConfigApplicationContext的構造函數中已經初始化。

/**
 * TODO:這裏是reader中的註冊bean的方法
 */
public void register(Class<?>... componentClasses) {
	for (Class<?> componentClass : componentClasses) {
		registerBean(componentClass); // --> 這裏進入到AnnotationBeanDefinitionReader 第148行,第148行業只是一個過渡的,真正的註冊會進入到該類中的doRegisterBean中
	}
}

真正的註冊

層層解包之後,我們發現其實就是調用到doRegisterBean方法,這個方法中詳細的操作如下所示

// 這裏進入到AnnotationBeanDefinitionReader 第148行
/**
 * TODO:註解容器中的註冊bean的方法,最終會到這裏
 */
<T> void doRegisterBean(Class<T> beanClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name,
		@Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) {

	/**
	 * 實例化一個AnnotatedGenericBeanDefinition,並根據傳入過來的beanClass進行實例化
	 * TODO:根據指定的bean創建一個AnnotatedGenericBeanDefinition,這個AnnotatedGenericBeanDefinition可以理解爲一個擴展的BeanDefinition
	 */
	AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);

	//TODO:判斷這個類是否要跳過解析,如果要跳過解析直接返回。
	if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
		return;
	}

	abd.setInstanceSupplier(instanceSupplier);

	//TODO:開始解析bean的Scope的元數據,解析出scope的信息之後,直接放入到abd中
	ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
	abd.setScope(scopeMetadata.getScopeName());

	//TODO:生成beanName,beanNameGenerator這個是beanName的生成器,官網文檔中有介紹
	String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

	//TODO:處理bean中的通用屬性配置,lazy,primary,dependsOn,Role,Description等
	AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
	//TODO:處理qulifiers註解(通過annotatedApplicationContext構造方法進來的,這個qualifiers爲空)
	if (qualifiers != null) {
		for (Class<? extends Annotation> qualifier : qualifiers) {
			if (Primary.class == qualifier) {
				abd.setPrimary(true);
			}
			else if (Lazy.class == qualifier) {
				abd.setLazyInit(true);
			}
			else {
				abd.addQualifier(new AutowireCandidateQualifier(qualifier));
			}
		}
	}
	//TODO:對自定義註解的處理,這個並不重要
	for (BeanDefinitionCustomizer customizer : definitionCustomizers) {
		customizer.customize(abd);
	}

	//TODO:BeanDefinitionHolder裏頭就三個屬性,一個BeanDefinition,一個BeanName,一個String[] alias別名的數組
	BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
	//TODO:這裏涉及ScopedProxyMode,這個後面再說,需要結合spring mvc來理解
	definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

	//TODO:通過registry將beanDefinitionHolder註冊到底層容器中,本類中維護了一個registry的屬性
	BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

這裏需要說一下BeanDefinition了,上述實例化AnnotatedGenericBeanDefinition的時候,直接根據傳入的class來進行實例化,這個類的具體繼承關係圖如下,可以看到其本身就是一個BeanDefinition。BeanDefinition其實就是對bean的一個描述對象,這個對象中的各種屬性都標示了bean的一些信息,比如bean是否是單例,是否懶加載,以及bean所繼承的類都在beanDefinition中有所描述。

在這裏插入圖片描述

相關注冊的操作,在上述代碼中都通過註釋的方式寫明瞭,得到了AnnotatedGenericBeanDefinition之後,我們解析各個註解的值,根據不同的值進行不同的處理,期間有一個BeanDefinitionHolder,這個不用太關心就是一個過渡的數據結構。我們需要關注的就是最後一行。

/** AnnotationBeanDefinitionReader 第267行
這個使調用BeanDefinitionReaderUtils中的註冊beanDefinition的方法,同時傳入的是BeanDefinitionHolder和AnnotationBeanDefinitionReader類中的registry(這個是BeanDefinitionRegistry類型的,專門用於註冊bean的)
*/
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);

進入上述方法

/** BeanDefinitionReaderUtils 第160行
*/
public static void registerBeanDefinition(
		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
		throws BeanDefinitionStoreException {

	// Register bean definition under primary name.
	String beanName = definitionHolder.getBeanName();

	//TODO:調用的是BeanDefinitionRegistry接口中的registerBeanDefinition方法
	registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

	// Register aliases for bean name, if any.
	//TODO:根據別名來進行註冊,這裏可以暫時忽略
	String[] aliases = definitionHolder.getAliases();
	if (aliases != null) {
		for (String alias : aliases) {
			registry.registerAlias(beanName, alias);
		}
	}
}

其實底層調用的就是registry的registerBeanDefinition方法,這個方法是在BeanDefinitionRegsitry中定義的,回顧一下上述的類圖,我們發現所謂的容器,其實也繼承了這個BeanDefinitionRegsitry類,因此,上述的registerBeanDefinition方法底層調用的是DefaultListableBeanFactory中的registerBeanDefinition方法,如下所示:

// TODO:registry中的註冊bean方法會走到這裏
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
		throws BeanDefinitionStoreException {

	Assert.hasText(beanName, "Bean name must not be empty");
	Assert.notNull(beanDefinition, "BeanDefinition must not be null");

	if (beanDefinition instanceof AbstractBeanDefinition) {
		try {
			((AbstractBeanDefinition) beanDefinition).validate();
		}
		catch (BeanDefinitionValidationException ex) {
			throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
					"Validation of bean definition failed", ex);
		}
	}

	//TODO:從BeanDefinitionMap中根據beanName拿出beanDefinition
	BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
	if (existingDefinition != null) {//TODO:如果不爲空,表示已經註冊了,其實這一堆都不太重要,主要是一些校驗
		if (!isAllowBeanDefinitionOverriding()) { //TODO:如果不允許覆蓋,則拋出不再重複註冊的異常
			throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
		}
		else if (existingDefinition.getRole() < beanDefinition.getRole()) {
			// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
			if (logger.isInfoEnabled()) {
				logger.info("Overriding user-defined bean definition for bean '" + beanName +
						"' with a framework-generated bean definition: replacing [" +
						existingDefinition + "] with [" + beanDefinition + "]");
			}
		}
		else if (!beanDefinition.equals(existingDefinition)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Overriding bean definition for bean '" + beanName +
						"' with a different definition: replacing [" + existingDefinition +
						"] with [" + beanDefinition + "]");
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("Overriding bean definition for bean '" + beanName +
						"' with an equivalent definition: replacing [" + existingDefinition +
						"] with [" + beanDefinition + "]");
			}
		}
		this.beanDefinitionMap.put(beanName, beanDefinition);
	}
	else {//TODO :如果沒有註冊
		if (hasBeanCreationStarted()) {//TODO:如果已經開始創建
			// Cannot modify startup-time collection elements anymore (for stable iteration)
			synchronized (this.beanDefinitionMap) {
				this.beanDefinitionMap.put(beanName, beanDefinition);
				List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
				updatedDefinitions.addAll(this.beanDefinitionNames);
				updatedDefinitions.add(beanName);
				this.beanDefinitionNames = updatedDefinitions;
				removeManualSingletonName(beanName);
			}
		}
		else { //TODO:如果沒有開始創建,則將其放入到beanDefinitionMap中,同時將beanName放到beanDefinitionNames中
			// Still in startup registration phase
            //最最重要的兩個屬性,beanDefintioinMap,beanDefinitionNames
			this.beanDefinitionMap.put(beanName, beanDefinition);
			this.beanDefinitionNames.add(beanName);
			removeManualSingletonName(beanName);
		}
		this.frozenBeanDefinitionNames = null;
	}

	if (existingDefinition != null || containsSingleton(beanName)) {
		resetBeanDefinition(beanName);
	}
}

上述代碼現在只需要明白有兩個重要的屬性——beanDefinitionMapbeanDefinitionNames,註冊的工作就是往這beanDefinitionMap中添加了一個key=beanName,value=beanDefinition的元素,往beanDefnitionNames列表集合中添加了一個beanName元素。至此,bean的註冊工作完成。這兩個屬性在DefaultListableBeanFactory中的定義如下:

/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

/** List of bean definition names, in registration order. */
private volatile List<String> beanDefinitionNames = new ArrayList<>(256);

從這裏就可以看出,其實就是一個ConcurrentHashMap的數據結構,後來我們調試的時候,發現除了我們自己定義的bean之外,還有很多其他的bean,如下所示

在這裏插入圖片描述

具體的如下所示,這幾個類是spring自動爲我們注入的,但是何時注入的?我們需要知道。每一個都非常重要,這些梳理OK了,spring源碼算是初步理解了。這幾個類等到我們後面用到的時候,再詳細總結。

internalConfigurationAnnotationProcessor
internalEventListenerFactory
internalEventListenerProcessor
internalAutowiredAnnotationProcessor
internalCommonAnnotationProcessor

BeanPostProcessor

現在讓我們回到開始的地方,畢竟註冊已經完成了,之後我們看到的是這樣一行代碼。

/**
 * AnnotationConfigApplicationContext 第94行
 */
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
	this();//這裏構建了一個類的掃描器和一個BeanDefinitionReader的讀取器
	register(componentClasses); //這裏完成了註冊
	refresh(); // 這裏從這行代碼開始 -> 跳轉到AbstractApplicationContext 第515行
}

這個方法是IOC的核心方式之一

//TODO : 這個是spring ioc的核心之一 跳轉到AbstractApplicationContext 第515行
@Override
public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		//TODO :refresh容器的前記錄一些時間戳的信息,並不重要
		prepareRefresh();

		// Tell the subclass to refresh the internal bean factory.
		//TODO : 獲取當前容器對應的工廠ConfigurableListableBeanFactory
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

		// Prepare the bean factory for use in this context.
		//TODO:爲beanFactory的refresh構建一些類
		prepareBeanFactory(beanFactory); ->//這裏進入到 AbstractApplicationContext 第648行

		// ...... 後面的代碼暫時省略,畢竟這篇博客中暫時不總結
	}
}

AbstractApplicationContext 第648行

	/**
	 * Configure the factory's standard context characteristics,
	 * such as the context's ClassLoader and post-processors.
	 * @param beanFactory the BeanFactory to configure
	 */
	protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		// Tell the internal bean factory to use the context's class loader etc.
		//TODO:前三行不太重要
		beanFactory.setBeanClassLoader(getClassLoader());
		beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
		beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

		
		//TODO:爲每一個bean加入一個後置處理器,
		beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
        
        //TODO : 後面的代碼暫時不總結,這篇博客暫時不梳理
	}

到這裏,我們纔看到我們真正需要談論的主角——ApplicationContextAwareProcessor這個是BeanPostProcessor的一個實現類,這裏需要詳細介紹一下BeanPostProcessor。這個類的源碼如下:

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;


//TODO:BeanPostProcessor 是spring框架提供的一個擴展點(不止一個)
//TODO:通過實現BeanPostProcessor接口,開發者就可以插手bean實例化的過程,從而減輕了BeanFactory的負擔
//TODO:值得說明的是這個接口可以設置多個,會形成一個列表(典型的責任鏈模式),然後依次執行
//TODO:比如AOP就是在bean實例化之後將切面邏輯織入到bean實例中
//TODO:AOP也正是通過BeanPostProcessor和IOC容器建立了聯繫,這個接口比較簡單,只有兩個方法,一個構建前的處理,一個構建後的處理
//TODO:但是這個接口的實現類有多個,非常複雜
public interface BeanPostProcessor {

	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

}

這個類就兩個方法,並且提供了默認實現,爲了熟悉這個接口,我們可以直接實現兩個類,如下所示:

// 如果定義多個BeanPostProcessor的執行順序,可以實現PriorityOrdered接口,複寫getOrder方法,其中返回的數字越小,表示順序越前
@Component
public class TestBeanPostProcessorOne implements BeanPostProcessor ,PriorityOrdered {

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		if(beanName.equals("testBean")){
			System.out.println("got you testBean,this is first beanPostProcessor before");
		}
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if(beanName.equals("testBean")){
			System.out.println("got you testBean,this is first beanPostProcessor after");
		}
		return bean;
	}

	@Override
	public int getOrder() {
		return 10;
	}
}

第二個BeanPostProcessor

@Component
public class TestBeanPostProcessorTow implements BeanPostProcessor,PriorityOrdered {

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		if(beanName.equals("testBean")){
			System.out.println("got you testBean,this is second beanPostProcessor before");
		}
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if(beanName.equals("testBean")){
			System.out.println("got you testBean,this is second beanPostProcessor after");
		}
		return bean;
	}

	@Override
	public int getOrder() {
		return 90;
	}
}

啓動上面的執行實例代碼,如下所示:

在這裏插入圖片描述

總的來說,spring ioc還有很多擴展點(BeanPostProcessor只是其中一個)還有一個BeanFactoryPostProcessor,作用原理和BeanPostProcessor差不多,只是BeanFactoryPostProcessor針對的是BeanFactory

這裏我們看看源碼中增加的ApplicationContextAwareProcessor

ApplicationContextAwareProcessor

//TODO : 在refresh方法中會實例化一個ApplicationContextAwareProcessor,添加到bean的回調責任鏈中
class ApplicationContextAwareProcessor implements BeanPostProcessor {

    //維護了一個ApplicationContext
	private final ConfigurableApplicationContext applicationContext;

	private final StringValueResolver embeddedValueResolver;


	/**
	 * Create a new ApplicationContextAwareProcessor for the given context.
	 */
	public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
		this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
	}


	//TODO :這裏就是實現了BeanPostProcessor接口中的方法
	@Override
	@Nullable
	public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
	
        //TODO : 省略一些檢驗邏輯。
        
		//TODO : 真正的處理邏輯在這個方法中
		invokeAwareInterfaces(bean);
		

		return bean;
	}

	//TODO:判斷bean是否實現了Aware的接口,並調用bean本身對這些接口的實現邏輯
	private void invokeAwareInterfaces(Object bean) {
		if (bean instanceof Aware) {
			/**
			省略一些差不多的代碼
			*/
			//TODO : 這裏,我們在總結IOC拾遺的時候,總結過單例中注入多例的方法,其中有一種就是實現ApplicationContextAware接口
			//TODO : 看到這裏的時候,可以聯繫到那篇博客中的介紹,這裏就是幹這個事兒的
			if (bean instanceof ApplicationContextAware) { 
				((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
			}
		}
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		return bean;
	}

}

spring ioc(拾遺)中我總結過,spring官網中提供過一種如果在單例的bean中注入真正意義上的多例的bean,其中的一種實現方法如下——就是在單例的bean中實現ApplicationContextAware接口即可,但是當時我們思考過,在setApplicationContext方法中的applicationContext對象來自何處,這裏就找到了答案,來自ApplicationContextAwareProcessor中的預處理

@Service("singletonBean")
@Scope("singleton")
public class SingletonBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private PrototypeBean prototypeBean; //這裏注入一個多例的bean

    public void testSingleton(){
        System.out.println("i am singleton");
    }

    public PrototypeBean getPrototypeBean() {
        //在getPrototype的時候,從容器中去獲取
        return applicationContext.getBean(PrototypeBean.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

總結

整篇博客看上去依舊有點散亂,只是從註冊一個bean到容器中,梳理到refresh的部分,同時總結了BeanPostProcessor這個擴展點。

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