最近文章

Redis内部数据结构——词典dictType以及哈希算法的选择

词典dict概览提到了使用Hash Table作为Redis 词典的内部实现,需要考虑三个要点。这章讨论第一个问题:Redis哈希算法的选择。哈希是把任意长度的输入通过哈希算法转换为固定长度的值。根据不同的使用场景,人们设计出了多种哈希算法,我们常见的有CRC,MD5,HMAC,SHA-256等等,关于多种哈希算法,可以在查看wiki。Redis作为一个高性能的key-value内存服务器,哈希算
标签:

Redis内部数据结构——词典哈希表dictht

这一节主要讨论”如何解决Hash碰撞“的问题,去理解Redis的哈希表dictht。解决Hash冲突方法我们知道,使用哈希函数对输入计算出的哈希值是相同,称为哈希碰撞。发生哈希碰撞通常有以下两种做法处理冲突的存储:链表法开放寻址法链表法链表法是Redis选择解决hash冲突的方法,数据结构为:数组 + 链表。数组的每个位置,称之为桶(bucket)或槽(slot)。当计算的hash值在同一个槽位时
标签:

Java初始化静态Map,且不可更改(含Java 9)

Java 9之前Java 9之前需要在static块里初始化静态Map。public class Demo{     private static final Map<Integer, String> myMap;    &nb
标签:

Java 10 赋值Arrays.asList给var变量:AssertionError: Unexpected intersection type

体验了下Java 10的局部变量类型推断var。使用List.of()给var赋值。List list1 = Arrays.asList(1, "a",10L); var list2 = list1; 上面通过中间的list1再赋值给var,编译通过。但如果直接使用Arrays.asList()赋值给var,如:var
标签:

Redis内部数据结构——dict词典概览

了解词典在了解Redis如何实现dict之前,先梳理下dict特征以及使用。词典数据结构通常用于以键值对的方式存储数据。词典存放了一组键,每个键关联一个值。词典存放的键是唯一的,通过查询词典里的键,可以获取键对应的值。如果存储重复的键,会导致新值覆盖键关联的旧值。词典数据结构支持以下几种操作:addOrUpdate(K key, V v):以键值对的形式插入数据,如果key在词典中存在,会用新值覆
标签:

[译]代码优先的Java 9模块系统教程(一)

Java平台模块系统(JPMS)将模块化带入Java和JVM,并改变了我们在大型应用中的编程方式。 为了充分利用它,我们需要很好地了解,第一步是学习基础知识。 在本教程中,我首先向你展示一个简单的Hello World示例,然后我们用Java 9将一个现有的demo程序模块化。我们将创建模块声明(module-info。java),使用模块路径来编译,打包,
标签:

Spring JPA报错:InvalidDataAccessResourceUsageException: could not extract ResultSet;

Spring JPA调用时报错InvalidDataAccessResourceUsageException: could not extract ResultSet错误,详细如下:org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nest
标签:

Spring JPA报错:InvalidDataAccessApiUsageException: Executing an update/delete query

集成Spring JPA,在执行delete,update时需要添加事务。错误信息:org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredExcepti
标签:

Spring JPA报错:Not supported for DML operations

在一个Spring JPA项目中,执行更新或删除是报错:Not supported for DML operations解决方法:只需要添加注解@Modifying即可。@Modifying@Query(value = "update User user set user.address = :address where user.id = :id ")void updateUserAddress
标签:

Ubuntu 18.10 LTS上安装JDK 11.0.6

下载JDK首先到Oracle官网下载JDK,JDK 11的下载地址:https://www.oracle.com/java/technologies/javase-jdk11-downloads.html下载的是linux通用版本:jdk-11.0.6_linux-x64_bin.tar.gz,如图: 安装JDK安装压缩包版本的jdk比较简单,创建一个安装路径,然后把压
标签:

Java防止非静态内部类内存泄漏

内存泄漏一个不会被使用的对象,因为另一个正在使用的对象持有该对象的引用,导致它不能正常被回收,而停留在堆内存中,从而导致内存泄漏。最坏的情况下,由于大量的内存泄漏,最终导致jvm的内存耗尽,致使程序奔溃。也可能会导致内存空间不足,jvm出现频繁的GC。代码示例import java.util.ArrayList;class OuterClass{ private int[] data; p
标签:

查询Redis数据库的数量

redis数据库的数量是固定的,可以在redis.conf查询到redis数据库的数量。使用grep命令:$ cat redis.conf | grep databasesdatabases 16当然,也可以在redis-cli上查询数据库的数量:127.0.0.1:6379> CONFIG GET databases1) "databases"2) "16"使用config get dat
标签:

Redis设置key过期以及删除过期key的策略

Redis有以下几个命令用于设置key的过期时间:expire <key> <seconds>:给key设置以秒为单位的过期时间pexpire <key> <milliseconds>:给key设置以毫秒为单位的过期时间expireat <key> <timestamp_in_seconds>:指定一个以秒为单位的时间戳,当到
标签:

Java Predicate接口的使用

