事情的开始

  1.4版本开始,java提供了另一套IO系统,称为NIO,(New I/O的意思),NIO支持面向缓冲区的、基于通道的IO操作。

  1.7版本的时候,java对NIO系统进行了极大的扩展,增强了对文件处理和文件系统特性的支持。

  在不断的进化迭代之中,IO的很多应用场景应该推荐使用NIO来取代。

  NIO系统构建于两个基础术语之上:缓冲区和通道。

缓冲区

Buffer类

  缓冲区是一个固定数据量的指定基本类型的数据容器,可以将它理解成一块内存,java将它封装成了Buffer类。

  每个非布尔基本数据类型都有各自对应的缓冲区操作类,所有缓冲区操作类都是Buffer类的子类。

  除了存储的内容之外,所有的缓冲区都具有通用的核心功能:当前位置、界限、容量。

  当前位置是要读写的下一个元素的索引

  界限是缓冲区中最后一个有效位置之后下一个位置的索引值

  容量是缓冲区能够容纳的元素的数量,一般来说界限等于容量。

  对于标记、位置、限制和容量值遵守以下不变式:0 <= 标记 <= 位置 <= 限制 <= 容量

方法列表:

方法 描述
Object array() 返回此缓冲区的底层实现数组
int arrayOffset() 返回此缓冲区的底层实现数组中第一个元素的索引
int capacity() 返回此缓冲区的容量
Buffer clear() 清除此缓冲区并返回缓冲区的引用
Buffer flip() 将缓冲区的界限设置为当前位置,并将当前位置重置为0,即反转缓冲区
boolean hasArray() 返回缓冲区是否具有可访问的底层实现数组。
boolean hasRemaining() 返回缓冲区中是否还有剩余元素
boolean isDirect() 返回此缓冲区是否是直接缓冲区(直接缓冲区可以直接对缓冲区进行IO)
boolean isReadOnly() 该缓冲区是否只读
int limit() 返回缓冲区的界限
Buffer limit(int n) 将缓冲区的界限设置为n
Buffer mark() 设置标记
int position() 返回此缓冲区的位置
Buffer position(int n) 将缓冲区的当前位置设置为n
int remaining() 返回当前位置与界限之间的元素数量(即界限减去当前位置的结果值)
Buffer reset() 将缓冲区的位置重置为之前设置标记的位置
Buffer rewind() 将缓冲区的位置设置为0
清除、反转、和重绕

  这三个词是在查阅JDK文档看到的,对应Buffer类的三个方法,个人觉得非常有助于理解。

  clear()使缓冲区为一系列新的通道读取或相对放置 操作做好准备:它将限制设置为容量大小,将位置设置为 0。

  flip()使缓冲区为一系列新的通道写入或相对获取 操作做好准备:它将限制设置为当前位置,然后将位置设置为 0。

  rewind()使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。

数据传输

  下面这些特定的缓冲区类派生字Buffer,这些类的名称暗含了他们所能容纳的数据类型:

  ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、MappedByteBuffer、ShortBuffer

  其中 MappedByteBuffer是ByteBuffer的子类,用于将文件映射到缓冲区。

  所有的缓冲区类都定义的有get()和put()方法,用于存取数据。(当然,如果缓冲区是只读的,就不能使用put操作)

通道

通道的用处

  通道,表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接,用于 I/O 操作的连接。

  通过通道,可以读取和写入数据。拿 NIO与原来的I/O 做个比较,通道就像是流,但它是面向缓冲区的。

  正如前面提到的,所有数据都通过 Buffer 对象来处理。你永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

  通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。

  通道实现了Channel接口并且扩展了Closeable接口和AutoCloseable接口,通过实现AutoCloseable接口,就可以使用带资源的try语句管理通道,那么当通道不再需要时会自动关闭。

获取通道

  获取通道的一种方式是对支持通道的对象调用getChannel()方法。

  例如,以下IO类支持getChannel()方法:

