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:
- module
fri.i18n.messages
: providing and translating messages - module
fri.text.format
: inserting runtime values into messages - module
fri.text.output
: configuring the output stream - module
fri.application.log
: prepending class names - 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
andfri.i18n.messages
are simple, they do not depend on anything except Java built-in modules. So theirmodule-info.java
contain just exports.The module
fri.text.output
depends on these two, therefore it requires them. Modulefri.text.format
will be used just internally, so a simple requires is enough. Modulefri.i18n.message
is a different case. No user offri.text.output
will be able to use it without also havingMessages
, sofri.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 tofri.application.log
, it would not be known infri.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 requiresfri.text.output
. It is not necessary to expose that to callers, so a simple requires statement is enough. But it must provideMessages
tofri.application
, and it does so by repeating the requires transitive statement that also is infri.text.output
. Alternative would be a simple requires, and modulefri.application
requiringfri.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 isfri.application.log.Logger
. So the modulefri.application
requiresfri.application.log
and hopes that this provides any necessary class, even if it's a class from another module (in this caseMessages
).
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:
Kommentar veröffentlichen