LazyConnectionDataSourceProxy.java 15.6 KB
Newer Older
A
Arjen Poutsma 已提交
1
/*
2
 * Copyright 2002-2013 the original author or authors.
A
Arjen Poutsma 已提交
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jdbc.datasource;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.Constants;

/**
 * Proxy for a target DataSource, fetching actual JDBC Connections lazily,
 * i.e. not until first creation of a Statement. Connection initialization
 * properties like auto-commit mode, transaction isolation and read-only mode
 * will be kept and applied to the actual JDBC Connection as soon as an
 * actual Connection is fetched (if ever). Consequently, commit and rollback
 * calls will be ignored if no Statements have been created.
 *
 * <p>This DataSource proxy allows to avoid fetching JDBC Connections from
 * a pool unless actually necessary. JDBC transaction control can happen
 * without fetching a Connection from the pool or communicating with the
 * database; this will be done lazily on first creation of a JDBC Statement.
 *
 * <p><b>If you configure both a LazyConnectionDataSourceProxy and a
 * TransactionAwareDataSourceProxy, make sure that the latter is the outermost
 * DataSource.</b> In such a scenario, data access code will talk to the
 * transaction-aware DataSource, which will in turn work with the
 * LazyConnectionDataSourceProxy.
 *
 * <p>Lazy fetching of physical JDBC Connections is particularly beneficial
 * in a generic transaction demarcation environment. It allows you to demarcate
 * transactions on all methods that could potentially perform data access,
 * without paying a performance penalty if no actual data access happens.
 *
 * <p>This DataSource proxy gives you behavior analogous to JTA and a
 * transactional JNDI DataSource (as provided by the J2EE server), even
 * with a local transaction strategy like DataSourceTransactionManager or
 * HibernateTransactionManager. It does not add value with Spring's
 * JtaTransactionManager as transaction strategy.
 *
 * <p>Lazy fetching of JDBC Connections is also recommended for read-only
 * operations with Hibernate, in particular if the chances of resolving the
 * result in the second-level cache are high. This avoids the need to
 * communicate with the database at all for such read-only operations.
 * You will get the same effect with non-transactional reads, but lazy fetching
 * of JDBC Connections allows you to still perform reads in transactions.
 *
69 70 71 72 73 74 75
 * <p><b>NOTE:</b> This DataSource proxy needs to return wrapped Connections
 * (which implement the {@link ConnectionProxy} interface) in order to handle
 * lazy fetching of an actual JDBC Connection. Therefore, the returned Connections
 * cannot be cast to a native JDBC Connection type such as OracleConnection or
 * to a connection pool implementation type. Use a corresponding
 * {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor}
 * or JDBC 4's {@link Connection#unwrap} to retrieve the native JDBC Connection.
A
Arjen Poutsma 已提交
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
 *
 * @author Juergen Hoeller
 * @since 1.1.4
 * @see DataSourceTransactionManager
 */
public class LazyConnectionDataSourceProxy extends DelegatingDataSource {

	/** Constants instance for TransactionDefinition */
	private static final Constants constants = new Constants(Connection.class);

	private static final Log logger = LogFactory.getLog(LazyConnectionDataSourceProxy.class);

	private Boolean defaultAutoCommit;

	private Integer defaultTransactionIsolation;


	/**
	 * Create a new LazyConnectionDataSourceProxy.
	 * @see #setTargetDataSource
	 */
	public LazyConnectionDataSourceProxy() {
	}

	/**
	 * Create a new LazyConnectionDataSourceProxy.
	 * @param targetDataSource the target DataSource
	 */
	public LazyConnectionDataSourceProxy(DataSource targetDataSource) {
		setTargetDataSource(targetDataSource);
		afterPropertiesSet();
	}


