Blog-Archiv

Montag, 18. Mai 2020

Basic Java Module Dependency Declarations

Java Modules provide techniques to declare dependencies beyond import statements. The exports and requires keywords will be subject of this Blog, as transitive will be.

I won't cover

  • requires static, which is similar to Maven <scope>provided</scope>,
  • provides and uses, which is about service-providing,
  • exports to, which is about restricting the export to certain modules,
  • open module, opens, opens to, which is about allowing reflection.

Introduction

Java had import statements right from start, but no exports. Imports refer to Java package-names, and they appear on top of Java source files. A module-import is expressed through the requires keyword, referring to a module-name, not a package name. It appears in a special file called module-info.java, located in the root directory of the module. The syntax used there is not part of the Java language.

The exports statement on the other hand refers to a package name, not a module name. Mind the difference. The module name always is in module-info.java. Package names are relative directory pathes, appearing on top of all Java sources in that directory.

Java modules do not allow circular dependencies.

Example Modules

Following modules are not real world examples, they are very small and serve just to show how things could be done with modules.

The idea is to have an application logger that can write in different languages, and builds together messages in a fluent way. All aspects of the log creation have been wrapped into their own modules:

  1. module fri.i18n.messages: providing and translating messages

  2. module fri.text.format: inserting runtime values into messages

  3. module fri.text.output: configuring the output stream

  4. module fri.application.log: prepending class names

  5. module fri.application: the demo-application that uses all modules directly or indirectly

To show the effect of requires transitive (reaches just one level) you need at least four modules.

Diagrams

Here is the graphical representation of the module dependencies, divided into exports and requirements. A module X that requires module Y will have access to all packages that module Y exports.

Source Code

By default, Java modules are named exactly like the main package they export (although they can export several packages). So if the main Java package-name was

fri.application

, the module sources would be in folder

fri.application/fri/application.

(But anyway you could call the module however you want.)

Here is the directory structure of my example, including sources (click to expand). It contains five modules (the four aspects and a main module).

fri.application
fri
application
Main.java
package fri.application;

import java.util.Date;
import java.util.Locale;
import fri.i18n.messages.Messages;
import fri.application.log.Logger;

public class Main
{
    public static void main(String[] args) {
        final Logger logger = new Logger(Main.class, Locale.GERMAN, System.out);
        logger.append(Messages.HELLO)
            .append(" ")
            .append(Messages.WORLD, new Date())
            .append("!")
            .flush();
    }
}
module-info.java
module fri.application
{
    requires fri.application.log;
}
fri.application.log
fri
application
log
Logger.java
package fri.application.log;

import java.io.PrintStream;
import java.util.Locale;
import fri.i18n.messages.I18nMessage;
import fri.text.output.Output;

public class Logger
{
    private Class<?> clazz;
    private Output output;
    private boolean inProgress;

    public Logger(Class<?> clazz, Locale locale, PrintStream printStream) {
        assert clazz != null;
        this.clazz = clazz;
        this.output = new Output(locale, printStream);
    }
    
    public Logger append(I18nMessage message, Object... parameters)  {
        ensureHeader();
        output.write(message, parameters);
        return this;
    }
    
    public Logger append(String text)  {
        ensureHeader();
        output.write(text);
        return this;
    }
    
    public void flush()  {
        output.newLine();
        inProgress = false;
    }

    private void ensureHeader() {
        if (inProgress == false)    {
            output.write(clazz.getName()+": ");
            inProgress = true;
        }
    }
}
module-info.java
module fri.application.log
{
    exports fri.application.log;
    
    requires fri.text.output;
    requires transitive fri.i18n.messages;
}
fri.text.output
fri
text
output
Output.java
package fri.text.output;

import java.io.PrintStream;
import java.util.Locale;
import fri.text.format.Format;
import fri.i18n.messages.I18nMessage;

public class Output
{
    private final Locale locale;
    private final PrintStream printStream;
    
    public Output(Locale locale, PrintStream printStream) {
        assert locale != null && printStream != null;
        this.locale = locale;
        this.printStream = printStream;
    }
    
