Java NIO的核心组件简介和使用

图片来自nidan-455298会员

在Java Socket通信中,Java NIO是一个很重要的底层实现类,理解其核心组件和使用方法,将会帮助了解更多在Java NIO基础之上搭建的其它Socket通信应用,例如Netty IO等。

本文将对Java NIO 核心组件做一个简单介绍,读完本文,你将可以读Java NIO有个总体上的认识,并能够通过代码样例,编程实现一个多线程异步消息通信。

1. Java NIO概览

Java NIO全称是Java New Input/Output,是很早在JDK1.4中引入的输入输出类库,之后在JDK7中提供了升级版的NIO2,提供了异步编程模型的IO操作。下文若没有说明,Java NIO是指在JDK7之后的最新NIO类库。

有一个首先需要了解的问题是,为什么会有NIO?和之前的IO类库相比,其带来了哪些优点和改进?

在JDK1.4之前,所有IO操作都是基于流(stream)进行数据读写,其读写操作方法会阻塞,这将大大影响程序的效率。Java NIO一方面解决了阻塞的问题,另一方面对数据块存储、数据处理方面进行了抽象,提供了缓存区、通信通道和选择器三个组件概念,使得程序开发者可以在更深入的层次上介入IO处理过程,对相关的通信进行多线程优化,编程可扩展性大大提高。

下表列出IO和NIO在主要特性上的区别对比,

IO NIO
操作对象 面向流 面向缓存区
操作特性 单向操作,或读,或写 支持读写双向操作
读写单位 按字节一一进行读/写 Bytes 按指定块存储大小进行读/写 Block Buffer
支持非阻塞 只支持阻塞读写 支持阻塞和非阻塞读写
通信通道 无,一个流本身就是一个通道 基于通道进行数据传输
单线程 多通道 不支持 支持,通过选择器实现一个线程处理多个通道
异步编程 不支持 支持异步编程模型(自JDK7)

可以看到Java NIO在IO操作上进行了很大幅度的改进和提升。

在Java NIO类库中,有四个核心组件接口,

  1. Channel通道
  2. Buffer缓存区
  3. Selector 选择器
  4. CompletionHandler 异步回调处理器

本文将对这四个组件逐一进行讲解,了解其作用和使用方法,并提供简单的代码使用样例。最后给出一个多线程异步通信的使用样例。

2. 通道Channel

一个通道是数据传输的连接,可以和IO设备建立连接,进行数据的获取和传送。在一定程度上,可以认为通道是流的升级版实现。

整个Java NIO中最常用的通道类有如下几个,

  • FileChannel,since 1.4
  • DatagramChannel,since 1.4
  • SocketChannel,since 1.4
  • ServerSocketChannel,since 1.4
  • AsynchronousFileChannel,since 1.7
  • AsynchronousSocketChannel,since 1.7
  • AsynchronousServerSocketChannel,since 1.7

其中Asynchronous*这几个类是在JDK7开始提供的NIO 2.0类库,主要提供了异步编程模型,详细见后面异步回调处理器的讨论。

(注:AsynchronousDatagramChannel在JDK7中并没有提供,原先是有准备发布这个类库的,但是由于某些原因,在发布前被删除。)

通道类中使用最多的四个方法是,

  • Channel.bind() 绑定连接
  • Channel.accept() 建立连接
  • Channel.read(buffer) 把数据从缓冲区读到通道
  • Channel.write(buffer) 把数据从通道写到缓冲区

一个简单的代码样例如下,

// please note: configure channel blocking state as false
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);

System.out.println("A server is started on port 9000");
ServerSocket serverSocket = server.socket();
serverSocket.bind(new InetSocketAddress(9000));

// please note: server.accept() will not block current thread
// Accept method will return null directly if no client is connected
SocketChannel channel = null;
int count = 0;
while (channel == null) {
    System.out.println("The server is trying to connect client, count=" + count++);
    channel = server.accept();
    Thread.sleep(500);
}
//channel.read(ByteBuffer)

System.out.println("the end");
channel.close();
server.close();

3. 缓冲区Buffer

Java NIO是基于块进行IO操作,缓冲区就是对这个块的抽象定义,在这个缓冲区中可以反复进行读写,通过通道实现数据的传输。

我们先看看缓冲区是什么样子,一个缓冲区是一个有指定大小的数组,其有读模式和写模式两种状态,请注意在读写不同模式下其Position和Limit的位置指向, 上图的缓冲区中显示了如下三个属性,

  • Capacity:缓冲区容量大小
  • Position:缓冲区的读写位置,根据当前读/写模式,含义如下
    • 在读模式下,读的当前位置
    • 在写模式下,写的当前位置
  • Limit:缓冲区的读写限制位,根据当前读/写模式,含义如下
    • 在读模式下,可以读的最大位置,等于当前缓冲区内数据量,防止读溢出
    • 在写模式下,可以写的最大位置,其实就等于Capacity值,防止写溢出

