Java模块化系统从精通到陌生

模块化

好处

  • 强封装
  • 显示依赖
  • 得益于模块化,衍生了jlink等技术

缺点

  • 不向后兼容,非模块的jar,上手难度较大
  • 生态是把双刃剑

如何使用

在项目src目录下创建module-info.java文件,有没有发现 名称居然带-,-在java中不能在-命名,但是为了防止非模块感知工具盲目地将module-info.java或module-info.class作为普通的Java类加以处理。
默认情况下,所有的包为强封装,别人不能访问到该模块的任何东西,隐式依赖java.base模块

module 模块名 {

 }

模块命名

模块名称与java的命名空间是分开的,模块名称必须唯一

参数说明

open 修饰符

open修饰符是可选的,它声明一个开放的模块,一个开放的模块导出所有的包,以便其他模块使用反射访问。是要定义的模块的名称,是一个模块语句。模块声明中可以包含零个或多个模块语句:

exports

导出语句,给其他模块访问。

exports  包名  

限制导出

exports  包名  to  模块名
opens

开放语句(opens),开放当前模块,如果模块没有exports导出项目,可以反射调用等。

requires

声明模块对另一个模块的依赖关系。

requires 模块

依赖传递 A —>B---->C A是不能访问到C的需要添加transitive
添加 transitive 表示为具有传递性(隐式可读性),如果是项目
,记得在idea 项目中的modules 设置Dependencies

requires transitive   模块

巧用 transitive 实现聚合器模式

module library {
    requires transitive library.A;
    requires transitive library.B;
    requires transitive library.C;
    requires transitive library.D;
}

使用static修饰,表示这个依赖的编译时需要

requires static 模块
uses

使用什么 ,表达服务消费。一般为接口 搭配ServiceLoader来加载

举个例子
模块C

open module C {
    exports packC;
}
public interface InterF {

   public void hello();

}

模块D

module D {
      requires C;
      provides packC.InterF with Implement;
}

核心代码

public class Implement implements InterF {
    @Override
    public void hello() {
        System.out.println("hhh");
    }
}

模块A

module A {

    exports A;
    exports pack;    
    requires  C;
    uses packC.InterF;
    requires D;

}

主要代码

    public static void main(String[] args) {
        ServiceLoader<InterF> interFS =
                ServiceLoader.load(InterF.class);
        interFS.stream().forEach((interFProvider) -> {
            System.out.println(interFProvider.type().getFields().length);
            interFProvider.get().hello();
        });
    }

}

输出:
hhh

注意:如果模块A中没有使用 uses packC.InterF; 还使用了ServiceLoader来找InterF 则会失败

provide

用于提供服务 provides 接口 with 具体实现
刚刚的D模块

module D {
      requires C;
      provides packC.InterF with Implement;
}

为什么要搞个消费和提供?
用于将接口和实现分离,当一个公共的接口有多个实现时,将api和实现分开,调用层只需user 需要的api 而无需关心具体实现
在这里插入图片描述
模块中的包名不能相同

用原生命令对模块进行编译打包和运行

编译
javac文档路径

javac -d   输出文件   需要编译的文件路径   module-info路径

打包

jar -cfe  路径/名称.jar  主程序路径  -C 编译文件存放的路径

运行

java -p 模块路径 -m 模块名称

-p == --module-path

-m == --module

如果模块路径 为 class路径 则 -m后为模块名/主程序类路径
多模块路径用分隔符隔开,linux、macOs用:,windows用;

例子
在这里插入图片描述

Microsoft Windows [版本 10.0.19043.1586]
(c) Microsoft Corporation。保留所有权利。

D:\study\JDKStudyN\C>javac -d out   src/packC/C.java src/module-info.java

