JAVA 探究NIO
事情的开始
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的更多相关文章
- JAVA bio nio aio
[转自]http://qindongliang.iteye.com/blog/2018539 在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 序号 问题 1 什么是同步? ...
- java的nio之:java的nio系列教程之buffer的概念
一:java的nio的buffer==>Java NIO中的Buffer用于和NIO通道Channel进行交互.==>数据是从通道channel读入缓冲区buffer,从缓冲区buffer ...
- java的nio之:java的nio系列教程之channel的概念
一:java的nio的channel Java NIO的通道类似流,但又有些不同: ==>既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的. ==>通道可以异步地读写. ...
- java的nio之:java的nio系列教程之概述
一:java的nio的核心组件?Java NIO 由以下几个核心部分组成: ==>Channels ==>Buffers ==>Selectors 虽然Java NIO 中除此之外还 ...
- java之NIO编程
所谓行文如编程,随笔好比java文件,文章好比类,参考文献是import,那么目录就是方法定义. 本篇文章处在分析thrift的nonblocking server之前,因为后者要依赖该篇文章的知识. ...
- 输入和输出--java的NIO
Java的NIO 实际开发中NIO使用到的并不多,我并不是说NIO使用情景不多,是说我自己接触的并不是很多,前面我在博客园和CSDN上转载了2篇别人写的文章,这里来大致总结下Java的NIO,大概了解 ...
- 理解Java的NIO
同步与阻塞 同步和异步是针对应用程序和内核的交互而言的. 同步:执行一个操作之后,进程触发IO操作并等待(阻塞)或者轮询的去查看IO的操作(非阻塞)是否完成,等待结果,然后才继续执行后续的操作. 异步 ...
- Java通过NIO实现快速文件拷贝的代码
将内容过程重要的内容片段做个记录,下面的内容段是关于Java通过NIO实现快速文件拷贝的内容. public static void fileCopy( File in, File out ) thr ...
- 一个小时就能理解Java的NIO必须掌握这三大要素!
同步与阻塞 同步和异步是针对应用程序和内核的交互而言的. 同步:执行一个操作之后,进程触发IO操作并等待(阻塞)或者轮询的去查看IO的操作(非阻塞)是否完成,等待结果,然后才继续执行后续的操作. 异步 ...
随机推荐
- k8s编排最佳实践
编排文件技巧 使用资源时指定最新稳定版的API version 编排文件应该纳入版本控制,这样可以在必要的时候快速回滚,同样也有利于资源恢复和重建 使用YAML格式而不是JSON格式,尽管两种格式的文 ...
- Django基础三(form和template)
上一篇博文学习了Django的View和urls,接下来是对django form 和 template的学习. 1 django form django form为我们提供了便捷的方式来创建一些HT ...
- typeof和instansof的区别
typeof typeof 是一个一元运算,放在一个运算数之前,运算数可以是任意类型. 它返回值是一个字符串,该字符串说明运算数的类型.(typeof 运算符返回一个用来表示表达式的数据类型的字符串. ...
- vue+axios访问本地json数据踩坑点
当我们想在vue项目中模拟后台接口访问json数据时,我们发现无论如何也访问不到本地的json数据. 注意:1.在vue-cli项目中,我们静态资源只能放在static文件夹中,axios使用get请 ...
- 我是庖丁,<肢解IOT平台>之物模型
前言 物模型是对设备在云端的功能描述,包括设备的属性,数据,服务和事件. 物联网平台通过定义一种物的描述语言来描述物模型,称之为 TSL(即 Thing Specification Language) ...
- SSRS报表服务随笔(rdl报表服务)-创建一个简单的报表
这段时间一直在敲rdl报表,在国内的不这么留在,在国外的话,还是挺流行的,国内的话,这方面的资料很少很少,也踏过不少坑 先从SSRS了解起,SSRS全称 SQL Server Reporting Se ...
- EFCore动态切换Schema
最近做个分库分表项目,用到schema的切换感觉还是有些坑的,在此分享下. 先简要说下我们的分库分表 分库分表规则 我定的规则是,订单号(数字)除以16,得出的结果为这个订单所在的数据库,然后他的余数 ...
- 首发福利!全球第一开源ERP Odoo系统架构部署指南 电子书分享
引言 Odoo,以前叫OpenERP,是比利时Odoo S.A.公司开发的一个企业应用软件套件,开源套件包括一个企业应用快速开发平台,以及几千个Odoo及第三方开发的企业应用模块.Odoo适用于各种规 ...
- vtigercrm特色功能介绍
1.邮件跟踪 市场营销活动中,我们给客户发出了大量的电子邮件,这些邮件被客户阅读的情况你了解吗?vtiger CRM中独特的邮件跟踪功能,可以让你了解到邮件是否被客户浏览.浏览的次数和时间.通过客户的 ...
- Java关于读取Excel文件~xlsx xls csv txt 格式文件~持续汇总~
所需的jar百度网盘链接:https://pan.baidu.com/s/146mrCImkZVvi1CJ5KoiEhQ提取码:c329 1 需要导入jar包,缺1不可 dom4j-1.6.1.jar ...