`

NIO 功能点【转】

    博客分类:
  • Java
 
阅读更多

新I/O?为什么我们需要新I/O?老的I/O有什么问题吗? (关于NIO和java.io的详细比较,可以参见对比Java.nio 和 Java.io一文)

java.io包提供的类没有任何问题,它们在职责范围内表现得非常好。然而有许多事情传统的Java I/O不能处理,比如非阻塞模式、文件锁、读选择、分散聚集等等。今天,大多数正规的操作系统都提供了这些功能(一些非主流操作系统也支持)。它们不再是可有可无的功能,而是建立高速、可扩展、健壮的应用不可或缺的,在企业级应用领域尤其如此。

NIO为Java平台引入了一组强大的新功能。尽管”N”代表新的(”New”),但是NIO并不是原来I/O类的替代者。它减少了对流式模型(Streaming Model)的关注,为I/O服务建模提供了另一种选择。NIO专注于提供一致、可移植的API,在访问各种I/O服务时尽可能地减小开销提升效率。NIO扫除了许多障碍,使得Java在I/O性能要求很高的场合也能够跟本地编译语言平等竞争。

碍于篇幅,在这篇文章我不会解释缓冲区、通道、选择器(selector)以及其他NIO的概念。我的书《Java NIO》已经阐述了这些。在这里,我想列出一些之前Java不能做但是NIO可以做的事情。如果你需要一点背景知识,请访这个页面(JDK文档的一部分内容)这里列出了一些J2SE1.4 Javadoc简要的大纲和链接。

10:文件锁

文件锁是一个大多数程序员不会经常使用的功能。然而,对那些真正需要用到它的人却是不可或缺的。NIO出现以前,如果要在Java应用中设置或检查文件锁除了调用本地函数(native method)别无它法。文件锁因其与操作系统(甚至是文件系统)绑定而臭名昭著,任何相关代码的移植都充斥着危险。

NIO文件锁基于FileChannel类构建。现在,只要在操作系统层支持文件锁任何平台都可以很轻易地创建、测试和管理文件锁。通常在集成非java应用程序时,文件锁充当访问共享数据文件的媒介。图1和图2(摘自我的书中)假定写进程(writer process)是一个不可替换的遗留软件。通过NIO,新编写的Java读取软件(reader application)能够采用相同的锁定规范与先前存在的非Java软件无缝集成。



 

图1:读进程持有共享锁



 

图2:写进程持有排它锁

文件锁通常在文件和进程级别操作,不适合用作JVM内线程之间的协调。操作系统一般不会区分同一个进程中不同线程的持锁权。这意味着同一个JVM中所有线程拥有同样的锁。文件锁主要是用来集成非Java应用或者不同的JVM。

虽然你可能从来不需要使用文件锁,现在NIO可以成为你的一个选择。在Java中添加基于文件的锁进一步消除了在企业级应用中使用Java的障碍,在需要与其他软件一起协作时作用更加明显。

9:建立在String类之上的正则表达式

正则表达式(java.util.regex)是NIO的一部分。我知道,它们既不“新”也不是“I/O”,但标准正则表达式库是JSR51的一部分,因此让我们继续吧。

正则表达式在Java中并不新(好几个附加包已经推出了很长时间了),但是现在它们基于J2SE的版本。按照Jeffrey E. F. Friedl最近的更新《学习正则表达式》一书中的阐述,J2SE1.4中的正则表达式引擎是最快和最好的——知道一下再好不过。

将正则表达式引擎集成到JDK中的一个比较好的“副作用”是JDK中其他的基础类可以使用它。在J2SE1.4中,String类扩展了如下与正则表达式相关的新方法:

1
2
3
4
5
6
7
8
9
10
11
package java.lang;
public final class String
implements java.io.Serializable, Comparable, CharSequence
{
  // This is a partial API listing
  public boolean matches (String regex)
  public String [] split (String regex)
  public String [] split (String regex, int limit)
  public String replaceFirst (String regex, String replacement)
  public String replaceAll (String regex, String replacement)
}

这些方法非常有用,因为你能在当前使用的字符串上直接调用它们。相比实例化Pattern和Matcher对象,在字符串对象上直接调用并检查结果,你能够很容易地像这样测试。通常,这种情况出错可能性更小而且可读性更好。

1
2
3
4
5
6
7
8
9
10
public static final String VALID_EMAIL_PATTERN =
"([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]"
+ "{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))"
+ "([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)";
...
if (emailAddress.matches (VALID_EMAIL_PATTERN)) {
  addEmailAddress (emailAddress);
} else {
  throw new IllegalArgumentException (emailAddress);
}

相比StringTokenizer, split()方法也更容易上手,它有两个优点:它对目标字符串应用了一个正则表达式(可能有些复杂),一次调用就得到了所有结果字符串而不是像StringTokenizer一样写一堆取得token的循环,你可以这样做:

1
String [] tokens = lineBuffer.split ("\\s*,\\s*");

这个方法将lineBuffer(包含了用逗号分割的值)分割成子串并且以类型安全的数组返回它们。这个正则表达式允许逗号前后有0个或多个白色字符。你也可以限制String分割的次数,在这种情况下,返回的最后一个子串就是还没有分割完剩下来的输入串。

8:缓冲区视图

 



 

 

图3:Buffer家族类图

NIO引入了缓冲区,这是一组在java.nio包中相关的类(见图3)。缓冲区一眼看去就像计算机科学101课上定义的那样,它们是一组封装了固定大小原生类型数组及其相应状态信息的简单对象。基本上就是这样。

缓冲区主要用来作为从通道发送或者接收数据的容器。通道是低级I/O服务的管道,他们是面向字节的;所以他们只能操作ByteBuffer对象。

那么我们用其他缓冲区类型干什么呢?(注:指的是CharBuffer,DoubleBuffer,IntBuffer等)可以从头创建或者包装一个类型合适的数组来生成非字节缓冲区实例,这些方式非常有用,但是这样的缓冲区不能用于I/O。(注,指的是视图缓冲区不能直接与channel互相访问)当然,还有第三种方式来创建基于ByteBuffer的非字节缓冲区视图。

例如,假设你有一个存储16比特Unicode(这里指的是UTF-16编码,不是普通文件中使用的UTF-8编码)字符的文件。如果你读了一块文件到字节缓冲区内,你可以像这样创建它们的字符缓冲区视图。

1
CharBuffer charBuffer = byteBuffer.asCharBuffer();

上面这段代码创建了一个带有CharBuffer行为的ByteBuffer视图。如图4所示,缓冲区中的每对字节组成一个16bit char字符(图中奇数字节的数组并没有包括在视图中,这里我们假设你从偶字节缓冲区开始)。

 



 

 

图4:一个ByteBuffer的CharBuffer视图

接着你可以用CharBuffer对象在数据上迭代(用相对get()方法),用绝对get()方法随机访问,或者将数据拷贝到char数组并把它传递给一个与缓冲无关的对象。

ByteBuffer类也有特殊方法可以访问独立原始值。例如访问缓冲区中4字节作为int型,你可以这么做:

1
int fileSize = byteBuffer.getInt();

这样就从缓冲区提取出4字节并将它们变成32bit的int值,更酷的是这4字节不需要与特殊地址边界对齐。如果下层的硬件不允许不对齐的内存访问,ByteBuffer实现就会自动按照要求组装字节(或者调用put()方法拆开)。

7: 字节擦试

如果你曾经处理过跨平台问题,你可能会担心之前示例中的字节顺序。CharBuffer视图会将字节按照16比特一组排列好,但是哪边是高字节哪边是低字节呢?字节的组织顺序就是我们平常所说的“端”。靠前的字节存储在低地址称为“大端”;相反,靠后的字节存在前面就是小端。



 

图5:缓冲区-大端



 

图6:缓冲区-小端

前面的例子中,16比特的Unicode字符到底是存储为小端(UTF-16LE)还是大端(UTF-16LE)呢?实际上它们能够以任何一种方式存储,因此我们需要知道这个缓冲区视图是怎样从字节映射到字符。

每一个缓冲区对象都具有字节顺序。除了ByteBuffer任何视图的字节顺序都是只读的,而ByteBuffer对象可以随时改变字节顺序。设置字节顺序会对所有基于ByteBuffer对象创建的视图的字节顺序产生影响。因此,如果我们知道文件中Unicode数据用小端法被编码为UTF-16LE,我们可以在创建CharBuffer前这样设置ByteBuffer的字节顺序:

1
2
3
byteBuffer.order (ByteOrder.LITTLE_ENDIAN);
 
CharBuffer charBuffer = byteBuffer.asCharBuffer();

新创建的缓冲区视图继承了ByteBuffer的字节顺序设置,之后如果改变ByteBuffer字节顺序不会对该视图产生影响。初始ByteBuffer字节序总是被设置为大端,而不考虑本地硬件平台上的字节序。

如果我们不知道文件中Unicode的字节序呢?如果该文件是用可移植的UTF-16编码,文件的头两个字节会包含字节序的标识(如果是直接编码为UTF-16LE或者UTF-16BE,你就得事先知道字节序)。如果测试过字节序标识,你需要在创建CharBuffer视图前设置合适的字节序。

一个ByteBuffer对象的字节序还会影响数据元素视图的字节擦拭(getInt()、getLong()、getFloat()等等)。缓冲区字节序设置在调用时会影响字节如何组合成返回值,或者破坏缓冲区存储。

6:直接缓冲区

封装在缓冲区中数据元素可以采用下列存储方式的一种:通过分配创建一个缓冲区对象的私有数组,或者包装你提供的数组,或者以直接缓冲区的方式存储在JVM内存堆以外的本地内存空间中。当你调用ByteBuffer.allocateDirect()创建一个直接缓冲区时,会分配本地系统内存并且用一个缓冲区对象来包装它。

直接缓冲区的主要用途是用做通道(channel)I/O。通道实现能够用直接缓冲区的本地内存空间来设置系统级I/O操作。这是一个强大的新功能,也是NIO效率的关键。虽然这些I/O函数是底层操作不能直接使用,但是你可以利用通道的直接缓冲区功能提升效率。

一些新的JNI方法也能够用本地内存来保存缓冲区数据。这是第一次让Java对象能够访问用本地代码分配的内存空间。1.4之前的本地代码能够在JVM堆上访问数据(谨慎的说——有许多限制),但是Java代码不能够访问本地代码分配的内存。

现在,不仅JNI代码能够定位Java使用ByteBuffer.allocateDirect()创建的本地内存空间地址,而且它还能够分配内存(例如:使用malloc)并且通过回调在JVM中把这个内存空间包装成新的ByteBuffer对象(JNI中方法是NewDirectByteBuffer())。

让人真正兴奋的地方是ByteBuffer对象能够包装任何本地代码的内存地址,甚至JVM以外的地址空间。一个例子是创建直接ByteBuffer对象来封装显卡的内存。这样的缓冲区允许纯Java代码直接读写显卡内存,不用做系统调用或者缓冲区拷贝。完全用Java编写显示驱动!你需要的仅仅是使用JNI来访问显示内存并且返回一个ByteBuffer对象。在NIO之前这是不可能完成的。

5:内存映射文件

让我们用一种特殊的ByteBuffer——MappedByteBuffer来继续讨论用任意内存空间包装ByteBuffer对象的主题。在大多数操作系统上,可以通过mmap系统调用(或者相似的操作)在一个已打开的文件描述符上做内存映射文件。调用mmap返回一个指向内存段的指针,实际上代表文件的内容。从内存区域获取数据实际上就是从相应文件的偏移位置处返回数据。而修改则会将文件从内存空间写入磁盘。



 

图7:内存映射介绍

内存映射文件有两个比很大好处。首先,“内存”通常不占用虚拟内存空间,或者更为确切的说一个文件映射在磁盘上备份的虚拟内存空间。这就意味着不需要为映射文件分配正式的页空间,因为这里的分页区域就是文件自身。如果你使用传统的方式打开文件并读入内存会消耗相应数量的页空间,因为你正把数据拷贝到内存。其次,多个相同的文件映射共享相同的虚地址空间。理论上,对于一个500M的文件100个不同的进程可以建立100个映射,每个进程在内存中都有整个500M的数据但系统总的内存消耗不变(注:即仍然只占用500M)。文件片段会作为引用被带入内存并与RAM竞争,但是页空间不会消耗。

在图7中,用户空间的其他进程通过相同的文件系统缓存(因此也是磁盘上相同的文件)会映射到同一块物理内存空间。每个进程都会看到其他进程对这块空间所做的改变。这可以用作持久化以及共享内存。操作系统随着虚拟内存子系统行为的改变而改变,所以你的性能也会跟着变。

调用map方法会在一个打开的FileChannel对象上创建MappedByteBuffer实例。MappedByteBuffer类有一组管理缓存和刷新更新文件的附加方法。

在NIO之前,一定得采用平台相关的本地代码来做内存映射文件。现在可以用任何纯Java程序来使用内存映射,操作简单并且可移植。

4:分散读和聚集写

下面是一段大家非常熟悉的代码:

1
2
3
byte [] byteArray = new byte [100];
...
int bytesRead = fileInputStream.read (byteArray);

这段代码会从流向一个字节数组读入数据。下面是采用ByteBuffer和FileChannel对象(把例子变为NIO)的等价读操作。

1
2
3
ByteBuffer byteBuffer = ByteBuffer.allocate (100);
...
int bytesRead = fileChannel.read (byteBuffer);

下面是一些常用模式:

1
2
3
4
5
6
7
ByteBuffer header = ByteBuffer.allocate (32);
ByteBuffer colorMap = ByteBuffer (256 * 3)
ByteBuffer imageBody = ByteBuffer (640 * 480);
 
fileChannel.read (header);
fileChannel.read (colorMap);
fileChannel.read (imageBody);

这段代码中三个独立的read()加载一个假定的图片文件,它工作得很好。但是难道就不能在一个read请求中告诉它把前32字节放在header缓冲区,接下来的768字节放到colorMap缓冲区,最后的放到imageBody中吗?

没问题,这很容易做到。大多数NIO通道支持分散/聚集,即向量I/O。分散读上面的缓冲区可以这样做:

1
2
3
ByteBuffer [] scatterBuffers = { header, colorMap, imageBody };
 
fileChannel.read (scatterBuffers);

这段代码用缓冲区数组来代替传递单个缓冲区对象。通道按顺序填充每个缓冲区直到所有缓冲区满或者没有数据可读为止。聚集写也是以类似的方式完成,数据从列表中的每个缓冲区中顺序取出来发送到通道就好像顺序写入一样。

当读写数据划分为固定大小的、逻辑上不同的段时,分散读和聚集写可以带来实际的性能提升。通过传入一组缓冲区列表来优化整个传输(例如用多个CPU)减少系统调用。

聚集写可以组合几个缓冲区中的结果。例如,一个HTTP响应会用一个只读缓冲区包含对每个响应相同的静态header、一些为特别响应而准备的动态缓冲区以及作为响应体用来连接的文件的MappedByteBuffer对象。一个给定的缓冲区可能会在一个或多个列表、同一个缓冲区的多个视图中出现。

3:直接通道传输

你是不是没有注意到,当你需要把数据拷贝到文件或者从文件拷贝出来时总是一遍又一遍地写同样的拷贝循环?总是一样的故事:读一块数据到缓冲区,然后立即写回到某个地方。你并没用用这些数据做什么事。但是为什么要一遍又一遍的把它们拿出来又放回去呢?为什么要重复发明轮子呢?

这里有一个想法,只需要告诉某些类“把数据从这个文件移动到另一个”或者“把所有从那个socket出来的数据写到这个文件”,难道这种方式不是更好吗?好吧,感谢“直接缓冲区传输”奇迹,现在你可以这么做了。

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
   // This is a partial API listing
 
   public abstract long transferTo (long position, long count,
      WritableByteChannel target)
 
   public abstract long transferFrom (ReadableByteChannel src, 
      long position, long count)
}

通道传输让你连接两个通道,这样数据可以直接从一个传到另一个而无需你进行任何干预。因为transferTo()和transferFrom()方法属于FileChannel类,FileChannel对象必须是一个通道传输的源或者目的(例如,你不能从一个socket传到另一个socket)。但是传输的另一端必须是合适的ReadableByteChannel或者WritableByteChannel。

基于操作系统提供的支持,整个通道传输能够在内核中完成。这不仅缓解了繁重的拷贝工作,而且绕过了JVM!底层系统调用万岁!甚至在操作系统内核不支持的情况下,利用这些方法同样可以把你从写拷贝循环中拯救出来。还有个好处,用本地代码或者其他优化来移动数据肯定要比你自己写的Java代码要快。最好的事情就是:不写代码就不会有bug。

2:非阻塞套接字

传统的Java I/O模型缺少非阻塞I/O从一开始就已经惹眼了,最后终于有了NIO。从SelectableChannel继承的Channel类可以用configureBlocking替换成为非阻塞模式。在J2SE1.4中只有套接字通道(SocketChannel, ServerSocketChannel和DatagramChannel)可以换为非阻塞模式,而FileChannel不能设为非阻塞模式。

当通道设置为非阻塞时,read()或者write()调用不管有没有传输数据总是立即返回。这样线程总是可以无停滞地检查数据是否已经准备好。

1
2
3
4
5
6
7
8
9
10
11
12
13
ByteBuffer buffer = ByteBuffer.allocate (1024);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking (false);
 
...
 
while (true) {
   ...
   if (socketChannel.read (buffer) != 0) {
      processInput (buffer);
   }
...
}

上面的代码代表了一个典型的轮询过程:非阻塞读不断的尝试,如果数据被读取了,就会处理它。从read()调用返回0表示没有可用的数据,线程通过主循环不断的滚动,在每次循环时做该做的事。

1:多路复用I/O

女士们先生们,接下来就是NIO10大新功能之首。

上一节示例代码中用轮询来确定在非阻塞通道上输入已经就绪。虽然有许多适合的场景,但是通常轮询并不十分有效。如果在处理循环主要是做别的事情并周期性的检查输入时,轮询也许是一个适合的选择。

但是如果应用程序的主要目的是响应许多不同连接上的输入(例如web服务器),轮询就不合适了。在响应式的应用中,你需要快速的轮转。然而快速轮询只会毫无意义地消耗大量的CPU指令周期并产生大量无效的I/O请求。I/O请求会差生系统调用,系统调用造成上下文切换,而上下文切换是相当费时的操作

当我们使用单线程来管理多个I/O通道时,就是所谓的多路复用I/O。对于多路复用,你希望管理线程一直阻塞,直到其中一个通道上有输入可用。但是,我们刚刚不还在为非阻塞I/O而手舞足蹈吗?我们之前就有阻塞I/O了,这个是……?

问题在于使用传统的阻塞模型,单线程是不能多路复用一组I/O流的。如果没有非阻塞模型,当线程尝试在套接字上读但没有数据时就会导致线程被阻塞,这样线程就没法处理其他流的可用数据了。综合起来会因为一个空闲流而将所服务的其他流挂起。

在Java的世界中,这个问题的解决方案就是让每个活跃的流有一个线程。当一个流上面的数据可用时相应的线程就会醒来,读取并处理数据,然后再度阻塞在read()方法直到有更多数据。实际上这种处理是有效的,但是并不具备扩展性。线程(重量级实现)乘以相同速率的套接字(相对轻量级)。我们可以用池技术或者是复用线程(更复杂而且代码需要调试)来降低线程创建的压力,但是主要问题是当线程数量太大时线程调度器将面临压力。JVM线程管理机制被设计为处理几十个而不是成百上千个线程。甚至空闲线程也会一定程度上拖慢速度。如果多个处理流的线程仅有一个通用的数据处理对象就会形成瓶颈,每个流一个线程的方式会将并发问题复杂化。

使用多路复用大量套接字的正确的方式是读选择器(NIO Selector类)。相比轮询或者每个流一个线程选择器是最好的方式,因为只需要一个线程就能够简单的监控大量的套接字。当任何流上有数据时(读的部分)线程也能够选择阻塞唤醒的方式(我们又回到了阻塞),并且精确的接收就绪流上来的信息(选择部分)。

读选择器建立在非阻塞模式上,所以只有通道是非阻塞模式它才会工作。如果你喜欢,还可以让实际的选择处理也变成非阻塞的。重点是Selector对象会帮助你卖力地检查大量通道的状态,你只需要操作选择的结果而不用亲自去管理每一个通道。

你创建一个Selector实例,然后在其上注册一个或者多个非阻塞通道,然后声明你感兴趣的是什么事件。下面是一个选择器的例子。在这个例子中,同一个循环里传入ServerSocketChannel对象的连接作为活动套接字连接来提供服务(更完整的例子在我的书中)。

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
ServerSocketChannel serverChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
 
serverChannel.socket().bind (new InetSocketAddress (port));
serverChannel.configureBlocking (false);
serverChannel.register (selector, SelectionKey.OP_ACCEPT);
 
while (true) {
   selector.select();
 
   Iterator it = selector.selectedKeys().iterator();
 
   while (it.hasNext()) {
      SelectionKey key = (SelectionKey) it.next();
 
      if (key.isAcceptable()) {
         ServerSocketChannel server = (ServerSocketChannel) key.channel();
         SocketChannel channel = server.accept();
 
         channel.configureBlocking (false);
         channel.register (selector, SelectionKey.OP_READ);
      }
 
      if (key.isReadable()) {
         readDataFromSocket (key);
      }
 
      it.remove();
   }
}

这种方式远比每个线程一个套接字简单且更具有扩展性,而且也更容易写和调试代码。最重要的是,它大量减少了服务和管理大量套接字的工作。相比NIO中的其他新特性,选择器更多的让操作系统来代理它繁重的工作。这就减少了JVM大量的工作,由于JVM不再花时间而是让操作系统来做这些事,所以能够释放内存和CPU资源并支持大规模扩展。

  • 大小: 5.2 KB
  • 大小: 5.2 KB
  • 大小: 8.2 KB
  • 大小: 8.9 KB
  • 大小: 1.4 KB
  • 大小: 1.4 KB
  • 大小: 8.8 KB
分享到:
评论

相关推荐

    Dubbo面试28题答案详解:核心功能+服务治理+架构设计等

     Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。  Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,...

    Java基础知识点总结.docx

    把map集合转成set的方法 82 使用集合的技巧 83 Collections--集合工具类 83 Arrays—数组对象工具类 84 增强for循环 85 可变参数(...) 86 枚举:关键字 enum 86 自动拆装箱 86 泛型 87 <java.lang>System 89 ...

    Eclipse

    由于eclipse-to-maven使用java.nio功能,因此您需要JDK 1.7+ 使用Maven“ mvn clean install”创建eclipse-to-maven版本 将现有工作空间复制到单独的目录中。 设置 在运行应用程序之前,请遵循以下src/main/java/...

    mir2.core-2.0.0.zip

    针对热血传奇2的资源文件解析核心库 使用纯java代码,不依赖awt/nio等java特定平台功能,可直接用于android等后端 基于maven,可顺利使用gradle进行转换和构建 项目地址https://github.com/jootnet/mir2.core

    pipeline:支持异步联网和数据转换

    该项目处于Alpha阶段。 准备好遇到错误。 建议首先实施更多编解码器子项目。 管道是带有附加规则的数据流节点的线性链:处理后,消息将返回给发件... Pipeline-nio:基于NIO2的异步网络I / O提供了两个基本类: Asyn

    JAVA_API1.6文档(中文)

    java.nio.channels.spi 用于 java.nio.channels 包的服务提供者类。 java.nio.charset 定义用来在字节和 Unicode 字符之间转换的 charset、解码器和编码器。 java.nio.charset.spi java.nio.charset 包的服务提供...

    近5年133个Java面试问题列表

    SOLID (单一功能、 开闭原则、 里氏替换、 接口隔离以及依赖反转) 设计原则 抽象类与接口 Java 基础, 如 equals 和 hashcode 泛型与枚举 Java IO 与 NIO 常用网络协议 Java 中的数据结构和算法 正则表达式 JVM ...

    一个可以直接运行的基于SpringMVC的web框架1.1.12

    Excel工具类 Word工具类 Java NIO实现socket工具类 分布式session jdk升级到1.7 嵌入式redis服务(只支持linux) 1.0.13 修改默认的beanName生成策略,controller参数扩展 1.0.14 分布式session使用zookeeper 1.0.15 ...

    可以直接运行的基于SpringMVC的web框架示例,也可以直接当公司框架

    Excel工具类 Word工具类 Java NIO实现socket工具类 分布式session jdk升级到1.7 嵌入式redis服务(只支持linux) 1.0.13 修改默认的beanName生成策略,controller参数扩展 1.0.14 分布式session使用zookeeper 1.0.15 ...

    人工智能文档:关于学习和AI相关系统的信息库

    IA激活功能品名:一种统一的线性指数连续性品系,其品名是“零消费趋势”或“预消费结果”。 Diferente de outrasfunçõesdeativação,ELU tem uma constante alfa extra que deve ser umnúmeropositivo。 ...

    java api最新7.0

    java.nio.channels.spi 用于 java.nio.channels 包的服务提供者类。 java.nio.charset 定义用来在字节和 Unicode 字符之间转换的 charset、解码器和编码器。 java.nio.charset.spi java.nio.charset 包的服务提供者...

    java jdk-api-1.6 中文 chmd

    java.nio.channels.spi 用于 java.nio.channels 包的服务提供者类。 java.nio.charset 定义用来在字节和 Unicode 字符之间转换的 charset、解码器和编码器。 java.nio.charset.spi java.nio.charset 包的服务提供...

    synapse

    一旦Apache Synapse调解了您的服务请求,它就可以执行许多功能,包括路由,负载平衡,转换和协议切换。 Apache Synapse可用于构建企业服务总线(ESB)或面向服务的体系结构(SOA)。 通过基于Apache Axiom的流XML...

Global site tag (gtag.js) - Google Analytics