DatagramSocket、FileInputStream、FileOutputStream、RandomAccessFile、ServerSocket、Socket

  根据调用getChannel()方法的对象类型返回特定类型的通道,比如对FileInputStream、FileOutputStream或RandomAccessFile对象调用getChannel()方法时,会返回FileChannel类型的通道,对Socket对象调用getChannel()方法时,会返回SocketChannel类型的通道。

  通道都支持各种read()和write()方法,使用这些方法可以通过通道执行IO操作。

方法如下:

方法 描述
int read(ByteBuffer b) 将字节读取到缓冲区,返回实际读取的字节数
int read(ByteBuffer b,long start) 从start指定的文件位置开始,从通道读取字节,并写入缓冲区
int write(ByteBuffer b) 将字节从缓冲区写入通道
int write(ByteBuffer b,long start) 从start指定的文件位置开始,将字节从缓冲区写入通道
字符集和选择器

  NIO使用的另外两个实体是字符集和选择器。

  字符集定义了将字节映射为字符的方法,可以使用编码器将一系列字符编码成字节,使用解码器将一系列字节解码成字符。

  字符集、编码器和解码器由java.nio.charset包中定义的类支持,因为提供了默认的编码器和解码器,所以通常不需要显式的使用字符集进行工作。

  选择器支持基于键的,非锁定的多通道IO,也就是说,它可以通过多个通道执行IO,当然,前提是通道需要调用register方法注册到选择器中,

  选择器的应用场景在基于套接字的通道。

Path接口

  Path是JDK1.7新增进来的接口,该接口封装了文件的路径。

  因为Path是接口,不是类,所以不能通过构造函数直接创建Path实例,通常会调用Paths.get()工厂方法来获取Path实例。

get()方法有两种形式:

  Path get(String pathname,String ...more)
  Path get(URI uri)

  创建链接到文件的Path对象不会导致打开或创建文件,理解这一点很重要,这仅仅只是创建了封装文件目录路径的对象而已。

以下代码示例常用用法(1.txt是一个不存在的文件):

        Path path = Paths.get("./nio/src/1.txt");
System.out.println("自身路径:"+path.toString());//输出.\nio\src\1.txt
System.out.println("文件或目录名称:"+path.getFileName());//输出1.txt
System.out.println("路径元素数量:"+path.getNameCount());//输出4
System.out.println("路径中第3截:"+path.getName(2));//输出src
System.out.println("父目录的路径"+path.getParent());//输出.\nio\src
System.out.println(path.getRoot());//输出null
System.out.println("是否绝对路径:"+path.isAbsolute());//输出false Path p = path.toAbsolutePath();//返回与该路径等价的绝对路径
System.out.println("看看我这个是不是绝对路径:"+p.toString());//输出E:\JAVA\java_learning\.\nio\src\1.txt File file = path.toFile();//从该路径创建一个File对象
System.out.println("文件是否存在:"+file.exists());//false Path path1 = file.toPath();//再把File对象转成Path对象
System.out.println("是不是同一个对象:"+path1.equals(path));//输出true

为基于通道的IO使用NIO

通过通道读取文件

手动分配缓冲区

  这是最常用的方式,手动分配一个缓冲区,然后执行显式的读取操作,读取操作使用来自文件的数据加载缓冲区。

  

        try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){
ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小
int count = seekableByteChannel.read(buffer);//将文件中的数据读取到缓冲区
buffer.rewind();
while (count > 0){
System.out.println((char)buffer.get());//读取缓冲区中的数据
count --;
}
}catch (Exception e){
e.printStackTrace();
}

  该示例使用了SeekableByteChannel对象,该对象封装了文件操作的通道,可以转成FileChannel(不是默认的文件系统不能转)。这里注意,分配缓冲区大小就代表了最多读取的数据字节大小,比如我的示例文件中字节数是8个,但是我只分配了5个字节的缓冲区,因此只能读出前5个字节的数据。

  为什么会有buffer.rewind()这行代码呢?因为调用了read()方法将文件内容读取到缓冲区后,当前位置处于缓冲区的末尾,所以要重绕缓冲区,将指针重置到缓冲区的起始位置。

