Blog-Archiv

Sonntag, 31. Mai 2020

Compiling Java Modules from Command Line

If you want to check your IDE whether it handles Java modules accurately, then you need to do a command line compilation and see what the JDK's javac compiler says. But how to do this?

Compilation Commands

Since Java 9, the javac compiler supports two kinds of module compilations: single- and multi-module.

Single Module

Assume following source directories and files:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java

When your terminal window stands above the module's root directory fri.i18n.messages, following command-line would compile the module's classes into the target directory given by the -d option, in this case I named it module-classes:

javac --module-path module-classes \
    -d module-classes/fri.i18n.messages \
    `find fri.i18n.messages -name '*.java'`

The --module-path option (also -p) is necessary only when the module depends on other modules that were already compiled into module-classes. It tells the compiler where to search for referenced modules, being the future replacement for CLASSPATH.

The -d option gives the output directory where to write compiled classes into. In this case the name of the module (fri.i18n.messages) must be appended to achieve a correct output path.

The find command is available on UNIX systems only, on WINDOWS you have to download and install it. It searches all .java files inside the fri.i18n.messages directory. The `backquotes` are a UNIX-shell mechanism that puts the output of the find command into the javac command line. (Not a good solution, because the list of sources could get huge. See below for a better way to pass sources to the compiler. The new --module-source-path option was not made for this!)

This single-module compilation command would result in following:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java
module-classes
fri.i18n.messages
module-info.class
fri
i18n
messages
I18nMessage.class
Messages.class

Multiple Modules

A certain directory structure is required to compile several modules in one run. All modules must be located below a directory that is given as --module-source-path, in this case I named it module-sources. Each module's sources are in a directory below that is named like the module. Mind that this name is duplicated in module-info.java!

module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java

You can compile all modules contained in module-sources by following command line:

javac --module-path module-classes \
    -d module-classes \
    --module-source-path module-sources \
    `find module-sources -name '*.java'`

Again the --module-path option could be left out when the modules do not depend on precompiled modules.

The --module-source-path option points to the directory where the module directories are below, in this case it is module-sources.

Result of this compilation command would be:

module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java
module-classes
fri.text.output
module-info.class
fri
text
output
Output.class
fri.text.format
module-info.class
fri
text
format
Format.class

Mind that I didn't have to put any module name into the command line, like I had to do for a single module compilation.

Huge List of Sources

Putting the find command as substitution into the javac command line is not a real-world solution. Most projects have tons of source files, and the shell command buffer may get out of bounds. There is an option to replace the list of sources by a file name tagged with '@'. You would let the find command write into a file, and then pass the file to javac. Here is an example for single module compilation:

find fri.i18n.messages -name '*.java' >sources.list
javac --module-path module-classes -d module-classes/fri.i18n.messages @sources.list
rm -f sources.list

The according multi-module commands would be:

find module-sources -name '*.java' >sources.list
javac --module-path module-classes -d module-classes --module-source-path module-sources @sources.list
rm -f sources.list

The '@' (ampersand) before the file name sources.list does the magic.

Automation Script

Here is a UNIX shell script, named modules-compile.sh, that can do both single and multi module compilations. Put this into the directory above your module source directory like the following:

fri.i18n.messages
module-info.java
fri
i18n
messages
I18nMessage.java
Messages.java
module-sources
fri.text.output
module-info.java
fri
text
output
Output.java
fri.text.format
module-info.java
fri
text
format
Format.java
module-classes
modules.list
modules-compile.sh

Put the list of your module directories into modules.list (no comments allowed here!):

fri.i18n.messages
module-sources

These can be single modules (fri.i18n.messages) or directories containing multiple modules (module-sources).
IMPORTANT: keep the list in dependency order, so that basic modules are already compiled when the modules that depend on them get compiled!

Then put following into modules-compile.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# compiled classes target directory
moduleTargetDir=module-classes

srclist=sources.list

# sources must be in dependency order!
moduleDirs=`cat modules.list`

