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.lang.Nullable;
028import org.springframework.util.Assert;
029import org.springframework.util.ObjectUtils;
030
031/**
032 * Implementation of {@link SmartDataSource} that wraps a single JDBC Connection
033 * which is not closed after use. Obviously, this is not multi-threading capable.
034 *
035 * <p>Note that at shutdown, someone should close the underlying Connection
036 * via the {@code close()} method. Client code will never call close
037 * on the Connection handle if it is SmartDataSource-aware (e.g. uses
038 * {@code DataSourceUtils.releaseConnection}).
039 *
040 * <p>If client code will call {@code close()} in the assumption of a pooled
041 * Connection, like when using persistence tools, set "suppressClose" to "true".
042 * This will return a close-suppressing proxy instead of the physical Connection.
043 *
044 * <p>This is primarily intended for testing. For example, it enables easy testing
045 * outside an application server, for code that expects to work on a DataSource.
046 * In contrast to {@link DriverManagerDataSource}, it reuses the same Connection
047 * all the time, avoiding excessive creation of physical Connections.
048 *
049 * @author Rod Johnson
050 * @author Juergen Hoeller
051 * @see #getConnection()
052 * @see java.sql.Connection#close()
053 * @see DataSourceUtils#releaseConnection
054 */
055public class SingleConnectionDataSource extends DriverManagerDataSource implements SmartDataSource, DisposableBean {
056
057        /** Create a close-suppressing proxy?. */
058        private boolean suppressClose;
059
060        /** Override auto-commit state?. */
061        @Nullable
062        private Boolean autoCommit;
063
064        /** Wrapped Connection. */
065        @Nullable
066        private Connection target;
067
068        /** Proxy Connection. */
069        @Nullable
070        private Connection connection;
071
072        /** Synchronization monitor for the shared Connection. */
073        private final Object connectionMonitor = new Object();
074
075
076        /**
077         * Constructor for bean-style configuration.
078         */
079        public SingleConnectionDataSource() {
080        }
081
082        /**
083         * Create a new SingleConnectionDataSource with the given standard
084         * DriverManager parameters.
085         * @param url the JDBC URL to use for accessing the DriverManager
086         * @param username the JDBC username to use for accessing the DriverManager
087         * @param password the JDBC password to use for accessing the DriverManager
088         * @param suppressClose if the returned Connection should be a
089         * close-suppressing proxy or the physical Connection
090         * @see java.sql.DriverManager#getConnection(String, String, String)
091         */
092        public SingleConnectionDataSource(String url, String username, String password, boolean suppressClose) {
093                super(url, username, password);
094                this.suppressClose = suppressClose;
095        }
096
097        /**
098         * Create a new SingleConnectionDataSource with the given standard
099         * DriverManager parameters.
100         * @param url the JDBC URL to use for accessing the DriverManager
101         * @param suppressClose if the returned Connection should be a
102         * close-suppressing proxy or the physical Connection
103         * @see java.sql.DriverManager#getConnection(String, String, String)
104         */
105        public SingleConnectionDataSource(String url, boolean suppressClose) {
106                super(url);
107                this.suppressClose = suppressClose;
108        }
109
110        /**
111         * Create a new SingleConnectionDataSource with a given Connection.
112         * @param target underlying target Connection
113         * @param suppressClose if the Connection should be wrapped with a Connection that
114         * suppresses {@code close()} calls (to allow for normal {@code close()}
115         * usage in applications that expect a pooled Connection but do not know our
116         * SmartDataSource interface)
117         */
118        public SingleConnectionDataSource(Connection target, boolean suppressClose) {
119                Assert.notNull(target, "Connection must not be null");
120                this.target = target;
121                this.suppressClose = suppressClose;
122                this.connection = (suppressClose ? getCloseSuppressingConnectionProxy(target) : target);
123        }
124
125
126        /**
127         * Set whether the returned Connection should be a close-suppressing proxy
128         * or the physical Connection.
129         */
130        public void setSuppressClose(boolean suppressClose) {
131                this.suppressClose = suppressClose;
132        }
133
134        /**
135         * Return whether the returned Connection will be a close-suppressing proxy
136         * or the physical Connection.
137         */
138        protected boolean isSuppressClose() {
139                return this.suppressClose;
140        }
141
142        /**
143         * Set whether the returned Connection's "autoCommit" setting should be overridden.
144         */
145        public void setAutoCommit(boolean autoCommit) {
146                this.autoCommit = (autoCommit);
147        }
148
149        /**
150         * Return whether the returned Connection's "autoCommit" setting should be overridden.
151         * @return the "autoCommit" value, or {@code null} if none to be applied
152         */
153        @Nullable
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 an 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.isDebugEnabled()) {
227                                logger.debug("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.info("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                @Nullable
299                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
300                        // Invocation on ConnectionProxy interface coming in...
301
302                        if (method.getName().equals("equals")) {
303                                // Only consider equal when proxies are identical.
304                                return (proxy == args[0]);
305                        }
306                        else if (method.getName().equals("hashCode")) {
307                                // Use hashCode of Connection proxy.
308                                return System.identityHashCode(proxy);
309                        }
310                        else if (method.getName().equals("unwrap")) {
311                                if (((Class<?>) args[0]).isInstance(proxy)) {
312                                        return proxy;
313                                }
314                        }
315                        else if (method.getName().equals("isWrapperFor")) {
316                                if (((Class<?>) args[0]).isInstance(proxy)) {
317                                        return true;
318                                }
319                        }
320                        else if (method.getName().equals("close")) {
321                                // Handle close method: don't pass the call on.
322                                return null;
323                        }
324                        else if (method.getName().equals("isClosed")) {
325                                return this.target.isClosed();
326                        }
327                        else if (method.getName().equals("getTargetConnection")) {
328                                // Handle getTargetConnection method: return underlying Connection.
329                                return this.target;
330                        }
331
332                        // Invoke method on target Connection.
333                        try {
334                                return method.invoke(this.target, args);
335                        }
336                        catch (InvocationTargetException ex) {
337                                throw ex.getTargetException();
338                        }
339                }
340        }
341
342}