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
<scope>provided</scope>
,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.
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:
fri.i18n.messages
: providing and translating messagesfri.text.format
: inserting runtime values into messagesfri.text.output
: configuring the output streamfri.application.log
: prepending class namesfri.application
: the demo-application that uses all modules directly or indirectlyTo show the effect of requires transitive (reaches just one level) you need at least four modules.
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.
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).
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 fri.application
{
requires fri.application.log;
}
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 fri.application.log
{
exports fri.application.log;
requires fri.text.output;
requires transitive fri.i18n.messages;
}
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 fri.text.output
{
exports fri.text.output;
requires transitive fri.i18n.messages;
requires fri.text.format;
}
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 fri.text.format
{
exports fri.text.format;
}
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;
}
}
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 fri.i18n.messages
{
exports fri.i18n.messages;
}
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
).
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. Fortunately you can rename the src
folder to a module name.
So now we declare our dependencies in
import
statementsmodule-info.java
filespom.xml
filesWhat do we need more:-?
ɔ⃝ Fritz Ritzberger, 2020-05-18