Java 8新增了Predicate接口,它是一个函数接口,提供的test函数会接收一个参数,并返回一个bool值,我们可以用它来做过滤,检测类等功能。源码说明@FunctionalInterfacepublic interface Predicate<T> { /** * 具体过滤操作 需要被子类实现. * 用来处理参数T是否满足要求,可以理解为 条件A
标签:

阿里云ECS默认禁用25端口导致发邮件失败:Couldn't connect to host, port: smtp.example.com

使用JavaMail发送邮件,用的是阿里云的邮件服务。在本地测试是没有问题,但是上传到阿里云的ecs上就报错:MailSendException: Mail server connection failed; nested exception is com.sun.mail.util.MailConnectExcept

Java验证公私密钥对

验证过程:创建一个足够长的随机字节数,称为challenge使用密钥签名challenge使用公钥验证challenge示例代码:KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");  //密钥生成器 keyGen.initialize(2048); Ke
标签:

[译]代码优先的Java 9模块系统教程(二)

服务监控(ServiceMonitor)让我们来想象一个提供娱乐服务的网络,可能是社交网络或者是视频网络。我们希望监控这些服务,以确定系统的健康状况,并且在发生问题时能够发现,而不是客户报告。 这就是ServiceMonitor示例程序所要做的:监视这些服务(另一个惊喜)。幸运的是,服务已经收集了我们想要的数据,ServiceMonitor所需要做的就是定期查询。 不幸的是,并
标签:

Redis内部数据结构——词典dictType以及哈希算法的选择

词典dict概览提到了使用Hash Table作为Redis 词典的内部实现,需要考虑三个要点。这章讨论第一个问题:Redis哈希算法的选择。

哈希是把任意长度的输入通过哈希算法转换为固定长度的值。根据不同的使用场景,人们设计出了多种哈希算法,我们常见的有CRC,MD5,HMAC,SHA-256等等,关于多种哈希算法,可以在查看wiki

Redis作为一个高性能的key-value内存服务器,哈希算法需要从计算效率以及减少碰撞的角度选择。

dictType

dict的数据结构:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

dictType *type;一个指向dictType结构的指针(type)。它通过自定义的方式使得dict的key和value能够存储任何类型的数据。其中包括自定义的哈希函数。

dict.h/dictType的数据结构:

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;

dictType结构包含几个函数指针,允许dict的调用者自定义涉及key和value的操作。

  • hashFunction:定义对key进行哈希值计算的哈希算法。
  • keyDup和valDup:定义key和value的拷贝函数,用于对key和value进行深拷贝,而不仅仅是传递对象指针。
  • keyCompare:定义key的比较操作,在根据key进行查找时会用到。
  • keyDestructor和valDestructor:定义对key和value的析构函数。
    私有数据指针(privdata)就是在dictType的某些操作被调用时会传回给调用者。

dict.c Redis哈希算法siphash

从dictType数据结构,我们知道Redis的哈希算法是由dictType的函数指针*hashFunction定义。在dict.c中有dictType的实现:

dictType BenchmarkDictType = {
    hashCallback,
    NULL,
    NULL,
    compareCallback,
    freeCallback,
    NULL,
    NULL
};
uint64_t hashCallback(const void *key) {
    return dictGenHashFunction((unsigned char*)key, strlen((char*)key));
}
uint64_t dictGenHashFunction(const void *key, int len) {
    return siphash(key,len,dict_hash_function_seed);
}

在dict.c定义了BenchmarkDictType,最后我们跟到提供哈希算法的是siphash。

如果对Redis历史版本了解,在3.2, 3.0, 2.8, 2.6 使用的MurmurHash2 哈希算法,而4.0以后改为了siphash。以下是Redis作者antirez对选择siphash的一个解释:

Redis uses a reduced rounds version of siphash that is faster than the hashing fucntion we were using, so upgrading was actually a speedup (verified experimentally). AFAIK there are no known attacks on the reduced rounds we use, so for know the current approach sounds good.

大意是:Redis对siphash做了部分删减,经实验验证,改后的siphash要比旧版本的哈希算法(murmurhash2)要快,并且没有发现一些已知的攻击。

这里要提一下哈希洪水攻击(Hash-Flooding Attack),n个key通过hash计算出来的值都相同,那么就会退化成链表,这样查找1个数的时候复杂度就是O(n)。哈希碰撞攻击在知道hash算法的前提下恶意构造n个相同hash值的key,这样hash表的查找效率就被拖慢到了O(n^2)。

查了一些资料,siphash是计算快,计算的哈希值分布均匀良好且不可以预测。可以认为是碰撞极少的一种哈希算法。

 

 

 

Redis内部数据结构——词典哈希表dictht

这一节主要讨论”如何解决Hash碰撞“的问题,去理解Redis的哈希表dictht。

解决Hash冲突方法

我们知道,使用哈希函数对输入计算出的哈希值是相同,称为哈希碰撞。发生哈希碰撞通常有以下两种做法处理冲突的存储:

  1. 链表法
  2. 开放寻址法

链表法

链表法是Redis选择解决hash冲突的方法,数据结构为:数组 + 链表

数组的每个位置,称之为桶(bucket)或槽(slot)。当计算的hash值在同一个槽位时,即发生哈希碰撞,后面的元素就依次添加到链表中。

查找过程就是先查到数组的槽位,再从链表中查找需要的key。

开放寻址法

开发地址法中,若发生哈希冲突,数据项不能直接存放在计算出来的数组下标时,就要寻找其他的位置。常用有三种方法:线性探测、二次探测以及再哈希法。

  • 线性探测:插入数据时,如果数据计算的数组下标已经被占用,就从当前位置开始,依次往后查找,直到找到为止
  • 二次探测:线性探测步长固定为1,而二次探测依次步长,是二的2次方,即hash(key)+0hash(key)+1^2,hash(key)+2^2
  • 再哈希法:当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突。

Redis Hash Table的实现dictht

哈希表在dict/dictht.h文件中定义:

typedef struct dictht{
  // 哈希表的数组
  dictEntry **table;
  // 哈希表的大小
  unsigned long size;
  // 哈希表的大小的掩码,用于计算索引值,总是等于 size-1
  unsigned long sizemask;
  // 哈希表中已有的节点数量
  unsigned long used;
}dictht;
  • table:哈希表数组,数组中的每个元素都是一个链表。
  • size:哈希表数组的大小。
  • sizemask:是哈希表数组大小的掩码,取值为size-1。
  • used:哈希表已有节点的数量,即dictEntry的总数。

计算下标公示hash(key) & sizemask,如图:

 

表节点定义dict/dictEntry:

ypedef struct dictEntry {
    // 键
    void *key;
    // 值,使用union以适应不同类型的值
    union {
        void *val; // 整数与浮点数直接存储,其它类型使用引用
        uint64_t u64; // 无符号整数
        int64_t s64;  // 有符号整数
        double d;  // 浮点数
    } v;
    // 指向下一个节点
    struct dictEntry *next;
} dictEntry;

dictEntry是哈希表的节点,键值对数据保存在dictEntry节点。

  • key:指针指向哈希表中的键。
  • v:用于保存字典的值,是一个union类型,u64和s64可直接保存64位的整数值。如果值不是整数,则通过指针val来指向数据所在内存。
  • next指向下一个dictEntry节点,从而形成一个链表。

完整结构如图:

 

Java初始化静态Map,且不可更改(含Java 9)

Java 9之前

Java 9之前需要在static块里初始化静态Map。

public class Demo{
    private static final Map<Integer, String> myMap;
    static {
        Map<Integer, String> aMap = new HashMap<>();
        aMap.put(1, "one");
        aMap.put(2, "two");
        myMap = Collections.unmodifiableMap(aMap);
    }
}

Java 9

Java 9的Map提供了of()工厂方法用来获取不可更改的Map

private static final Map<Integer, String> myMap = Map.of(1, "one", 2, "two");

Java 10 赋值Arrays.asList给var变量:AssertionError: Unexpected intersection type

体验了下Java 10的局部变量类型推断var。使用List.of()给var赋值。

List list1 = Arrays.asList(1, "a",10L);
var list2 = list1;

上面通过中间的list1再赋值给var,编译通过。但如果直接使用Arrays.asList()赋值给var,如:

var list3 = Arrays.asList(1, "a",10L);

报AssertionError:

java.lang.AssertionError: Unexpected intersection type: java.lang.Object&java.io.Serializable&java.lang.Comparable<? extends java.lang.Object&java.io.Serializable&java.lang.Comparable<?>> 
        at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter.enterInner(ClassWriter.java:1043) 
        at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter$CWSignatureGenerator.classReference(ClassWriter.java:312) 

原因:

这是java 10的bug,查看:https://bugs.openjdk.java.net/browse/JDK-8199910。现在这个bug已经修复。

解决方法:

1、javac编译不要使用-g

在IDEA禁用-g:

Settings → Build, Execution, Deployment → Compiler → Java Compiler → 去掉 "Generate Debugging Info"勾选
 

2、声明明确的类型,例如第一种情况的使用。

Redis内部数据结构——dict词典概览

了解词典

在了解Redis如何实现dict之前,先梳理下dict特征以及使用。

词典数据结构通常用于以键值对的方式存储数据。词典存放了一组键,每个键关联一个值。词典存放的键是唯一的,通过查询词典里的键,可以获取键对应的值。如果存储重复的键,会导致新值覆盖键关联的旧值。

词典数据结构支持以下几种操作:

  • addOrUpdate(K key, V v):以键值对的形式插入数据,如果key在词典中存在,会用新值覆盖旧值。
  • search(K key):传入搜索的键作为参数,返回此键关联的值。
  • delete(K key):删除指定键的条目。

词典的实现

根据词典的特征,有哪些实现方案呢。通常的实现方法有:

  • 简单实现:以直接寻址的方式存放在有序或无序的序列中。
  • Hash Table:对词典的key做哈希,存放在哈希表中。
  • Tree:以树状数据结构实现,例如:
    • 二叉查找树
    • AVL树
    • 红黑树
    • B树
    • 平衡树

Redis词典是基于Hash Table实现,Hash Table在增删查的平均时间复杂度是O(1),只有在发生了hash code碰撞差的情况才会到O(n)。但Hash Table需要预分配存储的空间,是以空间换时间的一种方案。

大部分的编程语言也比较常用Hash Table作为词典的内部实现。

Hash Table实现词典需要考虑的要点

Hash,中文称为散列,或音译为“哈希”,它会把任何长度的输入,通过哈希算法计算得到固定长度的hash值。相同的输入通过相同的哈希算法得到的hash值是相同,且通常hash值所占用的空间比输入值要小。

Hash Table把Hash的这个特点应用到存储上,这个过程简化是:使用hash算法计算输入hash值,再把hash值映射为存储的某个位置进行存储。

如图:

要高效存储查询,需要我们在这个映射的过程中考虑几点:

  1. 评估哈希算法的效率以及均衡性
    1. 哈希算法Hash Function计算要快,否在会影响存储以及查找效率
    2. 哈希算法计算得到的hash值,通过索引映射后,能够比较均匀的分配在存储上。
  2. 如何解决Hash碰撞:不同的输入计算得到的hash值可能是相同的,这种情况称为Hash碰撞。需要解决hash碰撞导致的存储位置冲突问题。
  3. 如何解决Hash Table的扩容:Hash Table一般会预分配好固定大小的存储空间,当存储不够时如何扩容。

带着这几个问题有助于我们去理解Redis的词典的实现。

Redis实现词典的数据结构

整个Redis实现词典的数据结构如图:

各个数据结构说明:

  • dict:词典数据结构,它包含了两个哈希表,其中ht[0]存放数据,ht[1]用于扩容的rehash(重新hash分配空间)。reshashidx存放rehash的索引。
  • dictht:哈希表,实现了哈希的数据结构,包含用于存散列节点的数组table,数组大小size以及数组已使用索引数used。
  • dictEntry:哈希表的节点,它存放了key-value的数据以及相同存储位置索引的链表的下一节点。

在后面章节带着问题再进一步了解Redis对词典的实现。

[译]代码优先的Java 9模块系统教程(一)

更新于 2021.12.18 1分钟阅读 0 评论 5 推荐

    Java

    作者: 陈川
  1. [译]代码优先的Java 9模块系统教程(一) Page 1
  2. [译]代码优先的Java 9模块系统教程(二) Page 2

Java平台模块系统(JPMS)将模块化带入Java和JVM,并改变了我们在大型应用中的编程方式。 为了充分利用它,我们需要很好地了解,第一步是学习基础知识。 在本教程中,我首先向你展示一个简单的Hello World示例,然后我们用Java 9将一个现有的demo程序模块化。我们将创建模块声明(module-info。java),使用模块路径来编译,打包,以及运行程序——先代码,然后再解释,所有你可以直入主题。

教程使用了两个项目,都可以在GitHub上找到:第一个是非常简单的Hello World示例,另一个是ServiceMonitor,这和我一本关于模块系统的书上是用的同一个项目。如果你想仔细看下就把它检出来(check out)。所有的命令(如javac、jar、java)都是指Java 9的变体。

你好,模块化世界

我们从最简单的应用开始,一个打印Hello,Modular World的应用! 下面是这个类:

package org.codefx.demo.jpms;
 
public class HelloModularWorld {
 
    public static void main(String[] args) {
        System.out.println("Hello, modular World!");
    }
 
}

要成为一个模块,它在项目源码的根目录下需要一个module-info.java:

module org.codefx.demo.jpms_hello_world {
    // 这个模块只需要基础模块‘java.base’里的类型;
    // 因为每一个Java模块都需要'java.base',没必要明确引入它——这里是为了演示做的
    requires java.base;
    // 此处的导出在这个应用里是没有意义的,再次是为演示做的
    exports org.codefx.demo.jpms;
}

按通用的目录结构src/main/java,程序的目录如下:

这些是编译,打包,以及启动的命令:

$ javac
    -d target/classes
    ${source-files}
$ jar --create
    --file target/jpms-hello-world.jar
    --main-class org.codefx.demo.jpms.HelloModularWorld
    -C target/classes .
$ java
    --module-path target/jpms-hello-world.jar
    --module org.codefx.demo.jpms_hello_world

除了使用一种称为“模块路径”的东西以及可以定义项目的主类(没有清单)外,非常类似于我们在非模块化应用里所做的。 我们看下它是如何工作的。

模块

JPMS的基本构建块是模块(惊喜!)。 像JAR一样,它们是类型和资源的容器; 但与JAR不同,它们有其他特征 - 这些是最基础的:

  • 名称,最好是全局唯一
  • 依赖其他模块的声明
  • 由导出的包组成的清晰定义的API

JDK被分为大约一百个所谓的平台模块。 可以使用java --list-modules列出它们,使用java --describe-module $ {module}查看单个模块。 继续,试用下java.sql或java.logging:

$ java --describe-module java.sql
 
> java.sql@9
> exports java.sql
> exports javax.sql
> exports javax.transaction.xa
> requires java.logging transitive
> requires java.base mandated
> requires java.xml transitive
> uses java.sql.Driver

模块的属性在模块声明中定义,它在项目根目录下的module-info.java文件里,如下所示:

module ${module-name} {
    requires ${module-name};
    exports ${package-name};
}

它被编译为module-info.class,称为模块描述符,放在JAR的根目录下。 这个描述符是普通JAR和模块化JAR之间的唯一区别。

我们逐个看模块的三个属性:name,dependencies,exports。

名字(Name)

JAR缺少的最基本的属性是编译器和JVM可以用来识别JAR的名称。 因此,它是模块最突出的特征。我们将有可能,甚至是有义务给每个模块创建一个名字。

命名模块通常是很自然的,因为我们每天使用的大多数工具,无论是IDE,构建工具,甚至发布问题跟踪以及版本控制系统,都已经让我们命名了项目。但是为了让名字在搜索模块名时有意义,明智的选择很重要!

模块系统严重依赖模块的名字。 冲突或演变的名字特别容易引起麻烦,所以重要的是名字是:

  • 全局唯一
  • 稳定

实现这一目标的最佳方式是已经常用于包名的反域名的命名方案:

module org.codefx.demo.jpms {
 
}

依赖(Dependencies)

JAR包里缺失的另一件是声明依赖关系的能力,但是使用模块系统,这些时代已经结束了:所有在JDK模块以及第三方库或框架上的依赖关系必须明确。

依赖关系用requires语句声明,语句包括关键字本身以及后面的模块名称。 扫描模块时,JPMS将构建一个模块图,其中模块是节点,而requires语句则变为所谓的可读边界 —— 如果模块org.codefx.demo.jpms requires模块java.base,在运行时org.codefx.demo .jpms读取java.base。

如果模块系统找不到所需正确名称的模块,这意味着如果模块缺失,编译以及启动应用将会失败。 这实现了模块系统其中一个目标——可靠配置,但可以非常严格 —— 检出我关于可选依赖的帖子,看下更宽松的替代方案。

Hello World示例所需的所有类型都可以在JDK模块java.base(所谓的基础模块)里找到。因为它包含了核心的类型如Object,所有的Java代码都需要它,所有它不必明确要求。我在这个例子里仍然这样做,以便给你们演示requires语句:

module org.codefx.demo.jpms {
    requires java.base;
}

导出(Exports)

模块列出了它导出的包。 一个模块的代码(例如org.codefx.demo.jpms)访问另一个模块的类型(例如java.base中的String),必须满足以下可访问性规则:

  • 访问类型(String)必须是public
  • 包含类型(java.lang)的包必须被其模块(java.base)导出
  • 访问模块(org.codefx.demo.jpms)必须读取被访问的包(java.base),通常是用requires来实现。

如果在编译或运行时违反了任何这些规则,模块系统会抛出错误。 这意味着public不再是真正公开。 非导出包里的public类型和导出包里的非public类型对外部来说都是不可访问的。 还要注意,反射也失去了超能力。 除非使用命令行标志,否则它受到完全相同的可访问性规则的约束。

我们的示例没有有意义的API,没有外部代码需要访问它,所以我们实际上不需要导出任何东西。这里的导出仅是为了演示:

module org.codefx.demo.jpms_hello_world {
    requires java.base;
    exports org.codefx.demo.jpms;
}

模块路径(Module Path)

现在已知道如何定义模块及其基本属性。 还有一点不清楚的是我们如何告诉编译器和运行时。 答案是一个与类路径(class path)相似的新概念:

模块路径是一个构件或包含构件目录的列表。 根据操作系统不同,模块路径在基于Unix的操作系统下使用冒号:隔开,在Windows使用分号;隔开。 模块系统用它来定位在平台模块中找不到的所需模块。 javac和java以及其他与模块相关的命令都可以处理它 —— 命令行选项是--module-path和-p。

模块路径上的所有构件都变成模块。 普通的JAR包则变成自动模块

编译、打包、运行(Compiling, Packaging, Running)

编译工作很像没有模块系统一样:

$ javac
    -d target/classes
    ${source-files}

(你必须用实际的文件替换$ {source-files},但是这是示例,我就不这样做了)。

源文件里一旦有module-info.java,模块系统就会启动。 编译所需模块的所有非JDK依赖项都需要在模块路径里。 对于Hello World示例,没有这样的依赖关系。

使用jar的打包也不变。 唯一的区别是,我们不再需要一个清单来声明应用程序的入口点 —— 我们可以使用--main-class来表示:

$ jar --create
    --file target/jpms-hello-world.jar
    --main-class org.codefx.demo.jpms.HelloModularWorld
    -C target/classes .

最后,启动看起来有点不同。 我们使用模块路径而不是类路径来告诉JPMS哪里可以找到模块。 我们需要做的只是用--module来命名主模块:

$ java
    --module-path target/jpms-hello-world.jar
    --module org.codefx.demo.jpms_hello_world

就是这样! 我们创建了一个非常简单但仍然是模块化的Hello-World应用程序,成功构建并启动。 现在是时候来看一个稍微不那么简单的例子来看看依赖和导出等机制。

原文:Code First Java 9 Module System Tutorial

Spring JPA报错:InvalidDataAccessResourceUsageException: could not extract ResultSet;

Spring JPA调用时报错InvalidDataAccessResourceUsageException: could not extract ResultSet错误,详细如下:

org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:279)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:253)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)

这个错误的原因时,字段不存在,或者错误。需要检查下是否拼写错误。


Spring JPA报错:InvalidDataAccessApiUsageException: Executing an update/delete query

集成Spring JPA,在执行delete,update时需要添加事务。

错误信息:

org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:402)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)

解决:通过添加注解@Transactional

@Transactional
@Modifying
@Query(value = "update User user set user.address = :address where user.id = :id ")
void updateUserAddress(@Param("id") int id,@Param("address") String address);

注意,添加的是import org.springframework.transaction.annotation.Transactional。

Spring JPA报错:Not supported for DML operations

在一个Spring JPA项目中,执行更新或删除是报错:

Not supported for DML operations

解决方法:只需要添加注解@Modifying即可。

@Modifying
@Query(value = "update User user set user.address = :address where user.id = :id ")
void updateUserAddress(@Param("id") int id,@Param("address") String address);




Ubuntu 18.10 LTS上安装JDK 11.0.6

下载JDK

首先到Oracle官网下载JDK,JDK 11的下载地址:

https://www.oracle.com/java/technologies/javase-jdk11-downloads.html

下载的是linux通用版本:jdk-11.0.6_linux-x64_bin.tar.gz,如图:

安装JDK

安装压缩包版本的jdk比较简单,创建一个安装路径,然后把压缩包解压的安装目录即可

mkdir -p /usr/local/java/
cp jdk-11.0.6_linux-x64_bin.tar.gz /usr/local/java
cd /usr/local/java
tar zxvf jdk-11.0.6_linux-x64_bin.tar.gz

配置环境

编辑/etc/profile:

sudo vi /etc/profile

在文件的末尾添加以下内容

JAVA_HOME=/usr/local/java/jdk-11.0.6
JRE_HOME=${JAVA_HOME}/jre
CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
export JAVA_HOME JRE_HOME CLASS_PATH PATH

注意:JAVA_HOME的值为你jdk安装的根目录

最后,使用java -version来验证安装的java版本。


Java防止非静态内部类内存泄漏

内存泄漏

一个不会被使用的对象,因为另一个正在使用的对象持有该对象的引用,导致它不能正常被回收,而停留在堆内存中,从而导致内存泄漏。

最坏的情况下,由于大量的内存泄漏,最终导致jvm的内存耗尽,致使程序奔溃。也可能会导致内存空间不足,jvm出现频繁的GC。

代码示例

import java.util.ArrayList;
class OuterClass
{
private int[] data;
public OuterClass(int size)
{
data = new int[size];
}
class InnerClass
{
}
InnerClass getInnerClassObject()
{
return new InnerClass();
}
}
public class MemoryLeak
{
public static void main(String[] args)
{
ArrayList al = new ArrayList<>();
int counter = 0;
while (true)
{
al.add(new OuterClass(100000).getInnerClassObject());
System.out.println(counter++);
}
}
}

执行以上代码,输出结果,最终导致了堆内存溢出。

7639
7640
7641
7642
7643
7644
7645
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at EnclosingClass.<init>(MemoryLeak.java:9)
at MemoryLeak.main(MemoryLeak.java:30)

原因分析

我们使用java提供的工具javap,可以对编译生成的.class文件做分析。也可以使用一些反编译工具对生成的.class反编译,也可以看到内部类的代码实现。

javap OuterClass$InnerClass

输出类似于:

Compiled from "OuterClass.java"
public class OuterClass$InnerClass {
final OuterClass this$0;
public OuterClass$InnerClass(OuterClass);
}

可以看到内部类InnerClass存在一个this$0的OuterClass的变量,此变量是通过InnerClass的构造函数传进来的。

示例代码中创建了一个数组列表ArrayList,它将用来存放InnerClass的对象。生成InnerClass对象前,需要先构造OuterClass。OuterClass构造时默认创建了一个100000大小的整型数组。相当于一个初始化的OuterClass默认占用10000个int整型的空间。在MemoryLeak,通过循环while不断向a1添加InnerClass对象。对于每次循环来说new OuterClass后,就不会再使用OuterClass对象。但通过分析内部了的实现,即使OuterClass对象不会再被使用,内部类InnerClass对象里还是保存了大对象OuterClass,导致OuterClass的生命周期是和InnerClass一样,最终导致内存泄漏。

非静态内部类使用注意

1、如果外部类是一个大对象,必须要谨慎使用非静态内部类,特别时生命周期长的非静态内部类,这样容易造成内存溢出OutOfMemoryError。

2、之所以强调非静态,对于静态内部类来说,它的内部实现是不存放外部类的,所以再合理编码的情况下,使用静态内部类。


查询Redis数据库的数量

redis数据库的数量是固定的,可以在redis.conf查询到redis数据库的数量。使用grep命令:

$ cat redis.conf | grep databases
databases 16

当然,也可以在redis-cli上查询数据库的数量:

127.0.0.1:6379> CONFIG GET databases
1) "databases"
2) "16"

使用config get databases可以获取的数据库的数量

Redis设置key过期以及删除过期key的策略

Redis有以下几个命令用于设置key的过期时间:

expire <key> <seconds>:给key设置以秒为单位的过期时间
pexpire <key> <milliseconds>:给key设置以毫秒为单位的过期时间
expireat <key> <timestamp_in_seconds>:指定一个以秒为单位的时间戳,当到达此时间时,key过期
pexpireat <key> <timestamp_in_milliseconds>:指定一个以毫秒为单位的时间戳,当到达此时间时,key过期
setex <key> <seconds> <value> : 设置key的同时,给key添加过期时间,单位为秒

查看过期时间的命令

ttl <key> : 查看Key的过期时间(秒数),用不过期返回(integer) -1,Key不存在返回(integer) -2
pttl <key>:同ttl,返回毫秒数

示例:

127.0.0.1:6379> set a test
OK
127.0.0.1:6379> EXPIRE a 5
(integer) 1
127.0.0.1:6379 > get a // distance setup lifetime command executed in 5 seconds
"test"
127.0.0.1:6379 > get a // distance setup lifetime command executed after 5 seconds
(nil)
127.0.0.1:6379> set b 12
OK
127.0.0.1:6379> EXPIREAT b 1545569500
(integer) 1
127.0.0.1:6379> time
1) "1545569486"
2) "108616"
127.0.0.1:6379> get b//distance setting 1545569500 specified seconds timestamp execution
"12"
127.0.0.1:6379> time
1) "1545569506"
2) "208567"
127.0.0.1:6379 > get B // distance setting 1545569500 specified seconds timestamp to execute
(nil)

取消key的过期时间的设置

persist < key >:用于取消对key设置的过期时间
127.0.0.1:6379> EXPIRE c 1000
(integer) 1
127.0.0.1:6379 > TTL C // expiration time
(integer) 9996
127.0.0.1:6379> PERSIST c
(integer) 1
127.0.0.1:6379 > TTL C // no expiration time
(integer) -1
PS: TTL is the unit of seconds, the remaining survival time of the return key; similarly, the pttl command is the unit of milliseconds, the remaining survival time of the return key

删除过期key的策略

1、定时删除

在设置key的到期时间时,创建一个定时器,当key到期时立即删除key。

优点:

定时删除的这种方式是对内存最友好的,当key到期就马上释放key占用的内存。

缺点

对CPU不友好,特别是存在很多过期的key时,删除一个过期的key需要占用相当大的cpu时间。所以在内存不紧张,而CPU紧张的场景下,不宜使用此策略。

2、懒删除

懒删除的方式是不会在key过期时马上删除key,它会在每次向键空间读写时,检查键是否过期,如果过期了才真正删除它,否则返回key的值。

优点:

对CPU友好,只有操作时检查键是否过期,而不是定时的浪费CPU时间去检查。

缺点:

对内存不友好,这是很明显的,如果有大量过期的key,在一定时间内没有操作,那么它就会常驻在内存里,如果一直不操作,就相当于内存泄漏了。

3、按规则删除

考虑到上面两种删除策略的优缺点,redis结合了这两种策略来对key进行删除:懒删除和定时删除,已达到CPU和内存使用的平衡。

在redis server,懒删除是内置的策略,可以对定时删除设置执行时间和频率。有两种方式对定时删除设置:

1、配置redis.conf的hz选项,默认是10,也就是说每秒执行10次,数值越大,执行定时删除的频率越大,redis的cpu损耗也越大。

hz 10

2、配置redis.conf的最大内存,当内存的使用超出了最大内存,就会触发一个清除策略

maxmemory <bytes>

设置内存主动清除策略:

maxmemory-policy noeviction

内存清除策略:

  1. volatile-lru 只删除设置了过期时间的Key,使用频率越少的Key优先删除,不会对没有设置过期时间的Key删除
  2. volatile-ttl 和上面一样,只删除设置过期时间的Key,TTL过期时间越少优先删除
  3. volatile-random 随机删除快要过期的Key
  4. allkeys-lru 和lru一样,删除所有的Key,没有设置过期时间的Key也会被删除
  5. allkeys-random 和上面一样,删除掉随机的Key

Java Predicate接口的使用

Java 8新增了Predicate接口,它是一个函数接口,提供的test函数会接收一个参数,并返回一个bool值,我们可以用它来做过滤,检测类等功能。

源码说明

@FunctionalInterface
public interface Predicate<T> {
/**
* 具体过滤操作 需要被子类实现.
* 用来处理参数T是否满足要求,可以理解为 条件A
*/
boolean test(T t);
/**
* 调用当前Predicate的test方法之后再去调用other的test方法,相当于进行两次判断
* 可理解为 条件A && 条件B
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* 对当前判断进行"!"操作,即取非操作,可理解为 ! 条件A
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* 对当前判断进行"||"操作,即取或操作,可以理解为 条件A ||条件B
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* 对当前操作进行"="操作,即取等操作,可以理解为 A == B
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}

在Predicate接口里,除了test()为接口函数外,其他的为default 方法,可以直接使用。

使用示例

public static void main(String[] args) {
/**
* 1、判断数字是否大于7
*/
//设置一个大于7的过滤条件
Predicate<Integer> predicate = x -> x > 5;
System.out.println(predicate.test(10)); //输出 true
System.out.println(predicate.test(3)); //输出 fasle
/**
* 2、大于7并且
*/
//在上面大于7的条件下,添加是偶数的条件
predicate = predicate.and(x -> x % 2 == 0);
System.out.println(predicate.test(4)); //输出 fasle
System.out.println(predicate.test(12)); //输出 true
System.out.println(predicate.test(13)); //输出 fasle
/**
* 3、add or 简化写法
*/
predicate = x -> x > 6 && x < 9;
System.out.println(predicate.test(10)); //输出 false
System.out.println(predicate.test(8)); //输出 true
}

