關於JNDI淺談(tomcat為例)
本文算我寫的一個筆記,參考瞭如下部落格:
http://gearever.iteye.com/blog/1560135
http://gearever.iteye.com/blog/1554295
http://blog.sina.com.cn/s/blog_542200310100eiva.html
一:TOMCAT部署web專案的方式:
1.將WebRoot整體複製到/tomcat/webapps下(也就是eclipse中選中TOMCAT伺服器右鍵ADD AND REMOVE就是這個方式)
2.在Tomcat的配置檔案中,一個Web應用就是一個特定的Context,Context配置有如下幾種方式:
1)conf/server.xml 新增Context節點;
2)conf / context.xml
3)conf / {enginename} /{hostname} / xxxx.xml 例如conf/Catalina/localhost/JNDIDemo.xml (檔名就是路徑名,
例如訪問的時候http://localhost:8086/JNDIDemo/hello)
二:
1.新建工程JNDIDemo 在apache-tomcat-7.0.57/conf/Catalina/localhost/JNDIDemo.xml中配置如下:
<Context docBase="C:\Users\hyang\workspace_ee\JNDIDemo\WebContent" debug="5" reloadable="true" crossContext="true"><Resource name="jdbc/stcms" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="root" password="123456" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8" /> </Context> 2.在web.xml 配置如下:
<resource-ref>
<description>資料來源</description>
<res-ref-name>jdbc/stcms</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
3.寫測試類
public class Demo1{
public static void main(String[] args) {
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/stcms");
conn = ds.getConnection();
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行時報錯javax.naming.NoInitialContextException ,百度後原因:不能直接在main方法裡面呼叫 ,要在頁面訪問
Demo2如下:
public class Demo2 extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/stcms");
conn = ds.getConnection();
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
res.setContentType("text/html");
PrintWriter w = res.getWriter();
w.println("<h1>Hello, Servlet.</h1>");
w.close();
}
}
然後再在web.xml中配置servlet
通過在瀏覽器輸入http://localhost:8086/JNDIDemo/hello
可以看到在Hello, Servlet. 在瀏覽器顯示
在eclipse的console控制檯可以輸出:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8, [email protected], MySQL-AB JDBC Driver 說明資料庫連線已經得到。
======
web程式的context元素既定義在META-INF/context.xml的案例如下:
還是JNDIDemo工程
1)刪除apache-tomcat-7.0.57/conf/Catalina/localhost/JNDIDemo.xml 檔案
2)web.xml 中<resource-ref>配置一樣
3)在C:\Users\hyang\workspace_ee\JNDIDemo\WebContent\META-INF\context.xml配置如下:
<Context docBase="C:\Users\hyang\workspace_ee\JNDIDemo\WebContent" debug="5" reloadable="true" crossContext="true"><Resource name="jdbc/stcms" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="root" password="123456" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8" /> </Context>
4)在web.xml配置 servlet如下:
<servlet>
<servlet-name>Demo3</servlet-name>
<servlet-class>com.Demo3</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Demo3</servlet-name>
<url-pattern>/hello2</url-pattern>
</servlet-mapping>
5)測試類Demo3如下:
public class Demo3 extends HttpServlet {
@Override
protected void service(HttpServletRequest ewq, HttpServletResponse res) throws ServletException, IOException {
Connection conn = null;
try {
//獲得對資料來源的引用
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/stcms");
//獲得資料庫的連線
conn = ds.getConnection();
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
res.setContentType("text/html");
PrintWriter w = res.getWriter();
w.println("<h1>hello,JNDI.</h1>");
w.close();
}
}
6)部署JNDIDemo工程到TOMCAT下
7)在瀏覽器輸入:http://localhost:8086/JNDIDemo/hello2
可以看到在瀏覽器顯示:hello,JNDI.
在eclipse控制檯console列印jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8, [email protected], MySQL-AB JDBC Driver 說明已經得到資料庫連線。
注意:DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/stcms");在生產上一般都把這個寫在一個xml檔案中,
在啟動工程的時候載入這個xml得到這個字串。
================
以下的關於JNDI的 描述,我只是個搬運工:
1.JNDI封裝了一個簡單name到實體物件的mapping,通過字串可以方便的得到想要的物件資源。通常這種物件資源有很多種,例如資料庫JDBC,JMS,EJB等。平時用的最多的就是資料庫了。在tomcat中,這些資源都是以java:comp/env開頭的字串來繫結的。
2.context中配置的ResourceLink屬於一箇中轉的作用,這主要是為了在tomcat啟動狀態下,如果新部署一個app,可以在app中指定到相應的全域性的resource。
它們的mapping關係是;
JNDI體系分為三個部分;
- 在tomcat架構分析 (容器類)中介紹了StandardContext類,它是每個app的一個邏輯封裝。當tomcat初始化時,將根據配置檔案,對StandardContext中的NamingResources物件進行賦值,同時,將例項化一個NamingContextListener物件作為這個context作用域內的事件監聽器,它會響應一些例如系統啟動,系統關閉等事件,作出相應的操作;
- 初始化完成後,tomcat啟動,完成啟動邏輯,丟擲一個系統啟動event,由那個NamingContextListener捕獲,進行處理,將初始化時的NamingResources物件中的資料,繫結到相應的JNDI物件樹(namingContext)上,即java:comp/env分支,然後將這個根namingContext與這個app的classloader進行繫結,這樣每個app只有在自己的JNDI物件樹上呼叫,互不影響;
- 每個app中的類都由自己app的classloader載入,如果需要用到JNDI繫結物件,也是從自己classloader對應的JNDI物件樹上獲取資源物件
這裡需要說明的是,在後面會經常涉及到兩類context,一個是作為tomcat內部實現邏輯的容器StandardContext;一個是作為JNDI內部分支物件NamingContext;它們實現不同介面,互相沒有任何關係,不要混淆。
開始看看每個部分詳細情況吧。
初始化NamingResources
先看看配置;
<tomcat>/conf/server.xml
Xml程式碼
- <Serverport="8005">
- <Service>
- <Engine>
- <Host>
- <Context>
- <Resource
- name="jdbc/mysql"
- type="javax.sql.DataSource"
- username="root"
- password="root"
- driverClassName="com.mysql.jdbc.Driver"
- maxIdle="200"
- maxWait="5000"
- url="……"
- maxActive="100"/>
- </Context>
- </Host>
- </Engine>
- </Service>
- ……
- </Server>
通過這個配置,可以非常清楚的看出tomcat內部的層次結構,不同的層次實現不同的作用域,同時每個層次都有相應的類進行邏輯封裝,這是tomcat面向物件思想的體現。那麼相應的,Context節點下的Resource節點也有類進行封裝;
Java程式碼
- org.apache.catalina.deploy.ContextResource
org.apache.catalina.deploy.ContextResource
上面例子中Resource節點配置的所有屬性會以鍵值對的方式存入ContextResource的一個HashMap物件中,這一步只是初始化,不會用到每個屬性,它只是為了每個真正處理的資源物件用到,例如後面會說的預設的tomcat的資料庫連線池物件BasicDataSourceFactory,如果用其他的資料庫連線池,例如c3p0,那麼其配置的屬性物件就應該按照c3p0中需要的屬性名稱來配。
但是,這些屬性中的name和type是ContextResource需要的,name是JNDI物件樹的分支節點,上面配的“jdbc/mysql”,那麼這個資料庫連線池物件就對應在“java:comp/env/jdbc/mysql”的位置。type是這個物件的型別,如果是“javax.sql.DataSource”,tomcat會有一些特殊的邏輯處理。
當tomcat初始化時,StandardContext物件內部會生成一個NamingResources物件,這個物件就是做一些預處理,儲存一些Resource物件,看一下NamingResources儲存Resource物件的邏輯;
Java程式碼
- publicvoid addResource(ContextResource resource) {
- //確保每一個資源物件的name都是唯一的
- //不僅是Resource物件之間,包括Service等所有的資源物件
- if (entries.containsKey(resource.getName())) {
- return;
- } else {
- entries.put(resource.getName(), resource.getType());
- }
- //建立一個name和資源物件的mapping
- synchronized (resources) {
- resource.setNamingResources(this);
- resources.put(resource.getName(), resource);
- }
- support.firePropertyChange("resource", null, resource);
- }
public void addResource(ContextResource resource) {
//確保每一個資源物件的name都是唯一的
//不僅是Resource物件之間,包括Service等所有的資源物件
if (entries.containsKey(resource.getName())) {
return;
} else {
entries.put(resource.getName(), resource.getType());
}
//建立一個name和資源物件的mapping
synchronized (resources) {
resource.setNamingResources(this);
resources.put(resource.getName(), resource);
}
support.firePropertyChange("resource", null, resource);
}
需要說明的是,不僅僅是Resource一種物件,還有Web Service資源物件,EJB物件等,這裡就是拿資料庫連線的Resource物件舉例。
啟動JNDI繫結
當tomcat啟動時,會丟擲一個start event,由StandardContext的NamingContextListener監聽物件捕捉到,響應start event。
Java程式碼
- publicvoid lifecycleEvent(LifecycleEvent event) {
- container = event.getLifecycle();
- if (container instanceof Context) {
- //這個namingResources物件就是StandardContext的namingResources物件
- namingResources = ((Context) container).getNamingResources();
- logger = log;
- } elseif (container instanceof Server) {
- namingResources = ((Server) container).getGlobalNamingResources();
- } else {
- return;
- }
- //響應start event
- if (event.getType() == Lifecycle.START_EVENT) {
- if (initialized)
- return;
- Hashtable contextEnv = new Hashtable();
- try {
- //生成這個StandardContext域的JNDI物件樹根NamingContext物件
- namingContext = new NamingContext(contextEnv, getName());
- } catch (NamingException e) {
- // Never happens
- }
- ContextAccessController.setSecurityToken(getName(), container);
- //將此StandardContext物件與JNDI物件樹根NamingContext物件繫結
- ContextBindings.bindContext(container, namingContext, container);
- if( log.isDebugEnabled() ) {
- log.debug("Bound " + container );
- }
- // Setting the context in read/write mode
- ContextAccessController.setWritable(getName(), container);
- try {
- //將初始化時的資源物件繫結JNDI物件樹
- createNamingContext();
- } catch (NamingException e) {
- logger.error
- (sm.getString("naming.namingContextCreationFailed", e));
- }
- // 針對Context下配置Resource物件而言
- if (container instanceof Context) {
- // Setting the context in read only mode
- ContextAccessController.setReadOnly(getName());
- try {
- //通過此StandardContext物件獲取到JNDI物件樹根NamingContext物件
- //同時將此app的classloader與此JNDI物件樹根NamingContext物件繫結
- ContextBindings.bindClassLoader
- (container, container,
- ((Container) container).getLoader().getClassLoader());
- } catch (NamingException e) {
- logger.error(sm.getString("naming.bindFailed", e));
- }
- }
- // 針對global資源而言,這裡不用關注
- if (container instanceof Server) {
- namingResources.addPropertyChangeListener(this);
- org.apache.naming.factory.ResourceLinkFactory.setGlobalContext
- (namingContext);
- try {
- ContextBindings.bindClassLoader
- (container, container,
- this.getClass().getClassLoader());
- } catch (NamingException e) {
- logger.error(sm.getString("naming.bindFailed", e));
- }
- if (container instanceof StandardServer) {
- ((StandardServer) container).setGlobalNamingContext
- (namingContext);
- }
- }
- initialized = true;
- }
- //響應stop event
- elseif (event.getType() == Lifecycle.STOP_EVENT) {
- ......
- }
- }
public void lifecycleEvent(LifecycleEvent event) {
container = event.getLifecycle();
if (container instanceof Context) {
//這個namingResources物件就是StandardContext的namingResources物件
namingResources = ((Context) container).getNamingResources();
logger = log;
} else if (container instanceof Server) {
namingResources = ((Server) container).getGlobalNamingResources();
} else {
return;
}
//響應start event
if (event.getType() == Lifecycle.START_EVENT) {
if (initialized)
return;
Hashtable contextEnv = new Hashtable();
try {
//生成這個StandardContext域的JNDI物件樹根NamingContext物件
namingContext = new NamingContext(contextEnv, getName());
} catch (NamingException e) {
// Never happens
}
ContextAccessController.setSecurityToken(getName(), container);
//將此StandardContext物件與JNDI物件樹根NamingContext物件繫結
ContextBindings.bindContext(container, namingContext, container);
if( log.isDebugEnabled() ) {
log.debug("Bound " + container );
}
// Setting the context in read/write mode
ContextAccessController.setWritable(getName(), container);
try {
//將初始化時的資源物件繫結JNDI物件樹
createNamingContext();
} catch (NamingException e) {
logger.error
(sm.getString("naming.namingContextCreationFailed", e));
}
// 針對Context下配置Resource物件而言
if (container instanceof Context) {
// Setting the context in read only mode
ContextAccessController.setReadOnly(getName());
try {
//通過此StandardContext物件獲取到JNDI物件樹根NamingContext物件
//同時將此app的classloader與此JNDI物件樹根NamingContext物件繫結
ContextBindings.bindClassLoader
(container, container,
((Container) container).getLoader().getClassLoader());
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
}
// 針對global資源而言,這裡不用關注
if (container instanceof Server) {
namingResources.addPropertyChangeListener(this);
org.apache.naming.factory.ResourceLinkFactory.setGlobalContext
(namingContext);
try {
ContextBindings.bindClassLoader
(container, container,
this.getClass().getClassLoader());
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
if (container instanceof StandardServer) {
((StandardServer) container).setGlobalNamingContext
(namingContext);
}
}
initialized = true;
}
//響應stop event
else if (event.getType() == Lifecycle.STOP_EVENT) {
......
}
}
注意上面方法中有兩層繫結關係;
ContextBindings.bindContext()
Java程式碼
- publicstaticvoid bindContext(Object name, Context context,
- Object token) {
- if (ContextAccessController.checkSecurityToken(name, token))
- //先是將StandardContext物件與JNDI物件樹根NamingContext物件繫結
- //注意,這裡第一個引數name是StandardContext物件
- contextNameBindings.put(name, context);
- }
public static void bindContext(Object name, Context context,
Object token) {
if (ContextAccessController.checkSecurityToken(name, token))
//先是將StandardContext物件與JNDI物件樹根NamingContext物件繫結
//注意,這裡第一個引數name是StandardContext物件
contextNameBindings.put(name, context);
}
ContextBindings.bindClassLoader()
Java程式碼
- publicstaticvoid bindClassLoader(Object name, Object token,
- ClassLoader classLoader)
- throws NamingException {
- if (ContextAccessController.checkSecurityToken(name, token)) {
- //根據上面的StandardContext物件獲取剛才繫結的NamingContext物件
- Context context = (Context) contextNameBindings.get(name);
- if (context == null)
- thrownew NamingException
- (sm.getString("contextBindings.unknownContext", name));
- //將classloader與NamingContext物件繫結
- clBindings.put(classLoader, context);
- clNameBindings.put(classLoader, name);
- }
- }
public static void bindClassLoader(Object name, Object token,
ClassLoader classLoader)
throws NamingException {
if (ContextAccessController.checkSecurityToken(name, token)) {
//根據上面的StandardContext物件獲取剛才繫結的NamingContext物件
Context context = (Context) contextNameBindings.get(name);
if (context == null)
throw new NamingException
(sm.getString("contextBindings.unknownContext", name));
//將classloader與NamingContext物件繫結
clBindings.put(classLoader, context);
clNameBindings.put(classLoader, name);
}
}
主要看一下將初始化時的資源物件繫結JNDI物件樹的createNamingContext()方法;
Java程式碼
- privatevoid createNamingContext()
- throws NamingException {
- // Creating the comp subcontext
- if (container instanceof Server) {
- compCtx = namingContext;
- envCtx = namingContext;
- } else {
- //對於StandardContext而言,在JNDI物件樹的根namingContext物件上
- //建立comp樹枝,以及在comp樹枝上建立env樹枝namingContext物件
- compCtx = namingContext.createSubcontext("comp");
- envCtx = compCtx.createSubcontext("env");
- }
- ......
- // 從初始化的NamingResources物件中獲取Resource物件載入到JNDI物件樹上
- ContextResource[] resources = namingResources.findResources();
- for (i = 0; i < resources.length; i++) {
- addResource(resources[i]);
- }
- ......
- }
private void createNamingContext()
throws NamingException {
// Creating the comp subcontext
if (container instanceof Server) {
compCtx = namingContext;
envCtx = namingContext;
} else {
//對於StandardContext而言,在JNDI物件樹的根namingContext物件上
//建立comp樹枝,以及在comp樹枝上建立env樹枝namingContext物件
compCtx = namingContext.createSubcontext("comp");
envCtx = compCtx.createSubcontext("env");
}
......
// 從初始化的NamingResources物件中獲取Resource物件載入到JNDI物件樹上
ContextResource[] resources = namingResources.findResources();
for (i = 0; i < resources.length; i++) {
addResource(resources[i]);
}
......
}
看一下addResource的具體載入邏輯;
Java程式碼
- publicvoid addResource(ContextResource resource) {
- // Create a reference to the resource.
- Reference ref = new ResourceRef
- (resource.getType(), resource.getDescription(),
- resource.getScope(), resource.getAuth());
- // 遍歷Resource物件的各個屬性,這些屬性存在一個HashMap中
- Iterator params = resource.listProperties();
- while (params.hasNext()) {
- String paramName = (String) params.next();
- String paramValue = (String) resource.getProperty(paramName);
- //封裝成StringRefAddr,這些都是JNDI的標準API
- StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);
- ref.add(refAddr);
- }
- try {
- if (logger.isDebugEnabled()) {
- logger.debug(" Adding resource ref "
- + resource.getName() + " " + ref);
- }
- //在上面建立的comp/env樹枝節點上,根據Resource配置的name繼續建立新的節點
- //例如配置的name=”jdbc/mysql”,則在comp/env樹枝節點下再建立一個jdbc樹枝節點
- createSubcontexts(envCtx, resource.getName());
- //繫結葉子節點,它不是namingContext物件,而是最後的Resource物件
- envCtx.bind(resource.getName(), ref);
- } catch (NamingException e) {
- logger.error(sm.getString("naming.bindFailed", e));
- }
- //這就是上面說的對於配置type="javax.sql.DataSource"時的特殊邏輯
- //將資料庫連線池型別的資源物件註冊到tomcat全域性的JMX中,方便管理及除錯
- if ("javax.sql.DataSource".equals(ref.getClassName())) {
- try {
- ObjectName on = createObjectName(resource);
- Object actualResource = envCtx.lookup(resource.getName());
- Registry.getRegistry(null, null).registerComponent(actualResource, on, null);
- objectNames.put(resource.getName(), on);
- } catch (Exception e) {
- logger.warn(sm.getString("naming.jmxRegistrationFailed", e));
- }
- }
- }
public void addResource(ContextResource resource) {
// Create a reference to the resource.
Reference ref = new ResourceRef
(resource.getType(), resource.getDescription(),
resource.getScope(), resource.getAuth());
// 遍歷Resource物件的各個屬性,這些屬性存在一個HashMap中
Iterator params = resource.listProperties();
while (params.hasNext()) {
String paramName = (String) params.next();
String paramValue = (String) resource.getProperty(paramName);
//封裝成StringRefAddr,這些都是JNDI的標準API
StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);
ref.add(refAddr);
}
try {
if (logger.isDebugEnabled()) {
logger.debug(" Adding resource ref "
+ resource.getName() + " " + ref);
}
//在上面建立的comp/env樹枝節點上,根據Resource配置的name繼續建立新的節點
//例如配置的name=”jdbc/mysql”,則在comp/env樹枝節點下再建立一個jdbc樹枝節點
createSubcontexts(envCtx, resource.getName());
//繫結葉子節點,它不是namingContext物件,而是最後的Resource物件
envCtx.bind(resource.getName(), ref);
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
//這就是上面說的對於配置type="javax.sql.DataSource"時的特殊邏輯
//將資料庫連線池型別的資源物件註冊到tomcat全域性的JMX中,方便管理及除錯
if ("javax.sql.DataSource".equals(ref.getClassName())) {
try {
ObjectName on = createObjectName(resource);
Object actualResource = envCtx.lookup(resource.getName());
Registry.getRegistry(null, null).registerComponent(actualResource, on, null);
objectNames.put(resource.getName(), on);
} catch (Exception e) {
logger.warn(sm.getString("naming.jmxRegistrationFailed", e));
}
}
}
這就是上面配置的jdbc/mysql資料庫連線池的JNDI物件樹;
到目前為止,完成了JNDI物件樹的繫結,可以看到,每個app對應的StandardContext對應一個JNDI物件樹,並且每個app的各個classloader與此JNDI物件樹分別繫結,那麼各個app之間的JNDI可以不互相干擾,各自配置及呼叫。
需要注意的是,NamingContext物件就是JNDI物件樹上的樹枝節點,類似檔案系統中的目錄,各個Resource物件則是JNDI物件樹上的葉子節點,類似檔案系統的具體檔案,通過NamingContext物件將整個JNDI物件樹組織起來,每個Resource物件才是真正儲存資料的地方。