如何使用Javac/Jar/Java工具对源代码进行编译打包执行

图片来自pixabay.com的qimono会员

Javac/Jar/Java是JDK的发布包中带的三个基本工具,用于对Java源码进行编译打包运行,本文将介绍这几个工具的使用方法。

1. 代码和演示环境

代码地址:https://gitee.com/pphh/blog/tree/master/171221_java_compile_run/ 演示环境:Windows 7 SP1, JDK 9.0.1

下面的所有命令演示都是在目录./171221_java_compile_run/中进行,文件的目录结构如下,

+ Simple.java
+ src
  - App.java
  + common
    - java/com/pphh/demo/Logger.java
  + main
    - java/com/pphh/demo/AppPkg.java
+ src2
  - AppLogger.java
+ src3
  - AppJ7.java
  - AppJ8.java
+ target
  - app.jar
  - common.jar
  + classes

2. Java源码编译

在开发完Java代码后,首先需要对源码进行编译。JDK中带的编译工具为Javac。

javac的命令格式为,

javac [options] [sourcefiles]

其中: options 命令行选项 sourcefiles 待编译的代码文件

a) 简单的编译

只需要输入代码的文件地址即可

javac ./Simple.java java Simple

b) 指定编译后的输出目录

使用-d选项,指定编译后的class文件输入目录

javac -d ./target/classes Simple.java javac -d ./target/classes ./src/main/java/com/pphh/demo/AppPkg.java

c) 编译某个目录下的多个代码文件

使用-sourcepath选项,指定一个代码目录,然后通过通配符*.java获取指定目录下的所有代码文件进行编译,

