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

Implement reflective support for Java Records #1381

Merged
merged 36 commits into from
Aug 23, 2021

Conversation

ZacSweers
Copy link
Collaborator

@ZacSweers ZacSweers commented Aug 20, 2021

Porting from https://github.com/ZacSweers/MoshiX/tree/main/moshi-records-reflect

This makes a few changes along the way to make the build all happy with how we're doing this:

  • Standardizing around Java 8 (which is the minimum supported by gradle toolchains and also soon to be the minimum supported by Kotlin)
  • Create a separate Java 16 source set for hosting the actual adapter implementation. This is compiled with Java 16 but is then packaged into the main Moshi jar and denoted as a Multi-Release JAR.
  • We gate access to this record adapter at runtime by checking if the requisite record APIs are available.
  • We separately have a shim copy of the record adapter in the main source set for linking in BUILT_IN_FACTORIES, but this is booted out by the real one during packaging.
  • Replace animal sniffer with -release 8, which functionally accomplishes the same
  • Tests are in a dedicated separate records-test subproject, making it easier to both write new ones and also ensure our packaging rules for the jar is working correctly (we've had success with this in the past with the codegen artifact's shading being tested in a dedicated tests project)

Resolves #1278

Tests are a little skimpy right now, happy to take suggestions on more we should add! Can also look at fleshing them out more in a followup PR

@ZacSweers
Copy link
Collaborator Author

ZacSweers commented Aug 20, 2021

It looks like animal sniffer ships a shaded old version of ASM and is also no longer being updated since the -release flag covers the same on JDK 9+. Thing is we want to support 8, so we can't use it yet. Seems like we'll need to drop animal sniffer if we want to proceed. Thoughts? Edit I believe we can just use a newer JDK to compile and set an older release flag, will update

moshi/build.gradle.kts Outdated Show resolved Hide resolved
Copy link
Member

@JakeWharton JakeWharton left a comment

Choose a reason for hiding this comment

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

Where do you actually pack the MR jar? I don't see any references to META-INF/versions/16/

moshi/src/main/java/com/squareup/moshi/internal/Util.java Outdated Show resolved Hide resolved
Constructor<Object> constructor;
try {
//noinspection unchecked
constructor = (Constructor<Object>) rawType.getDeclaredConstructor(constructorParams);
Copy link
Member

Choose a reason for hiding this comment

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

Is there no way to get a MethodHandle from the Class once we know it's a record? Can't believe they expect you to use ancient java.lang.reflect reflection to programmatically instantiate these...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Honestly never used that API, let me look

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good news is we can use MethodHandle for this but bad news is RecordComponent itself still returns only Method for its accessors :/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Constructor bit is in 355422c

I also wrapped the accessor Method with one too, but not sure (lack of familiarity) if this is worthwhile 4700a63. Let me know

build.gradle.kts Outdated Show resolved Hide resolved
@ZacSweers
Copy link
Collaborator Author

ZacSweers commented Aug 21, 2021

Where do you actually pack the MR jar? I don't see any references to META-INF/versions/16/

Yup removed! I misunderstood the requirements for this. This basically just packages it like a simple jar and gates java 16 code on the Util check

@JakeWharton
Copy link
Member

No MR jar probably means tools like Graal and D8 (and others) will choke processing the jar.

Instead of a gated use and stub you can make the stub return null from a factory method and make the 16 class return a real implementation. Then you unconditionally call the factory and gate behavior on non-null values.

@ZacSweers
Copy link
Collaborator Author

ah good idea. Thanks for bearing with me, learning something new with these MR jars!

@ZacSweers
Copy link
Collaborator Author

Ok think I've got it working now. This is neat!

image

We can just enable this everywhere now since we require JDK 16 anyway
@ZacSweers
Copy link
Collaborator Author

Being reminded how much the java module system sucks when you have to configure stuff

@ZacSweers ZacSweers merged commit 95250b0 into square:master Aug 23, 2021
@ZacSweers ZacSweers deleted the z/records branch August 23, 2021 16:00
- 13
- 14
- 15
- 16
Copy link
Member

Choose a reason for hiding this comment

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

Not testing on Java 8 is annoying. Something to look into separately from this change.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can make the main tests run with java 8 via Gradle toolchains

dependencies {
"compileOnly"(Dependencies.AnimalSniffer.annotations)
"signature"(Dependencies.AnimalSniffer.java7Signature)
if (project.name != "records-tests") {
Copy link
Member

Choose a reason for hiding this comment

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

Very nice

@@ -15,9 +15,36 @@
#

# For Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
Copy link
Member

Choose a reason for hiding this comment

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

These are ... unfortunate. The Dokka issue above seems unrelated. I think this is the tracking issue.
https://youtrack.jetbrains.com/issue/KT-45545

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The kapt parts are no longer relevant or on master now at least with Kotlin 1.6

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
tasks.withType<Test>().configureEach {
// For kapt to work with kotlin-compile-testing
Copy link
Member

Choose a reason for hiding this comment

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

}

val mainSourceSet by sourceSets.named("main")
val java16 by sourceSets.creating {
Copy link
Member

Choose a reason for hiding this comment

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

neat

@@ -35,6 +70,8 @@ tasks.withType<KotlinCompile>()
}

dependencies {
// So the j16 source set can "see" main Moshi sources
"java16Implementation"(mainSourceSet.output)
Copy link
Member

Choose a reason for hiding this comment

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

off topic: this syntax offends me so much

}

@Test
public void qualifiedValues() throws IOException {
Copy link
Member

Choose a reason for hiding this comment

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

awesome

import javax.annotation.Nullable;

/**
* This is just a simple shim for linking in {@link StandardJsonAdapters} and swapped with a real
Copy link
Member

Choose a reason for hiding this comment

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

I really like how this plugs in

Copy link
Member

Choose a reason for hiding this comment

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

Feels very reminiscent of expect/actual

Map<String, Type> mappedTypeArgs = null;
if (type instanceof ParameterizedType parameterizedType) {
Type[] typeArgs = parameterizedType.getActualTypeArguments();
var typeVars = rawType.getTypeParameters();
Copy link
Member

Choose a reason for hiding this comment

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

var! (My first time seeing this in real life)

}

Map<String, Type> mappedTypeArgs = null;
if (type instanceof ParameterizedType parameterizedType) {
Copy link
Member

Choose a reason for hiding this comment

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

This reads to me as very similar to Util.resolve(). Presumably we can’t use that here because this needs more records stuff? Either way this behavior really wants to be extracted into a separately-testable thing

Copy link
Member

Choose a reason for hiding this comment

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

return new RecordJsonAdapter<>(constructor, rawType.getSimpleName(), bindings).nullSafe();
};

private static record ComponentBinding<T>(
Copy link
Member

Choose a reason for hiding this comment

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

yo dog

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah I think the static is implicit but the IDE didn't give me a warning about it being redundant

private final String targetClass;
private final MethodHandle constructor;
private final ComponentBinding<Object>[] componentBindingsArray;
private final JsonReader.Options options;
Copy link
Member

Choose a reason for hiding this comment

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

Yay options

Copy link
Member

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

I think I’d like to do a follow-up to this to test its internal components, though that shouldn’t block release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Java record classes when they are stable
4 participants