	/**
	 * Set the default auto-commit mode to expose when no target Connection
	 * has been fetched yet (-> actual JDBC Connection default not known yet).
	 * <p>If not specified, the default gets determined by checking a target
	 * Connection on startup. If that check fails, the default will be determined
	 * lazily on first access of a Connection.
	 * @see java.sql.Connection#setAutoCommit
	 */
	public void setDefaultAutoCommit(boolean defaultAutoCommit) {
J
Juergen Hoeller 已提交
119
		this.defaultAutoCommit = defaultAutoCommit;
A
Arjen Poutsma 已提交
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
	}

	/**
	 * Set the default transaction isolation level to expose when no target Connection
	 * has been fetched yet (-> actual JDBC Connection default not known yet).
	 * <p>This property accepts the int constant value (e.g. 8) as defined in the
	 * {@link java.sql.Connection} interface; it is mainly intended for programmatic
	 * use. Consider using the "defaultTransactionIsolationName" property for setting
	 * the value by name (e.g. "TRANSACTION_SERIALIZABLE").
	 * <p>If not specified, the default gets determined by checking a target
	 * Connection on startup. If that check fails, the default will be determined
	 * lazily on first access of a Connection.
	 * @see #setDefaultTransactionIsolationName
	 * @see java.sql.Connection#setTransactionIsolation
	 */
	public void setDefaultTransactionIsolation(int defaultTransactionIsolation) {
J
Juergen Hoeller 已提交
136
		this.defaultTransactionIsolation = defaultTransactionIsolation;
A
Arjen Poutsma 已提交
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
	}

	/**
	 * Set the default transaction isolation level by the name of the corresponding
	 * constant in {@link java.sql.Connection}, e.g. "TRANSACTION_SERIALIZABLE".
	 * @param constantName name of the constant
	 * @see #setDefaultTransactionIsolation
	 * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
	 * @see java.sql.Connection#TRANSACTION_READ_COMMITTED
	 * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
	 * @see java.sql.Connection#TRANSACTION_SERIALIZABLE
	 */
	public void setDefaultTransactionIsolationName(String constantName) {
		setDefaultTransactionIsolation(constants.asNumber(constantName).intValue());
	}


154
	@Override
A
Arjen Poutsma 已提交
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
	public void afterPropertiesSet() {
		super.afterPropertiesSet();

		// Determine default auto-commit and transaction isolation
		// via a Connection from the target DataSource, if possible.
		if (this.defaultAutoCommit == null || this.defaultTransactionIsolation == null) {
			try {
				Connection con = getTargetDataSource().getConnection();
				try {
					checkDefaultConnectionProperties(con);
				}
				finally {
					con.close();
				}
			}
			catch (SQLException ex) {
				logger.warn("Could not retrieve default auto-commit and transaction isolation settings", ex);
			}
		}
	}

	/**
	 * Check the default connection properties (auto-commit, transaction isolation),
	 * keeping them to be able to expose them correctly without fetching an actual
	 * JDBC Connection from the target DataSource.
	 * <p>This will be invoked once on startup, but also for each retrieval of a
	 * target Connection. If the check failed on startup (because the database was
	 * down), we'll lazily retrieve those settings.
	 * @param con the Connection to use for checking
	 * @throws SQLException if thrown by Connection methods
	 */
	protected synchronized void checkDefaultConnectionProperties(Connection con) throws SQLException {
		if (this.defaultAutoCommit == null) {
J
Juergen Hoeller 已提交
188
			this.defaultAutoCommit = con.getAutoCommit();
A
Arjen Poutsma 已提交
189 190
		}
		if (this.defaultTransactionIsolation == null) {
J
Juergen Hoeller 已提交
191
			this.defaultTransactionIsolation = con.getTransactionIsolation();
A
Arjen Poutsma 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
		}
	}

	/**
	 * Expose the default auto-commit value.
	 */
	protected Boolean defaultAutoCommit() {
		return this.defaultAutoCommit;
	}

