diff --git a/exporters/common/build.gradle.kts b/exporters/common/build.gradle.kts index f311510f83d..bbca8bc416a 100644 --- a/exporters/common/build.gradle.kts +++ b/exporters/common/build.gradle.kts @@ -67,6 +67,9 @@ testing { } } } + suites { + register("testWithoutUnsafe") {} + } } tasks { diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/UnsafeString.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/UnsafeString.java index c581e7525fb..309b005fd49 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/UnsafeString.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/marshal/UnsafeString.java @@ -10,7 +10,8 @@ class UnsafeString { private static final long valueOffset = getStringFieldOffset("value", byte[].class); private static final long coderOffset = getStringFieldOffset("coder", byte.class); - private static final int byteArrayBaseOffset = UnsafeAccess.arrayBaseOffset(byte[].class); + private static final int byteArrayBaseOffset = + UnsafeAccess.isAvailable() ? UnsafeAccess.arrayBaseOffset(byte[].class) : -1; private static final boolean available = valueOffset != -1 && coderOffset != -1; static boolean isAvailable() { diff --git a/exporters/common/src/testWithoutUnsafe/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtilTest.java b/exporters/common/src/testWithoutUnsafe/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtilTest.java new file mode 100644 index 00000000000..8ff3ec2e04d --- /dev/null +++ b/exporters/common/src/testWithoutUnsafe/java/io/opentelemetry/exporter/internal/marshal/StatelessMarshalerUtilTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.marshal; + +import static io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil.getUtf8Size; +import static io.opentelemetry.exporter.internal.marshal.StatelessMarshalerUtil.writeUtf8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class StatelessMarshalerUtilTest { + + // Simulate running in an environment without sun.misc.Unsafe e.g. when running a modular + // application. To use sun.misc.Unsafe in modular application user would need to add dependency to + // jdk.unsupported module or use --add-modules jdk.unsupported. Here we use a custom child first + // class loader that does not delegate loading sun.misc classes to make sun.misc.Unsafe + // unavailable. + @Test + void encodeUtf8WithoutUnsafe() throws Exception { + ClassLoader testClassLoader = + new ClassLoader(this.getClass().getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // don't allow loading sun.misc classes + if (name.startsWith("sun.misc")) { + throw new ClassNotFoundException(name); + } + // load io.opentelemetry in the custom loader + if (name.startsWith("io.opentelemetry")) { + synchronized (this) { + Class clazz = findLoadedClass(name); + if (clazz != null) { + return clazz; + } + try (InputStream inputStream = + getParent().getResourceAsStream(name.replace(".", "/") + ".class")) { + if (inputStream != null) { + byte[] bytes = readBytes(inputStream); + // we don't bother to define packages or provide protection domain + return defineClass(name, bytes, 0, bytes.length); + } + } catch (IOException exception) { + throw new ClassNotFoundException(name, exception); + } + } + } + return super.loadClass(name, resolve); + } + }; + + // load test class in the custom loader and run the test + Class testClass = testClassLoader.loadClass(this.getClass().getName() + "$TestClass"); + assertThat(testClass.getClassLoader()).isEqualTo(testClassLoader); + Runnable test = (Runnable) testClass.getConstructor().newInstance(); + test.run(); + } + + private static byte[] readBytes(InputStream inputStream) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + + int readCount; + while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) { + out.write(buffer, 0, readCount); + } + return out.toByteArray(); + } + + @SuppressWarnings("unused") + public static class TestClass implements Runnable { + + @Override + public void run() { + // verify that unsafe can't be found + assertThatThrownBy(() -> Class.forName("sun.misc.Unsafe")) + .isInstanceOf(ClassNotFoundException.class); + // test the methods that use unsafe + assertThat(getUtf8Size("a", true)).isEqualTo(1); + assertThat(testUtf8("a", 0)).isEqualTo("a"); + } + + static String testUtf8(String string, int utf8Length) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(outputStream); + writeUtf8(codedOutputStream, string, utf8Length, true); + codedOutputStream.flush(); + return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + } catch (Exception exception) { + throw new IllegalArgumentException(exception); + } + } + } +}