    /** Prints the translation of given message with optionally inserted parameters. */
    public Output write(I18nMessage message, Object... parameters)  {
        final String translatedMessage = message.translate(locale);
        
        final String formattedMessage;
        if (parameters.length > 0)
            formattedMessage = new Format(translatedMessage, locale).format(parameters);
        else
            formattedMessage = translatedMessage;
        
        return write(formattedMessage);
    }
    
    /** Prints the given language-neutral text (space, symbol, ...). */
    public Output write(String text)  {
        printStream.print(text);
        return this;
    }
    
    /** Prints a platform-specific newline. */
    public Output newLine()  {
        printStream.println();
        return this;
    }
}
module-info.java
module fri.text.output
{
    exports fri.text.output;
    
    requires transitive fri.i18n.messages;
    requires fri.text.format;
}
fri.text.format
fri
text
format
Format.java
package fri.text.format;

import java.text.MessageFormat;
import java.util.Locale;

public class Format
{
    private final MessageFormat formatter;
    
    public Format(String message, Locale locale) {
        formatter = new MessageFormat(message, locale);
    }
    
    public String format(Object... parameters) {
        return formatter.format(parameters);
    }
}
module-info.java
module fri.text.format
{
    exports fri.text.format;
}
fri.i18n.messages
fri
i18n
messages
I18nMessage.java
package fri.i18n.messages;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class I18nMessage
{
    private final Map<Locale,String> translations = new HashMap<Locale,String>();
    
    I18nMessage()   {   // instantiation and adding internal only
    }
    
    I18nMessage add(Locale locale, String translation)   {
        assert locale != null;
        translations.put(locale, translation);
        return this;
    }
    
    /** @return the translation according to given locale. */
    public String translate(Locale locale)   {
        assert locale != null;
        final String translation = translations.get(locale);
        assert translation != null
            : "Please add a translation for "+locale+ "to "+translations;
        return translation;
    }
}
Messages.java
package fri.i18n.messages;

import java.util.Locale;

public interface Messages {

    I18nMessage HELLO = new I18nMessage()
            .add(Locale.ENGLISH, "Hello")
            .add(Locale.GERMAN, "Hallo");
    
    I18nMessage WORLD = new I18nMessage()
            .add(Locale.ENGLISH, "World {0,date}")
            .add(Locale.GERMAN, "Welt {0,date}");
}
module-info.java
module fri.i18n.messages
{
    exports fri.i18n.messages;
}

Discussion

Let's have a look at the dependency declarations in a bottom-up way.

The modules fri.text.format and fri.i18n.messages are simple, they do not depend on anything except Java built-in modules. So their module-info.java contain just exports.

The module fri.text.output depends on these two, therefore it requires them. Module fri.text.format will be used just internally, so a simple requires is enough. Module fri.i18n.message is a different case. No user of fri.text.output will be able to use it without also having Messages, so fri.i18n.message is provided "transitive" to any requirer. This is about parameter- and return-classes.
Mind that transitive reaches just one level, in this case up to fri.application.log, it would not be known in fri.application. Originally transitive was called public, but later was renamed.

Now we have output, formatting and translation. Module fri.application.log provides class prefixes for log messages. It is a pass-through module, a kind of "decorator". First it requires fri.text.output. It is not necessary to expose that to callers, so a simple requires statement is enough. But it must provide Messages to fri.application, and it does so by repeating the requires transitive statement that also is in fri.text.output. Alternative would be a simple requires, and module fri.application requiring fri.i18n.messages by itself.

The class fri.application.Main is the application. It outputs log messages in a certain language, and decorates them with symbols and runtime parameters. Thus it depends on ready-made messages that can be built together, and a provider of languages (locales), output streams and class prefixes. That is fri.application.log.Logger. So the module fri.application requires fri.application.log and hopes that this provides any necessary class, even if it's a class from another module (in this case Messages).

Conclusion

It took me a while to find out that "transitive" is not an infinite "transitive". Cryptic compiler messages like

The type ... cannot be resolved. It is indirectly referenced from required .class files

do not make life easier.

Eclipse provides support for modules, but you must place each module in its own Eclipse project. You could rename the src folder to a module name, but this breaks the concept of Maven structures.

So now we declare our dependencies in

  • Java package import statements
  • Java module-info.java files
  • OSGI bundle manifests
  • Maven pom.xml files
  • maybe also Spring bean annotations

What do we need more:-?




Keine Kommentare: