Optional Module

When a module optionally requires some other module, the second module is referred to as the optional module. If the optional module is available then it is resolved and, linked normally. If it is not available, however, then no exception is thrown and no error is reported. This allows a module to include optional functionality that requires the optional module(s) but the module can still be used in the absence of the optional module. See the optional module requirement.

For example, the java.util.Properties class provides the loadFromXML and storeToXML methods to load and store properties in a simple XML format. The implementation of these two methods depends on the JAXP API (XML module) while the rest of the Properties class does not require XML module to be present. To declare XML module as an optional module, this enables the Properties class be used in the absence of XML module when these two methods are not referenced.

module-info Declaration

The optional flag in the require statement indicates that it is an optional module.
   module P {
       require optional M;
   }
Module P can be used in the absence of module M if the functionality requiring module M is not used. The implementation must be carefully crafted to make sure that there is no reference to any type in module M when the optional functionality is not used at runtime and no type in module M will be loaded at class loading time (see the guideline of using optional module section below).

Proposed APIs

1. Test if a module is present

   package java.lang.reflect;
   public class Module {
       /**
        * Tests if a module of the given module name
        * has been resolved and linked with this module's context.
        *
        * @param mn a module's name
        */
       public boolean isModulePresent(String mn);

       /**
        * Checks if a module of the given module name
        * has been resolved and linked with this module's context.
        *
        * @param mn a module's name
        * @throws ModuleNotPresentException if the module of the
        * given name is not present in this module's context.
        */
       public boolean requireModulePresent(String mn);
   }

To access any class from a module, that module must be visible from the context of the caller's module. A module M that is present from the context of module P isn't necessarily present from the context of another module loaded by the running application. The isModulePresent method does not throw ModuleNotPresentException and can be used in cases where there is a fall-back when the optional module is not present.

2. ModuleNotPresentException

When the optional functionality is used but its required module is absent, ClassNotFoundException will be thrown when any class in the optional module is referenced. It would be helpful to throw a well-defined exception, ModuleNotPresentException that indicates that this functionality requires the optional module which is not present.
   package java.lang.module;
   
   /**
    * Thrown when a module is not present in the context of another module.
    */
   public class ModuleNotPresentException extends RuntimeException {
       ...
   }
Typically, an optional functionality should first check if a specific module is present using the requireModulePresent method; if it's absent, a ModuleNotPresentException as in the "foo" module example.

3. @RequireOptionalModule Annotation

@RequireOptionalModule annotation can be used to annotate functionality (method or class) that requires an optional module. This annotation is intended for developer tools such as javadoc and javac to determine the sources of optional dependencies.

For example, p.P.callIfMIsPresent is the only method in module P that requires module M. If M is not installed, module P can be used as long as the callIfMIsPresent method is not called at runtime.

   package p;
   public Class P {
       @RequireOptionalModule(modules={"M"})
       public void callIfMIsPresent() {
           this.class.getModule().requireModulePresent("M");
           ...
       } 
   }

JDK Optional Dependenceies

There are different kinds of optional dependencies.

1. APIs that throw ModuleNotPresentException

These methods will throw ModuleNotPresentException if its optional dependency is not satisfied. These methods are annotated with @RequireOptionalModule annotation.
  1. java.util.Properties.loadFromXML and storeToXML methods
  2. java.util.prefs.Preferences.importPreferences, java.util.prefs.AbstractPreferences.exportNode and exportSubtree methods
  3. SecurityManager.checkAwtEventQueueAccess, checkSystemClipboardAccess, checkTopLevelWindow
TODO: The JMX RMI connector using RMI-IIOP protocol requires CORBA/RMI-IIOP when the protocol part of the given JMXServiceURL is "iiop".

javax.management.remote.JMXConnector.connect() and/or RMIConnector.connect should be updated to throw ModuleNotPresentException instead. If the application uses the "iiop" protocol but is not configured properly (the corba module is absent), currently IOException is thrown.

2. Optional dependencies satisfied by its input arguments

The following APIs do not require the optional module to be present unless its input arguments specify it, in which case the optional module must be present to construct the arguments.
  1. The JMX Monitor API is specified to use the Java Beans introspector for complex types other than CompositeData or arrays. java.beans.Introspector is only used when the input attribute for monitoring is a Java Beans with a custom java.beans.BeanInfo. In which case, java.beans classes are guaranteed to be present.
  2. An applet can specify the JNDI properties as applet's parameter. In which case, the applet will set the javax.naming.Context.APPLET environment property to the applet's instance and JNDI will call the java.applet.Applet.getParameter method for constructing the InitialContext.
  3. java.text.Bidi(AttributedCharacterIterator) constructor allows to run through an input paragraph of text with optional attributes whose key is an java.awt.TextAttribute instance. These attribute keys represent its base direction, bidi embedding level, and whether to convert European digits to other Unicode digit shape. (RUN_DIRECTION, BIDI_EMBEDDING, NUMERIC_SHAPING).
