PS:本文代码来自tomcat7的源码,位于tomcat-jdbc-pool模块。
典型配置:
mysql:
<Resource type="javax.sql.DataSource"
name="jdbc/TestDB"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mysql"
username="mysql_user"
password="mypassword123"
/>
oracle:
<Resource name="jdbc/TestDB"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
type="javax.sql.DataSource"
username="scott"
password="scott"
driverClassName="oracle.jdbc.driver.OracleDriver"
url="jdbc:oracle:thin:@168.1.51.21:1522:orcl"
/>
tomcat配置数据源会涉及到 server.xml context.xml 和应用的 web.xml 三个文件,将上面的代码片段复制到tomcat server.xml的 GlobalNamingResources 标签内部,表示声明一个全局JNDI资源,其中factory="org.apache.tomcat.jdbc.pool.DataSourceFactory",不能修改,它用以表示一个JDNI的对象工厂,实现了javax.naming.spi.ObjectFactory,所以才能被JNDI管理,name表示jndi名称,type表示对象的类型,通过jndi查到对象后可以强转为此类型(其值为:javax.sql.DataSource 或者 javax.sql.XADataSource),
不过,这个JNDI的数据源还不能使用,你需要对应用的web.xml创建一个 ResourceLink element 使这个连接池对于web应用是可用的,
如果你想要用同一个名字让连接池对于所有的应用有效,最简单的方法就是对 context.xml 添加一个 ResourceLinkelement ,它的格式如下:
<ResourceLink type="javax.sql.DataSource"
name="jdbc/LocalTestDB"
global="jdbc/TestDB"/>
注意,如果你不想要全局的连接池,可以从
server.xml移除
Resource element到你的web应用的
context.xml 文件。
然后从刚配置好的连接池中获得连接,简单java代码:
Context initContext = new InitialContext();
Context envContext = (Context)initContext.lookup("java:/comp/env");
DataSource datasource = (DataSource)envContext.lookup("jdbc/LocalTestDB");
Connection con = datasource.getConnection();
注意:此种 Resource配置方式是不支持XADataSource的,即便设置了type="javax.sql.XADataSource"(这样lookup到的数据源确实是XADataSource的示例(instandeof 可通过)),但是其实是不能使用的,如果调用getXAConnection会报错,源码解析如下:
public XAConnection getXAConnection() throws SQLException {
Connection con = getConnection();
if (con instanceof XAConnection) {
return (XAConnection)con;
} else {
try {con.close();} catch (Exception ignore){}
throw new SQLException("Connection from pool does not implement javax.sql.XAConnection");
}
}
public Connection getConnection() throws SQLException {
//check out a connection
PooledConnection con = borrowConnection(-1,null,null);
return setupConnection(con);
}
<pre name="code" class="java"><pre name="code" class="java">//cache the constructor,通过 con.getXAConnection() != null 来判断 xa 变量是否是 true
if (proxyClassConstructor == null ) {
Class<?> proxyClass = xa ?
Proxy.getProxyClass(ConnectionPool.class.getClassLoader(), new Class[] {java.sql.Connection.class,javax.sql.PooledConnection.class, javax.sql.XAConnection.class}) :
Proxy.getProxyClass(ConnectionPool.class.getClassLoader(), new Class[] {java.sql.Connection.class,javax.sql.PooledConnection.class});
proxyClassConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
}
//省略了一些代码
getProxyConstructor(con.getXAConnection() != null);
//create the proxy
//TODO possible optimization, keep track if this connection was returned properly, and don't generate a new facade
Connection connection = null;
if (getPoolProperties().getUseDisposableConnectionFacade() ) {
connection = (Connection)proxyClassConstructor.newInstance(new Object[] { new DisposableConnectionFacade(handler) });
} else {
connection = (Connection)proxyClassConstructor.newInstance(new Object[] {handler});
}
//return the connection
return connection;
代码中通过 : con.getXAConnection() != null,判断是否是xa的连接,如果不是,那么返回的动态对象的类型(proxyClassConstructor)则不包含XAConnection,
这样,在强转为XADataSource后,调用getXAConnection,机会进入第一块代码的else里面,抛出异常:
throw new SQLException("Connection from pool does not implement javax.sql.XAConnection");
所以:通过 Resource 配置 driverClassName这种方式是不能获取到XAConnection(得到的XADataSource是徒有虚名的),那么如果才能获取到呢?
上面看到 con.getXAConnection() != null 这个是判断的关键,这个什么时候不是null呢?查看 org.apache.tomcat.jdbc.pool.ConnectionPool源码,连接池在每次创建一个连接的时候,都会调用 connect 方法,它的源码包含如下逻辑:
if (poolProperties.getDataSource()!=null) {
connectUsingDataSource();
} else {
connectUsingDriver();
}
在
connectUsingDataSource 方法里面则会更加数据源(这个是JNDI数据源,稍后讲到)类型(DataSource/XADataSource/ConnectionPoolDataSource)创建出合适的连接,如果是XADataSource则会创建XAConnection;
而connectUsingDriver方法,则只会根据配置的 driverClassName ,将其强制转换为java.sql.Driver,并反射创建一个实例,再调用其connect方法创建一个普通的数据库物理连接。
为题的关键在于:poolProperties.getDataSource() 这个什么时候不是null?查看 org.apache.tomcat.jdbc.pool.DataSourceFactory源码,可知其只有在配置了JNDI数据源的时候不是null,代码:
public DataSource createDataSource(Properties properties,Context context, boolean XA) throws Exception {
PoolConfiguration poolProperties = DataSourceFactory.parsePoolProperties(properties);
if (poolProperties.getDataSourceJNDI()!=null && poolProperties.getDataSource()==null) {
performJNDILookup(context, poolProperties);
}
org.apache.tomcat.jdbc.pool.DataSource dataSource = XA?
new org.apache.tomcat.jdbc.pool.XADataSource(poolProperties) :
new org.apache.tomcat.jdbc.pool.DataSource(poolProperties);
//initialise the pool itself
dataSource.createPool();
// Return the configured DataSource instance
return dataSource;
}
public void performJNDILookup(Context context, PoolConfiguration poolProperties) {
Object jndiDS = null;
try {
if (context!=null) {
jndiDS = context.lookup(poolProperties.getDataSourceJNDI());
} else {
log.warn("dataSourceJNDI property is configued, but local JNDI context is null.");
}
} catch (NamingException e) {
log.debug("The name \""+poolProperties.getDataSourceJNDI()+"\" can not be found in the local context.");
}
if (jndiDS==null) {
try {
context = new InitialContext();
jndiDS = context.lookup(poolProperties.getDataSourceJNDI());
} catch (NamingException e) {
log.warn("The name \""+poolProperties.getDataSourceJNDI()+"\" can not be found in the InitialContext.");
}
}
if (jndiDS!=null) {
poolProperties.setDataSource(jndiDS);
}
}
即,只要Resource配置文件配置的JNDI数据源可以找到,就会调用 poolProperties.setDataSource(jndiDS); 将其设置进来。
这个乍看起来似乎是个死套啊:像创建一个XA的数据源,必需先有一个XA的数据源,其实不然,这个死套是 org.apache.tomcat.jdbc.pool.DataSourceFactory 自己搞出来的,只要另外一个JNDI工厂不是这个类型就没有问题,tomcat还有另外一个对象工厂:org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory,这个工厂的逻辑很简单就是根据指定的type,直接反射生成对象,并将Resource设置的属性设置到生成的对象上就ok了,代码如下:
public class GenericNamingResourcesFactory implements ObjectFactory {
private static final Log log = LogFactory.getLog(GenericNamingResourcesFactory.class);
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if ((obj == null) || !(obj instanceof Reference)) {
return null;
}
Reference ref = (Reference) obj;
Enumeration<RefAddr> refs = ref.getAll();
String type = ref.getClassName();
Object o = Class.forName(type).newInstance();
while (refs.hasMoreElements()) {
RefAddr addr = refs.nextElement();
String param = addr.getType();
String value = null;
if (addr.getContent()!=null) {
value = addr.getContent().toString();
}
if (setProperty(o, param, value,false)) {
} else {
log.debug("Property not configured["+param+"]. No setter found on["+o+"].");
}
}
return o;
}
好了,原理清楚了(tomcat设计的真是巧妙,简单几个类就实现了一个连接池,据说性能高于dbcp和c3p0,建议大家多学习),那么最终的配置是这样的:
server.xml:
<Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
name="jdbc/MyXA1"
type="oracle.jdbc.xa.client.OracleXADataSource"
URL="jdbc:oracle:thin:@168.1.50.20:1522:orcl"
username="scott"
password="scott"/>
<Resource name="jdbc/TestDB"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
type="javax.sql.XADataSource"
dataSourceJNDI="MyXA1"
username="scott"
password="scott"/>
其它的都和配置普通driver类型的数据源一样,这样我们通过代码获取到数据源,强转为XADataSource,就可以成功调用到getXAConnection获取到XAConnection,
这里必须说明一下,基于tomcat的巧夺天工的设计,即使我们直接调用getConnection得到的对象虽然表象是Connection但其实也是XAConnection的实例,不信的话就 instanceof一下哦,它的奥妙就在于前面介绍的:proxyClassConstructor,说到最好还少了一个非常关键的类:org.apache.tomcat.jdbc.pool.ProxyConnection,在他是一个jdk动态代理的handler,通过数据源说到的Connection/Connection表象都只是一个句柄而已,通过句柄调入过来,最终都会被ProxyConnection拦截执行,来看看其拦截的庐山真面目吧:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (compare(ISCLOSED_VAL,method)) {
return Boolean.valueOf(isClosed());
}
if (compare(CLOSE_VAL,method)) {
if (connection==null) return null; //noop for already closed.
PooledConnection poolc = this.connection;
this.connection = null;
pool.returnConnection(poolc);
return null;
} else if (compare(TOSTRING_VAL,method)) {
return this.toString();
} else if (compare(GETCONNECTION_VAL,method) && connection!=null) {
return connection.getConnection();
} else if (method.getDeclaringClass().equals(XAConnection.class)) {
try {
return method.invoke(connection.getXAConnection(),args);
}catch (Throwable t) {
if (t instanceof InvocationTargetException) {
throw t.getCause() != null ? t.getCause() : t;
} else {
throw t;
}
}
}
if (isClosed()) throw new SQLException("Connection has already been closed.");
if (compare(UNWRAP_VAL,method)) {
return unwrap((Class<?>)args[0]);
} else if (compare(ISWRAPPERFOR_VAL,method)) {
return Boolean.valueOf(this.isWrapperFor((Class<?>)args[0]));
}
try {
PooledConnection poolc = connection;
if (poolc!=null) {
return method.invoke(poolc.getConnection(),args);
} else {
throw new SQLException("Connection has already been closed.");
}
}catch (Throwable t) {
if (t instanceof InvocationTargetException) {
throw t.getCause() != null ? t.getCause() : t;
} else {
throw t;
}
}
}
简单介绍:当调用句柄的close方法时,其实并没有关闭连接,而是归还到了连接池,当调用的方法是XAConnection声明的时就使用持有的真实的XAConnection对象来执行,这是两个比较关键的点,其他的自行体会吧,
相关参考:
Java 数据源 DataSource 架构分析 (jdk7/tomcat7)
其它可参考:http://blog.csdn.net/yp5185423/article/details/8299800