	/**
	 * Expose the default transaction isolation value.
	 */
	protected Integer defaultTransactionIsolation() {
		return this.defaultTransactionIsolation;
	}


	/**
	 * Return a Connection handle that lazily fetches an actual JDBC Connection
	 * when asked for a Statement (or PreparedStatement or CallableStatement).
	 * <p>The returned Connection handle implements the ConnectionProxy interface,
	 * allowing to retrieve the underlying target Connection.
	 * @return a lazy Connection handle
	 * @see ConnectionProxy#getTargetConnection()
	 */
218
	@Override
A
Arjen Poutsma 已提交
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
	public Connection getConnection() throws SQLException {
		return (Connection) Proxy.newProxyInstance(
				ConnectionProxy.class.getClassLoader(),
				new Class[] {ConnectionProxy.class},
				new LazyConnectionInvocationHandler());
	}

	/**
	 * Return a Connection handle that lazily fetches an actual JDBC Connection
	 * when asked for a Statement (or PreparedStatement or CallableStatement).
	 * <p>The returned Connection handle implements the ConnectionProxy interface,
	 * allowing to retrieve the underlying target Connection.
	 * @param username the per-Connection username
	 * @param password the per-Connection password
	 * @return a lazy Connection handle
	 * @see ConnectionProxy#getTargetConnection()
	 */
236
	@Override
A
Arjen Poutsma 已提交
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
	public Connection getConnection(String username, String password) throws SQLException {
		return (Connection) Proxy.newProxyInstance(
				ConnectionProxy.class.getClassLoader(),
				new Class[] {ConnectionProxy.class},
				new LazyConnectionInvocationHandler(username, password));
	}


	/**
	 * Invocation handler that defers fetching an actual JDBC Connection
	 * until first creation of a Statement.
	 */
	private class LazyConnectionInvocationHandler implements InvocationHandler {

		private String username;

		private String password;

		private Boolean readOnly = Boolean.FALSE;

		private Integer transactionIsolation;

		private Boolean autoCommit;

		private boolean closed = false;

		private Connection target;

		public LazyConnectionInvocationHandler() {
			this.autoCommit = defaultAutoCommit();
			this.transactionIsolation = defaultTransactionIsolation();
		}

