Spring源碼學習---IOC容器(解析篇)

前言

上一篇咱們大體地看了一下 ApplicationContext 的子類所擁有的一些東西,這一篇咱們結合Spring源碼學習---IOC容器介紹到的知識點來運用一下spring

使用IOC容器來獲取bean的不一樣方式(展開部分refresh()方法)

編寫了一些測試用例,而後用一個main方法去調用這些不一樣的實現express

① ClassPathXmlApplicationContext

// xml的方式
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

CombatService cs = context.getBean(CombatService.class);
cs.doInit();
cs.combating();
複製代碼

② FileSystemXmlApplicationContext

ApplicationContext context1 = new FileSystemXmlApplicationContext("f:/study/application.xml");
cs = context1.getBean(CombatService.class);
cs.doInit();
cs.combating();
複製代碼

③ GenericXmlApplicationContext

context1 = new GenericXmlApplicationContext("file:f:/study/application.xml");
cs = context1.getBean(CombatService.class);
cs.doInit();
cs.combating();

// 註解的方式
ApplicationContext context2 = new AnnotationConfigApplicationContext(YTApplication.class);
CombatService cs2 = context2.getBean(CombatService.class);
cs2.combating();
複製代碼

④ GenericApplicationContext

通用的 ApplicationContext的使用,將會把個人建立及使用的過程分步說明編程


1.建立一個GenericApplicationContext

這裏注意,咱們 new GenericApplicationContext()中並沒給入任何的參數,那將如何去判斷這是xml或者是註解的模式?數組

GenericApplicationContext context3 = new GenericApplicationContext();
複製代碼

2.使用XmlBeanDefinitionReader來確認爲xml模式

new XmlBeanDefinitionReader---前面在手寫得時候也說過,用來讀取xml配置的bean定義app

把context3(註冊器)給入,爲何context3是註冊器上一篇已經提到了,GenericApplicationContext實現了bean定義註冊接口,而後使用了loadBeanDefinitions()方法,而後指定了類目錄下的application.xmlide

new XmlBeanDefinitionReader(context3).loadBeanDefinitions("classpath:application.xml");
複製代碼

那可否加入註解形式的呢post


3.使用ClassPathBeanDefinitionScanner來確認爲註解模式

掃描器ClassPathBeanDefinitionScanner幫咱們完成註解bean定義的掃描註冊,而後把註冊器放入,指定掃描這個包下的bean學習

scan()方法是支持多個參數的,能夠註冊多個的註冊器---public int scan(String... basePackages)測試

new ClassPathBeanDefinitionScanner(context3).scan("com.study.SpringSource.service");
複製代碼

4.使用refresh()方法進行刷新

必定要刷新,否則它沒法正常建立beanui

context3.refresh();
複製代碼

5.對refresh()方法的一些簡單闡述

簡單地過一下refresh()方法的步驟,此處直接截取源碼的片斷

synchronized (this.startupShutdownMonitor) {
	// Prepare this context for refreshing.
	prepareRefresh();

	// Tell the subclass to refresh the internal bean factory.
	ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

	// Prepare the bean factory for use in this context.
	prepareBeanFactory(beanFactory);
	
        try {
		// Allows post-processing of the bean factory in context subclasses.
		postProcessBeanFactory(beanFactory);

		// Invoke factory processors registered as beans in the context.
		invokeBeanFactoryPostProcessors(beanFactory);

		// Register bean processors that intercept bean creation.
		registerBeanPostProcessors(beanFactory);

		// Initialize message source for this context.
		initMessageSource();

		// Initialize event multicaster for this context.
		initApplicationEventMulticaster();

		// Initialize other special beans in specific context subclasses.
		onRefresh();

		// Check for listener beans and register them.
		registerListeners();

		// Instantiate all remaining (non-lazy-init) singletons.
		finishBeanFactoryInitialization(beanFactory);

		// Last step: publish corresponding event.
		finishRefresh();
	}

	catch (BeansException ex) {
		if (logger.isWarnEnabled()) {
			logger.warn("Exception encountered during context initialization - " +
					"cancelling refresh attempt: " + ex);
		}

		// Destroy already created singletons to avoid dangling resources.
		destroyBeans();

		// Reset 'active' flag.
		cancelRefresh(ex);

		// Propagate exception to caller.
		throw ex;
	}

	finally {
		// Reset common introspection caches in Spring's core, since we
		// might not ever need metadata for singleton beans anymore...
		resetCommonCaches();
	}
}
複製代碼
first:synchronized (this.startupShutdownMonitor)

