The answer seems to be "it depends".
If the JAR(s) (or classes) in question do not have dependencies conflicting with other components also deployed on the server, both proposed solutions (CATALINA_HOME/lib/ext/
and CATALINA_HOME/conf/catalina.properties :: shared.loader
) should plainly work. Thus both are "correct answers" and I cannot see which one is "more correct" than the other.
However I missed a crucial detail when I first asked the question (but this does not invalidate it): In my case the JAR in question required Spring 4.2.9.RELEASE (and other dependencies), but other relevant WARs deployed on the same server contain and require Spring 3.0.7. (The objects to be cached do not depend on Spring, but the JAR was not designed with this problem in mind, and it also contains other related code depending on Spring which now would be very difficult to separate.)
Generally it should be possible to put into CATALINA_HOME/lib/ext/
what ever you want as long as all already deployed WARs contain everything they need: The "module first / parent last" class loading policy should prevent conflicts, even if (as in this example) Spring 4.2.9 is available to the parent classloader and Spring 3.0.7 is available to the WAR classloader. But it looks somewhat "unclean" and messy to me to mix-up things that way.
Therefore I decided to use the "to-be-cached" object's classloader hash code as the key in a map, in which the cached data are the values. Then all cached data is selected "by classloader" which automatically and transparently ensures assignment compatibility. If there is also another WAR deployed on the server which can change and thus invalidate the cached data, it can remove the whole map from the cache, forcing the "read-access" WARs to reload data on next access.
However this approach DOES NOT allow cross-WAR cacheing: Effectively every WAR will get its own private cache segment.
Another approach would be to deliberately transform all data to cache to/from e. g. JSON so as to get a "naturally global" data type like java.lang.String
for the cached data. If chosen from the beginning of the project, to me this seems to be the cleanest way, but if there is already a complex (and generally working) implementation in place, this may cause some work to do.
Comments on this self-answer are welcome!