将文件映射到缓冲区

  这种方式的优点是缓冲区自动包含文件的内容,不需要显式的读操作。同样的要先获取Path对象,再获取文件通道。

  用newByteChannel()方法得到的SeekableByteChannel对象转成FileChannel类型的对象,因为FileChannel对象有map()方法,将通道映射到缓冲区。

map()方法如下所示:

  MappedByteBuffer map(FileChannel.MapMode how,long begin,long size) throws IOException

参数how的值为:MapMode.READ_ONLY、MapMode.READ_WRITE、MapMode.PRIVATE 之一。

  映射的开始位置由begin指定,映射的字节数由size指定,作为MappedByteBuffer返回指向缓冲区的引用,MappedByteBuffer是ByteBuffer的子类,一旦将文件映射到缓冲区,就可以从缓冲区读取文件了。

        try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){
long size = fileChannel.size();//获取文件字节数量
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,0,size);
for(int i=0;i < size; i ++){
System.out.println((char)mappedByteBuffer.get());
}
}catch (Exception e){
e.printStackTrace();
}

通过通道写入文件

手动分配缓冲区
        try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.APPEND)){
ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小
for(int i=0;i<5;i++){
buffer.put((byte)('A'+i));
}
buffer.rewind();
seekableByteChannel.write(buffer);
}catch (Exception e){
e.printStackTrace();
}

因为是针对写操作而打开文件,所以参数必须指定为StandardOpenOption.WRITE,如果希望文件不存在就创建文件,可以指定StandardOpenOption.CREATE,但是我还希望是以追加的形式写入内容,所以又指定了StandardOpenOption.APPEND。

  需要注意的是buffer.put()方法每次调用都会向前推进当前位置,所以在调用write()方法之前,需要将当前位置重置到缓冲区的开头,如果没有这么做,write()方法会认为缓冲区中没有数据。

将文件映射到缓冲区
        Path path = Paths.get("./nio/src/4.txt");
try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(path,StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE)){
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
for(int i=0;i < 5; i ++){
buffer.put((byte) ('A'+i));
}
}catch (Exception e){
e.printStackTrace();
}

可以看出,对于通道自身并没有显式的写操作,因为缓冲区被映射到文件,所以对缓冲区的修改会自动反映到底层文件中。

  映射缓冲区要么是只读,要么是读/写,所以这里必须是READ和WRITE两个选项都得要。一旦将文件映射到缓冲区,就可以向缓冲区中写入数据,并且这些数据会被自动写入文件,所以不需要对通道执行显式的写入操作。

  另外,写入的文件大小不能超过缓冲区的大小,如果超过了之后会抛出异常,但是已经写入的数据仍然会成功。比如缓冲区5个字节,我写入10个字节,程序会抛出异常,但是前5个字节仍然会写入文件中。

使用NIO复制和移动文件

        Path path = Paths.get("./nio/src/4.txt");
Path path2 = Paths.get("./nio/src/40.txt");
try{
Files.copy(path2,path, StandardCopyOption.REPLACE_EXISTING);
//Files.move(path,path2, StandardCopyOption.REPLACE_EXISTING);
}catch (Exception e){
e.printStackTrace();
}

StandardCopyOption.REPLACE_EXISTING选项的意思是如果目标文件存在则替换。

为基于流的IO使用NIO

  如果拥有Path对象,那么可以通过调用Files类的静态方法newInputStream()或newOutputStream()来得到连接到指定文件的流。

方法原型如下:

  static InputStream newInputStream(Path path,OpenOption... how) throws IOException

how的参数值必须是一个或多个由StandardOpenOption定义的值,如果没有指定选项,默认打开方式为StandardOpenOption.READ。

  一旦打开文件,就可以使用InputStream定义的任何方法。

  因为newInputStream()方法返回的是常规流,所以也可以在缓冲流中封装流。

        try(BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get("./nio/src/2.txt")))){
int s = inputStream.available();
for(int i=0;i<s;i++){
int c = inputStream.read();
System.out.print((char) c);
}
}catch (Exception e){
e.printStackTrace();
}