Predicate接口除了直接使用test()方法对参数进行检测外,提供的and(), or() 以及negate()方法可以用来和其他的Predicate接口组合起来使用。


阿里云ECS默认禁用25端口导致发邮件失败:Couldn't connect to host, port: smtp.example.com

使用JavaMail发送邮件,用的是阿里云的邮件服务。在本地测试是没有问题,但是上传到阿里云的ecs上就报错:

MailSendException: Mail server connection failed; nested exception is com.sun.mail.util.MailConnectException: Couldn't connect to host, port: smtp.majing.io, 25; timeout -1;

原来是阿里云处于安全考虑,TCP 25 端口出方向默认被封禁。

解决方法

有两种解决方法:

  1. 向阿里云申请解封25端口。
  2. 改为ssl加密465端口发送。

向阿里云申请解封25端口,可以参考:https://help.aliyun.com/knowledge_detail/56130.html

JavaMail使用ssl加密465端口发送邮件

主要是在原来发送邮件的配置上添加ssl,并设置为465端口。

Properties javaMailProperties = new Properties();
javaMailProperties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
javaMailProperties.setProperty("mail.smtp.socketFactory.port", "465");
javaMailProperties.setProperty("mail.smtp.port", "465");

使用spring配置如下:

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
   <property name="host" value="${mail.host}"/>
   <property name="username" value="${mail.username}"></property>
       <property name="password" value="${mail.password}"></property>
   <property name="javaMailProperties">
      <props>
         <prop key="mail.smtp.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop>
         <prop key="mail.smtp.socketFactory.port">465</prop>
         <prop key="mail.smtp.port">465</prop>
      </props>
   </property>
