001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.datasource;
018
019import java.lang.reflect.InvocationHandler;
020import java.lang.reflect.InvocationTargetException;
021import java.lang.reflect.Method;
022import java.lang.reflect.Proxy;
023import java.sql.Connection;
024import java.sql.SQLException;
025
026import org.springframework.beans.factory.DisposableBean;
027import org.springframework.util.Assert;
028import org.springframework.util.ObjectUtils;
029
030/**
031 * Implementation of {@link SmartDataSource} that wraps a single JDBC Connection
032 * which is not closed after use. Obviously, this is not multi-threading capable.
033 *
034 * <p>Note that at shutdown, someone should close the underlying Connection
035 * via the {@code close()} method. Client code will never call close
036 * on the Connection handle if it is SmartDataSource-aware (e.g. uses
037 * {@code DataSourceUtils.releaseConnection}).
038 *
039 * <p>If client code will call {@code close()} in the assumption of a pooled
040 * Connection, like when using persistence tools, set "suppressClose" to "true".
041 * This will return a close-suppressing proxy instead of the physical Connection.
042 *
043 * <p>This is primarily intended for testing. For example, it enables easy testing
044 * outside an application server, for code that expects to work on a DataSource.
045 * In contrast to {@link DriverManagerDataSource}, it reuses the same Connection
046 * all the time, avoiding excessive creation of physical Connections.
047 *
048 * @author Rod Johnson
049 * @author Juergen Hoeller
050 * @see #getConnection()
051 * @see java.sql.Connection#close()
052 * @see DataSourceUtils#releaseConnection
053 */
054public class SingleConnectionDataSource extends DriverManagerDataSource implements SmartDataSource, DisposableBean {
055
056        /** Create a close-suppressing proxy? */
057        private boolean suppressClose;
058
059        /** Override auto-commit state? */
060        private Boolean autoCommit;
061
062        /** Wrapped Connection */
063        private Connection target;
064
065        /** Proxy Connection */
066        private Connection connection;
067
068        /** Synchronization monitor for the shared Connection */
069        private final Object connectionMonitor = new Object();
070
071
072        /**
073         * Constructor for bean-style configuration.
074         */
075        public SingleConnectionDataSource() {
076        }
077
078        /**
079         * Create a new SingleConnectionDataSource with the given standard
080         * DriverManager parameters.
081         * @param url the JDBC URL to use for accessing the DriverManager
082         * @param username the JDBC username to use for accessing the DriverManager
083         * @param password the JDBC password to use for accessing the DriverManager
084         * @param suppressClose if the returned Connection should be a
085         * close-suppressing proxy or the physical Connection
086         * @see java.sql.DriverManager#getConnection(String, String, String)
087         */
088        public SingleConnectionDataSource(String url, String username, String password, boolean suppressClose) {
089                super(url, username, password);
090                this.suppressClose = suppressClose;
091        }
092
093        /**
094         * Create a new SingleConnectionDataSource with the given standard
095         * DriverManager parameters.
096         * @param url the JDBC URL to use for accessing the DriverManager
097         * @param suppressClose if the returned Connection should be a
098         * close-suppressing proxy or the physical Connection
099         * @see java.sql.DriverManager#getConnection(String, String, String)
100         */
101        public SingleConnectionDataSource(String url, boolean suppressClose) {
102                super(url);
103                this.suppressClose = suppressClose;
104        }
105
106        /**
107         * Create a new SingleConnectionDataSource with a given Connection.
108         * @param target underlying target Connection
109         * @param suppressClose if the Connection should be wrapped with a Connection that
110         * suppresses {@code close()} calls (to allow for normal {@code close()}
111         * usage in applications that expect a pooled Connection but do not know our
112         * SmartDataSource interface)
113         */
114        public SingleConnectionDataSource(Connection target, boolean suppressClose) {
115                Assert.notNull(target, "Connection must not be null");
116                this.target = target;
117                this.suppressClose = suppressClose;
118                this.connection = (suppressClose ? getCloseSuppressingConnectionProxy(target) : target);
119        }
120
121
122        /**
123         * Set whether the returned Connection should be a close-suppressing proxy
124         * or the physical Connection.
125         */
126        public void setSuppressClose(boolean suppressClose) {
127                this.suppressClose = suppressClose;
128        }
129
130        /**
131         * Return whether the returned Connection will be a close-suppressing proxy
132         * or the physical Connection.
133         */
134        protected boolean isSuppressClose() {
135                return this.suppressClose;
136        }
137
138        /**
139         * Set whether the returned Connection's "autoCommit" setting should be overridden.
140         */
141        public void setAutoCommit(boolean autoCommit) {
142                this.autoCommit = (autoCommit);
143        }
144
145        /**
146         * Return whether the returned Connection's "autoCommit" setting should be overridden.
147         * @return the "autoCommit" value, or {@code null} if none to be applied
148         */
149        protected Boolean getAutoCommitValue() {
150                return this.autoCommit;
151        }
152
153
154        @Override
155        public Connection getConnection() throws SQLException {
156                synchronized (this.connectionMonitor) {
157                        if (this.connection == null) {
158                                // No underlying Connection -> lazy init via DriverManager.
159                                initConnection();
160                        }
161                        if (this.connection.isClosed()) {
162                                throw new SQLException(
163                                                "Connection was closed in SingleConnectionDataSource. Check that user code checks " +
164                                                "shouldClose() before closing Connections, or set 'suppressClose' to 'true'");
165                        }
166                        return this.connection;
167                }
168        }
169
170        /**
171         * Specifying a custom username and password doesn't make sense
172         * with a single Connection. Returns the single Connection if given
173         * the same username and password; throws a SQLException else.
174         */
175        @Override
176        public Connection getConnection(String username, String password) throws SQLException {
177                if (ObjectUtils.nullSafeEquals(username, getUsername()) &&
178                                ObjectUtils.nullSafeEquals(password, getPassword())) {
179                        return getConnection();
180                }
181                else {
182                        throw new SQLException("SingleConnectionDataSource does not support custom username and password");
183                }
184        }
185
186        /**
187         * This is a single Connection: Do not close it when returning to the "pool".
188         */
189        @Override
190        public boolean shouldClose(Connection con) {
191                synchronized (this.connectionMonitor) {
192                        return (con != this.connection && con != this.target);
193                }
194        }
195
196        /**
197         * Close the underlying Connection.
198         * The provider of this DataSource needs to care for proper shutdown.
199         * <p>As this bean implements DisposableBean, a bean factory will
200         * automatically invoke this on destruction of its cached singletons.
201         */
202        @Override
203        public void destroy() {
204                synchronized (this.connectionMonitor) {
205                        closeConnection();
206                }
207        }
208
209
210        /**
211         * Initialize the underlying Connection via the DriverManager.
212         */
213        public void initConnection() throws SQLException {
214                if (getUrl() == null) {
215                        throw new IllegalStateException("'url' property is required for lazily initializing a Connection");
216                }
217                synchronized (this.connectionMonitor) {
218                        closeConnection();
219                        this.target = getConnectionFromDriver(getUsername(), getPassword());
220                        prepareConnection(this.target);
221                        if (logger.isInfoEnabled()) {
222                                logger.info("Established shared JDBC Connection: " + this.target);
223                        }
224                        this.connection = (isSuppressClose() ? getCloseSuppressingConnectionProxy(this.target) : this.target);
225                }
226        }
227
228        /**
229         * Reset the underlying shared Connection, to be reinitialized on next access.
230         */
231        public void resetConnection() {
232                synchronized (this.connectionMonitor) {
233                        closeConnection();
234                        this.target = null;
235                        this.connection = null;
236                }
237        }
238
239        /**
240         * Prepare the given Connection before it is exposed.
241         * <p>The default implementation applies the auto-commit flag, if necessary.
242         * Can be overridden in subclasses.
243         * @param con the Connection to prepare
244         * @see #setAutoCommit
245         */
246        protected void prepareConnection(Connection con) throws SQLException {
247                Boolean autoCommit = getAutoCommitValue();
248                if (autoCommit != null && con.getAutoCommit() != autoCommit) {
249                        con.setAutoCommit(autoCommit);
250                }
251        }
252
253        /**
254         * Close the underlying shared Connection.
255         */
256        private void closeConnection() {
257                if (this.target != null) {
258                        try {
259                                this.target.close();
260                        }
261                        catch (Throwable ex) {
262                                logger.warn("Could not close shared JDBC Connection", ex);
263                        }
264                }
265        }
266
267        /**
268         * Wrap the given Connection with a proxy that delegates every method call to it
269         * but suppresses close calls.
270         * @param target the original Connection to wrap
271         * @return the wrapped Connection
272         */
273        protected Connection getCloseSuppressingConnectionProxy(Connection target) {
274                return (Connection) Proxy.newProxyInstance(
275                                ConnectionProxy.class.getClassLoader(),
276                                new Class<?>[] {ConnectionProxy.class},
277                                new CloseSuppressingInvocationHandler(target));
278        }
279
280
281        /**
282         * Invocation handler that suppresses close calls on JDBC Connections.
283         */
284        private static class CloseSuppressingInvocationHandler implements InvocationHandler {
285
286                private final Connection target;
287
288                public CloseSuppressingInvocationHandler(Connection target) {
289                        this.target = target;
290                }
291
292                @Override
293                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
294                        // Invocation on ConnectionProxy interface coming in...
295
296                        if (method.getName().equals("equals")) {
297                                // Only consider equal when proxies are identical.
298                                return (proxy == args[0]);
299                        }
300                        else if (method.getName().equals("hashCode")) {
301                                // Use hashCode of Connection proxy.
302                                return System.identityHashCode(proxy);
303                        }
304                        else if (method.getName().equals("unwrap")) {
305                                if (((Class<?>) args[0]).isInstance(proxy)) {
306                                        return proxy;
307                                }
308                        }
309                        else if (method.getName().equals("isWrapperFor")) {
310                                if (((Class<?>) args[0]).isInstance(proxy)) {
311                                        return true;
312                                }
313                        }
314                        else if (method.getName().equals("close")) {
315                                // Handle close method: don't pass the call on.
316                                return null;
317                        }
318                        else if (method.getName().equals("isClosed")) {
319                                return this.target.isClosed();
320                        }
321                        else if (method.getName().equals("getTargetConnection")) {
322                                // Handle getTargetConnection method: return underlying Connection.
323                                return this.target;
324                        }
325
326                        // Invoke method on target Connection.
327                        try {
328                                return method.invoke(this.target, args);
329                        }
330                        catch (InvocationTargetException ex) {
331                                throw ex.getTargetException();
332                        }
333                }
334        }
335
336}