Skip to content

Commit

Permalink
Clean up Platform to be more maintainable
Browse files Browse the repository at this point in the history
Break up functionality into groups to simplify their configuration and make it easier to see how things change on each platform.

For the interface default method handling, replace reflection on newer JVMs with a multi-release jar and version-based variants of the JVM support. In order to sufficiently test this, the Java tests are now in a sibling module similar to Android, Kotlin, and RoboVM so that they consume the final jar rather than exploded class directories.
  • Loading branch information
JakeWharton committed Jan 29, 2024
1 parent ea34f8c commit aa593fc
Show file tree
Hide file tree
Showing 31 changed files with 380 additions and 370 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ subprojects {
googleJavaFormat(libs.googleJavaFormat.get().version)
.formatJavadoc(false)
removeUnusedImports()
target 'src/*/java/**/*.java'
target 'src/*/java*/**/*.java'
}
kotlin {
ktlint(libs.ktlint.get().version)
Expand Down
59 changes: 31 additions & 28 deletions retrofit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@ apply plugin: 'java-library'
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'com.vanniktech.maven.publish'

def addMultiReleaseSourceSet(int version) {
def sourceSet = sourceSets.create("java$version")
sourceSet.java.srcDir("src/main/java$version")

// Propagate dependencies to be visible to this version's source set.
configurations.getByName("java${version}Implementation").extendsFrom(configurations.getByName('implementation'))
configurations.getByName("java${version}Api").extendsFrom(configurations.getByName('api'))
configurations.getByName("java${version}CompileOnly").extendsFrom(configurations.getByName('compileOnly'))

// Allow types in the main source set to be visible to this version's source set.
dependencies.add("java${version}Implementation", sourceSets.getByName("main").output)

tasks.named("compileJava${version}Java", JavaCompile) {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(version)
vendor = JvmVendorSpec.AZUL
}
}

tasks.named('jar', Jar) {
from(sourceSet.output) {
into("META-INF/versions/$version")
}
}
}

addMultiReleaseSourceSet(14)
addMultiReleaseSourceSet(16)

dependencies {
api libs.okhttp

Expand All @@ -11,37 +40,11 @@ dependencies {

compileOnly libs.animalSnifferAnnotations
compileOnly libs.findBugsAnnotations

testImplementation projects.retrofit.testHelpers
testImplementation libs.junit
testImplementation libs.truth
testImplementation libs.guava
testImplementation libs.mockwebserver
}

jar {
manifest {
attributes 'Automatic-Module-Name': 'retrofit2'
}
}

// Create a test task for each supported JDK.
(8..21).each { majorVersion ->
def jdkTest = tasks.register("testJdk$majorVersion", Test) {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(majorVersion)
vendor = JvmVendorSpec.AZUL
}

description = "Runs the test suite on JDK $majorVersion"
group = LifecycleBasePlugin.VERIFICATION_GROUP

// Copy inputs from normal Test task.
def testTask = tasks.getByName("test")
classpath = testTask.classpath
testClassesDirs = testTask.testClassesDirs
}
tasks.named("check").configure {
dependsOn(jdkTest)
attributes 'Automatic-Module-Name': 'retrofit2'
attributes 'Multi-Release': 'true'
}
}
6 changes: 6 additions & 0 deletions retrofit/java-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Retrofit Java Tests

These are in a separate module for two reasons:

- It ensures optional dependencies (Kotlin stuff) are completely absent.
- It uses the multi-release jar on the classpath rather than only the classes folder.
34 changes: 34 additions & 0 deletions retrofit/java-test/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apply plugin: 'java-library'

dependencies {
testImplementation projects.retrofit
testImplementation projects.retrofit.testHelpers
testImplementation libs.junit
testImplementation libs.truth
testImplementation libs.guava
testImplementation libs.mockwebserver
}

// Create a test task for each supported JDK.
(8..21).each { majorVersion ->
def jdkTest = tasks.register("testJdk$majorVersion", Test) {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(majorVersion)
vendor = JvmVendorSpec.AZUL
}

description = "Runs the test suite on JDK $majorVersion"
group = LifecycleBasePlugin.VERIFICATION_GROUP

// Copy inputs from normal Test task.
def testTask = tasks.getByName("test")
classpath = testTask.classpath
testClassesDirs = testTask.testClassesDirs
}
tasks.named("check").configure {
dependsOn(jdkTest)
}
}