When these APIs are called under the circumstances described above, the "desktop" module is guaranteed to be present for constructing the arguments and the application should be configured properly. The module for these API will need to declare the optional dependency in its module-info but these APIs are not annotated with @RequireOptionalModule annotation as they can be called when the optional module is absent.

3. APIs that fall back to the default implementation when the optional module is absent

ResourceBundle is a good example that falls back to use the default locale if the resource bundle for a locale is packaged in a different module. Security provider is another example where the default provider and the interface are in one module whereas other provider implementation is in another module. This kind of optional dependencies can potentially be adjusted to use the services mechanism.

These APIs are not annotated with @RequireOptionalModule annotation as they can be called when the optional module is absent.

Guideline of Using Optional Module

This section is intended to illustrate the class loading issue with optional module that should be used with caution and to serve as a starting point for further discussion of the best coding practice.

When module P optionally requires module M, module P's implementor must make sure that no type from module M will be loaded at runtime when the optional functionality is not used. A simple API will be provided so that running code can test whether any particular optional module has been linked in so that any reference to the classes in the optional module is conditional.

Besides direct reference to a type in the code (either symbolic reference or via reflection), there are other events that will cause other classes being loaded:

  1. linking a class that has a direct superclass or a direct superinterface
  2. bytecode verification that may load additional classes for performing verification
  3. serialization and deserialization
  4. reflection that may cause loading of some other referenced classes depending on the implementation

To use optional module, the code must be carefully written to avoid classes in the optional modules from being loaded in these events. One simple rule is not to reference any optional type in the class declaration (e.g. superclass or superinterface), the parameter types or return type in any method, and field types. To avoid optional types being loaded for bytecode verification, it is not impossible to reference to the optional types statically but non-trivial. It is recommended not to do symbolic reference to optional type or isolate all uses of optional types in a helper class that such helper class is loaded via reflection.

Optional Module Example

For example, a "foo" module provides an optional functionality that can save a Foo instance in XML format that requires the jdk.jaxp module to be present.

   module foo {
       require optional jdk.jaxp;
   }

   package com.foo;
   import javax.xml.transform.*;
   public class Foo {
       ...  // other functionality

       public void save(OutputStream os) {
           // ModuleNotPresentException will be thrown
           // if jdk.jaxp module is not present
           this.class.getModule().requireModulePresent("jdk.jaxp");

           // In this example, it's okay to directly call
           // XMLUtils.save method as its return type and 
           // argument types are known.  In other cases where the
           // verifier has to load classes from the optional module
           // for verification, such method call must be done through
           // reflection.
           XMLUtils.save(this, os);
       }

       class XMLUtils {
           public static void save(Foo foo, OutputStream os) {
               // use TransformerFactory etc.
               ...
           }
       }
   }

com.foo.XMLUtils is a helper class defining static method to aid Foo class to save the Foo instance in XML format. References to the javax.xml.** classes in the jdk.jaxp module are isolated in this helper class. As its return type and argument types are known to the calling class, it can be called directly. In other cases where the verifier has to load classes from the optional module for verification, such method call must be done through reflection.

Open Questions for Further Discussion

  1. For a library or application to support different versions of a module, some dependencies may be relative to specific versions of a module. Developers are used to call Class.forName as the tradition way to determine if an API is supported. In the modular world, the isModulePresent method tests if a given module is present. The presence of a module of a specific name implies the presence of a class only if that class is exported by that module of that version. If that class is removed in the next version (i.e. incompatible change), the application will need to be modified to handle ClassNotFoundException case if it supports to link with that new version:
     
        if (module.isModulePresent("M")) {
            try {
                // Version2Class only exists in version 2.*
                // m.M.Version2Class will be loaded if it exists
                Class<?> c = Class.forName("m.M.Version2Class");
                ...
            } catch (ClassNotFoundException e) {
                // version < 2.0 or >= 3.0
                ...
            } 
        }
    

    Some alternatives:
    a. Test for module and/or its version (e.g. isModulePresent("M @ [2.0,3.0]"))
    b. Another method to test if a class is exported in a given module (e.g. isClassExported("M", "m.M.Version2Class"))

    Developers of module P would want to avoid modifying the source code if possible. The application would not need to be modified if it tests for the presence of a specified class. The above example can be replaced with the following:

     
        if (ld.isClassExported("M", "m.M.Version2Class")) {
            // m.M.Version2Class exists but it's not loaded 
            ...
        }  else {
            // version < 2.0 or >= 3.0
            ...
        }
    
    On the other hand, Class.forName achieves the same effect except that it throws a checked CNFE. Need use cases to determine if the new isClassExported is needed or the test to check the presence of a module of a given version range is adequate.
  2. Can/should the compiler use the annotation to check that the references to classes within the optional module are referenced correctly to avoid unexpected verification errors?