for dir in $moduleDirs
do
  # try to find module-info.java in given directory
  moduleInfo=`find \$dir -name module-info.java`
  [ -z "$moduleInfo" ] && {
    echo "ERROR: Ignoring $dir because it contains no module-info.java!" >&2
    continue
  }
 
  # check how many module-info.java files were found
  modulesCount=`echo \$moduleInfo | wc -w`
  if [ "$modulesCount" = 1 ]
  then
    # try to extract the module name from module-info.java
    moduleName=`awk '/^[ \t]*module/ {print $2}' \$moduleInfo`
    [ -z "$moduleName" ] && {
      echo "ERROR: Could not extract module name from $moduleInfo" >&2
      continue
    }
    srcdir=`dirname \$moduleInfo`
  
    echo "Compiling module $moduleName in $srcdir to $moduleTargetDir ..." >&2
  else
    # the grandparent directory of first module-info.java must be the parent of all
    firstModulesDir=`echo \$moduleInfo | awk '{print $1}'`
    srcdir=`dirname \$firstModulesDir`
    srcdir=`dirname \$srcdir`
  
    echo "Multimodule compile in $srcdir to $moduleTargetDir ...." >&2
  fi

  # write sources list into file and use the javac @sources.list option
  find $srcdir -name '*.java' >$srclist
 
  if [ "$modulesCount" = 1 ]
  then
    javac --module-path $moduleTargetDir -d $moduleTargetDir/$moduleName @$srclist
  else
    javac --module-path $moduleTargetDir -d $moduleTargetDir --module-source-path $srcdir @$srclist
  fi
 
  rm -f $srclist
done

Most likely you'll have to make the script executable then:

chmod 754 modules-compile.sh

Mind that this script requires the name of the module written in same line as the module keyword in a module-info.java of a single module (script line 23). For multi-module compilation this is not needed.

This script works with both Maven and Eclipse projects. Eclipse projects have all sources inside a src/ directory, Maven has them in src/main/java/ and src/test/java/.

It would take too much space to explain the script. Shell scripts are not well readable, they are quick & dirty, you need an expert to maintain them. But they are quite useful sometimes, so try it out !-)




Sonntag, 24. Mai 2020

Java Resource Loading from Unnamed Module

In a recent article I discovered that resource loading between named modules is possible only when (1) the resource is in same module as the resource-anchor class, and (2) the caller of resourceAnchorClass.getResources() is in same module as the resource. But what about the "unnamed module", i.e. legacy classes that are not inside a module? Can a module-encapsulated class read a resource inside the unnamed module, and can a legacy class read a resource inside a named module?

Definitions

  1. Named Module:
    Follows the JPMS specification ("Java Platform Module System"). It has a root directory that is named like the module, with module-info.java descriptor and package directories below. It explicitly exports packages and/or service classes, all other contents can not be seen from outside. Any package or service it needs from outside must be listed in a "requires" or "uses" statement in module-info.java. It can not see the unnamed module, but can see packages in automatic modules when it "requires" them. Module dependency graphs do not allow cycles.

  2. Unnamed Module:
    All classes found on CLASSPATH not being in a JAR file belong to the one-and-only "unnamed module" (not really being a module). They can not be seen by classes in named modules, but by those in automatic modules. Vice versa they can see automatic modules, and all exported packages of named modules. If the exported classes of a named module are also found on the CLASSPATH, they would nevertheless be considered to be in the named module, not the unnamed.

  3. Automatic Module:
    Any JAR file on CLASSPATH that was not modularized gets converted to an automatic module at runtime by the Java virtual machine. Its name is derived from the JAR file name. Basically an automatic module is a named module that exports all its packages, but its classes (other than classes in named modules) can see also classes in the unnamed module and other automatic modules, and additionally all exported classes of named modules (without explicitly requiring them). Named modules can see classes of the automatic modules, but they need to have an according requires statement in module-info.java that points to the package of the classes to be used.

Example Sources

This example builds upon the sources of my recent Blog about resource loading with modules. I added another resource and resource-anchor class to fri.module2, and I added the "unnamed module". Mind that using underscore ('_') in a package name is possible but unwanted, Java plans to make it a reserved character in future.

The fri.module_unnamed project has CLASSPATH references to both fri.module1 and fri.module2. The module fri.module2 has a MODULEPATH dependency on fri.module1:

Here are the directories and files of the three involved Eclipse projects, including all source code (click to expand):

fri.module1
src
fri
module1
ResourceUtil.java
package fri.module1;

import java.io.InputStream;
import java.net.URL;

public final class ResourceUtil
{
    public static String asText(Class<?> resourceAnchor, String fileName) throws Exception {
        assert fileName != null;
        return asText(resourceAnchor.getResourceAsStream(fileName));
    }
    
