JVM(六)-類載入器
上一篇部落格介紹了類載入機制,知道類載入主要有載入、驗證、準備、解析、初始化五個階段,其中載入階段是可控性最強的階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入,這篇部落格主要介紹類載入器。
一、什麼是類載入器
類載入器(class loader)用來載入 Java 類到 Java 虛擬機器中。一般來說,Java 虛擬機器使用 Java 類的方式如下:Java 源程式(.java 檔案)在經過 Java 編譯器編譯之後就被轉換成 Java 位元組程式碼(.class 檔案)。類載入器負責讀取 Java 位元組程式碼,並轉換成 java.lang.Class類的一個例項。每個這樣的例項用來表示一個 Java 類。通過此例項的 newInstance()方法就可以創建出該類的一個物件。實際的情況可能更加複雜,比如 Java 位元組程式碼可能是通過工具動態生成的,也可能是通過網路下載的。
基本上所有的類載入器都是 java.lang.ClassLoader
類的一個例項。下面詳細介紹這個 Java 類。
java.lang.ClassLoader
類介紹
java.lang.ClassLoader
類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個 Java 類,即 java.lang.Class
類的一個例項。除此之外,ClassLoader
還負責載入 Java 應用所需的資源,如影象檔案和配置檔案等。不過本文只討論其載入類的功能。為了完成載入類的這個職責,ClassLoader
提供了一系列的方法,比較重要的方法如
表 1. ClassLoader 中與載入類相關的方法
方法 | 說明 |
---|---|
getParent() |
返回該類載入器的父類載入器。 |
loadClass(String name) |
載入名稱為 name 的類,返回的結果是 java.lang.Class 類的例項。 |
findClass(String name) |
查詢名稱為 name 的類,返回的結果是 java.lang.Class |
findLoadedClass(String name) |
查詢名稱為 name 的已經被載入過的類,返回的結果是 java.lang.Class 類的例項。 |
defineClass(String name, byte[] b, int off, int len) |
把位元組陣列 b 中的內容轉換成 Java 類,返回的結果是 java.lang.Class 類的例項。這個方法被宣告為 final 的。 |
resolveClass(Class<?> c) |
連結指定的 Java 類。 |
對於 表 1中給出的方法,表示類名稱的 name
引數的值是類的二進位制名稱。需要注意的是內部類的表示,如 com.example.Sample$1
和 com.example.Sample$Inner
等表示方式。這些方法會在下面介紹類載入器的工作機制時,做進一步的說明。
二、類載入器的樹狀組織結構
Java 中的類載入器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類載入器主要有下面三個:
- 引導類載入器(bootstrap class loader):它用來載入 Java 的核心庫,是用原生程式碼來實現的,並不繼承自
java.lang.ClassLoader
。 - 擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。
- 系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過
ClassLoader.getSystemClassLoader()
來獲取它。
除了系統提供的類載入器以外,開發人員可以通過繼承 java.lang.ClassLoader
類的方式實現自己的類載入器,以滿足一些特殊的需求。
除了引導類載入器之外,所有的類載入器都有一個父類載入器。通過 表 1中給出的 getParent()
方法可以得到。對於系統提供的類載入器來說,系統類載入器的父類載入器是擴充套件類載入器,而擴充套件類載入器的父類載入器是引導類載入器;對於開發人員編寫的類載入器來說,其父類載入器是載入此類載入器 Java 類的類載入器。因為類載入器 Java 類如同其它的 Java 類一樣,也是要由類載入器來載入的。一般來說,開發人員編寫的類載入器的父類載入器是系統類載入器。類載入器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類載入器。圖 1中給出了一個典型的類載入器樹狀組織結構示意圖,其中的箭頭指向的是父類載入器。
圖 1. 類載入器樹狀組織結構示意圖
程式碼清單 1演示了類載入器的樹狀組織結構。
清單 1. 演示類載入器的樹狀組織結構
public class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
每個 Java 類都維護著一個指向定義它的類載入器的引用,通過 getClassLoader()
方法就可以獲取到此引用。程式碼清單 1中通過遞迴呼叫 getParent()
方法來輸出全部的父類載入器。程式碼清單 1的執行結果如 程式碼清單 2所示。
清單 2. 演示類載入器的樹狀組織結構的執行結果
[email protected]
[email protected]
如 程式碼清單 2所示,第一個輸出的是 ClassLoaderTree
類的類載入器,即系統類載入器。它是 sun.misc.Launcher$AppClassLoader
類的例項;第二個輸出的是擴充套件類載入器,是 sun.misc.Launcher$ExtClassLoader
類的例項。需要注意的是這裡並沒有輸出引導類載入器,這是由於有些 JDK 的實現對於父類載入器是引導類載入器的情況,getParent()
方法返回 null
。
在瞭解了類載入器的樹狀組織結構之後,下面介紹類載入器的代理模式。
三、類載入器的代理模式
類載入器在嘗試自己去查詢某個類的位元組程式碼並定義它時,會先代理給其父類載入器,由父類載入器先去嘗試載入這個類,依次類推。在介紹代理模式背後的動機之前,首先需要說明一下 Java 虛擬機器是如何判定兩個 Java 類是相同的。Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample
,編譯之後生成了位元組程式碼檔案 Sample.class
。兩個不同的類載入器 ClassLoaderA
和 ClassLoaderB
分別讀取了這個 Sample.class
檔案,並定義出兩個 java.lang.Class
類的例項來表示這個類。這兩個例項是不相同的。對於 Java 虛擬機器來說,它們是不同的類。試圖對這兩個類的物件進行相互賦值,會丟擲執行時異常 ClassCastException
。下面通過示例來具體說明。程式碼清單 3中給出了 Java 類 com.example.Sample
。
清單 3. com.example.Sample 類
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
如 程式碼清單 3所示,com.example.Sample
類的方法 setSample
接受一個 java.lang.Object
型別的引數,並且會把該引數強制轉換成 com.example.Sample
型別。測試 Java 類是否相同的程式碼如 程式碼清單 4所示。
清單 4. 測試 Java 類是否相同
public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
程式碼清單 4中使用了類 FileSystemClassLoader
的兩個不同例項來分別載入類 com.example.Sample
,得到了兩個不同的 java.lang.Class
的例項,接著通過 newInstance()
方法分別生成了兩個類的物件 obj1
和 obj2
,最後通過 Java 的反射 API 在物件 obj1
上呼叫方法 setSample
,試圖把物件 obj2
賦值給 obj1
內部的 instance
物件。程式碼清單 4的執行結果如 程式碼清單 5所示。
清單 5. 測試 Java 類是否相同的執行結果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
從 程式碼清單 5給出的執行結果可以看到,執行時丟擲了 java.lang.ClassCastException
異常。雖然兩個物件 obj1
和 obj2
的類的名字相同,但是這兩個類是由不同的類載入器例項來載入的,因此不被 Java 虛擬機器認為是相同的。
瞭解了這一點之後,就可以理解代理模式的設計動機了。代理模式是為了保證 Java 核心庫的型別安全。所有 Java 應用都至少需要引用 java.lang.Object
類,也就是說在執行的時候,java.lang.Object
這個類需要被載入到 Java 虛擬機器中。如果這個載入過程由 Java 應用自己的類載入器來完成的話,很可能就存在多個版本的 java.lang.Object
類,而且這些類之間是不相容的。通過代理模式,對於 Java 核心庫的類的載入工作由引導類載入器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相相容的。
不同的類載入器為相同名稱的類建立了額外的名稱空間。相同名稱的類可以並存在 Java 虛擬機器中,只需要用不同的類載入器來載入它們即可。不同類載入器載入的類之間是不相容的,這就相當於在 Java 虛擬機器內部建立了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到,後面會詳細介紹。
下面具體介紹類載入器載入類的詳細過程。
四、載入類的過程
在前面介紹類載入器的代理模式的時候,提到過類載入器會首先代理給其它類載入器來嘗試載入某個類。這就意味著真正完成類的載入工作的類載入器和啟動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過呼叫 defineClass
來實現的;而啟動類的載入過程是通過呼叫 loadClass
來實現的。前者稱為一個類的定義載入器(defining loader),後者稱為初始載入器(initiating loader)。在 Java 虛擬機器判斷兩個類是否相同的時候,使用的是類的定義載入器。也就是說,哪個類載入器啟動類的載入過程並不重要,重要的是最終定義這個類的載入器。兩種類載入器的關聯之處在於:一個類的定義載入器是它引用的其它類的初始載入器。如類 com.example.Outer
引用了類 com.example.Inner
,則由類 com.example.Outer
的定義載入器負責啟動類 com.example.Inner
的載入過程。
方法 loadClass()
丟擲的是 java.lang.ClassNotFoundException
異常;方法 defineClass()
丟擲的是 java.lang.NoClassDefFoundError
異常。
類載入器在成功載入某個類之後,會把得到的 java.lang.Class
類的例項快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的例項,而不會嘗試再次載入。也就是說,對於一個類載入器例項來說,相同全名的類只加載一次,即 loadClass
方法不會被重複呼叫。
下面討論另外一種類載入器:執行緒上下文類載入器。
五、執行緒上下文類載入器
執行緒上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來獲取和設定執行緒的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl)
方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java 應用執行的初始執行緒的上下文類載入器是系統類載入器。線上程中執行的程式碼可以通過此類載入器來載入類和資源。
前面提到的類載入器的代理模式並不能解決 Java 應用開發中會遇到的類載入器的全部問題。Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在 javax.xml.parsers
包中。這些 SPI 的實現程式碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 介面中的程式碼經常需要載入具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory
類中的 newInstance()
方法用來生成一個新的 DocumentBuilderFactory
的例項。這裡的例項的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的 Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類載入器,因為它是系統類載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題。
執行緒上下文類載入器正好解決了這個問題。如果不做任何的設定,Java 應用的執行緒的上下文類載入器預設就是系統上下文類載入器。在 SPI 介面的程式碼中使用執行緒上下文類載入器,就可以成功的載入到 SPI 實現的類。執行緒上下文類載入器在很多 SPI 的實現中都會用到。
下面介紹另外一種載入類的方法:Class.forName
。
六、Class.forName
Class.forName
是一個靜態方法,同樣可以用來載入類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一種形式的引數 name
表示的是類的全名;initialize
表示是否初始化類;loader
表示載入時使用的類載入器。第二種形式則相當於設定了引數 initialize
的值為 true
,loader
的值為當前類的類載入器。Class.forName
的一個很常見的用法是在載入資料庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來載入 Apache Derby 資料庫的驅動。
在介紹完類載入器相關的基本概念之後,下面介紹如何開發自己的類載入器。
七、開發自己的類載入器
雖然在絕大多數情況下,系統預設提供的類載入器實現已經可以滿足需求。但是在某些情況下,您還是需要為應用開發出自己的類載入器。比如您的應用通過網路來傳輸 Java 類的位元組程式碼,為了保證安全性,這些位元組程式碼經過了加密處理。這個時候您就需要自己的類載入器來從某個網路地址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出要在 Java 虛擬機器中執行的類來。下面將通過兩個具體的例項來說明類載入器的開發。
檔案系統類載入器
第一個類載入器用來載入儲存在檔案系統上的 Java 位元組程式碼。完整的實現如 程式碼清單 6所示。
清單 6. 檔案系統類載入器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
如 程式碼清單 6所示,類 FileSystemClassLoader
繼承自類 java.lang.ClassLoader
。在 表 1中列出的 java.lang.ClassLoader
類的常用方法中,一般來說,自己開發的類載入器只需要覆寫 findClass(String name)
方法即可。java.lang.ClassLoader
類的方法 loadClass()
封裝了前面提到的代理模式的實現。該方法會首先呼叫 findLoadedClass()
方法來檢查該類是否已經被載入過;如果沒有載入過的話,會呼叫父類載入器的 loadClass()
方法來嘗試載入該類;如果父類載入器無法載入該類的話,就呼叫 findClass()
方法來查詢該類。因此,為了保證類載入器都正確實現代理模式,在開發自己的類載入器時,最好不要覆寫 loadClass()
方法,而是覆寫 findClass()
方法。
類 FileSystemClassLoader
的 findClass()
方法首先根據類的全名在硬碟上查詢類的位元組程式碼檔案(.class 檔案),然後讀取該檔案內容,最後通過 defineClass()
方法來把這些位元組程式碼轉換成 java.lang.Class
類的例項。
網路類載入器
下面將通過一個網路類載入器來說明如何通過類載入器來實現元件的動態更新。即基本的場景是:Java 位元組程式碼(.class)檔案存放在伺服器上,客戶端通過網路的方式獲取位元組程式碼並執行。當有版本更新的時候,只需要替換掉伺服器上儲存的檔案即可。通過類載入器可以比較簡單的實現這種需求。
類 NetworkClassLoader
負責通過網路下載 Java 類位元組程式碼並定義出 Java 類。它的實現與 FileSystemClassLoader
類似。在通過 NetworkClassLoader
載入了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用介面。需要注意的是,並不能直接在客戶端程式碼中引用從伺服器上下載的類,因為客戶端程式碼的類載入器找不到這些類。使用 Java 反射 API 可以直接呼叫 Java 類的方法。而使用介面的做法則是把介面的類放在客戶端中,從伺服器上載入實現此介面的不同版本的類。在客戶端通過相同的介面來使用這些實現類。網路類載入器的具體程式碼見 下載。
在介紹完如何開發自己的類載入器之後,下面說明類載入器和 Web 容器的關係。
八、類載入器與 Web 容器
對於執行在 Java EE™容器中的 Web 應用來說,類載入器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類載入器例項。該類載入器也使用代理模式,所不同的是它是首先嚐試去載入某個類,如果找不到再代理給父類載入器。這與一般類載入器的順序是相反的。這是 Java Servlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先順序高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查詢範圍之內的。這也是為了保證 Java 核心庫的型別安全。
絕大多數情況下,Web 應用的開發人員不需要考慮與類載入器相關的細節。下面給出幾條簡單的原則:
- 每個 Web 應用自己的 Java 類檔案和使用的庫的 jar 包,分別放在
WEB-INF/classes
和WEB-INF/lib
目錄下面。 - 多個應用共享的 Java 類檔案和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
- 當出現找不到類的錯誤時,檢查當前類的類載入器和當前執行緒的上下文類載入器是否正確。
在介紹完類載入器與 Web 容器的關係之後,下面介紹它與 OSGi 的關係。
九、類載入器與 OSGi
OSGi™是 Java 上的動態模組系統。它為開發人員提供了面向服務和基於元件的執行環境,並提供標準的方式用來管理軟體的生命週期。OSGi 已經被實現和部署在很多產品上,在開源社群也得到了廣泛的支援。Eclipse 就是基於 OSGi 技術來構建的。
OSGi 中的每個模組(bundle)都包含 Java 包和類。模組可以宣告它所依賴的需要匯入(import)的其它模組的 Java 包和類(通過 Import-Package
),也可以宣告匯出(export)自己的包和類,供其它模組使用(通過 Export-Package
)。也就是說需要能夠隱藏和共享一個模組中的某些 Java 包和類。這是通過 OSGi 特有的類載入器機制來實現的。OSGi 中的每個模組都有對應的一個類載入器。它負責載入模組自己包含的 Java 包和類。當它需要載入 Java 核心庫的類時(以 java
開頭的包和類),它會代理給父類載入器(通常是啟動類載入器)來完成。當它需要載入所匯入的 Java 類時,它會代理給匯出此 Java 類的模組來完成載入。模組也可以顯式的宣告某些 Java 包和類,必須由父類載入器來載入。只需要設定系統屬性 org.osgi.framework.bootdelegation
的值即可。
假設有兩個模組 bundleA 和 bundleB,它們都有自己對應的類載入器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample
,並且該類被宣告為匯出的,也就是說可以被其它模組所使用的。bundleB 聲明瞭匯入 bundleA 提供的類 com.bundleA.Sample
,幷包含一個類 com.bundleB.NewSample
繼承自 com.bundleA.Sample
。在 bundleB 啟動的時候,其類載入器 classLoaderB 需要載入類 com.bundleB.NewSample
,進而需要載入類 com.bundleA.Sample
。由於 bundleB 聲明瞭類 com.bundleA.Sample
是匯入的,classLoaderB 把載入類 com.bundleA.Sample
的工作代理給匯出該類的 bundleA 的類載入器 classLoaderA。classLoaderA 在其模組內部查詢類 com.bundleA.Sample
並定義它,所得到的類 com.bundleA.Sample
例項就可以被所有宣告匯入了此類的模組使用。對於以 java
開頭的類,都是由父類載入器來載入的。如果聲明瞭系統屬性 org.osgi.framework.bootdelegation=com.example.core.*
,那麼對於包 com.example.core
中的類,都是由父類載入器來完成的。
OSGi 模組的這種類載入器結構,使得一個類的不同版本可以共存在 Java 虛擬機器中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模組需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
- 如果一個類庫只有一個模組使用,把該類庫的 jar 包放在模組中,在
Bundle-ClassPath
中指明即可。 - 如果一個類庫被多個模組共用,可以為這個類庫單獨的建立一個模組,把其它模組需要用到的 Java 包宣告為匯出的。其它模組宣告匯入這些類。
- 如果類庫提供了 SPI 介面,並且利用執行緒上下文類載入器來載入 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了
NoClassDefFoundError
異常,首先檢查當前執行緒的上下文類載入器是否正確。通過Thread.currentThread().getContextClassLoader()
就可以得到該類載入器。該類載入器應該是該模組對應的類載入器。如果不是的話,可以首先通過class.getClassLoader()
來得到模組對應的類載入器,再通過Thread.currentThread().setContextClassLoader()
來設定當前執行緒的上下文類載入器。
總結
類載入器是 Java 語言的一個創新。它使得動態安裝和更新軟體元件成為可能。本文詳細介紹了類載入器的相關話題,包括基本概念、代理模式、執行緒上下文類載入器、與 Web 容器和 OSGi 的關係等。開發人員在遇到 ClassNotFoundException
和 NoClassDefFoundError
等異常的時候,應該檢查丟擲異常的類的類載入器和當前執行緒的上下文類載入器,從中可以發現問題的所在。在開發自己的類載入器的時候,需要注意與已有的類載入器組織結構的協調。