package kr.pe.kwonnam.replicationdatasource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.PrintWriter; 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; /** * Lazy Master/Slave(Write/Read) Replication DataSource Proxy. * You can route database connection to master or slave with this datasource proxy. * <p/> * This is copy & modify of Spring framework's LayzyConnectionDataSourceProxy class. * <p/> * This DataSource's connection can not be reused with different readOnly attributes. */ public class LazyReplicationConnectionDataSourceProxy implements DataSource { private Logger log = LoggerFactory.getLogger(LazyReplicationConnectionDataSourceProxy.class); private DataSource writeDataSource; private DataSource readDataSource; private Boolean defaultAutoCommit; private Integer defaultTransactionIsolation; /** * Default constructor. * After setting {@link #writeDataSource} and {@link #readDataSource}, * You must call {@link #init()} to initialize the configuration. */ public LazyReplicationConnectionDataSourceProxy() { } public LazyReplicationConnectionDataSourceProxy(DataSource writeDataSource, DataSource readDataSource) { this.writeDataSource = writeDataSource; this.readDataSource = readDataSource; init(); } public void init() { // 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 = getWriteDataSource().getConnection(); try { checkDefaultConnectionProperties(con); } finally { con.close(); } } catch (SQLException ex) { log.warn("Could not retrieve default auto-commit and transaction isolation settings", ex); } } } public DataSource setWriteDataSource(DataSource writeDataSource) { return this.writeDataSource = writeDataSource; } public DataSource getWriteDataSource() { return this.writeDataSource; } public DataSource setReadDataSource(DataSource readDataSource) { return this.readDataSource = readDataSource; } public DataSource getReadDataSource() { return this.readDataSource; } @Override public PrintWriter getLogWriter() throws SQLException { return getWriteDataSource().getLogWriter(); } @Override public void setLogWriter(PrintWriter out) throws SQLException { getWriteDataSource().setLogWriter(out); getReadDataSource().setLogWriter(out); } @Override public int getLoginTimeout() throws SQLException { return getWriteDataSource().getLoginTimeout(); } @Override public void setLoginTimeout(int seconds) throws SQLException { getWriteDataSource().setLoginTimeout(seconds); getReadDataSource().setLoginTimeout(seconds); } //--------------------------------------------------------------------- // Implementation of JDBC 4.0's Wrapper interface //--------------------------------------------------------------------- @Override @SuppressWarnings("unchecked") public <T> T unwrap(Class<T> iface) throws SQLException { if (iface.isInstance(this)) { return (T) this; } return getWriteDataSource().unwrap(iface); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return (iface.isInstance(this) || getWriteDataSource().isWrapperFor(iface)); } //--------------------------------------------------------------------- // Implementation of JDBC 4.1's getParentLogger method //--------------------------------------------------------------------- public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getLogger(java.util.logging.Logger.GLOBAL_LOGGER_NAME); } public void setDefaultAutoCommit(boolean defaultAutoCommit) { this.defaultAutoCommit = defaultAutoCommit; } public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { this.defaultTransactionIsolation = defaultTransactionIsolation; } /** * 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) { this.defaultAutoCommit = con.getAutoCommit(); } if (this.defaultTransactionIsolation == null) { this.defaultTransactionIsolation = con.getTransactionIsolation(); } } /** * Expose the default auto-commit value. */ protected Boolean defaultAutoCommit() { return this.defaultAutoCommit; } /** * Expose the default transaction isolation value. */ protected Integer defaultTransactionIsolation() { return this.defaultTransactionIsolation; } @Override public Connection getConnection() throws SQLException { return (Connection) Proxy.newProxyInstance( ReplicationConnectionProxy.class.getClassLoader(), new Class<?>[]{ReplicationConnectionProxy.class}, new LazyReplicationConnectionInvocationHandler()); } @Override public Connection getConnection(String username, String password) throws SQLException { return (Connection) Proxy.newProxyInstance( ReplicationConnectionProxy.class.getClassLoader(), new Class<?>[]{ReplicationConnectionProxy.class}, new LazyReplicationConnectionInvocationHandler(username, password)); } private static interface ReplicationConnectionProxy extends Connection { Connection getReplicationTargetConnection(); } /** * Invocation handler that defers fetching an actual JDBC Connection * until first creation of a Statement. */ private class LazyReplicationConnectionInvocationHandler 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 replicationTargetConnection; public LazyReplicationConnectionInvocationHandler() { this.autoCommit = defaultAutoCommit(); this.transactionIsolation = defaultTransactionIsolation(); } public LazyReplicationConnectionInvocationHandler(String username, String password) { this(); this.username = username; this.password = password; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ReplicationConnectionProxy interface coming in... if (method.getName().equals("equals")) { // We must avoid fetching a target Connection for "equals". // Only consider equal when proxies are identical. return (proxy == args[0]); } 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. return System.identityHashCode(proxy); } 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; } } else if (method.getName().equals("getReplicationTargetConnection")) { // Handle getReplicationTargetConnection method: return underlying connection. return getReplicationTargetConnection(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 write DataSource [" + getWriteDataSource() + "] and target read DataSource [" + getReadDataSource() + "]"; } 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; } else if (method.getName().equals("isClosed")) { return this.closed; } 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(getReplicationTargetConnection(method), args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } /** * Return whether the proxy currently holds a target Connection. */ private boolean hasTargetConnection() { return (this.replicationTargetConnection != null); } /** * Return the target Connection, fetching it and initializing it if necessary. */ private Connection getReplicationTargetConnection(Method operation) throws SQLException { if (this.replicationTargetConnection == null) { log.debug("Connecting to database for operation '{}'", operation.getName()); log.debug("current readOnly : {}", readOnly); DataSource targetDataSource = (readOnly == Boolean.TRUE) ? getReadDataSource() : getWriteDataSource(); // Fetch physical Connection from DataSource. this.replicationTargetConnection = (this.username != null) ? targetDataSource.getConnection(this.username, this.password) : targetDataSource.getConnection(); // If we still lack default connection properties, check them now. checkDefaultConnectionProperties(this.replicationTargetConnection); // Apply kept transaction settings, if any. if (this.readOnly) { try { this.replicationTargetConnection.setReadOnly(this.readOnly); } catch (Exception ex) { // "read-only not supported" -> ignore, it's just a hint anyway log.debug("Could not set JDBC Connection read-only", ex); } } if (this.transactionIsolation != null && !this.transactionIsolation.equals(defaultTransactionIsolation())) { this.replicationTargetConnection.setTransactionIsolation(this.transactionIsolation); } if (this.autoCommit != null && this.autoCommit != this.replicationTargetConnection.getAutoCommit()) { this.replicationTargetConnection.setAutoCommit(this.autoCommit); } } else { log.debug("Using existing database connection for operation '{}'", operation.getName()); } return this.replicationTargetConnection; } } }