|
5 | 5 |
|
6 | 6 | package io.opentelemetry.javaagent.tooling
|
7 | 7 |
|
| 8 | +import groovy.transform.CompileStatic |
8 | 9 | import io.opentelemetry.javaagent.spi.exporter.MetricExporterFactory
|
9 | 10 | import io.opentelemetry.javaagent.spi.exporter.SpanExporterFactory
|
10 | 11 | import io.opentelemetry.sdk.metrics.export.MetricExporter
|
11 | 12 | import io.opentelemetry.sdk.trace.export.SpanExporter
|
12 | 13 | import java.nio.charset.StandardCharsets
|
| 14 | +import java.util.jar.Attributes |
13 | 15 | import java.util.jar.JarEntry
|
| 16 | +import java.util.jar.JarFile |
14 | 17 | import java.util.jar.JarOutputStream
|
| 18 | +import java.util.jar.Manifest |
15 | 19 | import spock.lang.Specification
|
16 | 20 |
|
17 | 21 | class ExporterClassLoaderTest extends Specification {
|
18 | 22 |
|
19 | 23 | // Verifies https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/542
|
20 | 24 | def "does not look in parent classloader for metric exporters"() {
|
21 | 25 | setup:
|
22 |
| - def parentClassloader = new URLClassLoader([createJarWithClasses(MetricExporterFactoryParent)] as URL[]) |
| 26 | + def parentClassloader = new ParentClassLoader([createJarWithClasses(MetricExporterFactoryParent)] as URL[]) |
23 | 27 | def childClassloader = new ExporterClassLoader(createJarWithClasses(MetricExporterFactoryChild), parentClassloader)
|
24 | 28 |
|
25 | 29 | when:
|
26 | 30 | ServiceLoader<MetricExporterFactory> serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader)
|
27 | 31 |
|
28 | 32 | then:
|
29 | 33 | serviceLoader.size() == 1
|
| 34 | + |
| 35 | + and: |
| 36 | + childClassloader.manifest != null |
| 37 | + |
| 38 | + when: |
| 39 | + MetricExporterFactory instance = serviceLoader.iterator().next() |
| 40 | + Class clazz = instance.getClass() |
| 41 | + |
| 42 | + then: |
| 43 | + clazz.getClassLoader() == childClassloader |
30 | 44 | }
|
31 | 45 |
|
32 | 46 | def "does not look in parent classloader for span exporters"() {
|
33 | 47 | setup:
|
34 |
| - def parentClassloader = new URLClassLoader([createJarWithClasses(SpanExporterFactoryParent)] as URL[]) |
| 48 | + def parentClassloader = new ParentClassLoader([createJarWithClasses(SpanExporterFactoryParent)] as URL[]) |
35 | 49 | def childClassloader = new ExporterClassLoader(createJarWithClasses(SpanExporterFactoryChild), parentClassloader)
|
36 | 50 |
|
37 | 51 | when:
|
38 | 52 | ServiceLoader<SpanExporterFactory> serviceLoader = ServiceLoader.load(SpanExporterFactory, childClassloader)
|
39 | 53 |
|
40 | 54 | then:
|
41 | 55 | serviceLoader.size() == 1
|
| 56 | + |
| 57 | + and: |
| 58 | + childClassloader.manifest != null |
| 59 | + |
| 60 | + when: |
| 61 | + SpanExporterFactory instance = serviceLoader.iterator().next() |
| 62 | + Class clazz = instance.getClass() |
| 63 | + |
| 64 | + then: |
| 65 | + clazz.getClassLoader() == childClassloader |
| 66 | + } |
| 67 | + |
| 68 | + // Verifies that loading of exporter jar succeeds when there is a space in path to exporter jar |
| 69 | + def "load jar with space in path"() { |
| 70 | + setup: |
| 71 | + def parentClassloader = new ParentClassLoader() |
| 72 | + // " .jar" is used to make path to jar contain a space |
| 73 | + def childClassloader = new ExporterClassLoader(createJarWithClasses(" .jar", MetricExporterFactoryChild), parentClassloader) |
| 74 | + |
| 75 | + when: |
| 76 | + ServiceLoader<MetricExporterFactory> serviceLoader = ServiceLoader.load(MetricExporterFactory, childClassloader) |
| 77 | + |
| 78 | + then: |
| 79 | + serviceLoader.size() == 1 |
| 80 | + |
| 81 | + and: |
| 82 | + childClassloader.manifest != null |
| 83 | + |
| 84 | + when: |
| 85 | + MetricExporterFactory instance = serviceLoader.iterator().next() |
| 86 | + Class clazz = instance.getClass() |
| 87 | + |
| 88 | + then: |
| 89 | + clazz.getClassLoader() == childClassloader |
| 90 | + |
| 91 | + and: |
| 92 | + clazz.getPackage().getImplementationVersion() == "test-implementation-version" |
42 | 93 | }
|
43 | 94 |
|
44 | 95 | static class MetricExporterFactoryParent implements MetricExporterFactory {
|
@@ -93,15 +144,35 @@ class ExporterClassLoaderTest extends Specification {
|
93 | 144 | }
|
94 | 145 | }
|
95 | 146 |
|
96 |
| - static URL createJarWithClasses(final Class<?>... classes) |
| 147 | + static URL createJarWithClasses(final Class<?>... classes) { |
| 148 | + createJarWithClasses(".jar", classes) |
| 149 | + } |
| 150 | + |
| 151 | + static URL createJarWithClasses(final String suffix, final Class<?>... classes) |
97 | 152 | throws IOException {
|
98 |
| - File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "-", ".jar") |
| 153 | + File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "-", suffix) |
99 | 154 | tmpJar.deleteOnExit()
|
100 | 155 |
|
101 | 156 | JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar))
|
102 | 157 | for (Class<?> clazz : classes) {
|
103 | 158 | addToJar(clazz, clazz.getInterfaces()[0], target)
|
104 | 159 | }
|
| 160 | + |
| 161 | + Manifest manifest = new Manifest() |
| 162 | + Attributes attributes = manifest.getMainAttributes() |
| 163 | + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0") |
| 164 | + attributes.put(Attributes.Name.SPECIFICATION_TITLE, "test-specification-title") |
| 165 | + attributes.put(Attributes.Name.SPECIFICATION_VERSION, "test-specification-version") |
| 166 | + attributes.put(Attributes.Name.SPECIFICATION_VENDOR, "test-specification-vendor") |
| 167 | + attributes.put(Attributes.Name.IMPLEMENTATION_TITLE, "test-implementation-title") |
| 168 | + attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, "test-implementation-version") |
| 169 | + attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, "test-implementation-vendor") |
| 170 | + |
| 171 | + JarEntry manifestEntry = new JarEntry(JarFile.MANIFEST_NAME) |
| 172 | + target.putNextEntry(manifestEntry) |
| 173 | + manifest.write(target) |
| 174 | + target.closeEntry() |
| 175 | + |
105 | 176 | target.close()
|
106 | 177 |
|
107 | 178 | return tmpJar.toURI().toURL()
|
@@ -149,4 +220,34 @@ class ExporterClassLoaderTest extends Specification {
|
149 | 220 | private static String getResourceName(final String className) {
|
150 | 221 | return className.replace('.', '/') + ".class"
|
151 | 222 | }
|
| 223 | + |
| 224 | + @CompileStatic |
| 225 | + private static class ParentClassLoader extends URLClassLoader { |
| 226 | + |
| 227 | + ParentClassLoader() { |
| 228 | + super() |
| 229 | + } |
| 230 | + |
| 231 | + ParentClassLoader(URL[] urls) { |
| 232 | + super(urls) |
| 233 | + } |
| 234 | + |
| 235 | + @Override |
| 236 | + Package getPackage(String name) { |
| 237 | + // ExporterClassLoader uses getPackage to check whether package has already been |
| 238 | + // defined. As getPackage also searches packages from parent class loader we return |
| 239 | + // null here to ensure that package is defined in ExporterClassLoader. |
| 240 | + null |
| 241 | + } |
| 242 | + |
| 243 | + @Override |
| 244 | + Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
| 245 | + // test classes are available in system class loader filter them so that |
| 246 | + // they would be loaded by ExporterClassLoader |
| 247 | + if (name.startsWith(ExporterClassLoaderTest.getName())) { |
| 248 | + throw new ClassNotFoundException(name) |
| 249 | + } |
| 250 | + return super.loadClass(name, resolve) |
| 251 | + } |
| 252 | + } |
152 | 253 | }
|
0 commit comments