• java9 模块化 jigsaw



    java9并没有在语言层面做出很多改变,而是致力于一些新特性,如模块化,其核心就是解决历史遗留问题,为以后的jar包森林理清道路。模块化是一个很大的命题,就不讲那么细致了,关于java9的特性也有很多书籍可供参考,如
    《Java 9 Revealed: For Early Adoption and Migration》《Java 9 Modularity》还有很多很多(点击查看)

    模块化是一个很大的特性,也是一个很大的话题。模块化作为一个未来的的趋势,我们可能会关心项目的迁移,对接OSGI或者微服务等等,本文只是简单阐述下模块化解决的基本问题和模块化的一些用法。

    模块化

    两个问题,臃肿的jar包和混乱的jar包依赖关系。如果我们没被现在的jar包系统搞疯,首先应该感谢IDE和maven,gradle之类的版本控制软件。你可能见过这个大家伙,60多m的jar包,运行一个Hello World程序,也需要这么一个庞然大物支撑。

    nothing

    再或者,我们依赖一个大的工具包,但我们可能只需要其中几个类的功能。之前的jar包机制是很僵硬的,我们需要更灵活更轻便条理更清晰的环境,从化繁为简的角度来讲,我们需要模块化。

    不知道你经历过jar hell吗,可以参考下图:

    此处输入图片的描述

    jar包之间的依赖可谓错综复杂,jar包之间交叉,缠绕,冲突,旋转,跳跃,我觉得没有人会想理清项目中依赖的这一个jar包与其他Jar包之间千丝万缕的联系。不看依赖,便是晴天,但是我们有时候真的需要理清楚,因为我们在部署的时候可能会产生冲突,会报各种异常。我们理清楚了,我们对这个jar包理解更深了,我们变强了,我们秃了。我们需要更清晰的图像来描述依赖关系,我们不能任由jar缠绕旋转,我们应该让他们更守规矩,让依赖图像清楚并且有层次,我们需要模块化。

    其实我们不需要理解模块化的字面意义,我们需要知道我们面临的困难和要解决的问题,jdk9发布的解决这些问题的新特性的名称叫做模块化。

    漫长的模块化

    在使用模块化之前,我需要让你明白一些事情,模块化不是一蹴而就,java平台做了表率,从内部进行了模块化改造。我们先来看一下传统的jdk7的API:

    此处输入图片的描述

    很熟悉的api形式。

    再来看一看java9的api:

    此处输入图片的描述

    点进java.activation

    此处输入图片的描述

    我们还可以看到模块的图像,Java9不仅为我们提供了模块化的手段,整个java9平台自身,也已经被模块化改造了。整个java生态彻底改造成模块化是需要时间的,从jdk7时提出模块化概念,到整个模块化版本发布,经历了很长时间。把原来的第三方jar包彻底改造成模块化jar包会花费很多功夫,我们能够使用jdk的模块化包,不代表我们能够使用整个Java生态中所有的模块化包,这需要开发者对自己的jar包进行调整。

    • 模块化为我们梳理出了逻辑清晰的jar包系统。
    • 模块化需要额外的操作,处理相同的代码,我们需要理清代码之间的关系,对内容模块化,但IDE提供了一些便捷的功能。
    • 拥抱彻底的模块化生态需要时间,很多重量级的框架和jar包适应模块化是需要我们等待的,但是有自动化模块可以帮助我们构建模块化项目。

    简单入门模块化

    $JAVA_HOME/bin中的Java工具也为模块化提供了很多新功能,就不一一详细介绍了,感兴趣的话可以参考Oracle的介绍。下面的例子主要是参考openJDK模块化的快速入门GitHub上的一个项目,很好的例子,我也就不做太多更改了,会简要叙述下例子中的新事物。

    首先要下载JDK9,可以去官网下载,我是用Linux平台演示的代码。

    先来看第一个例子,输出Greetings!

    先来看一看整个项目的目录结构:

    src
    └── src/com.greetings
        ├── src/com.greetings/com
        │   └── src/com.greetings/com/greetings
        │       └── src/com.greetings/com/greetings/Main.java
        └── src/com.greetings/module-info.java
    

    整个项目包含两个文件,一个是module-info.java模块文件,还有一个是Main.java主类文件。让我们看一看这两个文件的内容。

    $ cat src/com.greetings/module-info.java
        module com.greetings { }
    

    模块文件中没有内容,因为这个Main.java中没有依赖别的模块的内容,也没有打算向外界暴露Main.java的内容,所以内容是空的。

    $ cat src/com.greetings/com/greetings/Main.java
    package com.greetings;
    public class Main {
        public static void main(String[] args) {
            System.out.println("Greetings!");
        }
    }
    

    然后,我们把这两个文件编译运行。

    javac -d mods/com.greetings 
            src/com.greetings/module-info.java 
            src/com.greetings/com/greetings/Main.java
    

    javac -d 指定放置生成的类文件的位置

    java --module-path mods 
    -m com.greetings/com.greetings.Main 
    #或者
    java --module-path mods 
    --module com.greetings/com.greetings.Main
    

    就可以运行了,我们会发现屏幕上打印出了Greetings!

    其中 --module-path <模块路径>...用 : 分隔的目录列表, 每个目录都是一个包含模块的目录。

    -m或--module是一样的,用于指定要解析的初始化模块名称,如果不指定模块名称,可以指定要执行的主类名称

    再来看第二个例子,Greetings world!

    我们先来看一下目录结构:

    src
    ├── src/com.greetings
    │   ├── src/com.greetings/com
    │   │   └── src/com.greetings/com/greetings
    │   │       └── src/com.greetings/com/greetings/Main.java
    │   └── src/com.greetings/module-info.java
    └── src/org.astro
        ├── src/org.astro/module-info.java
        └── src/org.astro/org
            └── src/org.astro/org/astro
                └── src/org.astro/org/astro/World.java
    

    再来看看代码内容:

    cat src/com.greetings/com/greetings/Main.java
    
    package com.greetings;
    
    import org.astro.World;
    
    public class Main {
            public static void main(String[] args) {
                    System.out.format("Greetings %s!%n", World.name());
            }
    }
    
    cat src/com.greetings/module-info.java
    
    module com.greetings { 
            requires org.astro;
    }
    
    cat src/org.astro/org/astro/World.java
    
    package org.astro;
    
    public class World {
            public static String name() {
                    return "world";
            }
    }
    
    cat src/org.astro/module-info.java
    
    module org.astro { 
            exports org.astro;
    }
    

    Main.java类中打印了Greetings!+World.name()com.greetings模块文件声明了requires org.astro,意思就是我这个模块需要依赖org.astro模块;World类中有一个静态方法,返回了world字符串,com.greetings模块依赖于它,而且他在自己的模块文件中做出了声明exports org.astro,将org.astro模块暴露出去,可供其他模块依赖。

    #我们先编译com.greetings模块试试
    
    javac -d mods/com.greetings 
    > >  src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
    src/com.greetings/module-info.java:2: 错误: 找不到模块: org.astro
            requires org.astro;
                        ^
    1 个错误
    

    那再先编译一下,org.astro模块

     javac -d mods/org.astro 
    > src/org.astro/module-info.java src/org.astro/org/astro/World.java
    

    然后编译一下com.greeetings

    javac --module-path mods -d mods/com.greetings src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java
    

    --module-path(或-p) 用来指定应用的模块的位置。

    运行一下

    java --module-path mods -m com.greetings/com.greetings.Main
    //Greetings world!
    

    这个例子中要注意exportsrequires的用法,exports对外界暴露内容,requires引入外界功能,如果没有引入,或者引入不存在的module编译无法通过。

    #如果没有声明requires
    javac --module-path mods -d mods/com.greetings 
        src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
    src/com.greetings/com/greetings/Main.java:2: error: package org.astro is not visible
        import org.astro.World;
                  ^
      (package org.astro is declared in module org.astro, but module com.greetings does not read it)
    1 error
    #如果没有声明exports
    javac --module-path mods -d mods/com.greetings 
       src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
    src/com.greetings/com/greetings/Main.java:2: error: package org.astro is not visible
        import org.astro.World;
                  ^
      (package org.astro is declared in module org.astro, which does not export it)
    1 error
    

    多模块的编译

    可以一次编译多个模块

    我们先找出所有.java文件

    echo $(find src -name "*.java")
    //src/org.astro/org/astro/World.java src/org.astro/module-info.java src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java
    

    一口气将所有.java文件编译到mods文件夹中

    javac -d mods --module-source-path src $(find ./src -name "*.java")
    

    看一下目录结构

    mods
    ├── mods/com.greetings
    │   ├── mods/com.greetings/com
    │   │   └── mods/com.greetings/com/greetings
    │   │       └── mods/com.greetings/com/greetings/Main.class
    │   └── mods/com.greetings/module-info.class
    └── mods/org.astro
        ├── mods/org.astro/module-info.class
        └── mods/org.astro/org
            └── mods/org.astro/org/astro
                └── mods/org.astro/org/astro/World.class
    

    效果是一样的,再运行一下

    java --module-path mods -m com.greetings/com.greetings.Main
    //Greetings world!
    
    
    • 打包

    我们可以把模块打成jar包,这样会更方便,模块jar包里顶级目录下会有一个module-info.calss文件。

    我们把已经编译好的mods文件夹中的模块文件打包成jar包

    注意:最后的句号不能漏掉

    jar -c -f mlib/org.astro@1.0.jar 
            --module-version=1.0 -C mods/org.astro .
    //或者 
    jar --create --file=mlib/org.astro@1.0.jar 
            --module-version=1.0 -C mods/org.astro .
    

    两个都可以的,--create缩写就是-c,意思是我们创建jar包;--file=缩写就是-f,指定文件位置和名称,--module-version是指定版本,创建模块化 jar 或更新非模块化 jar 时的模块版本;-C 就是更改为指定的目录并包含以下文件。关于jar命令还有很多操作,可以通过jar --help查阅,或者去官网查阅。

    $ jar --create --file=mlib/com.greetings.jar 
            --main-class=com.greetings.Main -C mods/com.greetings .
    

    产看一下生成的jar包

    $ ls mlib
    com.greetings.jar   org.astro@1.0.jar
    

    来执行一下,因为我们打包com.greetings模块时,指定了主类,所以运行时可以直接调用

    java -p mlib -m com.greetings
    //Greetings world!
    
    

    Service

    如何使用模块系统提供服务呢,让我们来看看。

    首先查看一下工程的结构:

    src
    ├── src/com.greetings
    │   ├── src/com.greetings/com
    │   │   └── src/com.greetings/com/greetings
    │   │       └── src/com.greetings/com/greetings/Main.java
    │   └── src/com.greetings/module-info.java
    ├── src/com.socket
    │   ├── src/com.socket/com
    │   │   └── src/com.socket/com/socket
    │   │       ├── src/com.socket/com/socket/NetworkSocket.java
    │   │       └── src/com.socket/com/socket/spi
    │   │           └── src/com.socket/com/socket/spi/NetworkSocketProvider.java
    │   └── src/com.socket/module-info.java
    └── src/org.fastsocket
        ├── src/org.fastsocket/module-info.java
        └── src/org.fastsocket/org
            └── src/org.fastsocket/org/fastsocket
                ├── src/org.fastsocket/org/fastsocket/FastNetworkSocket.java
                └── src/org.fastsocket/org/fastsocket/FastNetworkSocketProvider.java
    

    有点多,我们先理一理其中的关系
    image
    com.scoket中还有个NetworkSocketProvider.java,其实就是用来创建socket的一个工厂。我们再来看看他们的代码:

    先来看看org.socket模块:

    package com.socket;
    
    import com.socket.spi.NetworkSocketProvider;
    
    import java.io.Closeable;
    import java.util.Iterator;
    import java.util.ServiceLoader;
    
    public abstract class NetworkSocket implements Closeable {
        protected NetworkSocket() {}
    
        public static NetworkSocket open() {
            ServiceLoader<NetworkSocketProvider> sl = ServiceLoader.load(NetworkSocketProvider.class);
            Iterator<NetworkSocketProvider> iter = sl.iterator();
            if (!iter.hasNext()) {
                throw new RuntimeException("No service providers found!");
            }
            NetworkSocketProvider provider = iter.next();
            return provider.openNetworkSocket();
        }
    }
    

    其中有一个open()方法,其实就是使用provider生成一个NetworkSocket对象并返回。

    再来看看NetworkSocketProvider:

    package com.socket.spi;
    
    import com.socket.NetworkSocket;
    
    public abstract class NetworkSocketProvider {
        protected NetworkSocketProvider() {}
    
        public abstract NetworkSocket openNetworkSocket();
    }
    

    一个抽象类,openNetworkSocket()抽象方法由子类实现。

    module-info.java

    module com.socket {
            exports com.socket;
            exports com.socket.spi;
            uses com.socket.spi.NetworkSocketProvider;
    }
    
    

    我想上面两个exports你应该能够理解其意义,但是uses是什么意思呢。为了使用服务,它的提供者需要被发现和加载。java.util.ServiceLoader类来发现和加载服务。发现和加载服务提供者的模块必须在其声明中包含一个uses语句,该语句语法如下:
    uses <service-interface>;<service-interface>是服务接口的名称,它是Java接口名称,类名称或注解类型名称。如果一个模块使用ServiceLoader<S>类加载名为S的服务接口的服务提供者的实例,则模块声明必须包含以uses S。总而言之,就是你如果是个服务接口,你需要服务提供的类实现你的接口,你就需要声明uses

    那我们在看一看服务提供者类,org.fastsocket:

    //继承了NetWorkSocket类
    package org.fastsocket;
    
    import com.socket.NetworkSocket;
    
    public class FastNetworkSocket extends NetworkSocket {
        FastNetworkSocket() {}
        public void close() {}
    }
    

    再来看看FastNetworkSocketProvider类,它实现了NetworkSocketProvider抽象类,重写了抽象方法,用来返回一个NetworkSocket。

    package org.fastsocket;
    
    import com.socket.NetworkSocket;
    import com.socket.spi.NetworkSocketProvider;
    
    public class FastNetworkSocketProvider extends com.socket.spi.NetworkSocketProvider {
        public FastNetworkSocketProvider() {}
    
        @Override
        public NetworkSocket openNetworkSocket() {
            return new FastNetworkSocket();
        }
    }
    

    再来看看module-info.java

    module org.fastsocket {
            requires com.socket;
            provides com.socket.spi.NetworkSocketProvider
                    with org.fastsocket.FastNetworkSocketProvider;
    }
    
    

    其中有个provideswith,意思就是为NetworkSocketProvider提供一个实现类org.fastsocket.FastNetworkSocketProvider

    再来快速看一下客户端代码,我们运行一下整个服务系统。

    cat src/com.greetings/com/greetings/Main.java
    package com.greetings;
    
    import com.socket.NetworkSocket;
    
    public class Main {
            public static void main(String[] args) {
                    NetworkSocket s = NetworkSocket.open();
                    System.out.println(s.getClass());
            }
    }
    
    cat src/com.greetings/module-info.java
    module com.greetings {
            requires com.socket;
    }
    

    客户端代码很简单,客户端模块依赖了com.socketMain.java打印了提供服务的类名。

    我们先试着编译服务接口代码和客户端代码:

    javac  -d mods/com.socket  $(find src/com.socket -name "*.java")
    javac --module-path mods -d mods/com.greetings  $(find src/com.greetings -name "*.java")    
    

    编译时并没有报错。是的,服务接口中虽然声明了uses关键字,意味着这个模块需要实现类,但是在编译时他不会寻找需要的实现类模块。

    运行一下看看。

    java -p mods -m com.greetings/com.greetings.Main
    
    Exception in thread "main" java.lang.RuntimeException: No service providers found!
            at com.socket/com.socket.NetworkSocket.open(NetworkSocket.java:16)
            at com.greetings/com.greetings.Main.main(Main.java:7)
    

    报了一个运行时异常,意味着对于服务的具体实现是在运行是寻找的。

    我们把实现类编译一下,然后运行:

    javac --module-path mods -d mods/org.fastsocket $(find src/org.fastsocket -name "*.java") 
    
    java -p mods -m com.greetings/com.greetings.Main
    class org.fastsocket.FastNetworkSocket
    

    成功打印出了类名,是不是有点像OSGI的热插拔。通过模块系统,我们实现了对服务接口和服务提供者的解耦,我们确保服务提供者在调用服务之前能够编译到模块中即可。

    模块补丁

    什么是模块补丁呢,假如我们的项目依赖了一个A模块,但是觉得需要自定义A模块中某些类的功能,那可以修改这个类,然后把修改后的类打到依赖的A模块中。这样,我们的项目就可以使用拥有自定义类的模块了。下面以ConcurrentHashMap为例,ConcurrentHashMapjava.base模块中的一个类,我们将自定义的ConcurrentHashMap类替换java.baseConcurrentHashMap

    先来看一看整个工程的结构:

    ├── src/com.greetings
    │   ├── src/com.greetings/com
    │   │   └── src/com.greetings/com/greetings
    │   │       └── src/com.greetings/com/greetings/Main.java
    │   └── src/com.greetings/module-info.java
    └── src/java.base
        └── src/java.base/java
            └── src/java.base/java/util
                └── src/java.base/java/util/concurrent
                    └── src/java.base/java/util/concurrent/ConcurrentHashMap.java
    
    

    其中有两个模块,com.greeting模块中依赖了java.base中的ConcurrentHashMapjava.base模块里包含了我们重写的ConcurrentHashMap类,而且注意一点,这个模块中没有module-info.java描述文件。再来看看模块的内容:

    module com.greetings {
    }
    
    package com.greetings;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class Main {
        public static void main(String[] args) {
            Map<String, Integer> myMap = new ConcurrentHashMap<>();
            myMap.put("one", 1);
            myMap.put("two", 2);
            myMap.put("three", 3);
            System.out.println("Hello " + myMap);
            System.out.println("Is Duke home? " + myMap.containsKey("Duke"));
        }
    }
    

    虽然module-info.java中没有声明依赖java.base,但是其实所有的模块都是隐式依赖java.base模块的,java.base是所有模块的基本模块,他暴露了java.langjava.util等等基础包。Main.java中的内容也很简单,放入ConcurrentHashMap中几个元素,然后打印。

    再来看看我们重写的ConcurrentHashMap的内容,可以只看put()方法,containsKey()方法和toString()方法:

    package java.util.concurrent;
    
    import java.io.Serializable;
    import java.util.*;
    
    public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {
    
    
    
        private final Map<K, V> innerMap;
    
        public ConcurrentHashMap() {
            this.innerMap = new HashMap<>();
        }
    
        public ConcurrentHashMap(int initialCapacity) {
            this.innerMap = new HashMap<>(initialCapacity);
        }
    
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            this.innerMap = new HashMap<>(initialCapacity, loadFactor);
        }
    
        public Set<Map.Entry<K, V>> entrySet() {
            return innerMap.entrySet();
        }
    
        @Override
        public boolean replace(K key, V oldValue, V newValue) {
            return innerMap.replace(key, oldValue, newValue);
        }
    
        @Override
        public V replace(K key, V value) {
            return innerMap.replace(key, value);
        }
    
        @Override
        public V remove(Object o) {
            return innerMap.remove(o);
        }
    
        @Override
        public boolean remove(Object key, Object value) {
            return innerMap.remove(key, value);
        }
    
        @Override
        public V putIfAbsent(K key, V value) {
            return innerMap.putIfAbsent(key, value);
        }
    
        @Override
        public V put(K key, V value) {
            return innerMap.put(key, value);
        }
    
        @Override
        public V get(Object key) {
            return innerMap.get(key);
        }
    
        @Override
        public void putAll(Map<? extends K, ? extends V> map) {
            innerMap.putAll(map);
        }
        @Override
        public boolean containsKey(Object key) {
            if (Objects.equals(key, "Duke")) {
                return true;
            }
            return super.containsKey(key);
        }
    
        public String toString() {
            return "patched ConcurrentHashMap " + super.toString() + " + Duke";
        }
    }
    

    好了,这就是我们自定义的ConcurrentHashMap类,我们先运行一下不打补丁的com.greetings模块。

    javac --module-path mods 
           -d mods/com.greetings/ 
           src/com.greetings/module-info.java 
           src/com.greetings/com/greetings/Main.java
    
    
    java --module-path mods 
         --module com.greetings/com.greetings.Main
    
    Hello {one=1, two=2, three=3}
    Is Duke home? false
    

    在没有补丁的情况下,使用的是原来的ConcurrentHashMap类。

    试一试对java.base打上补丁:

    javac --patch-module java.base=src 
        -d mypatches/java.base 
         src/java.base/java/util/concurrent/ConcurrentHashMap.java
    
    
    java --module-path mods 
         --patch-module java.base=mypatches/java.base 
          --module com.greetings/com.greetings.Main
          
    Hello patched ConcurrentHashMap {one=1, two=2, three=3} + Duke
    Is Duke home? true
    

    成功了!我们在运行Main方法时候,使用的是自定义的ConcurrentHashMap类,来看看是如何做到的。首先在编译的时候,我们使用了--patch-module这个命令,查看一下javac的帮助文档javac --help-extra可以看到这个命令的解释

    --patch-module <模块>=<文件>(:<文件>)*,使用 JAR文件或目录中的类和资源覆盖

    所以呢,我们把java.base中的ConcurrentHashMap类给覆盖了,在运行时,也有一个--patch-module,其实和上面的解释是一样的,运行时使用mypatches/java.base文件覆盖java.base文件,然后得到了想要的结果。

    自动模块化

    当我们已经开始模块化的工程,但是需要依赖一些非模块化的jar包,这些库的开发人员并没有及时更新库使其适应模块化,他们缺少模块化的描述符,所以我们需要自动模块化--将非模块化的jar包转化成模块化jar。以junit为例,我们先来看看junitjar包的描述信息。

    jar -d --file=lib/junit-4.12.jar 
    找不到模块描述符。已派生自动模块。
    
    junit@4.12 automatic
    requires java.base mandated
    contains junit.extensions
    contains junit.framework
    contains junit.runner
    contains junit.textui
    contains org.junit
    ...
    

    -d等同于--describe-module,用于输出模块描述符或自动模块名称,--file=指定文件名,我们可以看到他为我们派生出junit的版本和描述符。但是不幸的是,他没有告诉我们junit其实还依赖了Hamcrest,我们通过jdeps工具来查看jar包的依赖。

    jdeps -s lib/junit-4.12.jar 
    junit-4.12.jar -> java.base
    junit-4.12.jar -> java.management
    junit-4.12.jar -> 找不到
    

    -s只是查看以来的概览,有些依赖是不确定的,那我们看看丢失了哪些依赖:

    jdeps lib/junit-4.12.jar | grep "找不到"   
    junit-4.12.jar -> 找不到
       org.junit                                          -> org.hamcrest                                       找不到
       org.junit.experimental.results                     -> org.hamcrest                                       找不到
       org.junit.internal                                 -> org.hamcrest                                       找不到
       org.junit.internal.matchers                        -> org.hamcrest                                       找不到
       org.junit.matchers                                 -> org.hamcrest                                       找不到
       org.junit.matchers                                 -> org.hamcrest.core                                  找不到
       org.junit.rules                                    -> org.hamcrest                                       找不到
    

    找到了!那我们看看Hamcrest需要哪些依赖:

    jar -d -f lib/hamcrest-core-1.3.jar 
    找不到模块描述符。已派生自动模块。
    
    hamcrest.core@1.3 automatic
    requires java.base mandated
    contains org.hamcrest
    contains org.hamcrest.core
    contains org.hamcrest.internal
    
    
    jdeps -s lib/hamcrest-core-1.3.jar
    hamcrest-core-1.3.jar -> java.base
    

    Hamcrest没有缺失的依赖了,下面我们看看src文件的内容:

    src
    └── src/com.greetings
        ├── src/com.greetings/main
        │   └── src/com.greetings/main/java
        │       └── src/com.greetings/main/java/com
        │           └── src/com.greetings/main/java/com/greetings
        │               ├── src/com.greetings/main/java/com/greetings/Greet.java
        │               └── src/com.greetings/main/java/com/greetings/Main.java
        ├── src/com.greetings/module-info.java
        └── src/com.greetings/test
            └── src/com.greetings/test/java
                └── src/com.greetings/test/java/com
                    └── src/com.greetings/test/java/com/greetings
                        └── src/com.greetings/test/java/com/greetings/GreetTest.java
    

    这是src的目录结构,然后再看看四个文件的内容:

    cat src/com.greetings/module-info.java
    module com.greetings {
            //1:声明需要的模块
            //2:设置对自动化模块可访问
    }
    
    cat src/com.greetings/main/java/com/greetings/Greet.java
    package com.greetings;
    
    import java.util.Arrays;
    import java.util.stream.Collectors;
    
    public class Greet {
    
        String greeting(String... names) {
            if ((names == null) || (names.length == 0)) {
                return "Hello World!";
            } else {
                return Arrays.stream(names)
                        .map(name -> String.format("Hello %s!", name))
                        .collect(Collectors.joining("
    "));
            }
        }
    
    }
    
    cat src/com.greetings/main/java/com/greetings/Main.java
    package com.greetings;
    
    public class Main {
    
        public static void main(String[] args) {
            Greet greet = new Greet();
            System.out.println(greet.greeting(args));
        }
    
    }
    
    cat src/com.greetings/test/java/com/greetings/GreetTest.java
    package com.greetings;
    
    import org.junit.Test;
    
    import static org.junit.Assert.assertEquals;
    
    public class GreetTest {
    
        @Test
        public void greetingNobody_greetsTheWorld() {
            Greet greet = new Greet();
    
            assertEquals(
                    greet.greeting(),
                    "Hello World!"
            );
        }
    
        @Test
        public void greetingEmptyArray_greetsTheWorld() {
            Greet greet = new Greet();
    
            assertEquals(
                    greet.greeting(new String[0]),
                    "Hello World!"
            );
        }
    
        @Test
        public void greetingOnePerson_greetsThatPerson() {
            Greet greet = new Greet();
    
            assertEquals(
                    greet.greeting("Alice"),
                    "Hello Alice!"
            );
        }
    
        @Test
        public void greetingSeveralPeople_greetsAllOfThem() {
            Greet greet = new Greet();
    
            assertEquals(
                    greet.greeting("Alice", "Bob", "Charlie"),
                    "Hello Alice!
    Hello Bob!
    Hello Charlie!"
            );
        }
    
        @Test
        public void greetingArrayOfPeople_greetsAllOfThem() {
            Greet greet = new Greet();
    
            assertEquals(
                    greet.greeting(new String[]{"Alice", "Bob", "Charlie"}),
                    "Hello Alice!
    Hello Bob!
    Hello Charlie!"
            );
        }
    }
    

    内容其实挺简单,打印Hello,world或者打印Hello,人名,在module-info.java文件中设置了一个小问题,可以试着填一填,答案在文章末尾。大体内容我们知道了,可是我们应该怎么编译呢:

    javac [3:设置自动化模块路径] 
          -d mods/main/com.greetings 
          src/com.greetings/module-info.java 
          src/com.greetings/main/java/com/greetings/Greet.java 
          src/com.greetings/main/java/com/greetings/Main.java
    

    答案也在文章末尾,可以自己试着编译一下。我们来运行一下:

    java --module-path mods/main:lib 
              --module com.greetings/com.greetings.Main
    //Hello World!
    
    java --module-path mods/main:lib 
             --module com.greetings/com.greetings.Main 
            Alice Bob Charlie
    //Hello Alice!
    //Hello Bob!
    //Hello Charlie!
    

    再来编译一下GreetingTest类:

    javac --module-path mods:lib 
              -d mods/test/com.greetings 
              src/com.greetings/module-info.java 
              src/com.greetings/main/java/com/greetings/Greet.java 
              src/com.greetings/test/java/com/greetings/GreetTest.java
    

    运行一下:

    java --module-path mods/main:lib 
             --add-modules com.greetings 
             --patch-module com.greetings=mods/test/com.greetings 
             --module junit/org.junit.runner.JUnitCore 
            com.greetings.GreetTest
    //JUnit version 4.12
    .....
    Time: 0.013
    
    OK (5 tests)
    

    完成!将非模块化jar包转换成自动模块化jar包大体就是这样了,如果遇见不会的java命令,可以通过--help查看说明。

    jlink有什么用呢,其实很简单,我们可以通过jlink构造自己的jre,前文提到rt.jar包很臃肿,jlink可以帮助我们构建精巧的运行环境。那我们先来看看整个项目的内容,内容很简单,和第二个小测试一样,可以快速浏览一下,主要是为了练习使用jlink工具的使用:

    //结构目录
    src
    ├── src/com.greetings
    │   ├── src/com.greetings/com
    │   │   └── src/com.greetings/com/greetings
    │   │       └── src/com.greetings/com/greetings/Main.java
    │   └── src/com.greetings/module-info.java
    └── src/org.astro
        ├── src/org.astro/module-info.java
        └── src/org.astro/org
            └── src/org.astro/org/astro
                └── src/org.astro/org/astro/World.java
                
    //文件内容
    cat src/com.greetings/module-info.java
    module com.greetings { 
            requires org.astro;
    }
    
    cat src/com.greetings/com/greetings/Main.java
    package com.greetings;
    
    import org.astro.World;
    
    public class Main {
            public static void main(String[] args) {
                    System.out.format("Greetings %s!%n", World.name());
            }
    }
    
    cat src/org.astro/module-info.java
    module org.astro { 
            exports org.astro;
    }
    
    cat src/org.astro/org/astro/World.java
    package org.astro;
    
    public class World {
            public static String name() {
                    return "World";
            }
    }
    

    那我们编译一下:

    javac -d mods/com.greetings 
            src/org.astro/module-info.java 
            src/org.astro/org/astro/World.java
            
    javac --module-path mods 
          -d mods/org.astro 
          src/com.greetings/module-info.java 
          src/com.greetings/com/greetings/Main.java
    

    jlink工具目前要求模块路径上的模块以模块化JAR或JMOD格式打包,所以我们将他们打成jar包:

    jar --create 
        --file mlib/org.astro@1.0.jar 
            --module-version 1.0  
            -C mods/org.astro .
            
    jar --create 
        --file mlib/com.greetings.jar 
            --main-class=com.greetings.Main 
            -C mods/com.greetings .
    

    好了,已经打成模块化jar包了,开始jlink!

     jlink --module-path "${JAVA_HOME}"/jmods:mlib 
            --add-modules com.greetings 
            --output executable
    

    完成!我们把执行文件输出到了excutable文件夹,如果对这些命令选项不太理解,可以通过jlink --help来查看命令解释。我们现在已经可以执行了:

    
    ./executable/bin/java --module-path mods --module com.greetings/com.greetings.Main
    //Greetings World!
    ./executable/bin/java -jar mlib/com.greetings.jar com.greetings.Main
    //Greetings World!
    

    是不是很神奇,我们使用了executable中的java命令,对,他就是自定义的jre环境。如果感兴趣可以查看一下excutable中的结构。

    jmod

    jmod可以对模块或jar包压缩的一个工具,但他的格式和jar包不一样。

    直接在上个练习目录中试用命令就好。

    jmod create 
                  --class-path mods/com.greetings:mods/org.astro 
                  greetings.jmod
    

    然后查看一下:

    jmod list greetings.jmod
    classes/module-info.class
    classes/com/greetings/Main.class
    classes/org/astro/World.class
    

    更多的命令解释可以查看一下jmod --help,我就不多做解释了。

    java9的新功能真的挺有意思的,我这只是冰山一角,想了解更详细的内容最好去看oracle的文档,最后附送一张图,是java9模块化的小表单

    最后的最后,上面命令的答案想出来了吗,公布一下结果:

    1.requires junit
    2.opens com.greetings to junit
    3.--module-path lib

  • 相关阅读:
    Redhat6 —— Yum详解
    jQuery——bind绑定多事件用法
    织梦之路——织梦自由列表页分页链接绝对路径化(SEO)
    PHP算法——等宽等像素值截取字符串
    PHP算法——文件大小排序(KB、MB、GB、TB)
    HDU 3647 Tetris
    HDU 2442 Bricks
    ZOJ 2112 Dynamic Rankings
    HDU 3001 Travelling
    HDU 3559 Frost Chain
  • 原文地址:https://www.cnblogs.com/krcys/p/8657282.html
Copyright © 2020-2023  润新知