由於執行refresh方法是須要必定時間的,在容器執行刷新時避免啓動或銷燬容器的操做

next: prepareRefresh()
protected void prepareRefresh() {
	this.startupDate = System.currentTimeMillis();
	this.closed.set(false);
	this.active.set(true);

	if (logger.isDebugEnabled()) {
		if (logger.isTraceEnabled()) {
			logger.trace("Refreshing " + this);
		}
		else {
			logger.debug("Refreshing " + getDisplayName());
		}
	}

	// Initialize any placeholder property sources in the context environment
	initPropertySources();

	// Validate that all properties marked as required are resolvable
	// see ConfigurablePropertyResolver#setRequiredProperties
	getEnvironment().validateRequiredProperties();

	// Allow for the collection of early ApplicationEvents,
	// to be published once the multicaster is available...
	this.earlyApplicationEvents = new LinkedHashSet<>();
}
複製代碼

刷新前的準備,一句話大概說明就實際上是系統屬性以及環境變量的初始化和驗證,記錄下容器的啓動時間、標記 兩個屬性 closed 爲 false 和 active 爲 true,也就是已啓動的一個標識而已,後面三行可直譯一下,而後進行日誌打印,initPropertySources()是處理了配置文件的佔位符,getEnvironment().validateRequiredProperties()是校驗了 xml 配置 , 最後把這個集合初始化了一下

then:obtainFreshBeanFactory()

其次經過obtainFreshBeanFactory()獲取到刷新後的beanFactory和爲每一個bean生成BeanDefinition等,也就是通過這個方法後ApplicationContext就已經擁有了BeanFactory的所有功能

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
	refreshBeanFactory();
	return getBeanFactory();
}
複製代碼

此時咱們再點開 refreshBeanFactory() 的方法裏面

protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
複製代碼

這裏會發現此方法爲一個父類提供的模板方法,交由子類來進行實現,此時咱們再轉到其實現中會有兩個方案,若是看通用 GenericApplicationContext 的實現,就是如下