OutputStream和前面的InputStream类似:

        try(BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get("./nio/src/2.txt")))){
for(int i=0;i<26;i++){
outputStream.write((byte)('A'+i));
}
}catch (Exception e){
e.printStackTrace();
}

为基于文件系统使用NIO

Files类

  要进行操作的文件是由Path指定的,但是对文件执行的许多操作都是由Files类中的静态方法提供的。

  java.nio.file.Files类就是为了替代java.io.File类而生。

以下列出部分常用方法:

方法 描述
static Path copy(Path from,Path to,CopyOption... opts) 将from复制到to,返回to
static Path move(Path from,Path to,CopyOption... opts) 将from移动到to,返回to
static Path createDirectory(Path path,FileAttribute<?> attribs) 创建一个目录,目录属性是由attribs指定的。
static Path createFile(Path path,FileAttribute<?> attrbs) 创建一个文件,文件属性是由attribs指定的。
static void delete(Path path) 删除一个文件
static boolean exists(Path path) path代表的路径是否存在(无论文件还是目录)
static boolean notExists(Path path) path代表的路径是否不存在(无论文件还是目录)
static boolean isRegularFile(Path path) 是否是文件
static boolean isDirectory(Path path) 是否是目录
static boolean isExecutable(Path path) 是否是可执行文件
static boolean isHidden(Path path) 是否是隐藏文件
static boolean isReadable(Path path) 是否可读
static boolean isWritable(Path path) 是否可写
static long size(Path path) 返回文件大小
static SeekableByteChannel newByteChannel(Path path,OpenOption... opts) 打开文件,opts指定打开方式,返回一个通道对象
static DirectoryStream<Path> newDirectoryStream(Path path) 打开目录,返回一个目录流
static InputStream newInputStream(Path path,OpenOption... opts) 打开文件,返回一个输入流
static OutputStream newOutputStream(Path path,OpenOption... opts) 打开文件,返回一个输出流

参数列表中出现的有类型为OpenOption的参数,它是一个接口,真实传入的参数是StandardOpenOption类中的枚举,这个枚举参数与newBufferedWriter/newInputStream/newOutputStream/write方法一起使用。

StandardOpenOption类中的枚举 描述
READ 用于读取打开文件
WRITE 用于写入打开文件
APPEND 如果是写入,则内容追加到末尾
CREATE 自动在文件不存在的情况下创建新文件
CREATE_NEW 创建新文件,如果文件已存在则抛出异常
DELETE_ON_CLOSE 当文件被关闭时删除文件
DSYNC 对文件内容的修改被立即写入物理设备
SYNC 对文件内容或元数据的修改被立即写入物理设备
TRUNCATE_EXISTING 如果用于写入而打开,那么移除已有内容

下面演示追加写入文件操作:

        try{
Path path = Paths.get("./nio/src/8.txt");
String str = "今天天气不错哦\n";
Files.write(path,str.getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}catch (Exception e){
e.printStackTrace();
}

目录流

遍历目录

  如果Path中的路径是目录,那么可以使用Files类的静态方法newDirectoryStream()来获取目录流。

方法原型如下:

  static DirectoryStream<Path> newDirectoryStream(Path dir) throw IOException

  调用此方法的前提是目标必须是目录,并且可读,否则会抛异常。

        try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"))){
for(Path path : paths){
System.out.println(path.getFileName());
}
}catch (Exception e){
e.printStackTrace();
}

  DirectoryStream<Path>实现了Iterable<Path>,所以可以用foreach循环对其进行遍历,但是它实现的迭代器针对每个实例只能获取一次,所以只能遍历一次。

匹配内容

  Files.newDirectoryStream方法还有一种形式,可以传入匹配规则:

  static DirectoryStream<Path> newDirectoryStream(Path dir,String glob) throws IOException

  第二个参数就是匹配规则,但是它不支持强大的正则,只支持简单的匹配,如"?"代表任意1个字符,"*"代表任意个任意字符。

使用示例 匹配所有.java结尾的文件:

        try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"),"*.java")){