// We don't need the built-in task which uses Gradle's JVM given the above variants.
tasks.getByName('test').enabled = false
1 change: 0 additions & 1 deletion retrofit/kotlin-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ dependencies {
testImplementation libs.junit
testImplementation libs.truth
testImplementation libs.mockwebserver
testImplementation libs.kotlin.stdLib
testImplementation libs.kotlinCoroutines
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@

public final class RoboVmPlatformTest {
public static void main(String[] args) {
Platform platform = Platform.get();
if (platform.createDefaultCallAdapterFactories(null).size() > 1) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com")
.callFactory(c -> { throw new AssertionError(); })
.build();

if (retrofit.callAdapterFactories().size() > 1) {
// Everyone gets the callback executor adapter. If RoboVM was correctly detected it will NOT
// get the Java 8-supporting CompletableFuture call adapter factory.
System.exit(1);
Expand Down
29 changes: 29 additions & 0 deletions retrofit/src/main/java/retrofit2/AndroidMainExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.Executor;

final class AndroidMainExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());

@Override
public void execute(Runnable r) {
handler.post(r);
}
}
52 changes: 52 additions & 0 deletions retrofit/src/main/java/retrofit2/BuiltInFactories.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import android.annotation.TargetApi;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;

class BuiltInFactories {
List<? extends CallAdapter.Factory> createDefaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
return singletonList(new DefaultCallAdapterFactory(callbackExecutor));
}

List<? extends Converter.Factory> createDefaultConverterFactories() {
return emptyList();
}

@TargetApi(24)
static final class Java8 extends BuiltInFactories {
@Override
List<? extends CallAdapter.Factory> createDefaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
return asList(
new CompletableFutureCallAdapterFactory(),
new DefaultCallAdapterFactory(callbackExecutor));
}

@Override
List<? extends Converter.Factory> createDefaultConverterFactories() {
return singletonList(new OptionalConverterFactory());
}
}
}
54 changes: 54 additions & 0 deletions retrofit/src/main/java/retrofit2/DefaultMethodSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import android.annotation.TargetApi;
import android.os.Build;
import java.lang.reflect.Method;
import javax.annotation.Nullable;

class DefaultMethodSupport {
boolean isDefaultMethod(Method method) {
return false;
}

@Nullable
Object invokeDefaultMethod(
Method method, Class<?> declaringClass, Object proxy, @Nullable Object[] args)
throws Throwable {
throw new AssertionError();
}

/**
* Android does not support MR jars, so this extends the baseline JVM support which targets
* Java 8 APIs. Default methods and the reflection API to detect them were added to API 24
* as part of the initial Java 8 set. MethodHandle, our means of invoking the default method
* through the proxy, was not added until API 26.
*/
@TargetApi(24)
static final class Android24 extends DefaultMethodSupportJvm {
@Override
Object invokeDefaultMethod(
Method method, Class<?> declaringClass, Object proxy, @Nullable Object[] args)
throws Throwable {
if (Build.VERSION.SDK_INT < 26) {
throw new UnsupportedOperationException(
"Calling default methods on API 24 and 25 is not supported");
}
return super.invokeDefaultMethod(method, declaringClass, proxy, args);
}
}
}
53 changes: 53 additions & 0 deletions retrofit/src/main/java/retrofit2/DefaultMethodSupportJvm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import org.jetbrains.annotations.Nullable;

/**
* From Java 8 to Java 13, the only way to invoke a default method on a proxied interface is by
* reflectively creating a trusted {@link Lookup} to invoke a method handle.
* <p>
* Note: This class has multi-release jar variants for newer versions of Java.
*/
class DefaultMethodSupportJvm extends DefaultMethodSupport {
private @Nullable Constructor<Lookup> lookupConstructor;

@Override
boolean isDefaultMethod(Method method) {
return method.isDefault();
}

@Override
Object invokeDefaultMethod(
Method method, Class<?> declaringClass, Object proxy, @Nullable Object[] args)
throws Throwable {
Constructor<Lookup> lookupConstructor = this.lookupConstructor;
if (lookupConstructor == null) {
lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class);
lookupConstructor.setAccessible(true);
this.lookupConstructor = lookupConstructor;
}
return lookupConstructor
.newInstance(declaringClass, -1 /* trusted */)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
}
}
Loading

0 comments on commit aa593fc

Please sign in to comment.