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

Replace core reflection usage with MethodHandles #5046

Merged

Conversation

stevenschlansker
Copy link

This should improve performance due to the new design of method handles.
Core reflection has to evaluate everything at the time that you call method.invoke - it has to do access checks, determine what parameter conversions are necessary, etc - and does not have any local state to save the result of that work.

With MethodHandle, you do more of the work up-front - determine access, determine what parameter boxing or casts are necessary - and store that computed state in a new MethodHandle. Later, when you invokeExact, the method handle is able to make the call much faster.

@cowtowncoder
Copy link
Member

Also looks like Android compatibility is broken? (wrt animal-sniffer failure for JDK 17 build)

@pjfanning
Copy link
Member

Java 17 compile issues

Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:614: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:6[9](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:10)3: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:814: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:161: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:190: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:208: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:225: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:[12](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:13)7: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:148: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:160: Undefined reference: Object java.lang.invoke.MethodHandle.invoke(Object)
Error:  Failed to execute goal org.codehaus.mojo:animal-sniffer-maven-plugin:1.24:check (default-cli) on project jackson-databind: Signature errors found. Verify them and ignore them with the proper annotation if needed. -> [Help 1]
Error:  
Error:  To see the full stack trace of the errors, re-run Maven with the -e switch.
Error:  Re-run Maven using the -X switch to enable full debug logging.
Error:  
Error:  For more information about the errors and possible solutions, please read the following articles:
Error:  [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Error: Process completed with exit code 1.

@iProdigy
Copy link
Contributor

Java 17 compile issues

I believe those animal sniffer warnings are erroneous & can be suppressed: mojohaus/animal-sniffer#67

also the min android sdk for MethodHandles is 26, which is already applied

@pjfanning
Copy link
Member

Java 17 compile issues

I believe those animal sniffer warnings are erroneous & can be suppressed: mojohaus/animal-sniffer#67

also the min android sdk for MethodHandles is 26, which is already applied

Can't we increase the min Android SDK?

@iProdigy
Copy link
Contributor

iProdigy commented Mar 24, 2025

Can't we increase the min Android SDK?

To clarify, jackson 3 declares compatibility with sdk 34+ while 2.14 - 2.18 declares sdk 26+, both of which support MethodHandles - I don't follow your question

@pjfanning
Copy link
Member

I'd prefer to find out from animal-sniffer team why these methods are causing issues for it instead of just ignoring the issues that animal sniffer seems to think there are.

@cowtowncoder
Copy link
Member

Maybe there are newer versions of Animal sniffer configs (dep version from pom.xml)?

@yawkat
Copy link
Member

yawkat commented Mar 24, 2025

A new sniffer config is not sufficient, as PolymorphicSignature methods can have an unlimited number of signatures. It needs real plugin support.

I also don't think we can expect a response from the maintainers anytime soon, judging by that issue and the linked issues. Netty even removed the sniffer plugin because of it: netty/netty#14032

I think the the only sensible path forward is to add an exclusion for MethodHandle for now

@cowtowncoder
Copy link
Member

cowtowncoder commented Mar 24, 2025

@yawkat Ok. I will update dependency just as routine maintenance (for 2.19 and master, both behind a bit), but not expecting this particular issue to be addressed.
It does sound like an override needed then.

See #5047.

@stevenschlansker
Copy link
Author

I added an ignore for MethodHandle.
Now, the remaining AnimalSniffer problem is:

/home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()

I checked the definition and it seems that the method is part of the java spec since Java 9. Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?

@iProdigy
Copy link
Contributor

Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?

android doesn't seem to implement that method on any SDK: https://developer.android.com/reference/java/lang/reflect/AccessibleObject

you'll have to switch to setAccessible (and catch the exception)

@stevenschlansker
Copy link
Author

Rats, that sucks. I'll make that update...

@stevenschlansker
Copy link
Author

Ok, I reworked the ClassUtil.checkAndFixAccess method. There is a behavior change - previously, if we could not setAccessible on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. sun.util.calendar.ZoneInfo which fails. LMK if this is a problem and we can discuss more.

@stevenschlansker
Copy link
Author

WTF, Android does not implement InaccessibleObjectException either?!

@cowtowncoder
Copy link
Member

Ok, I reworked the ClassUtil.checkAndFixAccess method. There is a behavior change - previously, if we could not setAccessible on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. sun.util.calendar.ZoneInfo which fails. LMK if this is a problem and we can discuss more.

Hmm. Ok, that might be ok. Was thinking that maybe "sun.*" was not checked as system class by ClassUtil.isJDKClass() (and if so, could just add), but it is included.

I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.

@stevenschlansker
Copy link
Author

I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.

Yeah, that's why I tried to replace it with trySetAccessible - while it is still overriding visibility rules, at least it fails normally instead of requiring an exception thrown / catch.

The good news is this only happens during inspection, not at deserialization time, so the performance impact would be muted.

cowtowncoder added a commit that referenced this pull request Mar 26, 2025
@cowtowncoder
Copy link
Member

cowtowncoder commented Mar 26, 2025

@stevenschlansker Excellent work! I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after -- esp. assuming we could get some performance numbers, maybe for https://github.com/FasterXML/jackson-benchmarks/ (I'll see if I could run some myself).

EDIT: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1. Hoping to test out writes too.
So maybe somewhat limited benefit at least for tested case.

@yawkat
Copy link
Member

yawkat commented Mar 26, 2025

Maybe you can use a method handle to call trySetAccessible 😄

@stevenschlansker
Copy link
Author

Thanks @cowtowncoder I am excited to finally get this work in :)
I rebased and squished the MR. Please let me know if there's any other tasks necessary pre-merge.

