Skip to content

Migrate legacy reflection properties to use MethodHandles, take 2 #5090

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

Merged
merged 22 commits into from
May 9, 2025

Conversation

stevenschlansker
Copy link

I'm pretty sure I figured out the main reason the previous attempt (#5046) did not work - in order to read modules from databind, we still have to call setAccessible(true) which was happening too late due to the virtual method e.g. BeanPropertyWriter.fixAccess happening after constructor finishes.

Work around this by using a lazy memoized Setter class. Also ends up helping to simplify some serialization wonkiness.

Fixes #2083

@stevenschlansker stevenschlansker force-pushed the methodhandle-hacking branch 3 times, most recently from fb6aa3f to 9d9b7d7 Compare April 11, 2025 04:25
@stevenschlansker
Copy link
Author

Sorry the first attempt did not go so well, but here is try #2. Instead of changing any of the rules around set accessible, defer using the method / constructors / etc until first-use.
It's unfortunate that the fixAccess / setAccessible is done after the constructor finishes...

I've tested this works with jackson-dataformat-xml jackson-jakarta-rs-providers. Working on one test failure so far in jackson-modules-base still.

@stevenschlansker
Copy link
Author

Ok, with FasterXML/jackson-modules-base#295, jackson-modules-base builds too :)

@stevenschlansker stevenschlansker force-pushed the methodhandle-hacking branch 5 times, most recently from c10f758 to 2b57c71 Compare April 11, 2025 14:59
@cowtowncoder
Copy link
Member

@stevenschlansker I think I will merge this post rc3 (which I hope to release maybe this weekend).
May be able to merge smaller pieces first, will see if some changes could be applied separately.

@cowtowncoder
Copy link
Member

cowtowncoder commented Apr 12, 2025

Ok made small change (Exception arg to Throwable) into 3.x, merged to this PR.

@stevenschlansker
Copy link
Author

Thanks - please let me know if there's any additional changes needed or validation I should perform.

@stevenschlansker
Copy link
Author

@cowtowncoder , while working on this, I ran into the BeanPropertyWriter.fixAccess and AnnotatedMember.fixAccess which is unfortunate in that it mutates state after the constructor completes. I am not sure if it is feasible, but it would be nice if we could push that decision up to the constructor - then more state can be final and we could get rid of the lazy memoized setter, for example

@cowtowncoder
Copy link
Member

@stevenschlansker Sounds like a good idea at high level. Unfortunately I don't have more context on this (as in, haven't worked in those aspects lately) so say much about feasibility.

I'll try to get #5094 in tonight, to be done with bigger messing up of introspection, to stabilize things.
(#5093 and #5094 are meant to allow reducing introspection but realized that on short term they won't... but lay some foundation for reduction as next steps. Not directly related to your suggestion, just a sidenote on concurrent changes in related area).

@cowtowncoder
Copy link
Member

@stevenschlansker Not sure how much (if at all) this helps, but with #5143 I removed java.io.Serializable from SettableBeanProperty -- so I think UnreflectHandleSupplier and SetterHolder no longer need to be JDK serializable either.

What else do we need to help with issue you mentioned wrt BeanPropertyWriter.fixAccess()?

_throwAsJacksonE(ctxt.getParser(), e, value);
return null;
}
}