for(Path path : paths){
System.out.println(path.getFileName());
}
}catch (Exception e){
e.printStackTrace();
}
复杂匹配

这种方式的原型为:

  static DirectoryStream<Path> newDirectoryStream(Path dir,DirectoryStream.Filter<? super Path> filter) throws IOException

其中的DirectoryStream.Filter是定义了以下方法的接口:

  boolean accept(T entry) throws IOException

这个方法中如果希望匹配entry就返回true,否则就返回false,这种形式的优点是可以基于文件名之外的其他内容过滤,比如说,可以只匹配目录、只匹配文件、匹配文件大小、创建日期、修改日期等各种属性。

下面是匹配文件大小的示例:

        String dirname = "./nio/src";
DirectoryStream.Filter<Path> filter = (entry)->{
if(Files.size(entry) > 25){
return true;
}
return false;
};
try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get(dirname),filter)){
for(Path path : paths){
System.out.println(path.getFileName());
}
}catch (Exception e){
e.printStackTrace();
}
目录树

  遍历目录下的所有资源以往的做法都是用递归来实现,但是在NIO.2的时候提供了walkFileTree方法,使得遍历目录变得优雅而简单,其中涉及4个方法,根据需求选择重写。

示例如下:

        String dir = "./nio";
try{
Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问文件:"+file);
return super.visitFile(file, attrs);
} @Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问目录:"+dir);
return super.preVisitDirectory(dir, attrs);
} @Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
System.out.println("访问失败的文件:"+file);
return super.visitFileFailed(file, exc);
} @Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("这个目录访问结束了:"+dir);
return super.postVisitDirectory(dir, exc);
}
});
}catch (Exception e){
e.printStackTrace();
}

打印结果如图:

文件加锁机制

  JDK1.4引入的文件加锁机制,要锁定一个文件,可以调用FileChannel类的lock或tryLock方法

  FileChannel channel = FileChannel.open(path);
  FileLock lock = channel.lock() 或者 FileLock lock1 = channel.tryLock()

第一个调用会阻塞直到获得锁,第二个调用立刻就会返回 要么返回锁 要么返回Null。

  获得锁后这个文件将保持锁定状态,直到这个通道关闭,或者释放锁:lock.release(); 点进源码可以轻易发现,FileChannel实现了AutoCloseable接口,也就是说,可以通过try语句来自动管理资源,不需要手动释放锁。

  还可以锁定文件内容的一部分:

  FileLock lock(long start,long size,boolean shared)
  FileLock lock(long start,long size,boolean shared)

  锁定区域为(从start到start+size),那么在start+size之外的部分不会被锁定。shared参数为布尔值,代表是否是读锁,读锁就是共享锁,写锁就是排他锁。

源码分享

https://gitee.com/zhao-baolin/java_learning/tree/master/nio

JAVA 探究NIO的更多相关文章

  1. JAVA bio nio aio

    [转自]http://qindongliang.iteye.com/blog/2018539 在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 序号 问题 1 什么是同步? ...

  2. java的nio之:java的nio系列教程之buffer的概念

    一:java的nio的buffer==>Java NIO中的Buffer用于和NIO通道Channel进行交互.==>数据是从通道channel读入缓冲区buffer,从缓冲区buffer ...

  3. java的nio之:java的nio系列教程之channel的概念

    一:java的nio的channel Java NIO的通道类似流,但又有些不同: ==>既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的. ==>通道可以异步地读写. ...

  4. java的nio之:java的nio系列教程之概述

    一:java的nio的核心组件?Java NIO 由以下几个核心部分组成: ==>Channels ==>Buffers ==>Selectors 虽然Java NIO 中除此之外还 ...

  5. java之NIO编程

    所谓行文如编程,随笔好比java文件,文章好比类,参考文献是import,那么目录就是方法定义. 本篇文章处在分析thrift的nonblocking server之前,因为后者要依赖该篇文章的知识. ...

  6. 输入和输出--java的NIO

    Java的NIO 实际开发中NIO使用到的并不多,我并不是说NIO使用情景不多,是说我自己接触的并不是很多,前面我在博客园和CSDN上转载了2篇别人写的文章,这里来大致总结下Java的NIO,大概了解 ...

  7. 理解Java的NIO

    同步与阻塞 同步和异步是针对应用程序和内核的交互而言的. 同步:执行一个操作之后,进程触发IO操作并等待(阻塞)或者轮询的去查看IO的操作(非阻塞)是否完成,等待结果,然后才继续执行后续的操作. 异步 ...

  8. Java通过NIO实现快速文件拷贝的代码

    将内容过程重要的内容片段做个记录,下面的内容段是关于Java通过NIO实现快速文件拷贝的内容. public static void fileCopy( File in, File out ) thr ...

  9. 一个小时就能理解Java的NIO必须掌握这三大要素!

    同步与阻塞 同步和异步是针对应用程序和内核的交互而言的. 同步:执行一个操作之后,进程触发IO操作并等待(阻塞)或者轮询的去查看IO的操作(非阻塞)是否完成,等待结果,然后才继续执行后续的操作. 异步 ...

