[Mod-Sys Reqs] Comment on "Substitution"
Jesse Glick
jesse.glick at oracle.com
Fri Jul 15 12:15:30 PDT 2011
http://openjdk.java.net/projects/jigsaw/doc/draft-java-module-system-requirements-12#substitution is clearly necessary for the modularization of some legacy code,
including existing APIs defined in the JRE. But I think this mechanism should not be considered the recommended practice for new APIs.
Current practice in the Java platform would result in an API module like the following:
module java.util.concurrent {export java.util.concurrent.*;}
public interface ExecutorService {...}
public class ThreadPoolExecutor implements ExecutorService {
// possibly buggy or slow impl coupled to other parts of Oracle JRE...
}
public class Executors {
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(possiblyPoorDefaultBehavior);
}
}
This module mixes together an API - the public signatures and Javadoc - with a particular implementation. If you wish to use a different implementation (or an entirely
different Java impl), you would be using another module
module my.concurrent {export java.util.concurrent.*;}
public interface ExecutorService {...identical...}
public class ThreadPoolExecutor implements ExecutorService {
// same signatures, but different impl
}
public class Executors {
public static ExecutorService newCachedThreadPool() {
return ...who knows...
}
}
and then the special module system feature would permit my.concurrent to declare that it substitutes for java.util.concurrent.
This is workable, but awkward. How do you verify that my.concurrent and java.util.concurrent have precisely the same public signatures? Some sort of special tool could be
employed to do so, but does the module system double-check this at installation time? Should the author of my.concurrent copy Javadoc comments from OpenJDK sources? Which
module should a client declare when compiling against this API? If the API is evolved over time, so that java.util.concurrent 1.1 introduces some new methods, does
my.concurrent need to declare that it substitutes for some version range of java.util.concurrent?
Now suppose most parts of a big modular application were originally tested and tuned against java.util.concurrent, but others rely on obscure bug fixes specific to
my.concurrent, so you wish to load both implementations in the app. If there is anywhere in the app's common infrastructure APIs that refers to ExecutorService, you may
be in trouble - the infrastructure will load java.util.concurrent's copy of the interface, but then some parts of the app would not be able to link against it, even
though they are able to call my.concurrent's version of Executors.newCachedThreadPool for their own purposes.
Furthermore, suppose some of your unit tests for code compiled against java.util.concurrent wish to create a mock ExecutorService that runs all its tasks synchronously,
or in lockstep with instructions given from the test, so as to behave deterministically. They can do so only if the tested code refrains from constructing an
ExecutorService itself.
This is all a mess. The alternative is to declare the API using only interfaces:
module java.util.concurrent {
export java.util.concurrent.*;
requires service java.util.concurrent.Executors;
}
public interface ExecutorService {...}
public interface ThreadPoolExecutorBuilder {
ThreadPoolExecutorBuilder threadFactory(ThreadFactory);
ThreadPoolExecutorBuilder corePoolSize(int);
interface ThreadPoolExecutorHooks {
void beforeExecute(Thread, Runnable);
void afterExecute(Runnable, Throwable t);
void terminated();
}
ThreadPoolExecutorBuilder hooks(ThreadPoolExecutorHooks);
ExecutorService build();
}
public interface Executors {
ThreadPoolExecutorBuilder threadPoolExecutorBuilder();
}
This actual module, in a particular version and with authoritative Javadoc, with the most liberal possible license, can be published by the JSR spec lead as the API and
used as a compilation dependency by all clients; no special module system directive is needed for that. The Oracle JRE would also include
module sun.java.util.concurrent {
require java.util.concurrent;
exports service java.util.concurrent.Executors class SunExecutors;
}
class SunExecutors implements Executors {
// the typical impl
}
and similarly for my.concurrent, which now need do nothing special to ensure that it complies with the API signature other than be compilable. (It is also impossible to
accidentally expose "bonus" APIs from my.concurrent, since it exports no types.) Client code would use a service locator or dependency injection rather than constructors
or static methods:
module my.stuff {require java.util.concurrent;}
class HeavyComputation {
@Inject Executors executors; // see previous mail
public void startRunning() {
executors.threadPoolExecutorBuilder().build().submit(....);
}
}
The definition of ExecutorService can be shared among all modules in the app, even if some of them use different implementations of Executors internally (say, using
@Qualifier). HeavyComputationTest can easily insert a MockExecutors which uses some sort of cooperative multitasking rather than actual threads.
More information about the jigsaw-dev
mailing list