</bean>

Java验证公私密钥对

验证过程:

  1. 创建一个足够长的随机字节数,称为challenge
  2. 使用密钥签名challenge
  3. 使用公钥验证challenge

示例代码:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");  //密钥生成器
keyGen.initialize(2048);

KeyPair keyPair = keyGen.generateKeyPair();    //密钥对
PublicKey publicKey = keyPair.getPublic();     //公钥
PrivateKey privateKey = keyPair.getPrivate();  //私钥

//创建challenge
byte[] challenge = new byte[10000];
ThreadLocalRandom.current().nextBytes(challenge);

// 使用私钥签名challenge
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(challenge);
byte[] signature = sig.sign();

// 使用公钥验证challenge
sig.initVerify(publicKey);
sig.update(challenge);

boolean keyPairMatches = sig.verify(signature);

如果公私密钥对匹配,keyPairMatches返回true。

[译]代码优先的Java 9模块系统教程(二)

更新于 2017.10.27 1分钟阅读 0 评论 5 推荐

    Java

    作者: 陈川
  1. [译]代码优先的Java 9模块系统教程(一) Page 1
  2. [译]代码优先的Java 9模块系统教程(二) Page 2

服务监控(ServiceMonitor)

让我们来想象一个提供娱乐服务的网络,可能是社交网络或者是视频网络。我们希望监控这些服务,以确定系统的健康状况,并且在发生问题时能够发现,而不是客户报告。 这就是ServiceMonitor示例程序所要做的:监视这些服务(另一个惊喜)。

