Skip to content

Commit

Permalink
Add Support for Java 9 in Multi-Release JAR for azure-core (Azure#23835)
Browse files Browse the repository at this point in the history
Add Support for Java 9 in Multi-Release JAR for azure-core
  • Loading branch information
alzimmermsft authored Sep 21, 2021
1 parent 3108ad8 commit 173670a
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 91 deletions.
65 changes: 63 additions & 2 deletions sdk/core/azure-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version> <!-- {x-version-update;org.apache.maven.plugins:maven-jar-plugin;external_dependency} -->
<executions>
<execution>
<id>default-jar</id>
<phase>package</phase>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</execution>
<execution>
<id>test-jar</id>
<phase>test-compile</phase>
Expand All @@ -203,8 +214,10 @@
<include>com.fasterxml.jackson.core:jackson-annotations:[2.12.4]</include> <!-- {x-include-update;com.fasterxml.jackson.core:jackson-annotations;external_dependency} -->
<include>com.fasterxml.jackson.core:jackson-core:[2.12.4]</include> <!-- {x-include-update;com.fasterxml.jackson.core:jackson-core;external_dependency} -->
<include>com.fasterxml.jackson.core:jackson-databind:[2.12.4]</include> <!-- {x-include-update;com.fasterxml.jackson.core:jackson-databind;external_dependency} -->
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-xml:[2.12.4]</include> <!-- {x-include-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
<include>com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.12.4]</include> <!-- {x-include-update;com.fasterxml.jackson.datatype:jackson-datatype-jsr310;external_dependency} -->
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-xml:[2.12.4]
</include> <!-- {x-include-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
<include>com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.12.4]
</include> <!-- {x-include-update;com.fasterxml.jackson.datatype:jackson-datatype-jsr310;external_dependency} -->
<include>org.slf4j:slf4j-api:[1.7.32]</include> <!-- {x-include-update;org.slf4j:slf4j-api;external_dependency} -->
</includes>
</bannedDependencies>
Expand Down Expand Up @@ -239,10 +252,58 @@
</configuration>
</plugin>

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version> <!-- {x-version-update;org.jacoco:jacoco-maven-plugin;external_dependency} -->
<configuration>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.0</version> <!-- {x-version-update;org.apache.maven.plugins:maven-failsafe-plugin;external_dependency} -->
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>java9-plus</id>
<activation>
<jdk>[9,)</jdk>
</activation>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <!-- {x-version-update;org.apache.maven.plugins:maven-compiler-plugin;external_dependency} -->
<executions>
<execution>
<id>java9-plus-mr</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>9</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

<profile>
<id>java-lts</id>
<activation>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.implementation.ReflectionUtilsApi;
import com.azure.core.implementation.serializer.HttpResponseDecoder;
import com.azure.core.util.logging.ClientLogger;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -66,7 +66,7 @@ MethodHandle get(Class<? extends Response<?>> responseClass) {
private MethodHandle locateResponseConstructor(Class<?> responseClass) {
MethodHandles.Lookup lookupToUse;
try {
lookupToUse = ReflectionUtils.getLookupToUse(responseClass);
lookupToUse = ReflectionUtilsApi.INSTANCE.getLookupToUse(responseClass);
} catch (Throwable t) {
throw logger.logExceptionAsError(new RuntimeException(t));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,12 @@

package com.azure.core.implementation;

import com.azure.core.util.logging.ClientLogger;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;

import static java.lang.invoke.MethodType.methodType;

/**
* Utility methods that aid in performing reflective operations.
*/
public final class ReflectionUtils {
// This lookup is specific to the com.azure.core module, specifically this class.
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

// Convenience pointer to the com.azure.core module.
// Since this is only Java 9+ functionality it needs to use reflection.
private static final String MODULE_CLASS = "java.lang.Module";
private static final Object CORE_MODULE;
private static final MethodHandle GET_MODULE;
private static final MethodHandle IS_MODULE_EXPORTED;
private static final MethodHandle CAN_READ_MODULE;
private static final MethodHandle ADD_MODULE_READ;

static {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Object coreModule = null;
MethodHandle getModule = null;
MethodHandle isModuleExported = null;
MethodHandle canReadModule = null;
MethodHandle addReadModule = null;

try {
Class<?> moduleClass = Class.forName(MODULE_CLASS);
Class<?> classClass = Class.class;
getModule = publicLookup.findVirtual(classClass, "getModule", methodType(moduleClass));
coreModule = getModule.invoke(ReflectionUtils.class);
isModuleExported = publicLookup.findVirtual(moduleClass, "isExported",
methodType(boolean.class, String.class));
canReadModule = publicLookup.findVirtual(moduleClass, "canRead", methodType(boolean.class, moduleClass));
addReadModule = MethodHandles.lookup()
.findVirtual(moduleClass, "addReads", methodType(moduleClass, moduleClass));
} catch (Throwable ex) {
new ClientLogger(ReflectionUtils.class)
.verbose("Failed to retrieve MethodHandles used to check module information. "
+ "If the application is not using Java 9+ runtime behavior will work as expected.", ex);
}

CORE_MODULE = coreModule;
GET_MODULE = getModule;
IS_MODULE_EXPORTED = isModuleExported;
CAN_READ_MODULE = canReadModule;
ADD_MODULE_READ = addReadModule;
}
final class ReflectionUtils implements ReflectionUtilsApi {

/**
* Gets the {@link MethodHandles.Lookup} to use when performing reflective operations.
Expand All @@ -74,37 +26,14 @@ public final class ReflectionUtils {
* reflectively.
* @throws Throwable If the underlying reflective calls throw an exception.
*/
public static MethodHandles.Lookup getLookupToUse(Class<?> targetClass) throws Throwable {
/*
* If we were able to write this using Java 9+ code.
*
* First check if the response class's module is exported to all unnamed modules. If it is we will use
* MethodHandles.publicLookup() which is meant for creating MethodHandle instances for publicly accessible
* classes.
*/
if (GET_MODULE == null) {
// Java 8 is being used, public lookup should be able to access the Response class.
return MethodHandles.publicLookup();
} else {
Object responseModule = GET_MODULE.invoke(targetClass);
if ((boolean) IS_MODULE_EXPORTED.invoke(responseModule, "")) {
return MethodHandles.publicLookup();
} else {
/*
* Otherwise, we use the MethodHandles.Lookup which is associated to this (com.azure.core) module, and
* more specifically, is tied to this class (ResponseConstructorsCache). But, in order to use this
* lookup we need to ensure that the com.azure.core module reads the response class's module as the
* lookup won't have permissions necessary to create the MethodHandle instance without it.
*/
if (!(boolean) CAN_READ_MODULE.invoke(CORE_MODULE, responseModule)) {
ADD_MODULE_READ.invoke(CORE_MODULE, responseModule);
}
public MethodHandles.Lookup getLookupToUse(Class<?> targetClass) throws Throwable {
return MethodHandles.publicLookup();
}

return LOOKUP;
}
}
public int getJavaImplementationMajorVersion() {
return 8;
}

private ReflectionUtils() {
ReflectionUtils() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.implementation;

import java.lang.invoke.MethodHandles;

/**
* API for {@link ReflectionUtils}.
*/
public interface ReflectionUtilsApi {
ReflectionUtilsApi INSTANCE = new ReflectionUtils();

/**
* Gets the {@link MethodHandles.Lookup} to use based on the target {@link Class}.
*
* @param targetClass The target {@link Class}.
* @return The {@link MethodHandles.Lookup} to use.
* @throws Throwable If an error occurs while attempting to find the lookup.
*/
MethodHandles.Lookup getLookupToUse(Class<?> targetClass) throws Throwable;

/**
* Gets the Java implementation major version.
*
* @return The Java implementation major version.
*/
default int getJavaImplementationMajorVersion() {
return 8;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

package com.azure.core.implementation.jackson;

import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.implementation.ReflectionUtilsApi;
import com.azure.core.util.logging.ClientLogger;

import java.lang.invoke.MethodHandle;
Expand Down Expand Up @@ -87,7 +87,7 @@ private boolean usePublicSetter(Object deserializedHeaders, ClientLogger logger)
MethodHandle setterHandler = getFromCache(declaringField, field -> {
MethodHandles.Lookup lookupToUse;
try {
lookupToUse = ReflectionUtils.getLookupToUse(clazz);
lookupToUse = ReflectionUtilsApi.INSTANCE.getLookupToUse(clazz);
} catch (Throwable t) {
logger.verbose("Failed to retrieve MethodHandles.Lookup for field {}.", field, t);
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.implementation;

import java.lang.invoke.MethodHandles;

/**
* Utility methods that aid in performing reflective operations.
*/
final class ReflectionUtils implements ReflectionUtilsApi {
// This lookup is specific to the com.azure.core module, specifically this class.
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

// Convenience pointer to the com.azure.core module.
private static final Module CORE_MODULE = ReflectionUtils.class.getModule();

/**
* Gets the {@link MethodHandles.Lookup} to use when performing reflective operations.
* <p>
* If Java 8 is being used this will always return {@link MethodHandles.Lookup#publicLookup()} as Java 8 doesn't
* have module boundaries that will prevent reflective access to the {@code targetClass}.
* <p>
* If Java 9 or above is being used this will return a {@link MethodHandles.Lookup} based on whether the module
* containing the {@code targetClass} exports the package containing the class. Otherwise, the {@link
* MethodHandles.Lookup} associated to {@code com.azure.core} will attempt to read the module containing {@code
* targetClass}.
*
* @param targetClass The {@link Class} that will need to be reflectively accessed.
* @return The {@link MethodHandles.Lookup} that will allow {@code com.azure.core} to access the {@code targetClass}
* reflectively.
* @throws Throwable If the underlying reflective calls throw an exception.
*/
public MethodHandles.Lookup getLookupToUse(Class<?> targetClass) throws Throwable {
Module responseModule = targetClass.getModule();

/*
* First check if the response class's module is exported to all unnamed modules. If it is we will use
* MethodHandles.publicLookup() which is meant for creating MethodHandle instances for publicly accessible
* classes.
*/
if (responseModule.isExported("")) {
return MethodHandles.publicLookup();
}

/*
* Otherwise, we use the MethodHandles.Lookup which is associated to this (com.azure.core) module, and more
* specifically, is tied to this class (ReflectionUtils). But, in order to use this lookup we need to ensure
* that the com.azure.core module reads the response class's module as the lookup won't have permissions
* necessary to create the MethodHandle instance without it.
*
* This logic is safe due to the fact that any SDK module calling into this code path will already need to open
* to com.azure.core as it needs to perform other reflective operations on classes in the module. Adding the
* com.azure.core reads is handling specifically required by MethodHandle.
*/
if (!CORE_MODULE.canRead(responseModule)) {
CORE_MODULE.addReads(responseModule);
}

return LOOKUP;
}

public int getJavaImplementationMajorVersion() {
return 9;
}

ReflectionUtils() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

/**
* Package containing implementation-specific APIs that should not be used by end-users.
*/
package com.azure.core.implementation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.implementation;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Integration tests for {@link ReflectionUtils}.
*/
public class ReflectionUtilsIT {
/*
* This is an integration test instead of a unit test because integration tests use the generated JAR, with
* multi-release support, instead of the class files in Maven's output directory. Given that, the integration tests
* will hook into the different Java version implementations.
*/
@Test
public void validateImplementationVersion() {
String javaSpecificationVersion = System.getProperty("java.specification.version");
if (javaSpecificationVersion.equals("1.8")) {
assertEquals(8, ReflectionUtilsApi.INSTANCE.getJavaImplementationMajorVersion());
} else {
assertEquals(9, ReflectionUtilsApi.INSTANCE.getJavaImplementationMajorVersion());
}
}
}
9 changes: 6 additions & 3 deletions sdk/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,19 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version> <!-- {x-version-update;org.jacoco:jacoco-maven-plugin;external_dependency} -->
<configuration>
<outputDirectory>${project.reporting.outputDirectory}/test-coverage</outputDirectory>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
<configuration>
<outputDirectory>${project.reporting.outputDirectory}/test-coverage</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
Expand Down
Loading

0 comments on commit 173670a

Please sign in to comment.