    public static String asText(URL url) throws Exception {
        assert url != null;
        return asText(url.openStream());
    }
    
    public static String asText(InputStream inputStream) throws Exception {
        assert inputStream != null;
        final StringBuilder sb = new StringBuilder();
        int c;
        try (inputStream) { // will close stream at end
            while ((c = inputStream.read()) != -1)
                sb.append((char) c);
        }
        return sb.toString();
    }
    
    private ResourceUtil() {}   // do not instantiate
}
ResourceAnchor.java
package fri.module1;

public class ResourceAnchor
{
}
resource.txt
I am resource of module-one.
module-info.java
module fri.module1
{
    exports fri.module1;
}
fri.module2
src
fri
module2
ResourceAnchorInModule2.java
package fri.module2;

public class ResourceAnchorInModule2
{
}
resource_in_module2.txt
I am resource of module 2.
module-info.java
module fri.module2
{
    requires fri.module1;
}
fri.module_unnamed
src
fri
module_unnamed
Main.java
package fri.module_unnamed;

import java.net.URL;
import fri.module1.ResourceAnchor;
import fri.module1.ResourceUtil;
import fri.module2.ResourceAnchorInModule2;

public class Main
{
    public static void main(String[] args) throws Exception {
        // direct access of a resource in a named module: works!
        final URL url = ResourceAnchor.class.getResource("resource.txt");
        System.out.println(url);
     
        // access of a resource in unnamed module through a named module: works!
        final String text = ResourceUtil.asText(ResourceAnchorInUnnamedModule.class, "resource_in_unnamed_module.txt");
        System.out.println(text);
     
        // access of a resource in a named module through a named module: works!
        final String text2 = ResourceUtil.asText(ResourceAnchorInModule2.class, "resource_in_module2.txt");
        System.out.println(text2);
    }
    
    private Main() {}   // do not instantiate
}
ResourceAnchorInUnnamedModule.java
package fri.module_unnamed;

public class ResourceAnchorInUnnamedModule
{
}
resource_in_unnamed_module.txt
I am resource of the unnamed module.

Mind that module_unnamed has no module_info.java file because the JPMS "unnamed module" is not a module.

Test

We are going to execute the test Main in the unnamed module fri.module_unnamed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package fri.module_unnamed;

import java.net.URL;
import fri.module1.ResourceAnchor;
import fri.module1.ResourceUtil;
import fri.module2.ResourceAnchorInModule2;

public class Main
{
    public static void main(String[] args) throws Exception {
        // direct access of a resource in a named module: works!
        final URL url = ResourceAnchor.class.getResource("resource.txt");
        System.out.println(url);
     
        // access of a resource in unnamed module through a named module: works!
        final String text = ResourceUtil.asText(ResourceAnchorInUnnamedModule.class, "resource_in_unnamed_module.txt");
        System.out.println(text);
     
        // access of a resource in a named module through a named module: works!
        final String text2 = ResourceUtil.asText(ResourceAnchorInModule2.class, "resource_in_module2.txt");
        System.out.println(text2);
    }
    
    private Main() {}   // do not instantiate
}

Output is:

file:/media/disk2/java/workspace-ee2/fri.module1/target/fri/module1/resource.txt
I am resource of the unnamed module.
I am resource of module 2.

All three resource loading statements succeeded.
But what do I test here?

In line 12, I directly load a resource from fri.module1. That would not be possible for a named module. When getResource() delivers non-null, you always should be able to read the returned URL's content, so this was successful.

In line 16, I ask the ResourceUtil class in fri.module1 to load a resource using a resource-anchor that is in the unnamed module. Although classes in named modules normally can not use classes of the unnamed module, this works.

In line 20, I use ResourceUtil in fri.module1 to load a resource in fri.module2, with a resource-anchor also being in fri.module2. That means, the getResource() call happens in fri.module1, but both the resource-anchor and the resource are in fri.module2. Also not possible between named modules, but works when executed by the unnamed module.

Conclusion

Resource loading is free like it always was for non-modularized classes, most likely due to backward compatibilty reasons. We can let read resources in the unnamed module through module classes, and we even can read resources inside named modules through classes of other named modules




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:-?




Montag, 4. Mai 2020

Java Modules and Resource Loading