protected final void refreshBeanFactory() throws IllegalStateException {
	if (!this.refreshed.compareAndSet(false, true)) {
		throw new IllegalStateException(
				"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
	}
	this.beanFactory.setSerializationId(getId());
}
複製代碼

簡單說明一下,就是會判斷當前是否被刷新,若是沒有,就報出一個異常,而後給了一個序列化後的id,若是是 AbstractRefreshableApplicationContext 的話就是如下代碼

protected final void refreshBeanFactory() throws BeansException {
	if (hasBeanFactory()) {
		destroyBeans();
		closeBeanFactory();
	}
	try {
		DefaultListableBeanFactory beanFactory = createBeanFactory();
		beanFactory.setSerializationId(getId());
		customizeBeanFactory(beanFactory);
		loadBeanDefinitions(beanFactory);
		synchronized (this.beanFactoryMonitor) {
			this.beanFactory = beanFactory;
		}
	}
	catch (IOException ex) {
		throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
	}
}
複製代碼

簡單說下,若是 ApplicationContext 中已經加載過 BeanFactory 了,銷燬全部 Bean,關閉 BeanFactory,注意這裏的 ApplicationContext 是指如今的這個this,由於應用中有多個 BeanFactory 再也正常不過了,而後咱們能夠看到使用了 DefaultListableBeanFactory 來建立工廠

爲何使用此類,那是由於這是 ConfigurableListableBeanFactory 可配置bean工廠的惟一實現,而 ConfigurableListableBeanFactory 是繼承自分層bean工廠 HierarchicalBeanFactory 接口的,以下圖

且實現了BeanDefinitionRegistry,功能已經十分強大,再而後序列化以後是兩個很是重要的方法 customizeBeanFactory() 和 loadBeanDefinitions()

protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
	if (this.allowBeanDefinitionOverriding != null) {
		beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	if (this.allowCircularReferences != null) {
		beanFactory.setAllowCircularReferences(this.allowCircularReferences);
	}
}
複製代碼

這個方法進行了兩個判斷,「是否容許 Bean 定義覆蓋」 和 「是否容許 Bean 間的循環依賴」 ,bean定義覆蓋是指好比定義 bean 時使用了相同的 id 或 name,若是在同一份配置中是會報錯的,可是若是不一樣,那就會進行覆蓋處理,循環依賴很簡單就不用說明了。

再往下,就要往咱們新new出來的bean工廠裏面增長bean定義,如何加,點進去loadBeanDefinitions(),又是咱們耳熟能詳的交由子類實現的例子

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
		throws BeansException, IOException;
複製代碼

這裏進行一個只突出重點的展開(全展開篇幅過長了···)

XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
複製代碼

再往下走會看到一個xml方式的bean定義reader,把DefaultListableBeanFactory類型的beanFactory參數放入,這個reader須要的是一個註冊器參數,那到底DefaultListableBeanFactory有沒有實現註冊器的接口呢

不難發現它實現了bean定義註冊器接口

beanDefinitionReader.setEnvironment(this.getEnvironment());
	beanDefinitionReader.setResourceLoader(this);
	beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
複製代碼

這三行setEnvironment()給入了一些環境參數(好比properties文件中的值和profile),ResourceLoader是負責加載xml文件,好比給入的是字符串如何轉化爲resource,setEntityResolver是xml的解析用的

initBeanDefinitionReader(beanDefinitionReader);
複製代碼

初始化這個reader,在子類的覆寫實現中也是什麼都沒幹···姑且雞肋一波,繼續往下又是一個loadBeanDefinitions

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
	Resource[] configResources = getConfigResources();
	if (configResources != null) {
		reader.loadBeanDefinitions(configResources);
	}
	String[] configLocations = getConfigLocations();
	if (configLocations != null) {
		reader.loadBeanDefinitions(configLocations);
	}
}
複製代碼

這裏getConfigResources()返回的是null,它的子類實現中是返回了它所持有的環境參數信息

protected Resource[] getConfigResources() {
	return this.configResources;
}
複製代碼

以後兩個if分別是判斷環境參數configResources是否爲空,加載進去Bean定義,而後判斷咱們所提供的字符串信息configLocations--->也就是咱們的application.xml是否爲空,有就又加載到bean定義,從根本上就是一個順序執行下來,咱們再點進第二個loadBeanDefinitions(configLocations)中去

@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
	Assert.notNull(locations, "Location array must not be null");
	int count = 0;
	for (String location : locations) {
		count += loadBeanDefinitions(location);
	}
	return count;
}
複製代碼

這裏就是一個計數,統計咱們給到的xml有多少個,每有一個都加載到bean定義中,再點進去這個loadBeanDefinitions,

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
	ResourceLoader resourceLoader = getResourceLoader();
	if (resourceLoader == null) {
		throw new BeanDefinitionStoreException(
				"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
	}

	if (resourceLoader instanceof ResourcePatternResolver) {
		// Resource pattern matching available.
		try {
			Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
			int count = loadBeanDefinitions(resources);
			if (actualResources != null) {
				Collections.addAll(actualResources, resources);
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
			}
			return count;
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"Could not resolve bean definition resource pattern [" + location + "]", ex);
		}
	}
	else {
		// Can only load single resources by absolute URL.
		Resource resource = resourceLoader.getResource(location);
		int count = loadBeanDefinitions(resource);
		if (actualResources != null) {
			actualResources.add(resource);
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
		}
		return count;
	}
}
複製代碼

第一行ResourceLoader resourceLoader = getResourceLoader()這裏的resourceLoader返回的是ApplicationContext,此時若是resource爲空,那就拋出異常,以後try代碼塊中int count = loadBeanDefinitions(resources)又是一個統計加載bean定義數目的,再往下點進去loadBeanDefinitions咱們能進入到XmlBeanDefinitionReader的loadBeanDefinitions

ps:爲了方便觀看,從這裏開始,方法內的源碼沒用上的我直接刪除不粘貼出來

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

	try {
		InputStream inputStream = encodedResource.getResource().getInputStream();
		try {
			InputSource inputSource = new InputSource(inputStream);
			if (encodedResource.getEncoding() != null) {
				inputSource.setEncoding(encodedResource.getEncoding());
			}
			return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
		}
        }