幸运的是,服务已经收集了我们想要的数据,ServiceMonitor所需要做的就是定期查询。 不幸的是,并不是所有的服务都暴露相同的REST API —— 有两个版本在用,Alpha和Beta。 这就是为什么ServiceObserver接口有两个实现。

一旦我们有了诊断数据,用DiagnosticDataPoint表示,这些数据会传给Statistician,它会把数据聚合给Statistics。这些数据会依次被存储在StatisticsRepository,并且会使用MonitorServer的REST公开。Monitor类会将所有内容联系起来。

总而言之,我们最终得到以下类型:

  • DiagnosticDataPoint: 一段时间内的服务数据
  • ServiceObserver: 返回DagnosticDataPoint的服务观察者接口
  • AlphaServiceObserver and BetaServiceObserver: ServiceObserver接口的Alpha和Beta的实现
  • Statistician: 根据DiagnosticDataPoint统计得到的Statistics
  • Statistics: 保存统计信息
  • StatisticsRepository: 保存和查询Statistics
  • MonitorServer: 统计信息的REST调用
  • Monitor: 联系所有的内容

程序依赖于Spark微型web框架,因此我们引用模块spark.core。 它可以在libs目录以及它的传递依赖关系中找到。

到目前为止,我们已经知道如何将应用程序组织为单个模块。 首先,我们在项目的根目录下创建模块声明module-info.java:

