1. 簡介
本篇文章是上一篇文章(建立單例 bean 的過程)的延續。在上一篇文章中,我們從戰略層面上領略了
doCreateBean
方法的全過程。本篇文章,我們就從戰術的層面上,詳細分析
doCreateBean
方法中的一個重要的調用,即
createBeanInstance
方法。在本篇文章中,你将看到三種不同的構造 bean 對象的方式。你也會了解到構造 bean 對象的兩種政策。如果你對這些内容感興趣,那麼不妨繼續往下讀。我會在代碼進行大量的注解,相信能幫助你了解代碼邏輯。好了,其他的就不多說了,進入正題吧。
2. 源碼分析
2.1 建立 bean 對象的過程
本節,我們一起來來分析一下本篇文章的主角
createBeanInstance
方法。按照慣例,我們還是先分析一下方法的大緻脈絡,然後我們再按照這個脈絡去分析一些重要的調用。So. Let`s go → ↓
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { Class<?> beanClass = resolveBeanClass(mbd, beanName); /* * 檢測類的通路權限。預設情況下,對于非 public 的類,是允許通路的。 * 若禁止通路,這裡會抛出異常 */ if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); } /* * 如果工廠方法不為空,則通過工廠方法建構 bean 對象。這種建構 bean 的方式 * 就不深入分析了,有興趣的朋友可以自己去看一下。 */ if (mbd.getFactoryMethodName() != null) { // 通過“工廠方法”的方式建構 bean 對象 return instantiateUsingFactoryMethod(beanName, mbd, args); } /* * 當多次建構同一個 bean 時,可以使用此處的快捷路徑,即無需再次推斷應該使用哪種方式構造執行個體, * 以提高效率。比如在多次建構同一個 prototype 類型的 bean 時,就可以走此處的捷徑。 * 這裡的 resolved 和 mbd.constructorArgumentsResolved 将會在 bean 第一次執行個體 * 化的過程中被設定,在後面的源碼中會分析到,先繼續往下看。 */ boolean resolved = false; boolean autowireNecessary = false; if (args == null) { synchronized (mbd.constructorArgumentLock) { if (mbd.resolvedConstructorOrFactoryMethod != null) { resolved = true; autowireNecessary = mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { // 通過“構造方法自動注入”的方式構造 bean 對象 return autowireConstructor(beanName, mbd, null, null); } else { // 通過“預設構造方法”的方式構造 bean 對象 return instantiateBean(beanName, mbd); } } // 由後置處理器決定傳回哪些構造方法,這裡不深入分析了 Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); /* * 下面的條件分支條件用于判斷使用什麼方式構造 bean 執行個體,有兩種方式可選 - 構造方法自動 * 注入和預設構造方法。判斷的條件由4部分綜合而成,如下: * * 條件1:ctors != null -> 後置處理器傳回構造方法數組是否為空 * * 條件2:mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR * -> bean 配置中的 autowire 屬性是否為 constructor * 條件3:mbd.hasConstructorArgumentValues() * -> constructorArgumentValues 是否存在元素,即 bean 配置檔案中 * 是否配置了 <construct-arg/> * 條件4:!ObjectUtils.isEmpty(args) * -> args 數組是否存在元素,args 是由使用者調用 * getBean(String name, Object... args) 傳入的 * * 上面4個條件,隻要有一個為 true,就會通過構造方法自動注入的方式構造 bean 執行個體 */ if (ctors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { // 通過“構造方法自動注入”的方式構造 bean 對象 return autowireConstructor(beanName, mbd, ctors, args); } // 通過“預設構造方法”的方式構造 bean 對象 return instantiateBean(beanName, mbd); }
以上就是 createBeanInstance 方法的源碼,不是很長。配合着注釋,應該不是很難懂。下面我們來總結一下這個方法的執行流程,如下:
- 檢測類的通路權限,若禁止通路,則抛出異常
- 若工廠方法不為空,則通過工廠方法建構 bean 對象,并傳回結果
- 若構造方式已解析過,則走快捷路徑建構 bean 對象,并傳回結果
- 如第三步不滿足,則通過組合條件決定使用哪種方式建構 bean 對象
這裡有三種構造 bean 對象的方式,如下:
- 通過“工廠方法”的方式構造 bean 對象
- 通過“構造方法自動注入”的方式構造 bean 對象
- 通過“預設構造方法”的方式構造 bean 對象
下面我将會分析第2和第3種構造 bean 對象方式的實作源碼。至于第1種方式,實作邏輯和第2種方式較為相似。是以就不分析了,大家有興趣可以自己看一下。
2.2 通過構造方法自動注入的方式建立 bean 執行個體
本節,我将會分析構造方法自動注入的實作邏輯。代碼邏輯較為複雜,需要大家耐心閱讀。代碼如下:
protected BeanWrapper autowireConstructor( String beanName, RootBeanDefinition mbd, Constructor<?>[] ctors, Object[] explicitArgs) { // 建立 ConstructorResolver 對象,并調用其 autowireConstructor 方法 return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs); } public BeanWrapper autowireConstructor(final String beanName, final RootBeanDefinition mbd, Constructor<?>[] chosenCtors, final Object[] explicitArgs) { // 建立 BeanWrapperImpl 對象 BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); Constructor<?> constructorToUse = null; ArgumentsHolder argsHolderToUse = null; Object[] argsToUse = null; // 确定參數值清單(argsToUse) if (explicitArgs != null) { argsToUse = explicitArgs; } else { Object[] argsToResolve = null; synchronized (mbd.constructorArgumentLock) { // 擷取已解析的構造方法 constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod; if (constructorToUse != null && mbd.constructorArgumentsResolved) { // 擷取已解析的構造方法參數清單 argsToUse = mbd.resolvedConstructorArguments; if (argsToUse == null) { // 若 argsToUse 為空,則擷取未解析的構造方法參數清單 argsToResolve = mbd.preparedConstructorArguments; } } } if (argsToResolve != null) { // 解析參數清單 argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); } } if (constructorToUse == null) { boolean autowiring = (chosenCtors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); ConstructorArgumentValues resolvedValues = null; int minNrOfArgs; if (explicitArgs != null) { minNrOfArgs = explicitArgs.length; } else { ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues(); resolvedValues = new ConstructorArgumentValues(); /* * 确定構造方法參數數量,比如下面的配置: * <bean id="persion" class="xyz.coolblog.autowire.Person"> * <constructor-arg index="0" value="xiaoming"/> * <constructor-arg index="1" value="1"/> * <constructor-arg index="2" value="man"/> * </bean> * * 此時 minNrOfArgs = maxIndex + 1 = 2 + 1 = 3,除了計算 minNrOfArgs, * 下面的方法還會将 cargs 中的參數資料轉存到 resolvedValues 中 */ minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues); } // 擷取構造方法清單 Constructor<?>[] candidates = chosenCtors; if (candidates == null) { Class<?> beanClass = mbd.getBeanClass(); try { candidates = (mbd.isNonPublicAccessAllowed() ? beanClass.getDeclaredConstructors() : beanClass.getConstructors()); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); } } // 按照構造方法的通路權限級别和參數數量進行排序 AutowireUtils.sortConstructors(candidates); int minTypeDiffWeight = Integer.MAX_VALUE; Set<Constructor<?>> ambiguousConstructors = null; LinkedList<UnsatisfiedDependencyException> causes = null; for (Constructor<?> candidate : candidates) { Class<?>[] paramTypes = candidate.getParameterTypes(); /* * 下面的 if 分支的用途是:若比對到到合适的構造方法了,提前結束 for 循環 * constructorToUse != null 這個條件比較好了解,下面分析一下條件 argsToUse.length > paramTypes.length: * 前面說到 AutowireUtils.sortConstructors(candidates) 用于對構造方法進行 * 排序,排序規則如下: * 1. 具有 public 通路權限的構造方法排在非 public 構造方法前 * 2. 參數數量多的構造方法排在前面 * * 假設現在有一組構造方法按照上面的排序規則進行排序,排序結果如下(省略參數名稱): * * 1. public Hello(Object, Object, Object) * 2. public Hello(Object, Object) * 3. public Hello(Object) * 4. protected Hello(Integer, Object, Object, Object) * 5. protected Hello(Integer, Object, Object) * 6. protected Hello(Integer, Object) * * argsToUse = [num1, obj2],可以比對上的構造方法2和構造方法6。由于構造方法2有 * 更高的通路權限,是以沒理由不選他(盡管後者在參數類型上更加比對)。由于構造方法3 * 參數數量 < argsToUse.length,參數數量上不比對,也不應該選。是以 * argsToUse.length > paramTypes.length 這個條件用途是:在條件 * constructorToUse != null 成立的情況下,通過判斷參數數量與參數值數量 * (argsToUse.length)是否一緻,來決定是否提前終止構造方法比對邏輯。 */ if (constructorToUse != null && argsToUse.length > paramTypes.length) { break; } /* * 構造方法參數數量低于配置的參數數量,則忽略目前構造方法,并重試。比如 * argsToUse = [obj1, obj2, obj3, obj4],上面的構造方法清單中, * 構造方法1、2和3顯然不是合适選擇,忽略之。 */ if (paramTypes.length < minNrOfArgs) { continue; } ArgumentsHolder argsHolder; if (resolvedValues != null) { try { /* * 判斷否則方法是否有 ConstructorProperties 注解,若有,則取注解中的 * 值。比如下面的代碼: * * public class Persion { * private String name; * private Integer age; * * @ConstructorProperties(value = {"coolblog", "20"}) * public Persion(String name, Integer age) { * this.name = name; * this.age = age; * } * } */ String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length); if (paramNames == null) { ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); if (pnd != null) { /* * 擷取構造方法參數名稱清單,比如有這樣一個構造方法: * public Person(String name, int age, String sex) * * 調用 getParameterNames 方法傳回 paramNames = [name, age, sex] */ paramNames = pnd.getParameterNames(candidate); } } /* * 建立參數值清單,傳回 argsHolder 會包含進行類型轉換後的參數值,比如下 * 面的配置: * * <bean id="persion" class="xyz.coolblog.autowire.Person"> * <constructor-arg name="name" value="xiaoming"/> * <constructor-arg name="age" value="1"/> * <constructor-arg name="sex" value="man"/> * </bean> * * Person 的成員變量 age 是 Integer 類型的,但由于在 Spring 配置中 * 隻能配成 String 類型,是以這裡要進行類型轉換。 */ argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames, getUserDeclaredConstructor(candidate), autowiring); } catch (UnsatisfiedDependencyException ex) { if (this.beanFactory.logger.isTraceEnabled()) { this.beanFactory.logger.trace( "Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex); } if (causes == null) { causes = new LinkedList<UnsatisfiedDependencyException>(); } causes.add(ex); continue; } } else { if (paramTypes.length != explicitArgs.length) { continue; } argsHolder = new ArgumentsHolder(explicitArgs); } /* * 計算參數值(argsHolder.arguments)每個參數類型與構造方法參數清單 * (paramTypes)中參數的類型差異量,差異量越大表明參數類型差異越大。參數類型差異 * 越大,表明目前構造方法并不是一個最合适的候選項。引入差異量(typeDiffWeight) * 變量目的:是将候選構造方法的參數清單類型與參數值清單類型的差異進行量化,通過量化 * 後的數值篩選出最合适的構造方法。 * * 講完差異量,再來說說 mbd.isLenientConstructorResolution() 條件。 * 官方的解釋是:傳回構造方法的解析模式,有寬松模式(lenient mode)和嚴格模式 * (strict mode)兩種類型可選。具體的細節沒去研究,就不多說了。 */ int typeDiffWeight = (mbd.isLenientConstructorResolution() ? argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes)); if (typeDiffWeight < minTypeDiffWeight) { constructorToUse = candidate; argsHolderToUse = argsHolder; argsToUse = argsHolder.arguments; minTypeDiffWeight = typeDiffWeight; ambiguousConstructors = null; } /* * 如果兩個構造方法與參數值類型清單之間的差異量一緻,那麼這兩個方法都可以作為 * 候選項,這個時候就出現歧義了,這裡先把有歧義的構造方法放入 * ambiguousConstructors 集合中 */ else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) { if (ambiguousConstructors == null) { ambiguousConstructors = new LinkedHashSet<Constructor<?>>(); ambiguousConstructors.add(constructorToUse); } ambiguousConstructors.add(candidate); } } // 若上面未能篩選出合适的構造方法,這裡将抛出 BeanCreationException 異常 if (constructorToUse == null) { if (causes != null) { UnsatisfiedDependencyException ex = causes.removeLast(); for (Exception cause : causes) { this.beanFactory.onSuppressedException(cause); } throw ex; } throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Could not resolve matching constructor " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)"); } /* * 如果 constructorToUse != null,且 ambiguousConstructors 也不為空,表明解析 * 出了多個的合适的構造方法,此時就出現歧義了。Spring 不會擅自決定使用哪個構造方法, * 是以抛出異常。 */ else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Ambiguous constructor matches found in bean '" + beanName + "' " + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + ambiguousConstructors); } if (explicitArgs == null) { /* * 緩存相關資訊,比如: * 1. 已解析出的構造方法對象 resolvedConstructorOrFactoryMethod * 2. 構造方法參數清單是否已解析标志 constructorArgumentsResolved * 3. 參數值清單 resolvedConstructorArguments 或 preparedConstructorArguments * * 這些資訊可用在其他地方,用于進行快捷判斷 */ argsHolderToUse.storeCache(mbd, constructorToUse); } } try { Object beanInstance; if (System.getSecurityManager() != null) { final Constructor<?> ctorToUse = constructorToUse; final Object[] argumentsToUse = argsToUse; beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return beanFactory.getInstantiationStrategy().instantiate( mbd, beanName, beanFactory, ctorToUse, argumentsToUse); } }, beanFactory.getAccessControlContext()); } else { /* * 調用執行個體化政策建立執行個體,預設情況下使用反射建立執行個體。如果 bean 的配置資訊中 * 包含 lookup-method 和 replace-method,則通過 CGLIB 增強 bean 執行個體 */ beanInstance = this.beanFactory.getInstantiationStrategy().instantiate( mbd, beanName, this.beanFactory, constructorToUse, argsToUse); } // 設定 beanInstance 到 BeanWrapperImpl 對象中 bw.setBeanInstance(beanInstance); return bw; } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean instantiation via constructor failed", ex); } }
上面的方法邏輯比較複雜,做了不少事情,該方法的核心邏輯是根據參數值類型篩選合适的構造方法。解析出合适的構造方法後,剩下的工作就是建構 bean 對象了,這個工作交給了執行個體化政策去做。下面羅列一下這個方法的工作流程吧:
- 建立 BeanWrapperImpl 對象
- 解析構造方法參數,并算出 minNrOfArgs
- 擷取構造方法清單,并排序
- 周遊排序好的構造方法清單,篩選合适的構造方法
- 擷取構造方法參數清單中每個參數的名稱
- 再次解析參數,此次解析會将 value 屬性值進行類型轉換,由 String 轉為合适的類型。
- 計算構造方法參數清單與參數值清單之間的類型差異量,以篩選出更為合适的構造方法
- 緩存已篩選出的構造方法以及參數值清單,若再次建立 bean 執行個體時,可直接使用,無需再次進行篩選
- 使用初始化政策建立 bean 對象
- 将 bean 對象放入 BeanWrapperImpl 對象中,并傳回該對象
由上面的流程可以看得出,通過構造方法自動注入的方式構造 bean 對象的過程還是很複雜的。為了看懂這個流程,我進行了多次調試,算是勉強弄懂大緻邏輯。由于時間有限,我并未能詳細分析 autowireConstructor 方法及其所調用的一些方法,比如 resolveConstructorArguments、 autowireConstructor 等。關于這些方法,這裡隻寫了個大概,有興趣的朋友自己去探索吧。
2.3 通過預設構造方法建立 bean 對象
看完了上面冗長的邏輯,本節來看點輕松的吧 - 通過預設構造方法建立 bean 對象。如下:
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { try { Object beanInstance; final BeanFactory parent = this; // if 條件分支裡的一大坨是 Java 安全相關的代碼,可以忽略,直接看 else 分支 if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return getInstantiationStrategy().instantiate(mbd, beanName, parent); } }, getAccessControlContext()); } else { /* * 調用執行個體化政策建立執行個體,預設情況下使用反射建立對象。如果 bean 的配置資訊中 * 包含 lookup-method 和 replace-method,則通過 CGLIB 建立 bean 對象 */ beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); } // 建立 BeanWrapperImpl 對象 BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; } catch (Throwable ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); } } public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { // 檢測 bean 配置中是否配置了 lookup-method 或 replace-method,若配置了,則需使用 CGLIB 建構 bean 對象 if (bd.getMethodOverrides().isEmpty()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { final Class<?> clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } try { if (System.getSecurityManager() != null) { constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor<?>>() { @Override public Constructor<?> run() throws Exception { return clazz.getDeclaredConstructor((Class[]) null); } }); } else { // 擷取預設構造方法 constructorToUse = clazz.getDeclaredConstructor((Class[]) null); } // 設定 resolvedConstructorOrFactoryMethod bd.resolvedConstructorOrFactoryMethod = constructorToUse; } catch (Throwable ex) { throw new BeanInstantiationException(clazz, "No default constructor found", ex); } } } // 通過無參構造方法建立 bean 對象 return BeanUtils.instantiateClass(constructorToUse); } else { // 使用 GCLIG 建立 bean 對象 return instantiateWithMethodInjection(bd, beanName, owner); } }
上面就是通過預設構造方法建立 bean 對象的過程,比較簡單,就不多說了。最後我們再來看看簡單看看通過無參構造方法剛建立 bean 對象的代碼(通過 CGLIB 建立 bean 對象的方式就不看了)是怎樣的,如下:
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException { Assert.notNull(ctor, "Constructor must not be null"); try { // 設定構造方法為可通路 ReflectionUtils.makeAccessible(ctor); // 通過反射建立 bean 執行個體,這裡的 args 是一個沒有元素的空數組 return ctor.newInstance(args); } catch (InstantiationException ex) { throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex); } catch (IllegalAccessException ex) { throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex); } catch (IllegalArgumentException ex) { throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex); } catch (InvocationTargetException ex) { throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException()); } }
到這裡,終于看到了建立 bean 對象的代碼了。在經曆層層調用後,我們總算是追到了調用棧的最深處。看到這裡,大家可以休息一下了,本文也差不多要結束了。好了,最後再容我多啰嗦一會,往下看。
3.寫在最後
寫到這裡,我也算是松了一口氣,終于快寫完了。這篇文章寫起來感覺挺不容易的,原因是 createBeanInstance 及其調用的方法是在太多了,而且很多方法邏輯還是比較複雜的,尤其是 autowireConstructor 中調用的一些方法。autowireConstructor 中調用的方法我基本上都看了一遍,但并非全部都弄懂了,有些方法隻是知道個大概。是以,這篇文章寫的我挺糾結的,生怕有些地方分析的不對。由于我後續還有很多東西要看,以至于我暫時沒法抽出大量的時間去詳細閱讀 Spring 的源碼。是以如果上面的分析有不對的地方,歡迎指正,我會虛心聽之。如果這些不對的地方給你造成了困擾,實在很抱歉,抱歉。
好了,本篇文章先到這裡。謝謝閱讀!
參考
- 《Spring 源碼深度解析》- 郝佳
附錄:Spring 源碼分析文章清單
Ⅰ. IOC
更新時間 | 标題 |
---|---|
2018-05-30 | Spring IOC 容器源碼分析系列文章導讀 |
2018-06-01 | Spring IOC 容器源碼分析 - 擷取單例 bean |
2018-06-04 | Spring IOC 容器源碼分析 - 建立單例 bean 的過程 |
2018-06-06 | Spring IOC 容器源碼分析 - 建立原始 bean 對象 |
2018-06-08 | Spring IOC 容器源碼分析 - 循環依賴的解決辦法 |
2018-06-11 | Spring IOC 容器源碼分析 - 填充屬性到 bean 原始對象 |
Spring IOC 容器源碼分析 - 餘下的初始化工作 |
Ⅱ. AOP
2018-06-17 | Spring AOP 源碼分析系列文章導讀 |
2018-06-20 | Spring AOP 源碼分析 - 篩選合适的通知器 |
Spring AOP 源碼分析 - 建立代理對象 | |
2018-06-22 | Spring AOP 源碼分析 - 攔截器鍊的執行過程 |
Ⅲ. MVC
2018-06-29 | Spring MVC 原理探秘 - 一個請求的旅行過程 |
2018-06-30 | Spring MVC 原理探秘 - 容器的建立過程 |
本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處
作者:田小波
本文同步釋出在我的個人部落格:http://www.tianxiaobo.com

本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定進行許可。