I would tend to agree. I don't understand why JBoss implemented it this way as it's totally non-intuitive and violates the principal of least surprise. I didn't check the JEE spec if this is documented behavior or not or just a JBoss special.
Here's the abbreviated code for reconnecting to the DataSource I wrote. It works great now.
<code lang="java" linenumbers="normal">
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import javax.annotation.concurrent.ThreadSafe;
import javax.resource.ResourceException;
import javax.sql.CommonDataSource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Objects.requireNonNull;
@ThreadSafe
public class SmartDataSourceWrapper implements DataSource {
@FunctionalInterface
private interface CheckedSqlFunction<R> {
R apply(DataSource dataSource) throws Exception;
}
private static final Logger LOG = LoggerFactory.getLogger(SmartDataSourceWrapper.class);
private final DataSourceLookup dataSourceLookup;
private final String dataSourceName;
private final AtomicReference<DataSource> cachedDataSource;
public SmartDataSourceWrapper(final DataSourceLookup dataSourceLookup, final String dataSourceName) {
this.dataSourceLookup = requireNonNull(dataSourceLookup);
this.dataSourceName = requireNonNull(dataSourceName);
this.cachedDataSource = new AtomicReference<>(dataSourceLookup.getDataSource(dataSourceName));
}
@Override
public Connection getConnection() throws SQLException {
return delegate("getConnection", DataSource::getConnection);
}
@Override
public Connection getConnection(final String username, final String password) throws SQLException {
return delegate("getConnection(String, String)", ds -> ds.getConnection(username, password));
}
…
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
try {
return delegate("getParentLogger()", CommonDataSource::getParentLogger);
} catch (SQLFeatureNotSupportedException ex) {
throw ex;
} catch (Exception ex) {
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
…
@Override
public void setLoginTimeout(int seconds) throws SQLException {
delegate("setLoginTimeout(int)", ds -> {
ds.setLoginTimeout(seconds);
return null;
});
}
private <T> T delegate(final String methodName, final CheckedSqlFunction<T> function) throws SQLException {
LOG.trace("Invoking '{}' on cached DataSource reference", methodName);
try {
return function.apply(cachedDataSource.get());
} catch (SQLException ex) {
if (ex.getCause() instanceof ResourceException) {
LOG.info("Caught ResourceException reloading DataSource from JNDI", ex);
reload();
return retry(methodName, function);
}
throw ex;
} catch (Exception ex) {
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
private void reload() {
LOG.info("Reloading '{}'", dataSourceName);
cachedDataSource.set(dataSourceLookup.getDataSource(dataSourceName));
}
/**
* Retry SQL operation after the {@link DataSource} reference was reloaded from JNDI.
*
* @param function the checked SQL function.
* @param <T> the result type.
* @return the result.
* @throws SQLException
*/
private <T> T retry(final String methodName, final CheckedSqlFunction<T> function) throws SQLException {
LOG.info("Retrying SQL operation '{}'", methodName);
try {
return function.apply(cachedDataSource.get());
} catch (Exception retryEx) {
LOG.info("SQL operation failed again", retryEx);
if (retryEx instanceof SQLException) {
throw (SQLException) retryEx;
}
throw new RuntimeException(retryEx);
}
}
}
</code>