I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after

That's fine by me. Were I making the call, I'd think that having the change in users' hands in an earlier RC for more testing would be better than delaying merging and finding problems closer to release - but again, 100% your call :)

EDIT: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1.
Hoping to test out writes too.
So maybe somewhat limited benefit at least for tested case.

I'm not too surprised. There was a time, a long time ago, when reflection was quite slow. The JVM has come a long way and now reflective calls are much, much faster than they used to be. That said, 5% is 5%, so I will take it :) especially since it comes with little downside.

Maybe you can use a method handle to call trySetAccessible

@yawkat yes, that would work :) I think since this is called only during (de)serializer instantiation, though, it's not worth the effort for now.

@cowtowncoder
Copy link
Member

@stevenschlansker Yeah, I plan on having quite a few RCs out, so by "not next" just means 2-3 week delay. Sort of to isolate things. But I get the point of as-early-as-possible wrt weeding out bugs.

On performance, yeah, +5% is measurable and has some value.

But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?
(I guess I should just spend time actually going through PR :) )

@stevenschlansker
Copy link
Author

Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?

MethodHandles unify method calls and field get / set with unreflectGetter
and unreflectSetter which present field read / write as trivial getter and setter methods. So we just use that then treat everything as a method now.

@stevenschlansker
Copy link
Author

stevenschlansker commented Mar 27, 2025

But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

It can be really hard to nail down performance on this kind of thing... my understanding from reading the literature is that it should help, but maybe hard to see among all the other work any particular workload does.

It's possible different shapes have different costs. For example when deserializing, method.invoke(arg) I think will always allocate an Object[] for varargs, whereas methodHandle.invokeExact(arg) should not need to do any varargs since it is a magic @PolymorphicSignature method

// 08-Sep-2016, tatu: wonder if we should verify it is `AnnotatedField` to be safe?
prop = new FieldProperty(propDef, type, typeDeser,
beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.getConfig().isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
Copy link
Member

Choose a reason for hiding this comment

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

I think ctxt.getConfig().isEnabled(...) can be shortened to ctxt.isEnabled(...) (there should be convenience accessor).

But aside from that, could you add a brief comment explaining reasons why code short-circuits here?
(cannot access I assume but... is that then quietly swallowing cases causing surprises?)

Copy link
Author

Choose a reason for hiding this comment

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

Added a brief comment. I agree, some conditions regarding module visibility could quietly swallow members here, if we cannot call setAccessible on them. I think in those cases though the whole operation will fail anyway until you allow Jackson module to open your module with beans in it.

There might be some follow-up tweaking to this logic necessary. I got all the existing tests passing but I am sure there are new exciting corner cases to discover with module visibility.

Come to think of it, can we remove the MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS now? I believe with MethodHandles used, there should no longer be any performance benefit to calling setAccessible unconditionally. I am happy to file a follow-up PR if this is acceptable. If not, we could consider turning it off by default now.

Copy link
Member

@cowtowncoder cowtowncoder Apr 7, 2025

Choose a reason for hiding this comment

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

Good idea -- I'll file an issue for changing defaults, will merge this PR and we'll go from there.

Actual removal of that feature is an option too, but at least start by changing its
default.

EDIT: issue -> #5074

}

constructors.removeIf(constructor ->
constructor == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, constructor)));
Copy link
Member

@cowtowncoder cowtowncoder Apr 4, 2025

Choose a reason for hiding this comment

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

Why is null check needed? It wasn't there earlier?

Copy link
Author

Choose a reason for hiding this comment

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

It's now necessary because access checks have been pushed up from the point of use (when constructor is called) to earlier in deserializer creation (when constructors and factories are collected) so they are now filtered out earlier too. I added a comment.

Copy link
Member

@cowtowncoder cowtowncoder left a comment

Choose a reason for hiding this comment

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

Looks pretty good, only minor final touches and I can merge it.

@cowtowncoder
Copy link
Member

@stevenschlansker Ok, looks like this also broke most downstream modules... :-(
Cascading builds at least gives that info in ~15 minutes after merge.

Will file issues, add a notes/refs here.

@cowtowncoder
Copy link
Member

cowtowncoder commented Apr 7, 2025

@cowtowncoder
Copy link
Member

Ugh. Seems like almost everything downstream fails, more or less catastrophically.

Will likely need to revert this PR later tonight.

@stevenschlansker
Copy link
Author

😢

@stevenschlansker
Copy link
Author

I'll try to find some time in the next week or two to check out the downstream modules and get those working.
I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.

@cowtowncoder
Copy link
Member

I'll try to find some time in the next week or two to check out the downstream modules and get those working. I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.

I have been trying to think of that over time and haven't thought of good maintainable solution.
Cascading rebuild is the first and only improvement -- which at least tightens up feedback loop from days (a week or more, even) to less than an hour.

Resource-wise full rebuild job for all PRs just for databind might be doable, although not sure how heavy resource-wise it'd be wrt Github charging (FasterXML has paid, not free, account).

@cowtowncoder
Copy link
Member

Will revert now.

@cowtowncoder
Copy link
Member

Reverted for now. Although failure was wide, it is possible there might be just small number of actual issues so maybe focusing on solving problems by just one downstream repo (like jackson-dataformat-xml) would make sense.

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.

5 participants