module monitor {
    requires spark.core;
}

注意到我们本应该选一个类似org.codefx.demo.monitor的模块名,但这将包含这些示例,所有我还是坚持使用较短的模块名monitor。如之前所说,它依赖于spark.core。因为程序没有有意义的API,所有它不导出任何包(package)。

接着我们可以编译,打包以及运行它,如下所示:

$ javac
    --module-path libs
    -d classes/monitor
    ${source-files}
$ jar --create
    --file mods/monitor.jar
    --main-class monitor.Main
    -C classes/monitor .
$ java
    --module-path mods
    --module monitor

你可以看到,我们不再使用Maven的target目录,而是在mods的classes和modules下新建类。 这使得示例更容易解析。 请注意,与以前不同的是,因为程序有非JDK的依赖,我们必须在编译期使用模块路径。

到此我们创建了单一模块ServiceMonitor!

拆分模块

现在我们有了一个模块,是时候真正开始使用模块系统将ServiceMonitor拆分。 对于这种大小的应用程序,将其变成几个模块当然是可笑的,但它只是一个演示,所有我们继续。

模块化应用程序的最常见的方法是关注点分离。 ServiceMonitor有以下内容,括号里包含相关类型:

  • 从服务收集数据 ( ServiceObserver, DiagnosticDataPoint)
  • 聚合数据来统计 ( Statistician, Statistics)
  • 持久化统计数据 ( StatisticsRepository)
  • 用REST API暴露统计数据 ( MonitorServer)

