Added class loader option to the project#4159
Added class loader option to the project#4159ThomasWega wants to merge 7 commits intoMorphiaOrg:2.5.xfrom
Conversation
evanchooly
left a comment
There was a problem hiding this comment.
Thanks for the PR and the detailed write-up — the motivation is solid. Environments like Minecraft Paper plugins (and OSGi, etc.) where the context classloader doesn't match the application classloader are a real pain point, and Morphia should support custom classloaders.
That said, I found several issues with the current implementation that would need to be addressed before this can be merged.
Critical Issues
1. MorphiaConfigHelper reflection bug — will crash at runtime
The change in MorphiaConfigHelper.getEntry():
.getDeclaredConstructor(morphiaConfig.getClass())
.newInstance(morphiaConfig);morphiaConfig.getClass() returns the runtime class (e.g., ManualMorphiaConfig), but all the converters declare their constructor parameter as MorphiaConfig (the interface). So getDeclaredConstructor(ManualMorphiaConfig.class) will throw NoSuchMethodException. This should be getDeclaredConstructor(MorphiaConfig.class).
2. SmallRye config loading is broken
The converters (ClassNameConverter, DiscriminatorFunctionConverter, NamingStrategyConverter, etc.) are referenced via @WithConverter annotations and instantiated by SmallRye Config using no-arg constructors. This PR removes all no-arg constructors by adding a required MorphiaConfig parameter. SmallRye won't be able to instantiate them, which would break MorphiaConfig.load() entirely.
3. Circular dependency in config loading
MorphiaConfig.load(path, classLoader) needs to create a config, but the converters used during loading now require a MorphiaConfig instance that doesn't exist yet — chicken-and-egg problem.
4. classLoader() on the MorphiaConfig interface is incompatible with SmallRye
MorphiaConfig is a @ConfigMapping interface. Adding ClassLoader classLoader() without @WithDefault means SmallRye would try to map it from the config file, but ClassLoader isn't a serializable config value. This needs to be excluded from SmallRye mapping or handled differently.
Design Issues
5. getClassLoader() on public Datastore interface
This is added without @MorphiaInternal, making it part of the public API contract. This is an implementation detail that shouldn't be exposed publicly — users shouldn't need to call datastore.getClassLoader().
6. DiscriminatorLookup holds a full Mapper reference
This introduces a bidirectional dependency: Mapper → DiscriminatorLookup → Mapper. It would be cleaner to pass just the ClassLoader or a Supplier<ClassLoader> rather than the entire Mapper.
7. Incomplete implementation (the FIXME)
As you noted, Conversions.java still uses the old classloader:
// FIXME still old classloader!
return Thread.currentThread().getContextClassLoader().loadClass(className);8. ManualMorphiaConfig.classLoader() default is evaluated lazily each call
return orDefault(classLoader, Thread.currentThread().getContextClassLoader());Each call could return a different classloader depending on which thread calls it. The default should be captured once at construction time.
Suggested Approach
Rather than threading MorphiaConfig through SmallRye converters (which breaks config loading), consider setting the classloader on the config after loading and using it only at runtime — not during config parsing. The converters don't need a custom classloader during config load since Morphia's own classes are being resolved at that point. The custom classloader is primarily needed at runtime for discriminator lookup, entity scanning, reference resolution, and proxy creation.
|
Hey. Thank you for the feedback, it's deeply appreciated. Before I start digging into it, is it okay that I'm doing this on the 2.5 branch, or will I need to do these changes on the master branch for it to get merged? As I was saying, I'm not able to get the master branch working properly on any of my PCs. |
This reverts commit d0e18df.
|
I have reverted the previous commit and went with a new approach. I think it's now much better and you'll like it more. Should not disrupt the other parts of the code that much. I will be looking into the Conversions class now. That one will probably need quite a lot of changes for it's usages, so that's why I want to do it in a separate commit |
|
sounds good. those comments were claude's (i'm not that verbose) but i was working on my own response as well. i'll wait for the follow up changes, though. :) |
c1adc84 to
93832c6
Compare
|
I suppose ready for review now. I've tried to keep it as clean and simple. Everything that's not super internal should be backwards compatible. Just added new methods with new parameters and kept the old ones. All usages of Let me know if there's anything more you need me to do :) |
evanchooly
left a comment
There was a problem hiding this comment.
Overall, I like the direction. There have been a few "using the wrong classloader" bugs "recently." They're hard to suss out because they require operating in contexts I don't have access to (glassfish/wildfly apps, minecraft plugins, etc.) and this change would help close some of those gaps I think we've gotten lucky so far to have not run in to. There are some simplifications in the comments that I think might serve to shrink this PR down even further. if I get time before you get to them, i'll see about pushing a patch to this PR showing what i'm thinking. But if you beat me to it, I won't complain either. Thanks for all the work you've put in so far. This revision is definitely cleaner than the first one.
| * @param classLoader the classloader to use when loading resources from the classpath | ||
| * @return the loaded config | ||
| */ | ||
| static MorphiaConfig load(ClassLoader classLoader) { |
There was a problem hiding this comment.
Adding new methods to an API in a patch release is verboten by semver. I'm trying to decide how much semver matters to me with the 2.5. On the one hand, semver is important. On the other hand, 3.0 is much slower in coming than I'd hoped (though I'm actually closer than I thought to what might be considered a beta or RC if I downscope a little and push things to a 3.1...) and adhering to such a rule seems a little counterproductive in that light. Maybe make a 2.6 out of all this and satisfy both? That gets close to too many active branches, though, so maybe mark 2.4 and 2.5 as "done" and any future fixes go in to 2.6 alone? Or maybe just 2.4 and 2.6 since I think 2.5 is mildly breaking because of the driver upgrade and some internal incompatibilities... Thoughts?
There was a problem hiding this comment.
I personally don't use this method and for anyone who's having issue with classloaders he can get around using config files as well just by using the ManualMorphiaConfig.
I'm inclining more towards 2.6 release myself, however I don't know how big the previous releases were to jump up a minor release version, but reading the definition, I think this PR suits perfectly for 2.6 (MINOR release). Depends whether you call this a new functionality or a bug fix I guess :D
MAJOR version when you make incompatible API changes
MINOR version when you add functionality in a backward compatible manner
PATCH version when you make backward compatible bug fixes
| * @since 3.0 | ||
| */ | ||
| static MorphiaConfig load(String path, ClassLoader classLoader) { | ||
| List<ConfigSource> configSources = classPathSources(path, classLoader); |
There was a problem hiding this comment.
Same comment on semver here but one additional. I would add a classLoader(ClassLoader) method to MorphiaConfig (if we're ditching semver 👎🏻 or bumping to 2.6 👍🏻 ) and then pass this class loader to the config once it's constructed.) Something similar used to exist for MapperOptions but was there primarily for the play framework which effectively doesn't exist anymore (for most of us anyway...).
There was a problem hiding this comment.
You are saying we should input classloader once the morphia config is constructed, but I've added the classloader here so it would even find the config class in the first place. Cuz in my specific environment if I went ahead and used a config file, I'd need to input the classloader just for morphia to be able to even find the file inside the plugin's resources
Wouldn't your proposal break this if we need to first initialize the morphia config and then set the classloader? How will morphia know which class loader to use to look for the file?
| private final PropertyModel idProperty; | ||
| private final PropertyModel versionProperty; | ||
| private final List<EntityListener<?>> listeners = new ArrayList<>(); | ||
| private final ClassLoader classLoader; |
There was a problem hiding this comment.
Not sure that is strictly necessary. Or that I like it. EntityModels should be pretty agnostic about such state. When I can finally implement multi tenancy this gets much more important.
There was a problem hiding this comment.
The current usages for it is LifecycleDecoder, ConstructorCreator and PropertyModel. For LifecycleDecoder I can switch to get the classloader from the mapper getter or from datastore getter (which just uses the mapper getter anyways). For the ConstructorCreator and PropertyModel there's no way for me to get the classloader there though. For PropertyModel I guess I could add it into the constructor and give it the classloader inside in the build method of PropertyModelBuilder, but not sure how you feel about that.
For the ConstructorCreator I'm not quite sure
| return type; | ||
| } | ||
|
|
||
| public Mapper mapper() { |
There was a problem hiding this comment.
a builder shouldn't need this reference. when the builder still exists, that context should have access to the mapper.
There was a problem hiding this comment.
Same answer as above.
"I guess I could add it into the constructor and give it the classloader inside in the build method, but not sure how you feel about that."
| if (idProperty != null) { | ||
| if (ObjectId.class.equals(idProperty.getType()) || String.class.equals(idProperty.getType())) { | ||
| idProperty.setValue(entity, convert(new ObjectId(), idProperty.getType())); | ||
| idProperty.setValue(entity, convert(new ObjectId(), idProperty.getType(), datastore.getClassLoader())); |
There was a problem hiding this comment.
same as above. Pretty sure I've removed getClassLoader() from Datastore once already. :)
There was a problem hiding this comment.
The datastore only uses the mapper getter. I guess I could remove it from the datastore and always get mapper and then the classloader from there, but I don't see a reason to do that when I can have this "shortcut"
| return newArray; | ||
| } | ||
| return Conversions.convert(o, type); | ||
| return Conversions.convert(o, type, classLoader); |
There was a problem hiding this comment.
Maybe a clean approach would be to make Conversions.convert() not static and create an instance of it from the classloader configured on MorphiaConfig. then the convert method signature doesn't really change and the interesting parts of the API that are nominally available for non-Morphia usage stays smaller (a partial focus of 3.0 is paring down the API surface area. so many overloads....) Then we just pass in the Conversions instance (stored on the Datastore?) when we create these kinds of classes. It reduce the amount of classloader parameters being passed from class to class, i think.
| .next(); | ||
|
|
||
| refreshCodec.decode(new DocumentReader(id), DecoderContext.builder().checkedDiscriminator(true).build()); | ||
| refreshCodec.decode(new DocumentReader(id, getClassLoader()), DecoderContext.builder().checkedDiscriminator(true).build()); |
There was a problem hiding this comment.
this argument would be the Conversions instance field we'd add to the DatastoreImpl, e.g.
| * @return a Datastore that you can use to interact with MongoDB | ||
| * @since 3.0.0 | ||
| */ | ||
| public static Datastore createDatastore(MongoClient mongoClient, MorphiaConfig config, ClassLoader classLoader) { |
There was a problem hiding this comment.
This wouldn't be necessary. We'd just use config.classLoader() inside DatastoreImpl to get what we need.
|
I removed the Datastore#getClassLoader for you and converted the usages. For the rest I'm not sure. Seems like you'll need to give me direct directions. I personally don't understand how you want to init morphia config from the file without knowing the classloader first. Maybe if you remove/add the methods/fields you want I can update the usages for you, but I most likely need you to help with the structure you desire |
|
I have some changes in my laptop at home |
Great. Thank you. I'll be here. Just let me know when you need any help |
|
um. this PR was supposed to be based on your branch but claude messed up. i can give it another go or if you like the changes, doing a merge in to your branch would be trivial as well. apologies for the delay. |
|
Hey. I think the changes look good, I like the approach. I understand a what you meant a bit better now, thank you. I'll test it on my project and let you know if it all works, although my testing environment is quite limited. In the meantime, is there anything else you need me to do? In this or the other PR? If you merge the one PR you've created or this one, I don't really mind either. I'd just like at least some of my contribution to be visible, that's all. |
|
I'm also very keen that your contributions remain visible. If it's not a lot of work for you, what if you merged my branch in to your PR and let's see how the record/history looks like that? I'm afraid if I do it on my end, it will trample the git blame and your name will get lost in the shuffle which I really don't want. If it's too much work, I can give it a shot, though. Maybe there's some to tell git "i know I'm committing these changes but credit should to this other username." I'll dig in to that in the meantime while I wait to see how you want to proceed. |
Hey. I'm for sure okay with merging your PR to this branch. Just a bit worried cause there's some commits that your PR does not have. Not worried about the history, but wouldn't that cause an issue with the merge? Should I proceed with that? |
Found this issue out when creating a Paper (Minecraft) plugin. Minecraft plugins each have their own ClassLoader. This makes it impossible for me to map classes as the class loader morphia is using is just a different one.
There was a workaround in earlier version as I was able to find here
https://stackoverflow.com/a/63698526
Even a suggested feature here: #1394
If I'm not mistaken, that's no longer the case. This pretty recent issue on github was a similar thing, but it suggested to use Thread.currentThread().getContextClassLoader() which 2.5.2 does I think, however that just does not solve my issue as you can see from this screenshot.

The classloader is just different.
I've tried adding the option to input custom classloader in the project. Take this more as a draft than a finished thing. I'm looking for feedback and will make any change you request. There is also still one place that uses the old classloader which I've marked with FIXME. I'm quite honestly not sure if all places require to use the custom class loader.
I have tested this version on my server with my project and it seems to work completely fine. It fixed the previous issues where no classes could be mapped and when they were mapped manually they would not resolve when being taken from the database.
I also would've done this change for 3.0.0 but I just wasn't able to get it to work with my maven. It's using the maven 4 which is in preview and seems like my PC is having issues loading it even though I've upgraded.
I'm also available on the discord server as
ThomasWegaif you want to discuss anything there