问题描述:
JBoss使用JDBC驱动时,第一次建立如果库连接会抛出类似于下面的异常
21:21:36,666 WARN [JBossManagedConnectionPool] Throwable while attempting to get a new connection: null
org.jboss.resource.JBossResourceException: Could not create connection; - nested throwable: (org.jboss.resource.JBossResourceException: Apparently wrong driver class specified for URL: class: oracle.jdbc.driver.OracleDriver, url: jdbc:oracle:thin:@ 10.0.32 .25:1521:sid)
at org.jboss.resource.adapter.jdbc.local.LocalManagedConnectionFactory.createManagedConnection(LocalManagedConnectionFactory.java:179)
at org.jboss.resource.connectionmanager.InternalManagedConnectionPool.createConnectionEventListener(InternalManagedConnectionPool.java:565)
但在第二次取得数据库连接时却是正常的。
解决方法一:
将JDBC驱动拷贝到jboss_server_home/lib目录下。这个方法对于每一个jboss环境都需要进行这样的拷贝,比较麻烦。
解决方法二:
将webapp打包到一个ear包里,在ear包的META-INF/application.xml文件里添加类似下面的内容:
<module>
<java>oracle_jdbc_path/ojdbc14.jar</java>
</module>
其中的oracle_jdbc_path是相对于ear包的一个路径,以指示驱动程序的位置,通常可以设置成ear中包含的war包的lib目录。这种方法使部署和jboss的环境无关。
对于第二次方法也可以将*-ds.xml文件一起打包到ear里,只要在ear包的META-INF/application.xml添加类似下面的内容就可以了:
<module>
<java>oracle-ds.xml</java>
</module>
这样使用数据库连接池的配置也随ear一起发布。
问题分析:
出现这个问题的JBoss服务器配置文件jboss_server_home/deploy/jbossweb-tomcat55.sar/META-INF/jboss-service.xml里的UseJBossWebLoader配置项应该都是配置成为false的,即使用的并不是JBoss共享扁平的ClasssLoader并且jboss_server_home/lib不包含jdbc驱动。下面我们来分析造成这个问题的具体原因。
首先我们来了解一下JDBC驱动的管理。JDBC驱动程序在载入的时候都会通过java.sql.DriverManager.registerDriver(Driver)方法将自身注册到驱动管理器中。在注册后我们就可以通过DriverManager.getDriver(String url)方法取得能够处理传入的数据库url的驱动程序,或者通过DriverManager.getConnection(String url, String user, String password)方法取得url对应驱动的连接。但在这里有一个问题需要我们注意,在取得连接或者驱动的时候,它需要从已注册的驱动里选择合适的驱动程序出来。这个合适的驱动总结出来有两条
一是当前调用getDriver或getConnect方法的类的ClassLoader能够载入相应的JDBC驱动程序,并且载入的JDBC驱动程序类要和已注册的驱动程序类相等,调用getDriver或getConnect方法的类ClassLoader通过本地方法DriverManager.getCallerClassLoader()获得,它得到调用类的ClassLoader。这一点理解起来可能不太容易,我们可以简单的举个例子。我们考虑这样的一个ClassLoader结构:
有个SystemClassLoader,它对应到System classpath。还有一个自定义的ClassLoader,如MyClassLoader,它覆盖了ClassLoader默认的parent first委派模型,即先载入自己classpath的类,如果找不到再通过parent ClassLoader来载入。同时我们在system classpath和自定义的ClassLoader对应的classpath里都存在一个JDBC驱动程序的jar包。这个时候,我们用MyClassLoader载入了一个类MyClass,MyClass类在system classpath应该不存在的,因此这个类的定义classLoader应该就是MyClassLoader。在调用MyClass的方法前,我们先设置TCL为MyClassLoader。在被调用的MyClass方法中,首先注册JDBC驱动,这个时候注册的驱动对应的ClassLoader为MyClassLoader,然后通过ConnectionManager取得数据库连接,ConnectionManager只在system classpath里存在,在ConnectionManager取得数据库连接的时候,它从DriverManager注册的驱动里找,能找到一个MyClassLoader注册的驱动,但由于这个驱动的载入ClassLoader是MyClassLoader,而DriverManager.getConnection方法是在ConnectionManager里调用的,因此ConnectionManager是DriverManager相应方法的调用类,ConnectionManager类的ClassLoader是SystemClassLoader,SystemClassLoader载入的驱动类和MyClass注册的驱动类并不相等,他们由不同的ClassLoader载入。因此MyClass里注册的JDBC驱动不符合要求,不被使用,这个时候SystemClassLoader会重新用SystemClassLoader注册一个JDBC驱动(通过DriverManagr里getCallerClass(callerCL, di.driverClassName)方法来注册),而不使用MyClass注册的驱动。
二是对于符合前面条件的驱动,还需要判断当前这个驱动能不能处理连接url,如果不符合,则从注册的驱动里查找下一次驱动,如果满足,则使用这个驱动来创建连接。
接下去我们来分析JBoss的DataSource管理部分,JBoss会将部署目录里的*-ds.xml转换来MBean service由SarDeployer进行部署(参见:http://jdeveloper.zhan.cn.yahoo.com/articles/080118/79403_23.html),而SARDeployer在部署这个DataSource的时候是在JBoss全局共享的UCL(JBoss unified ClassLoader)里进行部署的。我们通过JNDI得到JBoss的DataSource后,会调用DataSource上的getConnection()方法得到连接。JBoss的DataSource会调用org.jboss.resource.adapter.jdbc.local. LocalManagedConnectionFactory.createManagedConnection()方法来取得连接,这个方法在获取连接前会首先判断自己的一个成员变量driver是否已经初始化过,如果已经初始化过了,则用这个driver来建立数据库连接,如果没有,则通过DriverManager判断已注册的驱动能不能处理连接请求(判断逻辑参考上面的内容),如果不能处理,则用TCL(当前线程ClassLoader,如果在Webapp里调用,这个TCL往往是WebAppClassLoader)来载入JDBC驱动,在载入完后,再判断已注册的驱动能不能处理连接语法请求,如果不能,则用刚才通过TCL初始化的Driver新建一个Driver实例,并赋值给成员变量driver,同时再判断一次已注册的驱动能否处理当前连接请求,如果不能,则抛出JBossResourceException异常,如果可以,则返回driver实例,并用这个实例创建连接。但由于LocalManagedConnectionFactory类是由JBoss全局共享的UCL载入的,它在调用DriverManager上的方法取的数据库驱动时,它会用这个UCL来载入JDBC驱动,这个时候如果UCL的classpath不存在JDBC驱动,使它载入驱动失败,因而导致LocalManagedConnectionFactory判断当前没有可用的驱动可以处理当前请求,而抛出异常。但在第二次进行接连请求的时候,由于LocalManagedConnectionFactory的成员变量driver已被初始化过了,所以不再进行连接请求的能否处理的判断,而使连接可以正常建立。从上面的分析可以看出,要避免这个问题的发生就需要将相应的JDBC驱动放入到这个全局共享的UCL中。
如果将JDBC放入到这个全局共享的UCL中呢,一个方法就是将JDBC driver拷贝到jboss_server_home/lib目录下,这个时候驱动将自动的载入到这个UCL中。其次在ear使用非隔离的classloader的情况下,可以在some.ear/META-INF/application.xml文件里添加类似如下的内容
<module>
<java>oracle_jdbc_path/ojdbc14.jar</java>
</module>