So far, a mechanism is still missing to create the complete definitive TypeDictionary for using MicroStream as a means for communication.
Currently, in the proof-of-concept example, the TypeDictionary is defined manually via an API call (ComBinary.Foundation().registerEntityTypes(A.class, B.class, ... );).
This works, but is not a practical solution. No one will manually compile a complete definitive list of all required entity types, collection types, enum types, nested whatsoever classes that will ever be encountered by the application's communication logic.
The database usage has a much simpler situation when it comes to defninig relevant (allowed) entity types:
The application is the only participant in the process of persisting and restoring data. Whatever new classes it encounteres during persisting instances, it can be 100% sure that it itself has all its classes. So it is perfectly sufficient to analyze types and extend the type dictionary dynamically while storing data.
In a communication situation, this is not the case. Every new class encountered poses the question or challenge, if the other peer will be able to understand it. Either in the form of a directly suitable class or in the form of a sufficient mapping to some other class. If the type dictionary was extended dynamically here, there might sooner or later be a class (type dictionary entry) that is not handleable by the communication peer. But then, it might already be too late, given that a lot of communication will have happened already until then. Application state, maybe even persistent database state, will have been changed.
To avoid this, it is preferable, if not mandatory, to synchronize the participating classes at the time the connection is established to make sure none of them will turn out to be a problem.
Sadly, there is no good way to collect the necessary classes in a sufficient, efficient and convenient way.
First of all, there is no "iterate all classes" functionality in Java. Classes are loaded from the class path on demand when the first occurance of a non existing class is encountered. So it simply cannot be done to iterate all classes of the application and let some logic (annotation-based or whatever) decide which ones are deemed suitable / desired / allowed / designed for communication.
There can be no "dry run" or such of the application logic that produces an entity graph containing ALL required classes. No one would take the time to develop it and even if someone did, there would be no guarantee it would be really complete.
One half working solution is to iterate all classes in all libraries on the class path and decide on their relevance for communication.
But this brings with it multiple problems:
- it would not be a good idea to just load ALL classes into the JVM just to do the check. Performance-wise, memory-wise and maybe even application-state-consistency-wise (static initializers doing unpredictable stuff etc.)
- so the decision logic would have to be based on the class (the .class file's) full qualified name itself. This might work well for some (e.g. all "com.my.app.entites.*" classes are allowed) but not at all for others (e.g. is com.third.party.library.internal.processing.Processor$Part$Entry relevant for communication or not? No one knows until it actually shows up in a reference...).
- the jigsaw module system might deny access to loading classes.
- dynamic class loading might happen after the type dictionary has been compiled. Then it would have to be dynamically extended yet again.
Some sophisticated tool, maybe in the form of an IDE plugin, would be conceivable, but that still would not really solve the problem of how to efficiently determine a complete definitive list of all relevant classes.
This situation leads to the following conclusion:
The option of having a preemptively complete definitive type dictionary is good (e.g. for security reasons), but it cannot be the only strategy.
Another strategy that dynamically extends a type dictionary, even during an ongoing communication, is required.
In the end, any potential problem (exception) that can arise from that is no different to any other potential exception: Somewhere down the line, something can go wrong and the application logic must be designed robust enough to prevent its internal state getting inconsistent by that.
For communication, that means: Yes, at some unknown point during communication and after sending an arbitrary amount of messages, the communication layer can encounter an incompatibility problem, causing a message to not being processable, maybe even terminating the whole connection.
As a consequence, there must be the following strategies regarding dynamic type analysis during communiction:
1.) None
All viable types are preemptively defined by a (host-side) type dictionary and there can be no extensions.
This might still be the best solution for many applications, e.g. by keeping and using a dynamically created type dictionary during testing.
2.) Host-Only
The host will dynamically extend its type dictionary as needed and notify the client(s) on every new type for a corresponding type synchronization.
The client is only allowed to use types defined in the type dictionary.
3.) All
All participants are allowed to extend the type dictionary upon encountering a previously not contained class.
More precisely, clients request extending the type dictionary by a specific class at the host, which in turn then does validation and analysis and then, on success, sends a type dictionary extension notification to the client, similar to # 2.
The notifying and requesting logic necessary for microstream-one/microstream-private#2 and microstream-one/microstream-private#3 are yet to be implemented.
[GitHub's parsing of "#" drives me crazy] # 3 can be perfectly safe for a trusted environment (e.g. a cluster running a distributed application), but for an untrusted environment (e.g. communicating with another server, a fat client or "app"), the type dictionary extension request would have to be followed by a lot of security validations, checking black lists, white lists and whatnot. This not trivial.
Consider the following example
(Inspired by this talk: https://www.youtube.com/watch?v=m1sH240pEfw)
A server might have a dynamic code interpreter on its class path.
The client requests the interpreter class to be accepted into the type dictionary.
A sent entity graph is special-tailored on the client side to contain an instance of that interpreter, an arbitrary piece of code in plain string form and a way to make that string get passed to the interpreter.
Result: arbitrary code execution.
Of course, a generic type analysis logic would not understand the intention or danger that comes with such a type dictionary extension. As long as the class is technically serializable, everything is fine.
Even some special logic patterns in the application's business logic (or one of its libraries) that make the special-tailored entity graph and code execution possible might not be design flaw in itself. Only when applying sophisticated ingeniuety that exploits certain characteristics of existing logic it all turns into being a security breach.
That might or might not be seen as a security flaw in the serialization layer itself, but surely it has to provide means to prevent that.
Not allowing "too abstract" classes in is one aspect, e.g. anonymous inner classes instances, lambdas, proxies, etc.
Not allowing known system parts (IO handling instances, ClassLoader, Thread, etc.) is another one.
Maintaining and checking black lists, white lists and such will be another aspect that is required.