set src_dir=./src/common/java/com/pphh/demo/ javac -d ./target/classes -sourcepath %src_dir% %src_dir%/*.java

上述命令会将指定目录下Logger.java文件进行编译,并按照package(com.pphh.demo)放置编译好的class文件,具体位置为, ./target/classes/com/pphh/demo/Logger.class

d) 有依赖的编译

可以通过-cp选项,指定依赖的class/Jar文件或所在目录。 例如程序代码src2/AppLogger.java有对前一步产生的Logger.class有依赖,其编译命令为,

javac -cp ./target/classes -d ./target/classes src2/AppLogger.java

上述命令指定了在依赖文件目录为/target/classes,编译代码后,将在目录./target/classes中产生一个AppLogger.class文件。

e) CLASSPATH

在编译的时候,Javac会从如下几个地方查找依赖class文件,

  1. 环境变量CLASSPATH定义的依赖文件和目录
  2. 当前执行命令的目录
  3. 通过-cp/-classpath指定的依赖文件和目录

在指定依赖的时候,可以通过上述几个方法定义依赖的class文件。

f) 编译时指定源码和目标代码的Java版本

可以通过-source/-target选项指定。

javac -d ./target/classes ./src3/AppJ7.java -source 7 -target 7 javac -d ./target/classes ./src3/AppJ8.java -source 8 -target 8

在JDK 9中Javac支持的Java版本包括6, 7, 8, 9,JDK9不再支持-source 5之前的版本。

需要注意的是,源发行版必须高于目标发行版,换句话说Java 7语言级别的代码,可以编译成Java 7/8/9语言级别的目标class文件,但不能编译成Java 6语言级别目标代码。

3. class文件打包

对代码编译之后,为了方便发布和使用,需要使用工具jar对这些class文件进行打包成Jar包。Jar是Java Archives的缩写,意思为Java归档文件。

jar的命令格式为,

jar [options] [ [–release VERSION] [-C dir] files] …

其中: options 命令行选项 –release VERSION创建多版本的Jar包,这是JDK9新功能 -C DIR 更改目录路径

a) 简单的打包命令

在打包前,先切换到class文件目录,然后执行打包命令,

cd ./target/classes jar cvf ../common.jar ./

上述命令将./target/classes目录下所有文件打好包为common.jar,并放置在上一层目录中,也就是./target/下面。

将common.jar放置在classes目录之外,这是为了区分class和jar包文件,防止在下一次运行命令时,将刚刚生成的common.jar也打入下一个包中,形成循环。

b) 打包时更改目录路径到根目录

很多时候我们希望直接指定class的目录路径,但是我们不要使用如下的命令,

jar cvf ./target/common.jar ./target/classes(不推荐此命令,请使用-C选项)

因为上面的命令没有更改目录结构。如果通过解压工具解压common.jar包,会发现里面classes文件目录结构是不对的,classes文件并没有放置在根目录,而且放置在target/classes/中,这个是不对的。

这个时候要使用-C选项,更改目录路径到根目录

jar -cvf ./target/common.jar -C ./target/classes/ .

c) 设置主程序

每一个Jar包都可以设置一个运行主程序,当使用Java -jar命令运行Jar包时,会自动执行这个主程序。

设置主程序要使用-e选项,

jar -cvfe ./target/common1.jar Simple -C ./target/classes/ . jar -cvfe ./target/common2.jar AppLogger -C ./target/classes/ . jar -cvfe ./target/common3.jar com.pphh.demo.AppPkg -C ./target/classes/ .

上面的命令将Jar包中的主程序分别设置为Simple\AppLogger\com.pphh.demo.AppPkg,包的运行见后面。

d) 选择指定的class文件打包

除了指定一个class目录,我们还可以指定某些class文件打成Jar包。

下面将指定的一个App.class打入app.jar包中,

jar -cvf ./target/app.jar -C ./target/classes/ Simple.class

下面将一个Logger.class归档为一个日志类库包中,

jar -cvf ./target/logger.jar -C ./target/classes/ ./com/pphh/demo/Logger.class

4. 执行Java程序

在将源码编译成可执行的class文件后,接下来就可以使用java工具来启动运行。

java的命令格式为,

java [options] mainclass [args…]

其中: options 命令行选项 mainclass 主程序类文件 args 命令行参数

a) 简单的编译运行

javac Simple.java java Simple

b) 指定class文件或目录
javac -d ./target/classes Simple.java
java -classpath ./target/classes Simple
java -classpath "./target/classes/Simple.class;" Simple
c) 运行一个在指定package的运行程序

javac -d ./target/classes ./src/main/java/com/pphh/demo/AppPkg.java java -classpath ./target/classes com.pphh.demo.AppPkg

d) 运行一个已配置主程序的Jar包

java -jar ./target/common1.jar java -jar ./target/common2.jar java -jar ./target/common3.jar

e) 运行一个未配置主程序的Jar包,通过命令指定运行的主程序
java -classpath "./target/common.jar" Simple
java -classpath "./target/common.jar" AppLogger
java -classpath "./target/common.jar" com.pphh.demo.AppPkg

5. 参考文献

  1. Java 9 SE技术文档:Javac工具命令
  2. Java 9 SE技术文档:Jar工具命令
  3. Java 9 SE技术文档:Java工具命令
  4. Java 7 SE技术文档:Javac工具命令
  5. Java 7 SE技术文档:Jar工具命令
  6. Java 7 SE技术文档:Java工具命令

Java对象的类型转换和属性复制

1. 问题的源由

在J2EE项目开发中,会涉及很多领域模型对象,例如,

  • VO (View Object) 视图对象,也叫展示对象,用于前端页面渲染所需要的数据
  • DTO (Data Transfer Object) 数据传输对象,一般用于Service和Manager向外传输数据
  • PO (Persistent Object) 持久化对象,一般和数据库表结构会形成一一映射关系,通过DAO层向上传输数据源对象

更多领域分层模型,及其所在的应用分层,可见阿里巴巴Java开发手册中的工程规约定义。

这些VO/DTO/PO都是POJO对象,每个属性都有getter/setter方法的定义,其数据转换链条大概为,

数据库DB 》PO 》DTO 》VO

可以看到,在项目中不同的应用分层DB/DAO/Service/Manager/Web,经常会需要把一个数据对象转换为其它类型的数据对象,转换的同时复制部分对象属性,有时部分属性需要从多个数据对象获取进行组装。这些数据的转换有一个共同的需求,那就是从一个源对象变为目标对象

源对象 Source s 》目标对象 Target t

接下来我们就需要讨论这个转换过程。

2. 单个对象的转换

1)简单的Java对象转换,使用BeanUtils.copyProperties

使用Spring都知道Spring Beans中提供了一个BeanUtils,可以用于对象的属性复制,其使用方法为,

    Target t = new Target();
    BeanUtils.copyProperties(s, t);
    BeanUtils.copyProperties(s, t, 'password');

其中,第二个copyProperties方法中第三个参数是告知复制过程中忽略复制’password’这个属性。

2)单个对象的转换,指定目标类

BeanUtils.copyProperties不提供目标对象的创建,因此每次都需要new出一个目标对象。为了能够省去这一步,可以使用下面的方法。

实现代码

public static  T convert(S s, Class clazz) {
  T t = null;

  try {
    t = clazz.newInstance();
    BeanUtils.copyProperties(s, t);
  } catch (Exception e) {
    e.printStackTrace();
  }

  return t;
}

上述方法将对象的生成封装起来返回。

使用样例

Target t = convert(s, Target.class);

使用中直接指定目标类,代码更加简洁易懂。

3. List/Set集合之间的转换

在很多时候,我们不仅处理一个对象的转换,而是面对集合的对象,这时需要能够对集合中的对象进行批量处理转换。

1)List集合转换,将列表中每一个实体转换为目标类

实现代码

public static  List convert(Iterable iterable, Class clazz) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(s -> convert(s, clazz))
                      .collect(Collectors.toList());
}

使用样例

List targets = convert(srcCollection, Target.class);
2)List集合转换,提供一个mapper转换方法,将列表中每一个实体对象转换为目标类

实现代码

public static  List convert(Iterable iterable, 
           Function mapper) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(mapper)
                      .collect(Collectors.toList());
}

使用样例

List targets = convert(srcCollection, mapper);

其中mapper的定义为Function,即该函数需要一个源对象作为输入参数,返回一个目标对象

3)List集合转换,转换为指定的List/Set容器类,集合中的实体对象不变

实现代码

public static > U collect(Iterable iterable, CollectionFactory factory) {
  U collection = factory.createCollection();
  iterable.forEach(collection::add);
  return collection;
}

其中CollectionFactory为一个集合类工厂。

使用样例

Iterable iterable = IntStream.range(0, 5).boxed().collect(Collectors.toList());
ArrayList arrayList = collect(iterable, ArrayList::new);
HashSet hashSet = collect(iterable, HashSet::new);
LinkedList linkedList = collect(iterable, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,集合中的整型变量对象保持不变。

4)List集合转换,转换为指定的List/Set容器类,并将集合中的实体对象转换为目标类

实现代码

public static , T> U collect(Iterable iterable, Class clazz, CollectionFactory factory) {
  Iterable list = convert(iterable, clazz);
  U collection = factory.createCollection();
  list.forEach(collection::add);
  return collection;
}

使用样例

Iterable users = UserDao.SingleInstance.findAll();
ArrayList arrayList = collect(users, UserVO.class, ArrayList::new);
HashSet hashSet = collect(users, UserVO.class, HashSet::new);
LinkedList linkedList = collect(users, UserVO.class, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,同时集合中的UserEntity变量对象转换为了UserVO。

4. List/Set/Map之间的转换

Java中Map的处理效率比List/Set高效不少,有些时候需要在List/Set/Map之间进行转换。

1)List集合转换,转换为Map集合,提供一个keyGenerator生成key,实体对象保存为value

实现代码

public static  Map map(Iterable iterable, Function keyGenerator) {
  return StreamSupport.stream(iterable.spliterator(), false)
         .collect(Collectors.toMap(keyGenerator, Function.identity()));
}

使用样例

List users = UserDao.SingleInstance.findAll();
Map result = map(users, UserEntity::getName);

上述样例中由UserEntity::getName来生成对象的键值,其必须为保证唯一。

2)Map集合转换,转换为List集合,实体对象保持不变

实现代码

public static > U collect(Map map, CollectionFactory factory) {
  U collection = factory.createCollection();
  map.values().iterator().forEachRemaining(collection::add);
  return collection;
}

使用样例

Map users;
List list1 = collect(users, ArrayList::new);
Set set = collect(users, HashSet::new);
List list2 = collect(users, LinkedList::new);
Collection collection = users.values();

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

3)Map集合转换,转换为指定的Map容器类

实现代码

public static > U map(Map srcMap, Class clazz,  MapFactory factory) {
  U u = factory.createMap();
  srcMap.entrySet()
        .forEach(ksEntry -> u.put(ksEntry.getKey(),
                          convert(ksEntry.getValue(), clazz)));
  return u;
}

使用样例

Map users;
Map m1 = map(users, UserVO.class, HashMap::new);
Map m2 = map(users, UserVO.class, LinkedHashMap::new);
Hashtable m3 = map(users, UserVO.class, Hashtable::new);

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

5. 源代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

本篇博文的程序代码样例在文件路径171201_java_object_copy\171201_java_object_copy_convert中,请使用JDK8及后续版本编译并运行项目代码。

6. 参考资料

  • POJO阿里巴巴Java开发手册(2017版本):应用分层和领域模型PO/DTO/VO
  • 百度百科:POJO简单Java对象