複製代碼

簡單分析一下

InputStream inputStream = encodedResource.getResource().getInputStream();
複製代碼

此處的try代碼塊就開始對字符集進行包裝處理了,以後往下

doLoadBeanDefinitions(inputSource, encodedResource.getResource());
複製代碼

此時開始讀取xml,解析xml裏面的標籤取到bean定義的做用,點進去這個方法

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
		throws BeanDefinitionStoreException {

	try {
		Document doc = doLoadDocument(inputSource, resource);
		int count = registerBeanDefinitions(doc, resource);
		if (logger.isDebugEnabled()) {
			logger.debug("Loaded " + count + " bean definitions from " + resource);
		}
		return count;
	}

}
複製代碼

此處的Document doc = doLoadDocument(inputSource, resource)就是把xml文件解析成一個Document對象,以後registerBeanDefinitions()方法就是重點了

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
	BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
	int countBefore = getRegistry().getBeanDefinitionCount();
	documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
	return getRegistry().getBeanDefinitionCount() - countBefore;
}
複製代碼

建立DocumentReader,取到以前咱們已經統計好的bean定義數countBefore,其實這個計數並非什麼重點,咱們重點是它的registerBeanDefinitions--->註冊bean定義的這個方法

@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
	this.readerContext = readerContext;
	doRegisterBeanDefinitions(doc.getDocumentElement());
}
複製代碼

這裏doRegisterBeanDefinitions方法會從 xml 根節點開始解析文件

再深刻點進去看一眼

@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
protected void doRegisterBeanDefinitions(Element root) {

	BeanDefinitionParserDelegate parent = this.delegate;
	this.delegate = createDelegate(getReaderContext(), root, parent);

	if (this.delegate.isDefaultNamespace(root)) {
		String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
		if (StringUtils.hasText(profileSpec)) {
			String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
					profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
			// We cannot use Profiles.of(...) since profile expressions are not supported
			// in XML config. See SPR-12458 for details.
			if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
				if (logger.isDebugEnabled()) {
					logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
							"] not matching: " + getReaderContext().getResource());
				}
				return;
			}
		}
	}

	preProcessXml(root);
	parseBeanDefinitions(root, this.delegate);
	postProcessXml(root);

	this.delegate = parent;
}
複製代碼

這裏面咱們只要知道BeanDefinitionParserDelegate是負責解析bean定義的一個類,主要看最後面的三個方法,preProcessXml(root)和postProcessXml(root)是兩個提供給子類的鉤子方法,而parseBeanDefinitions(root, this.delegate)是核心解析bean定義的方法,好的,看來這就是咱們的最後了

這樣看下來好像基本沒看懂個啥,可是咱們發現了最關鍵的兩個分支

if (delegate.isDefaultNamespace(ele)) {
	parseDefaultElement(ele, delegate);
}
else {
	delegate.parseCustomElement(ele);
}
複製代碼

點開在parseDefaultElement(ele, delegate) 中咱們能夠看到定義了4個字符串常量

public static final String IMPORT_ELEMENT = "import";
public static final String ALIAS_ATTRIBUTE = "alias";
public static final String NESTED_BEANS_ELEMENT = "beans";
public static final String BEAN_ELEMENT = BeanDefinitionParserDelegate.BEAN_ELEMENT;
其中
public static final String BEAN_ELEMENT = "bean";
複製代碼

它們之因此是屬於默認的,是由於它們是處於這個 namespace 下定義的:www.springframework.org/schema/bean…

more then :prepareBeanFactory(beanFactory);

再經過 prepareBeanFactory(beanFactory) 去實現諸如之前提到的對bean的建立銷燬等一系列過程進行處理加強的這些功能,嘗試點開此方法的源碼也可看到諸如 addBeanPostProcessor等在 手寫Spring---AOP面向切面編程(4)中說起的定義一個監聽接口BeanPostProcessor來監聽Bean初始化先後過程的有關事項