class SetterHolder extends UnreflectHandleSupplier {
private static final long serialVersionUID = 1L;
Copy link
Member

Choose a reason for hiding this comment

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

Almost certainly no longer needed (I removed most implements java.io.Serializable cases in #5143)

Copy link
Author

Choose a reason for hiding this comment

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

@cowtowncoder , thank you for suggesting this, unfortunately I do not think it can be removed entirely. This is because we would like to initialize these MethodHandle instances in the constructor, however we cannot do so because fixAccess is an instance method. So we must defer init until after fixAccess might be called. Unfortunately, there is no guarantee fixAccess is called at all - it might be disabled - leaving no natural place to actually initialize the MethodHandle.

So, I think lazily setting it up once at time of use is the least bad path for now. This is why I think it would be nice if the access-fixing could happen at constructor time, rather than a virtual method call.

I pushed an update to remove Serializable, at least, since that is no longer needed.

Copy link
Author

Choose a reason for hiding this comment

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

Oh, I see, you are literally referring to the implements Serializable. So yes, I removed that 😄

@cowtowncoder
Copy link
Member

@stevenschlansker Hoping to merge this in right before 3.0.0-rc4 -- anything you want to change before merging? (we can still do follow-ups of course)

@stevenschlansker
Copy link
Author

I can make the changes to remove the unneeded fields today.

@cowtowncoder
Copy link
Member

I can make the changes to remove the unneeded fields today.

That would be great -- I hope to get rc4 out asap, to help with Spring Boot folks' using it for their preview builds.

@stevenschlansker
Copy link
Author

Unfortunately I think further simplifications here require refactoring the fixAccess method. So I think this is good to go.

@cowtowncoder
Copy link
Member

Unfortunately I think further simplifications here require refactoring the fixAccess method. So I think this is good to go.

Ok, we can do that. I am open to suggestion for changes -- I think fixAccess() requires access to MapperConfig (for MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS) but I assume we would want forcing access to occur at the point where MethodProperty is being constructed, by caller. Or if for some reason makes more sense, in constructor itself.

I'll do one last perf run to see there's no unforeseen changes and will likely merge this PR then.

@cowtowncoder
Copy link
Member

Ok no performance difference to observe for JSON read/write, vanilla/afterburner.

Looking at added processing for accessing MethodHandle I am half surprised there's no actual slow down. :)
But we definitely will want to remove lazy construction, wrapping and so on ASAP.
(but after RC4 probably).

I am thinking of doing release tomorrow, for what that is worth: but will no merge this PR.

@cowtowncoder cowtowncoder merged commit dc10783 into FasterXML:3.x May 9, 2025
6 checks passed
@cowtowncoder
Copy link
Member

cowtowncoder commented May 9, 2025

@stevenschlansker Things look better but 2 modules have failures: Kotlin and Scala. All may be due to same root cause it looks like.

For Kotlin, 4 test errors (https://github.com/FasterXML/jackson-module-kotlin/actions/runs/14920232278/job/41914075112) all with something like

tools.jackson.databind.exc.InvalidDefinitionException: 
No fallback setter/field defined for creator property 'id' (of `tools.jackson.module.kotlin.test.github.TestGithub194$WithIdentityAndDefaultId`)
 at [No location information]
	at tools.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:70)
	at tools.jackson.databind.deser.CreatorProperty._reportMissingSetter(CreatorProperty.java:305)

and for Scala, 8 failures (https://github.com/FasterXML/jackson-module-scala/actions/runs/14920234484/job/41914081698) with similar looking exception

tools.jackson.databind.exc.InvalidDefinitionException: No fallback setter/field defined for creator property 'field1' (of `tools.jackson.module.scala.deser.ClassWithMutableMaps`)
[info]  at [No location information] (through reference chain: tools.jackson.module.scala.deser.ClassWithMutableMaps["field1"])
[info]   at tools.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:70)
[info]   at tools.jackson.databind.deser.CreatorProperty._reportMissingSetter(CreatorProperty.java:305)

whether these are bad tests (relying on something they shouldn't) I don't know.

But need to figure it out quite soon... :-/

@stevenschlansker
Copy link
Author

Sorry :( it's too late for me to look tonight, but I can look tomorrow morning

@cowtowncoder
Copy link
Member

No problem & sorry for bothering you. I hope this is something easy enough to fix, one way or another.

@stevenschlansker
Copy link
Author

My bad for breaking it! I do think this highlights how useful a CI build would be - I could have had weeks with this MR open to diagnose it instead of discovering right as you want to build a release :)

We had a bit of a meltdown with the commute over here (subway system totally shut down) but I'm in the office now and can look at this soon.

@cowtowncoder
Copy link
Member

My bad for breaking it! I do think this highlights how useful a CI build would be - I could have had weeks with this MR open to diagnose it instead of discovering right as you want to build a release :)

We had a bit of a meltdown with the commute over here (subway system totally shut down) but I'm in the office now and can look at this soon.

Yeah we do have CI but only after merge to 2.x and 3.x, not before that (nor for PRs)
And although I do get notifications, person whose PR was merged (like you) won't, I think.
For me notifications have some delay (within 15 minutes from merge) which is not ideal either.
It is still better than before (year or more ago) when it'd be anywhere from a day to month before breakages were noticed, but certainly not optimal.

So I'd love to have one that worked for PRs, but probably that would need to be bit more limited as it'd need to work different from cascading builds: basically databind that ./mvnw installs and then clones couple of most important/likely to break other repos, builds them (jackson-modules-base, jackson-module-kotlin, jackson-module-scala).
And to do that, would need to know details about building and so on (most notably Scala module using sbt).
Or at least this is one way I could think it works. But others have solved similar problems before.

stevenschlansker added a commit to stevenschlansker/jackson-databind that referenced this pull request May 9, 2025
@stevenschlansker
Copy link
Author

Ok I think I see the problem @cowtowncoder . The Kotlin and Scala data class support expects to be able to set fields after construction despite being final.

Please consider #5150 (preferably while rocking out to Van Halen )

cowtowncoder pushed a commit that referenced this pull request May 9, 2025
@stevenschlansker stevenschlansker deleted the methodhandle-hacking branch May 9, 2025 23:20
@cowtowncoder
Copy link
Member

Now that 3.0.0-rc4 is released I hope we get some actual testing, feedback etc.

sdeleuze added a commit to sdeleuze/spring-framework that referenced this pull request May 12, 2025
This commits introduces Jackson 3 support which uses a different package
than Jackson 2 (com.fasterxml.jackson -> tools.jackson) except for
jackson-annotation (@JSONVIEW, @JsonTypeInfo, etc.).

TODO:
- Deprecate for removal (in Spring Framework 7.2) Jackson 2 support
  (autodetection will be disabled in 7.1)
- Add support for hints in RestClient to support @JSONVIEW
- Check native support, especially against the new usage of method handles,
  see FasterXML/jackson-databind#5090

General:
 - No Jackson 3 based Jackson2ObjectMapperBuilder, just use
   JsonMapper.builder(), CBORMapper.builder(), etc.
 - As of 3.0.0-rc4, Jackson 3 default are aligned to Spring ones
 - By default, cached registration of Jackson modules using service
   loaders via MapperBuilder#findModules(java.lang.ClassLoader)
   (implemented in JacksonCodecSupport and AbstractJacksonHttpMessageConverter)
 - org.springframework.http.converter.json.SpringHandlerInstantiator in
   spring-web enabled by default in Spring MVC ->
   org.springframework.binding.jackson.AutowiredHandlerInstantiator in
   spring-context-support requiring explicit configuration
 - Jackson 3 support is configured if found in the classpath otherwise
   fallback to Jackson 2
 - No more invocation of ObjectMapper#canDeserialize or
   ObjectMapper#canSerialize as they have been removed without replacement,
   Spring converters and codecs just test mime types. Lead to better
   performance and looks ok since Jackson is a "catch all" codec/converter.

WebMVC:
- Switch from GenericHttpMessageConverter to SmartHttpMessageConverter
- Use hints instead of MappingJacksonValue and MappingJacksonInputMessage
- Introduction of RequestBodyAdvice#determineReadHints and
  ResponseBodyAdvice#determineWriteHints
- JsonViewRequestBodyAdvice and JsonViewResponseBodyAdvice refined to support
  hints when Jackson 3 is used
- org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter ->
  org.springframework.http.converter.AbstractJacksonHttpMessageConverter
- MappingJackson2HttpMessageConverter -> JacksonJsonHttpMessageConverter
	- MappingJackson2SmileHttpMessageConverter -> JacksonSmileHttpMessageConverter
	- MappingJackson2CborHttpMessageConverter -> JacksonCborHttpMessageConverter
	- MappingJackson2XmlHttpMessageConverter -> JacksonXmlHttpMessageConverter
	- MappingJackson2YamlHttpMessageConverter -> JacksonYamlHttpMessageConverter

WebFlux:
 - org.springframework.http.codec.json.Jackson2CodecSupport ->
   org.springframework.http.codec.JacksonCodecSupport
 - org.springframework.http.codec.json.Jackson2Tokenizer ->
   org.springframework.http.codec.JacksonTokenizer
 - org.springframework.http.codec.json.Jackson2SmileDecoder ->
   org.springframework.http.codec.smile.JacksonSmileDecoder
 - org.springframework.http.codec.json.Jackson2SmileEncoder ->
   org.springframework.http.codec.smile.JacksonSmileEncoder
 - Jackson2JsonDecoder -> JacksonJsonDecoder
 - Jackson2JsonEncoder -> JacksonJsonEncoder

Messaging:
- MappingJackson2MessageConverter -> JacksonJsonMessageConverter
sdeleuze added a commit to sdeleuze/spring-framework that referenced this pull request May 12, 2025
This commits introduces Jackson 3 support which uses a different package
than Jackson 2 (com.fasterxml.jackson -> tools.jackson) except for
jackson-annotation (`@JsonView`, `@JsonTypeInfo`, etc.).

TODO:
- Deprecate for removal (in Spring Framework 7.2) Jackson 2 support
  (autodetection will be disabled in 7.1)
- Add support for hints in RestClient to support `@JsonView`
- Check native support, especially against the new usage of method handles,
  see FasterXML/jackson-databind#5090
- Register ProblemDetailJacksonXmlMixin automatically

General:
 - No Jackson 3 equivalent of Jackson2ObjectMapperBuilder, just use
   JsonMapper.builder(), CBORMapper.builder(), etc.
 - As of 3.0.0-rc4, Jackson 3 default are aligned to Spring ones
 - By default, registration of cached Jackson modules using service
   loaders via MapperBuilder#findModules(java.lang.ClassLoader)
   (implemented in JacksonCodecSupport and AbstractJacksonHttpMessageConverter)
 - org.springframework.http.converter.json.SpringHandlerInstantiator in
   spring-web enabled by default in Spring MVC ->
   org.springframework.binding.jackson.AutowiredHandlerInstantiator in
   spring-context-support requiring explicit configuration
 - Jackson 3 support is configured if found in the classpath otherwise
   fallback to Jackson 2
 - No more invocation of ObjectMapper#canDeserialize or
   ObjectMapper#canSerialize as they have been removed without replacement,
   Spring converters and codecs just test mime types. Lead to better
   performance and looks ok since Jackson is a "catch all" codec/converter.
 - Slashes are now escaped by default (impact ProblemDetail)

WebMVC:
- Switch from GenericHttpMessageConverter to SmartHttpMessageConverter
- Use hints instead of MappingJacksonValue and MappingJacksonInputMessage
- Introduction of RequestBodyAdvice#determineReadHints and
  ResponseBodyAdvice#determineWriteHints
- JsonViewRequestBodyAdvice and JsonViewResponseBodyAdvice refined to support
  hints when Jackson 3 is used
- org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter ->
  org.springframework.http.converter.AbstractJacksonHttpMessageConverter
- MappingJackson2HttpMessageConverter -> JacksonJsonHttpMessageConverter
	- MappingJackson2SmileHttpMessageConverter -> JacksonSmileHttpMessageConverter
	- MappingJackson2CborHttpMessageConverter -> JacksonCborHttpMessageConverter
	- MappingJackson2XmlHttpMessageConverter -> JacksonXmlHttpMessageConverter
	- MappingJackson2YamlHttpMessageConverter -> JacksonYamlHttpMessageConverter

WebFlux:
 - org.springframework.http.codec.json.Jackson2CodecSupport ->
   org.springframework.http.codec.JacksonCodecSupport
 - org.springframework.http.codec.json.Jackson2Tokenizer ->
   org.springframework.http.codec.JacksonTokenizer
 - org.springframework.http.codec.json.Jackson2SmileDecoder ->
   org.springframework.http.codec.smile.JacksonSmileDecoder
 - org.springframework.http.codec.json.Jackson2SmileEncoder ->
   org.springframework.http.codec.smile.JacksonSmileEncoder
 - Jackson2JsonDecoder -> JacksonJsonDecoder
 - Jackson2JsonEncoder -> JacksonJsonEncoder

Messaging:
- MappingJackson2MessageConverter -> JacksonJsonMessageConverter

Tests:
 - Switch to using Jackson 3 instead of Jackson 2
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.

2 participants