但不仅只有领域逻辑方面的需求, 还有技术方面:

  • 数据收集必须隐藏在API后面
  • Alpha和Beta服务都需要单独实现该API(AlphaServiceObserver和BetaServiceObserver)
  • 编排所有关注点(Monitor)

这得到以下模块以及上述公开可见的类型:

  • monitor.observer ( ServiceObserver, DiagnosticDataPoint)
  • monitor.observer.alpha ( AlphaServiceObserver)
  • monitor.observer.beta ( BetaServiceObserver)
  • monitor.statistics ( Statistician, Statistics)
  • monitor.persistence ( StatisticsRepository)
  • monitor.rest ( MonitorServer)
  • monitor ( Monitor)

将这些模块放在在类图上,很容易看出模块之间的依赖关系:


现实的项目由许多不同类型的文件组成。 显然,源文件是最重要的文件,但只有一种 —— 其他的是测试代码,资源,构建脚本或项目描述,文档,源码管理信息等等。 任何项目都必须选择一个目录结构来组织这些文件,重要的是要确保它不会与模块系统的特性相冲突。

如果你已经遵照Jigsaw项目下的模块系统开发,学习过官方的快速入门指南或一些早期教程,你可能注意到了他们使用了一个特别的目录结构,在src目录下每个项目有一个子目录。这样ServiceMonitor将如下所示:

ServiceMonitor
 + classes
 + mods
 - src
    + monitor
    - monitor.observer
       - monitor
          - observer
             DiagnosticDataPoint.java
             ServiceObserver.java
       module-info.java
    + monitor.observer.alpha
    + monitor.observer.beta
    + monitor.persistence
    + monitor.rest
    + monitor.statistics
 - test-src
    + monitor
    + monitor.observer
    + monitor.observer.alpha
    + monitor.observer.beta
    + monitor.persistence
    + monitor.rest
    + monitor.statistics

这会得到层次,我不喜欢这样。 大多数由几个子项目(我们现在称为模块)组成的项目都喜欢单独的根目录,其中每个子目录包含单个模块的源码,测试,资源以及之前提到的其他所有内容。 他们使用层次,这是建立的项目结构提供的。

Maven和Gradle等工具的默认目录结构就是按这种层次结构实现。 首先,他们给每个模块自己的目录树。 在该树中,src目录包含产品代码和资源(分别在main/java和main/resources中)以及测试代码和资源(分别在test/java和test/resources中):

ServiceMonitor
 + monitor
 - monitor.observer
    - src
       - main
          - java
             - monitor
                - observer
                   DiagnosticDataPoint.java
                   ServiceObserver.java
             module-info.java
          + resources
       + test
          + java
          + resources
    + target
 + monitor.observer.alpha
 + monitor.observer.beta
 + monitor.persistence
 + monitor.rest
 + monitor.statistics

我将会像这样组织ServiceMonitor,唯一的区别是,我将在classes目录下创建字节码,在mods目录下创建JAR包,它们都放在ServiceMonitor下,因为这让脚本更短,更易读。

现在我们来看看这些声明的含义,以及我们如何编译和运行应用程序。
我们已经介绍了如何使用module-info.java来声明模块,因此不需要详细介绍。 一旦你知道模块是如何相互依赖的(你的构建工具应该知道这一点;否则请问JDeps)。

module monitor.observer {
    exports monitor.observer;
}
 
module monitor.observer.alpha {
    requires monitor.observer;
    exports monitor.observer.alpha;
}
 
module monitor.observer.beta {
    requires monitor.observer;
    exports monitor.observer.beta;
}
 
module monitor.statistics {
    requires monitor.observer;
    exports monitor.statistics;
}
 
module monitor.persistence {
    requires monitor.statistics;
    exports monitor.persistence;
}
 
module monitor.rest {
    requires spark.core;
    requires monitor.statistics;
    exports monitor.rest;
}
 
module monitor {
    requires monitor.observer;
    requires monitor.observer.alpha;
    requires monitor.observer.beta;
    requires monitor.statistics;
    requires monitor.persistence;
    requires monitor.rest;
}

顺便说一下,你可以使用JDep创建初始模块声明。 无论是自动或手动创建,在实际项目中,你应该验证依赖关系和API是否符合要求。 很可能随着时间的推移,一些快速修复会引入一些你想避免的关系。 现在解决它,或是挤压一些问题。
与以前非常相似,把它当作单一的模块,只是更多而已:

$ javac
    -d classes/monitor.observer
    ${source-files}
$ jar --create
    --file mods/monitor.observer.jar
    -C classes/monitor.observer .
 
# monitor.observer.alpha depends on monitor.observer,
# so we place 'mods', which contains monitor.observer.jar,
# on the module path
$ javac
    --module-path mods
    -d classes/monitor.observer.alpha
    ${source-files}
$ jar --create
    --file mods/monitor.observer.alpha.jar
    -C classes/monitor.observer.alpha .
 
# more of the same ... until we come to monitor,
# which once again defines a main class
$ javac
    --module-path mods
    -d classes/monitor
    ${source-files}
$ jar --create
    --file mods/monitor.jar
    --main-class monitor.Main
    -C classes/monitor .

恭喜,你已经掌握了基础知识! 你现在知道如何组织,声明,编译,打包和启动模块,并且了解了模块路径,可读性图和模块化JAR的作用。

原文:Code First Java 9 Module System Tutorial