D:\study\JDKStudyN\C>jar -cfe  mods/c.jar  packC.C  -C out
解析文件参数时出错
尝试使用 `jar --help' 获取详细信息。
# 注意后面 有个空格  和点
D:\study\JDKStudyN\C>jar -cfe  mods/c.jar  packC.C  -C out .  

D:\study\JDKStudyN\C>java -p mods -m C
hhh

D:\study\JDKStudyN\C>

自定义运行时镜像

使用jlink 可以创建应用程序所需要的运行时镜像

常用参数

  • –add-modules mod[, mod…] 将命名模块 , 添加mod到默认的根模块集中。默认的根模块集是空的。
  • –bind-services 链接服务提供者模块及其依赖项。
  • -c={0|1|2}要么–compress={0|1|2} 0:无压缩,1:常量字符串共享,2:压缩
  • –no-header-files 不包括头文件。
  • -p要么–module-path modulepath 指定模块路径。
    如果未指定此选项,则默认模块路径为 J A V A H O M E / j m o d s . 该 目 录 包 含 j a v a . b a s e 模 块 以 及 其 他 标 准 和 J D K 模 块 。 如 果 指 定 了 此 选 项 , 但 j a v a . b a s e 无 法 从 中 解 析 模 块 , 然 后 j l i n k 命 令 附 加 JAVA_HOME/jmods. 该目录包含java.base模块以及其他标准和 JDK 模块。如果指定了此选项,但java.base无法从中解析模块,然后jlink命令附加 JAVAHOME/jmods.java.baseJDKjava.basejlinkJAVA_HOME/jmods到模块路径。
  • –no-man-pages 不包括手册页。
  • –output path 指定生成的运行时映像的位置。
  • –save-opts filename 将选项保存jlink在指定文件中。
  • –launcher 一个启动的脚本程序 --launcher name=module/main-class
    • name是为可执行文件的名称;
    • main-class可以省略

例子
还是我们刚刚的项目

jlink -p  mods/;$JAVA_HOME/jmods  --add-modules C  --launcher hello=C --output  C-image

加载模块资源

因为模块化的存在,通过class.getModule获取所在模块

      Class clazz = ResourcesInModule.class;
      InputStream cz_pkg = clazz.getResourceAsStream("/resource_in_package.txt"); //<1>
      if (cz_pkg!=null)
      System.out.println(new String(cz_pkg.readAllBytes()));

      URL cz_tl = clazz.getResource("/top.txt"); //<2>

      System.out.println(cz_tl);
      
      Module m = clazz.getModule(); //<3>
      
      InputStream m_pkg = m.getResourceAsStream(
              "resource_in_package.txt"); //<4>

      InputStream m_tl = m.getResourceAsStream("top.txt"); //<5>
      System.out.println(new String(m_tl.readAllBytes()));
      System.out.println(new String(m_pkg.readAllBytes()));

加载其它模块的资源呢?

      Optional<Module> otherModule = ModuleLayer.boot().findModule("C"); //<1>
      otherModule.ifPresent(other -> {
         try {

            InputStream resourceAsStream = other.getResourceAsStream("packC/C.class"); //<1> 可以获取到

            System.out.println(resourceAsStream.available());
            
            
            InputStream pkg = other.getResourceAsStream(
                    "packC/in_package.txt"); //<2>  包中资源封闭的为null

            InputStream m_pkg = other.getResourceAsStream(
                    "top_in_package.txt"); //<3>

            System.out.println(new String(m_pkg.readAllBytes()));
            System.out.println(new String(pkg.readAllBytes()));

         } catch (Exception e) {
            throw new RuntimeException(e);
         }
      });

2步骤是获取不到的,包中的资源都是封闭的,但是.class例外,不过要注意不应该依赖其它模块,这样会关系变乱
在 module 上增加open 可开放包

实战

目标创建一个最小的跑Vert.x的JVM runtime image

创建 Maven项目

maven 中使用 模块化和以前的maven项目没什么区别,添加module-info.java
如果jar为自动化路径则他的名称一般为包名去掉版本号然后横杠转化为.
还可以借助强大的Idea,它会提示你

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jmodsTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <vertx.version>4.0.0</vertx.version>
        <main.class>io.vertx.core.Launcher</main.class>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-web</artifactId>
            <version>${vertx.version}</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/modules
                            </outputDirectory>

                            <includeScope>runtime</includeScope>
                            <excludeArtifactIds>
                            </excludeArtifactIds>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <!--打包后的启动入口  -->
                                        <Main-Class>${main.class}</Main-Class>
                                        <Main-Verticle>com.mods.app.App</Main-Verticle>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                            <artifactSet />
                            <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar</outputFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

module-info

module jmodsTest {
     requires io.vertx.web;
     requires io.vertx.core;
     exports com.mods.app;
}

Main-Class

public class App extends AbstractVerticle {

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new App())
                .onFailure(Throwable::printStackTrace);
    }

    @Override
    public void start(Promise<Void> startPromise) throws Exception {
        vertx.createHttpServer().requestHandler(req -> {
            req.response()
                    .putHeader("content-type", "text/plain")
                    .end("Hello World!");
        }).listen(8080);
    }

}

自定义运行时镜像

Maven 中使用 Jlink插件

Jlink插件
使用Jlink 项目中的包不能是非模块的jar,Jlink不支持

非模块Jar转化为模块化
1.导出需要转化的jar

基于dependency插件,使用如下配置,在打包时会将jar导入项目build/modules目录中

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/modules
                            </outputDirectory>

                            <includeScope>runtime</includeScope>
                            <excludeArtifactIds>
                            </excludeArtifactIds>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
2.转化步骤代码

    public static void main(String[] args) throws Exception {
        //jdeps  --module-path D:\study\JDKStudyN\jmodsTest\target\modules --multi-release 11   --ignore-missing-deps   --generate-module-info .      D:\study\JDKStudyN\jmodsTest\target\modules\vertx-web-4.0.0.jar

        String modulePath = "D:\\study\\JDKStudyN\\jmodsTest\\target\\modules";
        String javaHome="C:\\Users\\NINGMEI\\.jdks\\azul-11.0.14";
        String moduleInfoSavePath = modulePath + "\\mod";

        String projectJar="jmodsTest-1.0-SNAPSHOT.jar";
        Files.list(Path.of(modulePath)).forEach((path) -> {
            String jarPath = path.toString();
            String jarName = path.getFileName().toString();
            if (jarName.equals(projectJar)||path.toFile().isDirectory()) {
                return;
            }
            generateModuleInfo( javaHome,  jarPath,  modulePath,  moduleInfoSavePath);
        });

    }

    private static void generateModuleInfo(String javaHome, String jarPath, String modulePath, String moduleInfoSavePath) {
        System.out.println(String.format("开始创建ModuleInfo{%s}",jarPath));
        Path bin = Path.of(javaHome, "bin");
        List<String> cmds = List.of("jdeps",
                "--module-path",
                modulePath,
                "--multi-release",
                "11",
                "--ignore-missing-deps",
                "--generate-module-info",
                moduleInfoSavePath,
                jarPath
        );

        executeCmd(bin.toString(), cmds, (msg) -> {
            System.out.println(msg);
            msg.lines().reduce((first, second) -> second).ifPresent((m) -> {
                if (m.contains("writing")) {
                    String moduleInfoPath = m.replace("writing to ", "");

                    List<String> mInfo = FileUtil.readLines(moduleInfoPath, StandardCharsets.UTF_8);

                    String moduleName = mInfo.get(0).replace("module", "").replace("{", "").trim();

                    compileModuleInfo( javaHome,  modulePath,  jarPath,  moduleName,  moduleInfoPath);
                }
            });

        });

    }

    private static void compileModuleInfo(String javaHome, String modulePath, String jarPath, String moduleName, String moduleInfoPath) {
        System.out.println(String.format("开始编译{%s}",jarPath));
        Path jmods = Path.of(javaHome, "jmods");
        Path bin = Path.of(javaHome, "bin");
        List<String> cmds = List.of("javac",
                "--module-path",
                String.format("%s;%s", modulePath, jmods),
                "--patch-module",
                String.format("%s=%s", moduleName, jarPath),
                moduleInfoPath
        );
        executeCmd(bin.toString(), cmds, (msg) -> {
            System.out.println(msg);
            updJar( javaHome, jarPath, moduleInfoPath);
        });
    }

    private static void updJar(String javaHome,String jarPath,String moduleInfoPath) {
        System.out.println(String.format("开始更新jar{%s}",jarPath));
        Path bin = Path.of(javaHome, "bin");
        List<String> cmds3 = List.of("jar",
                "uf",
                jarPath,
                "-C",
                StrUtil.subBefore(moduleInfoPath, "\\module-info.java", false),
                "module-info.class");

        executeCmd(bin.toString(), cmds3, (msg2) -> {


        });
    }


    private static void executeCmd(String directory, List<String> cmd, Consumer<String> msgHandle) {
        ProcessBuilder processBuilder = new ProcessBuilder();
        // setting up a working directory
        processBuilder.directory(new File(directory));
        processBuilder.command(cmd);
        Process process = null;
        try {
            processBuilder.redirectErrorStream(true);
            process = processBuilder.start();
            process.waitFor(3, TimeUnit.SECONDS);
            msgHandle.accept(getUtf8String(IoUtil.readBytes(process.getInputStream())));
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }


    }


    public static String getUtf8String(byte[] bytes) throws UnsupportedEncodingException {

        String encode = "GBK";
        boolean pdUtf = false;
        pdUtf = validUtf8(bytes);
        if (pdUtf) {
            encode = String.valueOf(StandardCharsets.UTF_8);
        }
        return new String(bytes, encode);

    }

    public static boolean validUtf8(byte[] data) {
        int i = 0;
        int count = 0;
        while (i < data.length) {
            int v = data[i];
            if (count == 0) {
                if ((v & 240) == 240 && (v & 248) == 240) {
                    count = 3;
                } else if (((v & 224) == 224) && (v & 240) == 224) {
                    count = 2;
                } else if ((v & 192) == 192 && (v & 224) == 192) {
                    count = 1;
                } else if ((v | 127) == 127) {
                    count = 0;
                } else {
                    return false;
                }
            } else {
                if ((v & 128) == 128 && (v & 192) == 128) {
                    count--;
                } else {
                    return false;
                }
            }

            i++;
        }

        return count == 0;
    }

一些警告的可以忽略
当出现错误:程序包XX不存在

D:\study\JDKStudyN\jmodsTest\target\modules\mod\io.netty.common\versions\11\module-info.java:16: 错误: 程序包reactor.blockhound.integration不存在
    provides reactor.blockhound.integration.BlockHoundIntegration with
                                           ^
1 个错误

手动将这个jar放到modules目录下,然后执行一下脚本就ok了

3.打包运行时镜像
jlink  --module-path $JAVA_HOME/jmods;modules --add-modules jmodsTest --launcher run=jmodsTest/com.mods.app.App  --output image
4.启动

进入image文件和本地的结构相似,直接进入bin然后cmd启动run.bat

在这里插入图片描述
启动成功!!!

使用 list-modules 可以看jvm中包含的modules

 java --list-modules

输出:

com.fasterxml.jackson.core@2.11.3
io.netty.buffer
io.netty.codec
io.netty.codec.dns
io.netty.codec.http
io.netty.codec.http2
io.netty.codec.socks
io.netty.common
io.netty.handler
io.netty.handler.proxy
io.netty.resolver
io.netty.resolver.dns
io.netty.transport
io.vertx.auth.common
io.vertx.core
io.vertx.eventbusbridge.common
io.vertx.web
io.vertx.web.common
java.base@11.0.14
java.compiler@11.0.14
java.instrument@11.0.14
java.logging@11.0.14
java.management@11.0.14
java.naming@11.0.14
java.security.sasl@11.0.14
jdk.unsupported@11.0.14
jmodsTest@1.0-SNAPSHOT
reactor.blockhound

在这里插入图片描述
只有65M,jdk8 的jre 108M不好含运行jar
在这里插入图片描述

5.启动时或者运行时出现 XX: module XX does not declare uses

这是因为项目中使用了ServiceLoader 如果忘了uses 可以跳到前面看一下。

警告: Default DNS servers: [/8.8.8.8:53, /8.8.4.4:53] (Google Public DNS as a fallback)
Exception in thread "main" java.util.ServiceConfigurationError: io.vertx.core.spi.VerticleFactory: module io.vertx.core does not declare `uses`

说明jdeps在生成的时候没有分析到,这时候需要我们手动修改模块 io.vertx.core的module-info

   .....省略
   uses io.vertx.core.spi.VerticleFactory;

然后使用刚刚转化步骤中的compileModuleInfo 重新编译更新jar或者手动输入命令

 javac  --module-path $JAVA_HOME/jmods;modules  --patch-module io.vertx.core=D:\study\JDKStudyN\jmodsTest\target\modules\vertx-core-4.0.0.jar     D:\study\JDKStudyN\jmodsTest\target\modules\mod\io.vertx.core\versions\11\module-info.java

 jar uf modules\vertx-core-4.0.0.jar  -C modules\mod\io.vertx.core\versions\11 module-info.class

然后重新jlink一下就ok

极致压缩

有人说60M和100M差不多啊没什么区别啊

jlink  --module-path $JAVA_HOME/jmods;modules --no-header-files --no-man-pages --compress=2  --add-modules jmodsTest --launcher run=jmodsTest/com.mods.app.App  --output miniImage

在这里插入图片描述
有人说:磁盘现在不怎么要钱啊!
在这里插入图片描述
其实这意味着jvm可以和项目捆绑发布,做到用户无感知。适应云原生

创建不同平台的运行时镜像

下载你需要的jdk 并解压,然后将module-path 指向下载的jdk中的jmods即可

参考:

深入理解java模块化系统
Java9模块化开发:核心原则和实践
jdk11 官方文档

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值