随机推荐

  1. k8s编排最佳实践

    编排文件技巧 使用资源时指定最新稳定版的API version 编排文件应该纳入版本控制,这样可以在必要的时候快速回滚,同样也有利于资源恢复和重建 使用YAML格式而不是JSON格式,尽管两种格式的文 ...

  2. Django基础三(form和template)

    上一篇博文学习了Django的View和urls,接下来是对django form 和 template的学习. 1 django form django form为我们提供了便捷的方式来创建一些HT ...

  3. typeof和instansof的区别

    typeof typeof 是一个一元运算,放在一个运算数之前,运算数可以是任意类型. 它返回值是一个字符串,该字符串说明运算数的类型.(typeof 运算符返回一个用来表示表达式的数据类型的字符串. ...

  4. vue+axios访问本地json数据踩坑点

    当我们想在vue项目中模拟后台接口访问json数据时,我们发现无论如何也访问不到本地的json数据. 注意:1.在vue-cli项目中,我们静态资源只能放在static文件夹中,axios使用get请 ...

  5. 我是庖丁,<肢解IOT平台>之物模型

    前言 物模型是对设备在云端的功能描述,包括设备的属性,数据,服务和事件. 物联网平台通过定义一种物的描述语言来描述物模型,称之为 TSL(即 Thing Specification Language) ...

  6. SSRS报表服务随笔(rdl报表服务)-创建一个简单的报表

    这段时间一直在敲rdl报表,在国内的不这么留在,在国外的话,还是挺流行的,国内的话,这方面的资料很少很少,也踏过不少坑 先从SSRS了解起,SSRS全称 SQL Server Reporting Se ...

  7. EFCore动态切换Schema

    最近做个分库分表项目,用到schema的切换感觉还是有些坑的,在此分享下. 先简要说下我们的分库分表 分库分表规则 我定的规则是,订单号(数字)除以16,得出的结果为这个订单所在的数据库,然后他的余数 ...

  8. 首发福利!全球第一开源ERP Odoo系统架构部署指南 电子书分享

    引言 Odoo,以前叫OpenERP,是比利时Odoo S.A.公司开发的一个企业应用软件套件,开源套件包括一个企业应用快速开发平台,以及几千个Odoo及第三方开发的企业应用模块.Odoo适用于各种规 ...

  9. vtigercrm特色功能介绍

    1.邮件跟踪 市场营销活动中,我们给客户发出了大量的电子邮件,这些邮件被客户阅读的情况你了解吗?vtiger CRM中独特的邮件跟踪功能,可以让你了解到邮件是否被客户浏览.浏览的次数和时间.通过客户的 ...

  10. Java关于读取Excel文件~xlsx xls csv txt 格式文件~持续汇总~

    所需的jar百度网盘链接:https://pan.baidu.com/s/146mrCImkZVvi1CJ5KoiEhQ提取码:c329 1 需要导入jar包,缺1不可 dom4j-1.6.1.jar ...