View Javadoc
1   /*
2    * Copyright 2002-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.jdbc.datasource;
18  
19  import java.lang.reflect.InvocationHandler;
20  import java.lang.reflect.InvocationTargetException;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Proxy;
23  import java.sql.Connection;
24  import java.sql.SQLException;
25  
26  import org.springframework.beans.factory.DisposableBean;
27  import org.springframework.util.Assert;
28  import org.springframework.util.ObjectUtils;
29  
30  /**
31   * Implementation of {@link SmartDataSource} that wraps a single JDBC Connection
32   * which is not closed after use. Obviously, this is not multi-threading capable.
33   *
34   * <p>Note that at shutdown, someone should close the underlying Connection
35   * via the {@code close()} method. Client code will never call close
36   * on the Connection handle if it is SmartDataSource-aware (e.g. uses
37   * {@code DataSourceUtils.releaseConnection}).
38   *
39   * <p>If client code will call {@code close()} in the assumption of a pooled
40   * Connection, like when using persistence tools, set "suppressClose" to "true".
41   * This will return a close-suppressing proxy instead of the physical Connection.
42   * Be aware that you will not be able to cast this to a native
43   * {@code OracleConnection} or the like anymore; you need to use a
44   * {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor} then.
45   *
46   * <p>This is primarily intended for testing. For example, it enables easy testing
47   * outside an application server, for code that expects to work on a DataSource.
48   * In contrast to {@link DriverManagerDataSource}, it reuses the same Connection
49   * all the time, avoiding excessive creation of physical Connections.
50   *
51   * @author Rod Johnson
52   * @author Juergen Hoeller
53   * @see #getConnection()
54   * @see java.sql.Connection#close()
55   * @see DataSourceUtils#releaseConnection
56   * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor
57   */
58  public class SingleConnectionDataSource extends DriverManagerDataSource
59  		implements SmartDataSource, DisposableBean {
60  
61  	/** Create a close-suppressing proxy? */
62  	private boolean suppressClose;
63  
64  	/** Override auto-commit state? */
65  	private Boolean autoCommit;
66  
67  	/** Wrapped Connection */
68  	private Connection target;
69  
70  	/** Proxy Connection */
71  	private Connection connection;
72  
73  	/** Synchronization monitor for the shared Connection */
74  	private final Object connectionMonitor = new Object();
75  
76  
77  	/**
78  	 * Constructor for bean-style configuration.
79  	 */
80  	public SingleConnectionDataSource() {
81  	}
82  
83  	/**
84  	 * Create a new SingleConnectionDataSource with the given standard
85  	 * DriverManager parameters.
86  	 * @param url the JDBC URL to use for accessing the DriverManager
87  	 * @param username the JDBC username to use for accessing the DriverManager
88  	 * @param password the JDBC password to use for accessing the DriverManager
89  	 * @param suppressClose if the returned Connection should be a
90  	 * close-suppressing proxy or the physical Connection
91  	 * @see java.sql.DriverManager#getConnection(String, String, String)
92  	 */
93  	public SingleConnectionDataSource(String url, String username, String password, boolean suppressClose) {
94  		super(url, username, password);
95  		this.suppressClose = suppressClose;
96  	}
97  
98  	/**
99  	 * Create a new SingleConnectionDataSource with the given standard
100 	 * DriverManager parameters.
101 	 * @param url the JDBC URL to use for accessing the DriverManager
102 	 * @param suppressClose if the returned Connection should be a
103 	 * close-suppressing proxy or the physical Connection
104 	 * @see java.sql.DriverManager#getConnection(String, String, String)
105 	 */
106 	public SingleConnectionDataSource(String url, boolean suppressClose) {
107 		super(url);
108 		this.suppressClose = suppressClose;
109 	}
110 
111 	/**
112 	 * Create a new SingleConnectionDataSource with a given Connection.
113 	 * @param target underlying target Connection
114 	 * @param suppressClose if the Connection should be wrapped with a Connection that
115 	 * suppresses {@code close()} calls (to allow for normal {@code close()}
116 	 * usage in applications that expect a pooled Connection but do not know our
117 	 * SmartDataSource interface)
118 	 */
119 	public SingleConnectionDataSource(Connection target, boolean suppressClose) {
120 		Assert.notNull(target, "Connection must not be null");
121 		this.target = target;
122 		this.suppressClose = suppressClose;
123 		this.connection = (suppressClose ? getCloseSuppressingConnectionProxy(target) : target);
124 	}
125 
126 
127 	/**
128 	 * Set whether the returned Connection should be a close-suppressing proxy
129 	 * or the physical Connection.
130 	 */
131 	public void setSuppressClose(boolean suppressClose) {
132 		this.suppressClose = suppressClose;
133 	}
134 
135 	/**
136 	 * Return whether the returned Connection will be a close-suppressing proxy
137 	 * or the physical Connection.
138 	 */
139 	protected boolean isSuppressClose() {
140 		return this.suppressClose;
141 	}
142 
143 	/**
144 	 * Set whether the returned Connection's "autoCommit" setting should be overridden.
145 	 */
146 	public void setAutoCommit(boolean autoCommit) {
147 		this.autoCommit = (autoCommit);
148 	}
149 
150 	/**
151 	 * Return whether the returned Connection's "autoCommit" setting should be overridden.
152 	 * @return the "autoCommit" value, or {@code null} if none to be applied
153 	 */
154 	protected Boolean getAutoCommitValue() {
155 		return this.autoCommit;
156 	}
157 
158 
159 	@Override
160 	public Connection getConnection() throws SQLException {
161 		synchronized (this.connectionMonitor) {
162 			if (this.connection == null) {
163 				// No underlying Connection -> lazy init via DriverManager.
164 				initConnection();
165 			}
166 			if (this.connection.isClosed()) {
167 				throw new SQLException(
168 						"Connection was closed in SingleConnectionDataSource. Check that user code checks " +
169 						"shouldClose() before closing Connections, or set 'suppressClose' to 'true'");
170 			}
171 			return this.connection;
172 		}
173 	}
174 
175 	/**
176 	 * Specifying a custom username and password doesn't make sense
177 	 * with a single Connection. Returns the single Connection if given
178 	 * the same username and password; throws a SQLException else.
179 	 */
180 	@Override
181 	public Connection getConnection(String username, String password) throws SQLException {
182 		if (ObjectUtils.nullSafeEquals(username, getUsername()) &&
183 				ObjectUtils.nullSafeEquals(password, getPassword())) {
184 			return getConnection();
185 		}
186 		else {
187 			throw new SQLException("SingleConnectionDataSource does not support custom username and password");
188 		}
189 	}
190 
191 	/**
192 	 * This is a single Connection: Do not close it when returning to the "pool".
193 	 */
194 	@Override
195 	public boolean shouldClose(Connection con) {
196 		synchronized (this.connectionMonitor) {
197 			return (con != this.connection && con != this.target);
198 		}
199 	}
200 
201 	/**
202 	 * Close the underlying Connection.
203 	 * The provider of this DataSource needs to care for proper shutdown.
204 	 * <p>As this bean implements DisposableBean, a bean factory will
205 	 * automatically invoke this on destruction of its cached singletons.
206 	 */
207 	@Override
208 	public void destroy() {
209 		synchronized (this.connectionMonitor) {
210 			closeConnection();
211 		}
212 	}
213 
214 
215 	/**
216 	 * Initialize the underlying Connection via the DriverManager.
217 	 */
218 	public void initConnection() throws SQLException {
219 		if (getUrl() == null) {
220 			throw new IllegalStateException("'url' property is required for lazily initializing a Connection");
221 		}
222 		synchronized (this.connectionMonitor) {
223 			closeConnection();
224 			this.target = getConnectionFromDriver(getUsername(), getPassword());
225 			prepareConnection(this.target);
226 			if (logger.isInfoEnabled()) {
227 				logger.info("Established shared JDBC Connection: " + this.target);
228 			}
229 			this.connection = (isSuppressClose() ? getCloseSuppressingConnectionProxy(this.target) : this.target);
230 		}
231 	}
232 
233 	/**
234 	 * Reset the underlying shared Connection, to be reinitialized on next access.
235 	 */
236 	public void resetConnection() {
237 		synchronized (this.connectionMonitor) {
238 			closeConnection();
239 			this.target = null;
240 			this.connection = null;
241 		}
242 	}
243 
244 	/**
245 	 * Prepare the given Connection before it is exposed.
246 	 * <p>The default implementation applies the auto-commit flag, if necessary.
247 	 * Can be overridden in subclasses.
248 	 * @param con the Connection to prepare
249 	 * @see #setAutoCommit
250 	 */
251 	protected void prepareConnection(Connection con) throws SQLException {
252 		Boolean autoCommit = getAutoCommitValue();
253 		if (autoCommit != null && con.getAutoCommit() != autoCommit) {
254 			con.setAutoCommit(autoCommit);
255 		}
256 	}
257 
258 	/**
259 	 * Close the underlying shared Connection.
260 	 */
261 	private void closeConnection() {
262 		if (this.target != null) {
263 			try {
264 				this.target.close();
265 			}
266 			catch (Throwable ex) {
267 				logger.warn("Could not close shared JDBC Connection", ex);
268 			}
269 		}
270 	}
271 
272 	/**
273 	 * Wrap the given Connection with a proxy that delegates every method call to it
274 	 * but suppresses close calls.
275 	 * @param target the original Connection to wrap
276 	 * @return the wrapped Connection
277 	 */
278 	protected Connection getCloseSuppressingConnectionProxy(Connection target) {
279 		return (Connection) Proxy.newProxyInstance(
280 				ConnectionProxy.class.getClassLoader(),
281 				new Class<?>[] {ConnectionProxy.class},
282 				new CloseSuppressingInvocationHandler(target));
283 	}
284 
285 
286 	/**
287 	 * Invocation handler that suppresses close calls on JDBC Connections.
288 	 */
289 	private static class CloseSuppressingInvocationHandler implements InvocationHandler {
290 
291 		private final Connection target;
292 
293 		public CloseSuppressingInvocationHandler(Connection target) {
294 			this.target = target;
295 		}
296 
297 		@Override
298 		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
299 			// Invocation on ConnectionProxy interface coming in...
300 
301 			if (method.getName().equals("equals")) {
302 				// Only consider equal when proxies are identical.
303 				return (proxy == args[0]);
304 			}
305 			else if (method.getName().equals("hashCode")) {
306 				// Use hashCode of Connection proxy.
307 				return System.identityHashCode(proxy);
308 			}
309 			else if (method.getName().equals("unwrap")) {
310 				if (((Class<?>) args[0]).isInstance(proxy)) {
311 					return proxy;
312 				}
313 			}
314 			else if (method.getName().equals("isWrapperFor")) {
315 				if (((Class<?>) args[0]).isInstance(proxy)) {
316 					return true;
317 				}
318 			}
319 			else if (method.getName().equals("close")) {
320 				// Handle close method: don't pass the call on.
321 				return null;
322 			}
323 			else if (method.getName().equals("isClosed")) {
324 				return false;
325 			}
326 			else if (method.getName().equals("getTargetConnection")) {
327 				// Handle getTargetConnection method: return underlying Connection.
328 				return this.target;
329 			}
330 
331 			// Invoke method on target Connection.
332 			try {
333 				return method.invoke(this.target, args);
334 			}
335 			catch (InvocationTargetException ex) {
336 				throw ex.getTargetException();
337 			}
338 		}
339 	}
340 
341 }