When we load resources in Java, we normally use a resource-anchor class that is in the same package or archive as the resource. Such an archive can be the representation of a Maven module or a Java module (supported since version 9).

Why don't we simply open the resource as file? Because the file is available only since development of the Java app, after deployment the file is inside an archive and can not be opened by FileInputStream or similar. Thus we always use getClass().getResource(relativePath) to open any resource, because this works in both cases.

This Blog is about the fact that

resourceAnchorClass.getResource(relativePath) in Java since 9 works only when the caller of that method is in the same module as the resource-anchor class.

In other words: not only the resource-anchor class needs to be in the same module as the resource, also the caller of resourceAnchorClass.getResource() needs to be in that same module!

Example Modules

You need two Eclipse Java projects that you should name like your modules. Standard is the to use the package name of the module's main class. I chose following module names:

  • fri.module1
  • fri.module2

Here is the directory structure of both:

You see that these are Java 11 projects, so modules are supported. You establish a module by putting module-info.java in the module's root directory. Here is the one of fri.module1, which depends on no others (except the Java-built-in modules):

module fri.module1
{
    exports fri.module1;
}

You can see that this defines the name of the module, and states that it exposes the Java package fri.module1.

And here is module-info.java of fri.module2, which is a dependent module that uses fri.module1:

module fri.module2
{
    requires fri.module1;
}

Because this is a final application module, it exposes nothing.

Now we have two modules and can try out whether module2 can load resources from module1.

Example Resource and Classes

Module 1

This is what I put into fri.module1. First the ResourceAnchor.java class:

package fri.module1;

public class ResoureAnchor
{
}

Then the resource.txt file:

I am resource of module-one.

Module 2

Following is a utility class that can load a resource and turn its content into a plain text string (not usable for Unicode, just ASCII!). I called it ResourceUtil.java:

package fri.module2;

import java.io.InputStream;

public final class ResourceUtil
{
    public static String asText(Class<?> resourceAnchor, String fileName) throws Exception {
        InputStream inputStream = resourceAnchor.getResourceAsStream(fileName);
        StringBuilder sb = new StringBuilder();
        int c;
        while ((c = inputStream.read()) != -1)  {
            sb.append((char) c);
        }
        return sb.toString();
    }
    
    private ResourceUtil() {}   // do not instantiate
}

And here finally is the application that uses all these parts, called Main.java, we will call this class to see what happens:

package fri.module2;

import fri.module1.ResoureAnchor;

public final class Main
{
    public static void main(String[] args) throws Exception {
        String text = ResourceUtil.asText(ResoureAnchor.class, "resource.txt");
        System.out.println(text);
    }
    
    private Main() {}   // do not instantiate
}

This loads a resource from another module and prints its content to the console. In Eclipse you can execute such a class via context menu "Run As" - "Java Application".

Result

When you run this app, you will see the following:


Exception in thread "main" java.lang.NullPointerException
 at fri.module2/fri.module2.ResourceUtil.asText(ResourceUtil.java:11)
 at fri.module2/fri.module2.Main.main(Main.java:8)

So this failed in ResourceUtil on line 11 where the input-stream gets read. That means resourceAnchor.getResourceAsStream(fileName) returned null. We have no evidence why. This is how developers spend their time :-)

Fix

I guess that this has something to do with modules. So I move the ResourceUtil class into fri.module1. In Eclipse you can do that quickly with context menu "Refactor" - "Move". The modules look like the following now:

You see that ResourceUtil now is in fri.module1. The refactoring has put the new import into class Main, everything else is unchanged:

package fri.module2;

import fri.module1.ResourceUtil;
import fri.module1.ResoureAnchor;

public final class Main
{
    public static void main(String[] args) throws Exception {
        String text = ResourceUtil.asText(ResoureAnchor.class, "resource.txt");
        System.out.println(text);
    }
    
    private Main() {}   // do not instantiate
}

When you run the app now, you will see:


I am resource of module-one.

That means it worked now!

Conclusion

Using resourceAnchor.getResource(relativePath) in Java since 9 works only when the caller of that method is in the same module as the resource-anchor class.

This will not affect you as long as all your classes are in the "unnamed" module. But after module introduction you should put the code of ResourceUtil.asText() into ResourceAnchor, and never load a resource using just its anchor class (unless you are in the same module).