diff options
6 files changed, 244 insertions, 70 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java deleted file mode 100644 index ca00f9a0691..00000000000 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapClassLoader.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * Sonar is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.batch.bootstrap; - -import org.sonar.api.utils.SonarException; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; - -/** - * ClassLoader extended with the JDBC Driver hosted on the server-side. - */ -public class BootstrapClassLoader { - - private URLClassLoader classLoader; - - public BootstrapClassLoader(ArtifactDownloader extensionDownloader) { - this(extensionDownloader.downloadJdbcDriver()); - } - - BootstrapClassLoader(File jdbcDriver) { - try { - ClassLoader parentClassLoader = BootstrapClassLoader.class.getClassLoader(); - classLoader = URLClassLoader.newInstance(new URL[]{jdbcDriver.toURI().toURL()}, parentClassLoader); - - } catch (MalformedURLException e) { - throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e); - } - } - - public URLClassLoader getClassLoader() { - return classLoader; - } -} diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java index fda540156a8..3127e7a3e50 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java @@ -56,9 +56,9 @@ public class BootstrapModule extends Module { addComponent(TempDirectories.class);// registered here because used by BootstrapClassLoader addComponent(HttpDownloader.class);// registered here because used by BootstrapClassLoader addComponent(ArtifactDownloader.class);// registered here because used by BootstrapClassLoader - addComponent(BootstrapClassLoader.class); + addComponent(JdbcDriverHolder.class); - URLClassLoader bootstrapClassLoader = getComponent(BootstrapClassLoader.class).getClassLoader(); + URLClassLoader bootstrapClassLoader = getComponent(JdbcDriverHolder.class).getClassLoader(); // set as the current context classloader for hibernate, else it does not find the JDBC driver. Thread.currentThread().setContextClassLoader(bootstrapClassLoader); diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java new file mode 100644 index 00000000000..427ac20e3ab --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java @@ -0,0 +1,132 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.utils.SonarException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +/** + * Contains and provides class loader extended with the JDBC Driver hosted on the server-side. + */ +public class JdbcDriverHolder { + + private static Logger LOG = LoggerFactory.getLogger(JdbcDriverHolder.class); + private JdbcDriverClassLoader classLoader; + + public JdbcDriverHolder(ArtifactDownloader extensionDownloader) { + this(extensionDownloader.downloadJdbcDriver()); + } + + JdbcDriverHolder(File jdbcDriver) { + try { + ClassLoader parentClassLoader = JdbcDriverHolder.class.getClassLoader(); + classLoader = new JdbcDriverClassLoader(jdbcDriver.toURI().toURL(), parentClassLoader); + + } catch (MalformedURLException e) { + throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e); + } + } + + public URLClassLoader getClassLoader() { + return classLoader; + } + + /** + * This method automatically invoked by PicoContainer and deregisters JDBC drivers, which were forgotten. + * <p> + * Dynamically loaded JDBC drivers can not be simply used and this is a well known problem of {@link java.sql.DriverManager}, + * so <a href="http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location">workaround is to use proxy</a>. + * However DriverManager also contains memory leak, thus not only proxy, but also original driver must be deregistered, + * otherwise our class loader would be kept in memory. + * </p> + * <p> + * This operation contains unnecessary complexity because: + * <ul> + * <li>DriverManager checks the class loader of the calling class. Thus we can't simply ask it about deregistration.</li> + * <li>We can't use reflection against DriverManager, since it would create a dependency on DriverManager implementation, + * which can be changed (like it was done - compare Java 1.5 and 1.6).</li> + * <li>So we use companion - {@link JdbcLeakPrevention}. But we can't just create an instance, + * since it will be loaded by parent class loader and again will not pass DriverManager's check. + * So, we load the bytes via our parent class loader, but define the class with this class loader + * thus JdbcLeakPrevention looks like our class to the DriverManager.</li> + * </li> + * </p> + */ + public void stop() { + classLoader.clearReferencesJdbc(); + classLoader = null; + } + + private static class JdbcDriverClassLoader extends URLClassLoader { + + public JdbcDriverClassLoader(URL jdbcDriver, ClassLoader parent) { + super(new URL[] { jdbcDriver }, parent); + } + + public void clearReferencesJdbc() { + InputStream is = getResourceAsStream("org/sonar/batch/bootstrap/JdbcLeakPrevention.class"); + byte[] classBytes = new byte[2048]; + int offset = 0; + try { + int read = is.read(classBytes, offset, classBytes.length - offset); + while (read > -1) { + offset += read; + if (offset == classBytes.length) { + // Buffer full - double size + byte[] tmp = new byte[classBytes.length * 2]; + System.arraycopy(classBytes, 0, tmp, 0, classBytes.length); + classBytes = tmp; + } + read = is.read(classBytes, offset, classBytes.length - offset); + } + + Class<?> lpClass = defineClass("org.sonar.batch.bootstrap.JdbcLeakPrevention", classBytes, 0, offset, this.getClass().getProtectionDomain()); + Object obj = lpClass.newInstance(); + + @SuppressWarnings("unchecked") + List<String> driverNames = (List<String>) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj); + + for (String name : driverNames) { + LOG.debug("To prevent a memory leak, the JDBC Driver [{}] has been forcibly deregistered", name); + } + } catch (Exception e) { + LOG.warn("JDBC driver deregistration failed", e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ioe) { + LOG.warn(ioe.getMessage(), ioe); + } + } + } + } + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java new file mode 100644 index 00000000000..6da6514d45e --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcLeakPrevention.java @@ -0,0 +1,64 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.batch.bootstrap; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; + +/** + * Companion of {@link JdbcDriverHolder} and allows it to deregister JDBC drivers. + * <p> + * Some hacks are involved in the loading of the class - see {@link JdbcDriverHolder#stop()}, + * so this class can refer to classes only from java.* package and must not be referred from other classes. + * Placement and naming of this class and methods are very important, since it loaded and invoked via reflection. + * </p> + */ +public class JdbcLeakPrevention { + + /** + * @return list of names of deregistered drivers + */ + public List<String> clearJdbcDriverRegistrations() throws SQLException { + List<String> driverNames = new ArrayList<String>(); + HashSet<Driver> originalDrivers = new HashSet<Driver>(); + Enumeration<Driver> drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + originalDrivers.add(drivers.nextElement()); + } + drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + if (driver.getClass().getClassLoader() != this.getClass().getClassLoader()) { + continue; + } + if (originalDrivers.contains(driver)) { + driverNames.add(driver.getClass().getCanonicalName()); + } + DriverManager.deregisterDriver(driver); + } + return driverNames; + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BootstrapClassLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BootstrapClassLoaderTest.java index b28a4c33431..dfee29db761 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BootstrapClassLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BootstrapClassLoaderTest.java @@ -37,7 +37,7 @@ public class BootstrapClassLoaderTest { assertNull(getClass().getClassLoader().getResource("foo/foo.txt")); URL url = getClass().getResource("/org/sonar/batch/bootstrap/BootstrapClassLoaderTest/foo.jar"); - BootstrapClassLoader classloader = new BootstrapClassLoader(new File(url.toURI())); + JdbcDriverHolder classloader = new JdbcDriverHolder(new File(url.toURI())); assertNotNull(classloader.getClassLoader()); assertNotNull(classloader.getClassLoader().getResource("foo/foo.txt")); diff --git a/sonar-core/src/main/java/org/sonar/jpa/session/DriverDatabaseConnector.java b/sonar-core/src/main/java/org/sonar/jpa/session/DriverDatabaseConnector.java index 0b11068040f..00e91fd8f38 100644 --- a/sonar-core/src/main/java/org/sonar/jpa/session/DriverDatabaseConnector.java +++ b/sonar-core/src/main/java/org/sonar/jpa/session/DriverDatabaseConnector.java @@ -27,11 +27,13 @@ import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Enumeration; import java.util.Properties; public class DriverDatabaseConnector extends AbstractDatabaseConnector { private ClassLoader classloader; + private boolean driverProxyRegistered = false; public DriverDatabaseConnector(Configuration configuration) { super(configuration, true); @@ -74,24 +76,53 @@ public class DriverDatabaseConnector extends AbstractDatabaseConnector { } public Connection getConnection() throws SQLException { - try { - /* - * The sonar batch downloads the JDBC driver in a separated classloader. - * This is a well-know problem of java.sql.DriverManager. The workaround - * is to use a proxy. - * See http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location - */ - Driver driver = (Driver) classloader.loadClass(getDriver()).newInstance(); - DriverManager.registerDriver(new DriverProxy(driver)); - - } catch (Exception e) { - SQLException ex = new SQLException("SQL driver not found " + getDriver()); - throw (SQLException) ex.initCause(e); + /* + * The Sonar batch downloads the JDBC driver in a separated class loader. + * This is a well-know problem of java.sql.DriverManager. The workaround + * is to use a proxy. + * See http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location + */ + if (!driverProxyRegistered) { + driverProxyRegistered = true; + try { + Driver driver = (Driver) classloader.loadClass(getDriver()).newInstance(); + DriverManager.registerDriver(new DriverProxy(driver)); + } catch (Exception e) { + SQLException ex = new SQLException("SQL driver not found " + getDriver()); + throw (SQLException) ex.initCause(e); + } } return DriverManager.getConnection(getUrl(), getUsername(), getPassword()); } @Override + public void stop() { + super.stop(); + + deregisterDriverProxy(); + } + + /** + * Due to memory leak in DriverManager we also should deregister original driver, + * but we can't do it here, because DriverManager checks the class loader of the calling class. + * So actually we might have a memory leak, but it supposed to be handled by Sonar batch. + */ + private void deregisterDriverProxy() { + Enumeration<Driver> drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + if (driver instanceof DriverProxy) { + try { + DriverManager.deregisterDriver(driver); + LOG.debug("JDBC Driver [{}] deregistered", driver); + } catch (SQLException e) { + LOG.warn("JDBC driver deregistration failed", e); + } + } + } + } + + @Override public void setupEntityManagerFactory(Properties factoryProps) { factoryProps.put("hibernate.connection.url", getUrl()); factoryProps.put("hibernate.connection.driver_class", getDriver()); @@ -163,4 +194,4 @@ final class DriverProxy implements Driver { org.sonar.jpa.session.DriverProxy other = (org.sonar.jpa.session.DriverProxy) obj; return this.target.equals(other.target); } -}
\ No newline at end of file +} |