beanFactory的準備工做,代碼量也是很是大

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	// Tell the internal bean factory to use the context's class loader etc.
	beanFactory.setBeanClassLoader(getClassLoader());
	beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
	beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

	// Configure the bean factory with context callbacks.
	beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
	beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
	beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
	beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
	beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
	beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
	beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

	// BeanFactory interface not registered as resolvable type in a plain factory.
	// MessageSource registered (and found for autowiring) as a bean.
	beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
	beanFactory.registerResolvableDependency(ResourceLoader.class, this);
	beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
	beanFactory.registerResolvableDependency(ApplicationContext.class, this);

	// Register early post-processor for detecting inner beans as ApplicationListeners.
	beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

	// Detect a LoadTimeWeaver and prepare for weaving, if found.
	if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
		beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
		// Set a temporary ClassLoader for type matching.
		beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
	}

	// Register default environment beans.
	if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
		beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
	}
	if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
		beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
	}
	if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
		beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
	}
}
複製代碼

咱們簡單地分步進行說明

這裏分別是爲此beanFactory設置一個類加載器,2,3句是表達式的解析器,第4句爲添加一個 BeanPostProcessor ,這我想已經見怪莫怪了,由於這個明顯又是對節點處進行了處理加強


這裏的代碼被分紅了兩串,第一串是是幾個接口的實現,若是 bean 依賴於如下接口的實現類,在自動裝配時將會忽略它們, 由於Spring 會經過其餘方式來處理這些依賴。第二串是爲特殊的幾個 bean 賦值


在 bean 完成實例化後,若是是 ApplicationListener 這個類的子類,就將其添加到 listener 列表中


一個特殊的bean,可不展開討論


Register default environment beans 這句註釋說明了,Spring是會幫咱們主動去註冊一些有用的bean的,固然這些bean咱們都是能夠經過手動覆蓋的


final:經過註釋也能夠直接大概理解的步驟(先不展開)
try {
	// Allows post-processing of the bean factory in context subclasses.
	postProcessBeanFactory(beanFactory);

	// Invoke factory processors registered as beans in the context.
	invokeBeanFactoryPostProcessors(beanFactory);

	// Register bean processors that intercept bean creation.
	registerBeanPostProcessors(beanFactory);

	// Initialize message source for this context.
	initMessageSource();

	// Initialize event multicaster for this context.
	initApplicationEventMulticaster();

	// Initialize other special beans in specific context subclasses.
	onRefresh();

	// Check for listener beans and register them.
	registerListeners();

	// Instantiate all remaining (non-lazy-init) singletons.
	finishBeanFactoryInitialization(beanFactory);

	// Last step: publish corresponding event.
	finishRefresh();
}
複製代碼

5.獲取並使用bean

//刷新完以後就能夠進行獲取了
cs2 = context3.getBean(CombatService.class);
cs2.combating();
Abean ab = context3.getBean(Abean.class);
ab.doSomething();
複製代碼

如今咱們再來回答如下問題

如何加載解析bean定義(對於xml的解析過程)

在這篇文章開始以前咱們進行xml建立使用bean的時候編寫了一個main方法來進行測試,咱們就使用斷點調試的形式去跟着類的執行流程跑一遍,來看看這個 ApplicationContext 的建立流程

此時咱們點擊下一步,將會跳轉到這個構造方法中

public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
	this(new String[] {configLocation}, true, null);
}
複製代碼

再往下咱們發現它執行了另外的一個構造方法

public ClassPathXmlApplicationContext(
		String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
		throws BeansException {

	super(parent);
	setConfigLocations(configLocations);
	if (refresh) {
		refresh();
	}
}
複製代碼

此時咱們能夠再次查看傳過來的三個參數,第一個參數是咱們提供的字符串類型的"application.xml",第二個是是否要刷新爲true,parent父容器爲null ,setConfigLocations()方法就是把configLocations字符串放入一個String類型的數組而已,以後執行了refresh()方法,關於refresh()咱們在上面已經闡述過了,因此整套連起來就是關於xml的解析的簡單過程

尾聲

能夠說是篇幅很長,也是源碼篇的第一個有進行部分展開的解讀,少吃多餐的原則在這又忘得一乾二淨了,只想去儘可能解釋清楚一點又容易看懂一點,道阻且長,望多多總結,互相進步··

相關文章
相關標籤/搜索