Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close JarFile #1970

Merged
merged 4 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
Expand Down Expand Up @@ -127,10 +128,9 @@ private static String getPackageName(String className) {
}

private static Manifest getManifest(URL url) {
try {
JarFile jarFile = new JarFile(url.getFile());
try (JarFile jarFile = new JarFile(url.toURI().getPath())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to change the url.getFile()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If path contains a space then url.getFile() will leave it as %20, url.toURI().getPath() decodes it as an actual space character

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it cause a problem? I'm surprised the more direct way of getting a File would cause JarFile constructor to fail, if you're seeing an exception let's add a comment above this line on what exception we are trying to prevent with the indirect code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.getFile returns a String. As the url is constructed with new File(exporterJar).toURI().toURL(); which encodes space -> %20 when getting file from url we should use a way that translates %20 back to space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it sounds like there is a problem when the path has a space. If so yeah we need to fix it :) But it's confusing, so we need at least a comment documenting the failure case. Even better is a test that fails before the change and passes after, then no one will accidentally revert and regress.

Alternatively, we can keep the code similarly as before and file an issue - the PR title is about closing the jar file but is doing more than just that. It's ok but we need to do it completely, otherwise let's not make too many changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test

return jarFile.getManifest();
} catch (IOException e) {
} catch (IOException | URISyntaxException e) {
log.warn(e.getMessage(), e);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,91 @@

package io.opentelemetry.javaagent.tooling

import groovy.transform.CompileStatic
import io.opentelemetry.javaagent.spi.exporter.MetricExporterFactory
import io.opentelemetry.javaagent.spi.exporter.SpanExporterFactory
import io.opentelemetry.sdk.metrics.export.MetricExporter
import io.opentelemetry.sdk.trace.export.SpanExporter
import java.nio.charset.StandardCharsets
import java.util.jar.Attributes
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import spock.lang.Specification

class ExporterClassLoaderTest extends Specification {

// Verifies https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/542
def "does not look in parent classloader for metric exporters"() {
setup:
def parentClassloader = new URLClassLoader([createJarWithClasses(MetricExporterFactoryParent)] as URL[])
def parentClassloader = new ParentClassLoader([createJarWithClasses(MetricExporterFactoryParent)] as URL[])
def childClassloader = new ExporterClassLoader(createJarWithClasses(MetricExporterFactoryChild), parentClassloader)

when:
ServiceLoader<MetricExporterFactory> serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader)

then:
serviceLoader.size() == 1

and:
childClassloader.manifest != null

when:
MetricExporterFactory instance = serviceLoader.iterator().next()
Class clazz = instance.getClass()

then:
clazz.getClassLoader() == childClassloader
}

def "does not look in parent classloader for span exporters"() {
setup:
def parentClassloader = new URLClassLoader([createJarWithClasses(SpanExporterFactoryParent)] as URL[])
def parentClassloader = new ParentClassLoader([createJarWithClasses(SpanExporterFactoryParent)] as URL[])
def childClassloader = new ExporterClassLoader(createJarWithClasses(SpanExporterFactoryChild), parentClassloader)

when:
ServiceLoader<SpanExporterFactory> serviceLoader = ServiceLoader.load(SpanExporterFactory, childClassloader)

then:
serviceLoader.size() == 1

and:
childClassloader.manifest != null

when:
SpanExporterFactory instance = serviceLoader.iterator().next()
Class clazz = instance.getClass()

then:
clazz.getClassLoader() == childClassloader
}

// Verifies that loading of exporter jar succeeds when there is a space in path to exporter jar
def "load jar with space in path"() {
setup:
def parentClassloader = new ParentClassLoader()
// " .jar" is used to make path to jar contain a space
def childClassloader = new ExporterClassLoader(createJarWithClasses(" .jar", MetricExporterFactoryChild), parentClassloader)

when:
ServiceLoader<MetricExporterFactory> serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader)

then:
serviceLoader.size() == 1

and:
childClassloader.manifest != null

when:
MetricExporterFactory instance = serviceLoader.iterator().next()
Class clazz = instance.getClass()

then:
clazz.getClassLoader() == childClassloader

and:
clazz.getPackage().getImplementationVersion() == "test-implementation-version"
}

static class MetricExporterFactoryParent implements MetricExporterFactory {
Expand Down Expand Up @@ -93,15 +144,35 @@ class ExporterClassLoaderTest extends Specification {
}
}

static URL createJarWithClasses(final Class<?>... classes)
static URL createJarWithClasses(final Class<?>... classes) {
createJarWithClasses(".jar", classes)
}

static URL createJarWithClasses(final String suffix, final Class<?>... classes)
throws IOException {
File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "-", ".jar")
File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "-", suffix)
tmpJar.deleteOnExit()

JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar))
for (Class<?> clazz : classes) {
addToJar(clazz, clazz.getInterfaces()[0], target)
}

Manifest manifest = new Manifest()
Attributes attributes = manifest.getMainAttributes()
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0")
attributes.put(Attributes.Name.SPECIFICATION_TITLE, "test-specification-title")
attributes.put(Attributes.Name.SPECIFICATION_VERSION, "test-specification-version")
attributes.put(Attributes.Name.SPECIFICATION_VENDOR, "test-specification-vendor")
attributes.put(Attributes.Name.IMPLEMENTATION_TITLE, "test-implementation-title")
attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, "test-implementation-version")
attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, "test-implementation-vendor")

JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME)
target.putNextEntry(manifestEntry)
manifest.write(target)
target.closeEntry()

target.close()

return tmpJar.toURI().toURL()
Expand Down Expand Up @@ -149,4 +220,34 @@ class ExporterClassLoaderTest extends Specification {
private static String getResourceName(final String className) {
return className.replace('.', '/') + ".class"
}

@CompileStatic
private static class ParentClassLoader extends URLClassLoader {

ParentClassLoader() {
super()
}

ParentClassLoader(URL[] urls) {
super(urls)
}

@Override
Package getPackage(String name) {
// ExporterClassLoader uses getPackage to check whether package has already been
// defined. As getPackage also searches packages from parent class loader we return
// null here to ensure that package is defined in ExporterClassLoader.
null
}

@Override
Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// test classes are available in system class loader filter them so that
// they would be loaded by ExporterClassLoader
if (name.startsWith(ExporterClassLoaderTest.getName())) {
throw new ClassNotFoundException(name)
}
return super.loadClass(name, resolve)
}
}
}