		public LazyConnectionInvocationHandler(String username, String password) {
			this();
			this.username = username;
			this.password = password;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			// Invocation on ConnectionProxy interface coming in...

			if (method.getName().equals("equals")) {
				// We must avoid fetching a target Connection for "equals".
				// Only consider equal when proxies are identical.
J
Juergen Hoeller 已提交
282
				return (proxy == args[0]);
A
Arjen Poutsma 已提交
283 284 285 286 287
			}
			else if (method.getName().equals("hashCode")) {
				// We must avoid fetching a target Connection for "hashCode",
				// and we must return the same hash code even when the target
				// Connection has been fetched: use hashCode of Connection proxy.
J
Juergen Hoeller 已提交
288
				return System.identityHashCode(proxy);
A
Arjen Poutsma 已提交
289
			}
290 291 292 293 294 295 296 297 298 299
			else if (method.getName().equals("unwrap")) {
				if (((Class) args[0]).isInstance(proxy)) {
					return proxy;
				}
			}
			else if (method.getName().equals("isWrapperFor")) {
				if (((Class) args[0]).isInstance(proxy)) {
					return true;
				}
			}
A
Arjen Poutsma 已提交
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
			else if (method.getName().equals("getTargetConnection")) {
				// Handle getTargetConnection method: return underlying connection.
				return getTargetConnection(method);
			}

			if (!hasTargetConnection()) {
				// No physical target Connection kept yet ->
				// resolve transaction demarcation methods without fetching
				// a physical JDBC Connection until absolutely necessary.

				if (method.getName().equals("toString")) {
					return "Lazy Connection proxy for target DataSource [" + getTargetDataSource() + "]";
				}
				else if (method.getName().equals("isReadOnly")) {
					return this.readOnly;
				}
				else if (method.getName().equals("setReadOnly")) {
					this.readOnly = (Boolean) args[0];
					return null;
				}
				else if (method.getName().equals("getTransactionIsolation")) {
					if (this.transactionIsolation != null) {
						return this.transactionIsolation;
					}
					// Else fetch actual Connection and check there,
					// because we didn't have a default specified.
				}
				else if (method.getName().equals("setTransactionIsolation")) {
					this.transactionIsolation = (Integer) args[0];
					return null;
				}
				else if (method.getName().equals("getAutoCommit")) {
					if (this.autoCommit != null) {
						return this.autoCommit;
					}
					// Else fetch actual Connection and check there,
					// because we didn't have a default specified.
				}
				else if (method.getName().equals("setAutoCommit")) {
					this.autoCommit = (Boolean) args[0];
					return null;
				}
				else if (method.getName().equals("commit")) {
					// Ignore: no statements created yet.
					return null;
				}
				else if (method.getName().equals("rollback")) {
					// Ignore: no statements created yet.
					return null;
				}
				else if (method.getName().equals("getWarnings")) {
					return null;
				}
				else if (method.getName().equals("clearWarnings")) {
					return null;
				}
				else if (method.getName().equals("close")) {
					// Ignore: no target connection yet.
					this.closed = true;
					return null;
				}
361 362 363
				else if (method.getName().equals("isClosed")) {
					return this.closed;
				}
A
Arjen Poutsma 已提交
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
				else if (this.closed) {
					// Connection proxy closed, without ever having fetched a
					// physical JDBC Connection: throw corresponding SQLException.
					throw new SQLException("Illegal operation: connection is closed");
				}
			}

			// Target Connection already fetched,
			// or target Connection necessary for current operation ->
			// invoke method on target connection.
			try {
				return method.invoke(getTargetConnection(method), args);
			}
			catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}

		/**
		 * Return whether the proxy currently holds a target Connection.
		 */
		private boolean hasTargetConnection() {
			return (this.target != null);
		}

		/**
		 * Return the target Connection, fetching it and initializing it if necessary.
		 */
		private Connection getTargetConnection(Method operation) throws SQLException {
			if (this.target == null) {
				// No target Connection held -> fetch one.
				if (logger.isDebugEnabled()) {
					logger.debug("Connecting to database for operation '" + operation.getName() + "'");
				}

				// Fetch physical Connection from DataSource.
				this.target = (this.username != null) ?
						getTargetDataSource().getConnection(this.username, this.password) :
						getTargetDataSource().getConnection();

				// If we still lack default connection properties, check them now.
				checkDefaultConnectionProperties(this.target);

				// Apply kept transaction settings, if any.
J
Juergen Hoeller 已提交
408
				if (this.readOnly) {
409 410 411 412 413 414 415
					try {
						this.target.setReadOnly(this.readOnly);
					}
					catch (Exception ex) {
						// "read-only not supported" -> ignore, it's just a hint anyway
						logger.debug("Could not set JDBC Connection read-only", ex);
					}
A
Arjen Poutsma 已提交
416 417 418
				}
				if (this.transactionIsolation != null &&
						!this.transactionIsolation.equals(defaultTransactionIsolation())) {
J
Juergen Hoeller 已提交
419
					this.target.setTransactionIsolation(this.transactionIsolation);
A
Arjen Poutsma 已提交
420
				}
J
Juergen Hoeller 已提交
421 422
				if (this.autoCommit != null && this.autoCommit != this.target.getAutoCommit()) {
					this.target.setAutoCommit(this.autoCommit);
A
Arjen Poutsma 已提交
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
				}
			}

			else {
				// Target Connection already held -> return it.
				if (logger.isDebugEnabled()) {
					logger.debug("Using existing database connection for operation '" + operation.getName() + "'");
				}
			}

			return this.target;
		}
	}

}