diff --git a/src/main/java/com/zaxxer/hikari/HikariPool.java b/src/main/java/com/zaxxer/hikari/HikariPool.java index f59d2ff1..4fb30898 100644 --- a/src/main/java/com/zaxxer/hikari/HikariPool.java +++ b/src/main/java/com/zaxxer/hikari/HikariPool.java @@ -36,6 +36,7 @@ import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.zaxxer.hikari.proxy.HikariInstrumentationAgent; import com.zaxxer.hikari.proxy.IHikariConnectionProxy; import com.zaxxer.hikari.proxy.JavassistProxyFactoryFactory; import com.zaxxer.hikari.util.ClassLoaderUtils; @@ -53,10 +54,10 @@ public class HikariPool implements HikariPoolMBean private final AtomicInteger idleConnectionCount; private final DataSource dataSource; private final boolean jdbc4ConnectionTest; + private volatile boolean delegationProxies; private final Timer houseKeepingTimer; - /** * Construct a HikariPool with the specified configuration. * @@ -79,6 +80,13 @@ public class HikariPool implements HikariPoolMBean Class clazz = ClassLoaderUtils.loadClass(configuration.getDataSourceClassName()); this.dataSource = (DataSource) clazz.newInstance(); PropertyBeanSetter.setTargetFromProperties(dataSource, configuration.getDataSourceProperties()); + + HikariInstrumentationAgent instrumentationAgent = new HikariInstrumentationAgent(dataSource); + if (!instrumentationAgent.loadTransformerAgent()) + { + delegationProxies = true; + LOGGER.info("Falling back to Javassist delegate-based proxies."); + } } catch (Exception e) { @@ -245,7 +253,15 @@ public class HikariPool implements HikariPoolMBean try { Connection connection = dataSource.getConnection(); - IHikariConnectionProxy proxyConnection = (IHikariConnectionProxy) JavassistProxyFactoryFactory.getProxyFactory().getProxyConnection(this, connection); + IHikariConnectionProxy proxyConnection; + if (delegationProxies) + { + proxyConnection = (IHikariConnectionProxy) JavassistProxyFactoryFactory.getProxyFactory().getProxyConnection(this, connection); + } + else + { + proxyConnection = (IHikariConnectionProxy) connection; + } boolean alive = isConnectionAlive((Connection) proxyConnection, configuration.getConnectionTimeout()); if (alive) diff --git a/src/main/java/com/zaxxer/hikari/proxy/HikariClassTransformer.java b/src/main/java/com/zaxxer/hikari/proxy/HikariClassTransformer.java index c7dd6475..5637422e 100644 --- a/src/main/java/com/zaxxer/hikari/proxy/HikariClassTransformer.java +++ b/src/main/java/com/zaxxer/hikari/proxy/HikariClassTransformer.java @@ -21,57 +21,46 @@ import java.io.DataInputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; -import java.lang.management.ManagementFactory; -import java.net.URL; -import java.nio.ByteBuffer; import java.security.ProtectionDomain; -import java.util.HashMap; -import java.util.HashSet; import javassist.bytecode.ClassFile; -import com.sun.tools.attach.VirtualMachine; - /** * * @author Brett Wooldridge */ public class HikariClassTransformer implements ClassFileTransformer { - private String sniffPackage; + // private static final Logger LOGGER = LoggerFactory.getLogger(HikariClassTransformer.class); - public static void loadTransformerAgent(String sniffPackage) - { - String jarPath = getSelfJarPath(); - if (jarPath == null) - { - return; - } - - String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); - int p = nameOfRunningVM.indexOf('@'); - String pid = nameOfRunningVM.substring(0, p); + private static Instrumentation ourInstrumentation; + private static HikariClassTransformer transformer; - try - { - VirtualMachine vm = VirtualMachine.attach(pid); - vm.loadAgent("/Users/brettw/Documents/dev/HikariCP/target/HikariCP-0.9-SNAPSHOT.jar", sniffPackage); - vm.detach(); - } - catch (Exception e) - { - return; - } - } + private String sniffPackage; - public static void agentmain(String agentArgs, Instrumentation inst) + /** + * Private constructor. + * + * @param sniffPackage the package name used to filter only classes we are interested in + */ + private HikariClassTransformer(String sniffPackage) { - inst.addTransformer(new HikariClassTransformer(agentArgs), false); + this.sniffPackage = sniffPackage; + HikariClassTransformer.transformer = this; } - HikariClassTransformer(String sniffPackage) + /** + * The method that is called when VirtualMachine.loadAgent() is invoked to register our + * class transformer. + * + * @param agentArgs arguments to pass to the agent + * @param inst the virtual machine Instrumentation instance used to register our transformer + */ + public static void agentmain(String agentArgs, Instrumentation instrumentation) { - this.sniffPackage = sniffPackage.replace('.', '/'); + ourInstrumentation = instrumentation; + + ourInstrumentation.addTransformer(new HikariClassTransformer(agentArgs), false); } /** {@inheritDoc} */ @@ -86,123 +75,49 @@ public class HikariClassTransformer implements ClassFileTransformer try { ClassFile classFile = new ClassFile(new DataInputStream(new ByteArrayInputStream(classfileBuffer))); - String[] interfaces = classFile.getInterfaces(); - return classfileBuffer; - } - catch (Exception e) - { - return classfileBuffer; - } - } + for (String iface : classFile.getInterfaces()) + { + if (!iface.startsWith("java.sql")) + { + continue; + } - /** - * High-speed class file sniffer to determine if the class in question - * implements the specified interface. - * - * @param classFileBytes - * @return true if the - */ - private boolean sniffClass(byte[] classFileBytes) - { - ByteBuffer buffer = ByteBuffer.wrap(classFileBytes); + if (iface.equals("java.sql.Connection")) + { - buffer.getInt(); // 0xCAFEBABE - buffer.getInt(); // minor/major version + } + else if (iface.equals("java.sql.PreparedStatement")) + { - HashMap stringPool = new HashMap(128); - HashMap classRefs = new HashMap(); - HashMap nameAndDescriptor = new HashMap(); - HashSet interfaceRefs = new HashSet(); + } + else if (iface.equals("java.sql.CallableStatement")) + { - int constantPoolSize = buffer.getShort(); - int slot = 1; - while (slot < constantPoolSize) - { - byte tag = buffer.get(); - switch (tag) - { - case 1: // UTF-8 String - short len = buffer.getShort(); - byte[] buf = new byte[len]; - buffer.get(buf); - stringPool.put(slot, new String(buf)); - slot++; - break; - case 3: // Integer: a signed 32-bit two's complement number in big-endian format - buffer.getInt(); - slot++; - break; - case 4: // Float: a 32-bit single-precision IEEE 754 floating-point number - buffer.getFloat(); - slot++; - break; - case 5: // Long: a signed 64-bit two's complement number in big-endian format (takes two slots in the constant pool table) - buffer.getLong(); - slot += 2; - break; - case 6: // Double: a 64-bit double-precision IEEE 754 floating-point number (takes two slots in the constant pool table) - buffer.getDouble(); - slot += 2; - break; - case 7: // Class reference: an index within the constant pool to a UTF-8 string containing the fully qualified class name - int index = buffer.getShort(); - classRefs.put(slot, index); - slot++; - break; - case 8: // String reference: an index within the constant pool to a UTF-8 string (big-endian too) - int sRef = buffer.getShort(); - slot++; - break; - case 9: // Field reference: two indexes within the constant pool, the first a Class reference, the second a Name and Type descriptor. - int fRef1 = buffer.getShort(); - int fRef2 = buffer.getShort(); - slot++; - break; - case 10: // Method reference: two indexes within the constant pool, ... - int mRef1 = buffer.getShort(); - int mRef2 = buffer.getShort(); - slot++; - break; - case 11: // Interface method reference: two indexes within the constant pool, ... - int iRef1 = buffer.getShort(); - int iRef2 = buffer.getShort(); - interfaceRefs.add(iRef1); - slot++; - break; - case 12: // Name and type descriptor: two indexes to UTF-8 strings within the constant pool, the first representing a name - // (identifier) and the second a specially encoded type descriptor. - int nameIndex = buffer.getShort(); - int descIndex = buffer.getShort(); - nameAndDescriptor.put(slot, nameIndex); - slot++; - break; - } - } + } + else if (iface.equals("java.sql.Statement")) + { - return false; - } + } + else if (iface.equals("java.sql.ResultSet")) + { - private static String getSelfJarPath() - { - URL resource = HikariClassTransformer.class.getResource('/' + HikariClassTransformer.class.getName().replace('.', '/') + ".class"); - if (resource == null) - { - return null; - } + } + } - System.out.println(resource); - String jarPath = resource.toString(); - jarPath = jarPath.replace("file:", ""); - jarPath = jarPath.replace("jar:", ""); - if (jarPath.indexOf('!') > 0) - { - jarPath = jarPath.substring(0, jarPath.indexOf('!')); + // None of the interfaces we care about were found, so just return the class file buffer + return classfileBuffer; } - else + catch (Exception e) { - jarPath = jarPath.substring(0, jarPath.lastIndexOf('/')); + return classfileBuffer; } + } - return jarPath; + /** + * + */ + static void unregisterInstrumenation() + { + ourInstrumentation.removeTransformer(transformer); } } diff --git a/src/main/java/com/zaxxer/hikari/proxy/HikariInstrumentationAgent.java b/src/main/java/com/zaxxer/hikari/proxy/HikariInstrumentationAgent.java new file mode 100644 index 00000000..58b84a68 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/proxy/HikariInstrumentationAgent.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2013 Brett Wooldridge + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zaxxer.hikari.proxy; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.net.URI; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javassist.bytecode.ClassFile; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.tools.attach.AgentInitializationException; +import com.sun.tools.attach.AgentLoadException; +import com.sun.tools.attach.AttachNotSupportedException; +import com.sun.tools.attach.VirtualMachine; +import com.zaxxer.hikari.util.ClassLoaderUtils; + +/** + * + * @author Brett Wooldridge + */ +public class HikariInstrumentationAgent +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HikariInstrumentationAgent.class); + + private static final HashMap completionMap; + + // Static initializer + static + { + completionMap = new HashMap(); + completionMap.put("java.sql.Connection", false); + completionMap.put("java.sql.ResultSet", false); + completionMap.put("java.sql.Statement", false); + completionMap.put("java.sql.CallableStatement", false); + completionMap.put("java.sql.PreparedStatement", false); + } + + private DataSource dataSource; + private String sniffPackage; + + public HikariInstrumentationAgent(DataSource dataSource) + { + this.dataSource = dataSource; + this.sniffPackage = getDataSourcePackage(dataSource); + } + + public boolean loadTransformerAgent() + { + String jarPath = getSelfJarPath(); + if (jarPath == null) + { + LOGGER.warn("Cannot find the HikariCP jar file through introspection."); + return false; + } + + try + { + registerInstrumentation(jarPath); + LOGGER.info("Successfully loaded instrumentation agent. Scanning classes..."); + } + catch (Exception e) + { + LOGGER.warn("Instrumentation agent could not be loaded. Please report at http://github.com/brettwooldridge/HikariCP.", e); + return false; + } + + try + { + boolean success = searchInstrumentable(dataSource); + completionMap.entrySet(); + if (!success) + { + LOGGER.warn("Unable to find and instrument necessary classes. Please report at http://github.com/brettwooldridge/HikariCP."); + LOGGER.info("Using delegation instead of instrumentation"); + } + else + { + LOGGER.info("Successfully instrumented required JDBC classes."); + } + + return success; + } + catch (Exception e) + { + return false; + } + finally + { + unregisterInstrumenation(); + } + } + + /** + * Search the jar (or directory hierarchy) that contains the DataSource and force the class + * loading of the classes we are about instrumenting. See loadIfInstrumentable() for more + * detail. + * + * @param dataSource + * @throws IOException + */ + private boolean searchInstrumentable(DataSource dataSource) throws Exception + { + String searchPath = getSearchPath(dataSource); + if (searchPath == null) + { + return false; + } + + long start = System.currentTimeMillis(); + try + { + if (searchPath.endsWith(".jar")) + { + return searchInstrumentableJar(searchPath); + } + else + { + String dsSubPath = dataSource.getClass().getPackage().getName().replace('.', '/'); + String classRoot = searchPath.replace(dsSubPath, ""); + // Drop one segment off of the path for a slightly broader search + searchPath = searchPath.substring(0, searchPath.lastIndexOf('/')); + return seachInstrumentableDirectory(classRoot, searchPath); + } + } + finally + { + LOGGER.info("Instrumentation completed in {}ms.", System.currentTimeMillis() - start); + } + } + + private boolean searchInstrumentableJar(String searchPath) throws IOException, ClassNotFoundException + { + File jarPath = new File(URI.create(searchPath)); + if (!jarPath.isFile()) + { + return false; + } + + JarFile jarFile = new JarFile(jarPath, false, JarFile.OPEN_READ); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) + { + JarEntry jarEntry = entries.nextElement(); + if (jarEntry.isDirectory()) + { + continue; + } + + String entryName = jarEntry.getName(); + if (entryName.endsWith(".class") && entryName.startsWith(sniffPackage) && entryName.indexOf('$') == -1) + { + String className = entryName.replace(".class", "").replace('/', '.'); + InputStream inputStream = jarFile.getInputStream(jarEntry); + loadIfInstrumentable(className, new DataInputStream(inputStream)); + inputStream.close(); + } + } + + jarFile.close(); + + return true; + } + + /** + * @param classRoot + * @param searchPath + * @return true if the search completed without error + * @throws IOException + * @throws ClassNotFoundException + */ + private boolean seachInstrumentableDirectory(String classRoot, String searchPath) throws IOException, ClassNotFoundException + { + File directory = new File(searchPath); + if (!directory.isDirectory()) + { + return false; + } + + for (File fileEntry : directory.listFiles()) + { + if (fileEntry.isDirectory()) + { + seachInstrumentableDirectory(classRoot, fileEntry.getPath()); + continue; + } + + String fileName = fileEntry.getPath(); + String className = fileName.replace(classRoot, ""); + if (className.endsWith(".class") && className.startsWith(sniffPackage) && className.indexOf('$') == -1) + { + className = className.replace(".class", "").replace('/', '.'); + InputStream inputStream = new FileInputStream(fileEntry); + loadIfInstrumentable(className, new DataInputStream(inputStream)); + inputStream.close(); + } + } + + return true; + } + + /** + * If the specified class implements one of the java.sql interfaces we are interested in + * instrumenting, use the class loader to cause the class to be loaded. This will force + * the class in question through the HikariClassTransformer. + * + * @param className the name of the class that might be instrumentable + * @param classInputStream the stream of bytes for the class file + * @throws IOException thrown if there is an error reading the class file + * @throws ClassNotFoundException thrown if the referenced class is not loadable + */ + private void loadIfInstrumentable(String className, DataInputStream classInputStream) throws IOException, ClassNotFoundException + { + ClassFile classFile = new ClassFile(classInputStream); + for (String iface : classFile.getInterfaces()) + { + if (!iface.startsWith("java.sql")) + { + continue; + } + + if (completionMap.containsKey(iface)) + { + LOGGER.info("Instrumenting class {}", className); + ClassLoaderUtils.loadClass(className); + completionMap.put(iface, true); + } + } + } + + /** + * Get the path to the JAR or file system directory where the class of the user + * specified DataSource implementation resides. + * + * @param dataSource the user specified DataSource + * @return the path to the JAR (including the .jar file name) or a file system classes directory + */ + private String getSearchPath(DataSource dataSource) + { + URL resource = dataSource.getClass().getResource('/' + dataSource.getClass().getName().replace('.', '/') + ".class"); + if (resource == null) + { + return null; + } + + String path = resource.toString(); + if (path.startsWith("jar:")) + { + // original form jar:file:/path, make a path like file:///path + path = path.substring(4, path.indexOf('!')).replace(":/", ":///"); + } + else if (path.startsWith("file:")) + { + path = path.substring(0, path.lastIndexOf('/')).replace("file:", ""); + } + else + { + LOGGER.warn("Could not determine path type of {}", path); + return null; + } + + return path; + } + + /** + * Given a DataSource class, find the package name that is one-level above the package of + * the DataSource. For example, org.hsqldb.jdbc.DataSource -> org.hsqldb. This is used + * to filter out packages quickly that we are not interested in instrumenting. + * + * @param dataSource a DataSource + * @return the shortened package name used for filtering + */ + private String getDataSourcePackage(DataSource dataSource) + { + String packageName = dataSource.getClass().getPackage().getName(); + + // Count how many segments in the package name. For example, org.hsqldb.jdbc has three segments. + int dots = 0; + int[] offset = new int[16]; + for (int ndx = packageName.indexOf('.'); ndx != -1; ndx = packageName.indexOf('.', ndx + 1)) + { + offset[dots] = ndx; + dots++; + } + + if (dots > 1) + { + packageName = packageName.substring(0, offset[dots - 1]); + } + + return packageName.replace('.', '/'); + } + + /** + * Get the path to the JAR file from which this class was loaded. + * + * @return the path to the jar file that contains this class + */ + private String getSelfJarPath() + { + URL resource = HikariClassTransformer.class.getResource('/' + HikariClassTransformer.class.getName().replace('.', '/') + ".class"); + if (resource == null) + { + return null; + } + + String jarPath = resource.toString(); + jarPath = jarPath.replace("file:", ""); + jarPath = jarPath.replace("jar:", ""); + if (jarPath.indexOf('!') > 0) + { + jarPath = jarPath.substring(0, jarPath.indexOf('!')); + } + else + { + return System.getProperty("com.zaxxer.hikari.selfJar"); + } + + return jarPath; + } + + /** + * Attempt to register our instrumentation (class transformer) with the virtual machine + * dynamically. + * + * @param jarPath the path to our own jar file + * @throws AttachNotSupportedException thrown if the JVM does not support attachment + * @throws IOException thrown if the instrumentation JAR cannot be read + * @throws AgentLoadException thrown if the instrumentation jar does not have proper headers + * @throws AgentInitializationException thrown if the agent had an error during initialization + */ + private void registerInstrumentation(String jarPath) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException + { + VirtualMachine vm = VirtualMachine.attach(getPid()); + vm.loadAgent(jarPath, sniffPackage); + vm.detach(); + } + + /** + * Unregister the instrumentation (class transformer). + */ + private void unregisterInstrumenation() + { + HikariClassTransformer.unregisterInstrumenation(); + } + + /** + * Get the PID of the running JVM. + * + * @return the process ID (PID) of the running JVM + */ + private String getPid() + { + String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); + int p = nameOfRunningVM.indexOf('@'); + return nameOfRunningVM.substring(0, p); + } +} diff --git a/src/test/java/com/zaxxer/hikari/CreationTest.java b/src/test/java/com/zaxxer/hikari/CreationTest.java index 578c533e..52fec6fb 100644 --- a/src/test/java/com/zaxxer/hikari/CreationTest.java +++ b/src/test/java/com/zaxxer/hikari/CreationTest.java @@ -39,13 +39,12 @@ public class CreationTest @Test public void testCreate() throws SQLException { - HikariClassTransformer.loadTransformerAgent("com.zaxxer.hikari.mocks"); - HikariConfig config = new HikariConfig(); config.setMinimumPoolSize(1); config.setAcquireIncrement(1); config.setConnectionTestQuery("VALUES 1"); - config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + // config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setDataSourceClassName("org.hsqldb.jdbc.JDBCDataSource"); HikariDataSource ds = new HikariDataSource(config);