Java NIO简介和使用

本文介绍了Java NIO的基本概念,包括Channel、Buffer和Selector。NIO提供了同步非阻塞的I/O操作,通过Selector可以使用一个线程管理多个通道。文章详细讲解了如何使用FileChannel、Buffer以及Selector,并给出了实际项目中利用NIO处理大文件内容截取的示例。此外,还提到了SelectionKey的重要角色。
摘要由CSDN通过智能技术生成

前言

Java NIO其实就是JDK1.4中加入的新的基于Channel和Buffer的Iuput/Output方式。我个人认为NIO主要有以下两个优点:

  1. 同步非阻塞:这点是指在结合使用Selector时,会不断的轮询,查看注册的事件是否就绪;而非阻塞模式可以是当前线程不需要傻傻的等待所以的数据都读取完毕再继续向下执行。所以通过通过非阻塞模式加Selector可以仅仅使用一个线程就能管理多个输入输出通道(FileChannel不能结合Selector使用)。
  2. Buffer操作更加灵活普通:IO的缓冲区是不能向前向后移动;NIO的缓冲区可以向前向后的移动。并且Channel可以对文件读取位置进行定位,大大增加了大文件处理的灵活性。还有就是在获取直接内存映射(filechannel.map)时,也可以之间定位到某一个大文件的某一段区域来获取。

NIO中主要有三个主要的结构:Channel(数据通道)、Buffer(缓存区)、Selector(选择器)。

NIO中的基本概念

Channel

Channel就是通道的意思,类似与IO中的流(InputStream/OutputStream),只不过Channel是有读写两种模式,而IO中的流都是单一模式的。Channel的是实现主要分为网络读写和文件读写两大块,主要实现有:

  • FileChannel(文件读写)
  • SocketChannel(网络读写,TCP客户端)
  • ServerSocketChannel(网络读写,TCP服务端)
  • DatagramChannel(UDP数据报)

Buffer

Buffer就是缓冲区的意思,在Buffer中主要有以下几个属性:

  • mark:就是标记的意思。对于Buffer来说,当我们标记了Buffer当前位置,那么使用reset方法就能返回之前标记的位置,实现缓冲区的前后移动(postion方法能直接指定缓存的位置)。
  • postion:位置就是当前读取的位置
  • limit:边界,就是我们读取或者写入不能超过这个数值。
  • capacity:就是缓冲区的容量。
public abstract class Buffer {
    // ......

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    // ......
}

一般情况下,这四个元素能组成缓冲区的读和写两种基本模式。

  1. 写模式:在写模式中postion就是当前写入的位置,limit=capacity表示最多可以写和容量一样大。
  2. 读模式:一般在读模式前需要使用flip()方法将写模式转换为读模式,具体的改变就是将limit=position,position=0。

Buffer有多种类型,最常用的比如ByteBuffer、CharBuffer、MappedByteBuffer,其他的还有DoubleBuffer、FloatBuffer等等。

使用Buffer之前还需要对Buffer的大小通过allocate(int size)方法进行分配,其实就是创建数组,然后初始化mark、position、capacity等属性。对于文件读写来说,上面两个组件就能完成任务,因为文件读取是阻塞模式的,所以不能使用Selector。下面是一个文件读取的简单例子:

public static void main(String[] args) throws IOException {
    RandomAccessFile file = new RandomAccessFile("C:\\Users\\AAA\\Desktop\\工作\\Nio.txt", "r");
    FileChannel channel = file.getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    StringBuilder sb = new StringBuilder();
    while ((channel.read(buffer)) != -1) { // 循环读取文件
        buffer.flip(); // 写->读
        while (buffer.hasRemaining()) {
            char ch = (char)buffer.get();
            if(ch == 'D') {
                buffer.mark(); // 标记
            }
            sb.append(ch);
        }
        // mark的使用
        buffer.reset();
        while (buffer.hasRemaining()) {
            sb.append((char)buffer.get());
        }
        buffer.clear(); // 读->写
    }
    // 元素数据:ABCDEFGHIJK ->现在输出数据:ABCDEFGHIJKEFGHIJK
    System.out.println(sb.toString());
}

下面说明Buffer中主要使用到的一些方法:

  1. position(int newPosition):设置新的缓存区的位置
  2. limit(int newLimit):设置缓冲区的限制,注意mark>limit时,mark将失效
  3. mark():对当前位置打一个标记,就是mark=position
  4. reset():将此缓冲区的位置重置为先前标记的位置,一般mark和reset合在一起使用
  5. clear():清楚mark标识(-1),将position设置为0,limit=capacity,即将读模式转换为写模式
  6. flip():和clear()对应,将写模式转换为读模式,具体的操作是limit=position,position=0,mark=-1
  7. rewind():取消mark标识,重新读取缓冲区
  8. hasRemaining():是否以及读取完毕

Selector

Selector选择器具有同时管理多个Channel的能力(通过询问每一个Channel是否已经准备好I/O操作实现),这样做的好处就是可以通过一个线程来管理多个Tcp或者Udp通道,减少线程的上下文切换。

Selector使用

下面先上一个例子来说明Selector的使用:

/**
*   服务器端
*/
public class NioSocketServer {

    //通道管理器
    private Selector selector;

    //获取一个ServerSocket通道,并初始化通道
    public NioSocketServer init(int port) throws IOException{
        //获取一个ServerSocket通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.socket().bind(new InetSocketAddress(port));
        //获取通道管理器
        selector=Selector.open();
        //将通道管理器与通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,
        //只有当该事件到达时,Selector.select()会返回,否则一直阻塞。
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        return this;
    }

    public void listen() throws IOException{
        System.out.println("服务器端启动成功");

        //使用轮询访问selector
        while(true){
            //当有注册的事件到达时,方法返回,否则阻塞。
            selector.select();

            //获取selector中的迭代器,选中项为注册的事件
            Iterator<SelectionKey> ite=selector.selectedKeys().iterator();

            while(ite.hasNext()){
                SelectionKey key = ite.next();
                //删除已选key,防止重复处理
                ite.remove();
                //客户端请求连接事件
                if(key.isAcceptable()){
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    //获得客户端连接通道
                    SocketChannel channel = server.accept();
                    channel.configureBlocking(false);
                    //向客户端发消息
                    channel.write(ByteBuffer.wrap(new String("send message to client").getBytes()));
                    //在与客户端连接成功后,为客户端通道注册SelectionKey.OP_READ事件。
                    channel.register(selector, SelectionKey.OP_READ);

                    System.out.println("客户端请求连接事件");
                }else if(key.isReadable()){//有可读数据事件
                    //获取客户端传输数据可读取消息通道。
                    SocketChannel channel = (SocketChannel)key.channel();
                    //创建读取数据缓冲器
                    ByteBuffer buffer = ByteBuffer.allocate(10);
                    int read = channel.read(buffer);
                    byte[] data = buffer.array();
                    String message = new String(data);

                    System.out.println("receive message from client, size:" + buffer.position() + " msg: " + message);
//                    ByteBuffer outbuffer = ByteBuffer.wrap(("server.".concat(msg)).getBytes());
//                    channel.write(outbuffer);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new NioSocketServer().init(9981).listen();
    }

}

/**
* 客户端
*/
public class NioClient {
    //管道管理器
    private Selector selector;

    public NioClient init(String serverIp, int port) throws IOException{
        //获取socket通道
        SocketChannel channel = SocketChannel.open();

        channel.configureBlocking(false);
        //获得通道管理器
        selector=Selector.open();

        //客户端连接服务器,需要调用channel.finishConnect();才能实际完成连接。
        channel.connect(new InetSocketAddress(serverIp, port));
        //为该通道注册SelectionKey.OP_CONNECT事件
        channel.register(selector, SelectionKey.OP_CONNECT);
        return this;
    }

    public void listen() throws IOException {
        System.out.println("客户端启动");
        //轮询访问selector
        while(true){
            //选择注册过的io操作的事件(第一次为SelectionKey.OP_CONNECT)
            selector.select();
            Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
            while(ite.hasNext()){
                SelectionKey key = ite.next();
                //删除已选的key,防止重复处理
                ite.remove();
                if(key.isConnectable()){
                    SocketChannel channel=(SocketChannel)key.channel();

                    //如果正在连接,则完成连接
                    if(channel.isConnectionPending()){
                        channel.finishConnect();
                    }

                    channel.configureBlocking(false);
                    //向服务器发送消息
                    channel.write(ByteBuffer.wrap(new String("send message to server.").getBytes()));

                    //连接成功后,注册接收服务器消息的事件
                    channel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                }else if(key.isReadable()){ //有可读数据事件。
                    SocketChannel channel = (SocketChannel)key.channel();

                    ByteBuffer buffer = ByteBuffer.allocate(10);
                    channel.read(buffer);
                    byte[] data = buffer.array();
                    String message = new String(data);

                    System.out.println("recevie message from server:, size:" + buffer.position() + " msg: " + message);
//                    ByteBuffer outbuffer = ByteBuffer.wrap(("client.".concat(msg)).getBytes());
//                    channel.write(outbuffer);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new NioClient().init("127.0.0.1", 9981).listen();
    }
}
  1. 通过Selector.open()来实例化一个Selector,默认通过SelectorProvider类openSelector()方法返回一个实例。
  2. 将当前channel设置为非阻塞模式(serverChannel.configureBlocking(false);),Filechannel不能使用,因为FileChannel没有继承SelectableChannel。
  3. 注册Channel到Selector上(serverChannel.register(selector, SelectionKey.OP_ACCEPT);),这里要注意的是第二个参数SelectorKey,代表的是Channel中将感兴趣的事件注册到Selector上。很明显Selector在Selector中存放着Set<SelectionKey>集合,而SelectorKey中存放着对应channel的信息,包括就绪状态等。
  4. 通过Selector的select()阻塞式获取通道上已注册的事件发送,如果没有事件发生就一直阻塞,直到有事件发生。也可以通过设置超时时间或者使用另外一个非阻塞的selectNow()方法。同时我们也可以通过wakeup()方法唤醒阻塞的selector。
  5. 事件处理,通过selector.selectedKeys()方法获取SelectorKey集合,然后遍历处理,注意要删除已经处理的key,不然下次还会重复处理。SelectionKey中会有isAcceptable()、isReadable()等方法判断哪些事件准备就绪,cancel()可以取消事件监听。
  6. 当Selector使用完毕,可以选项关闭Selector,该关闭并不会影响channel,channel仍然需要自己关闭通道。

SelectionKey

在注册时我们会返回SelectionKey对像,这里说一下SelectionKey包含哪些内容,通过源码我们可以看到:

  1. interestOps(感兴趣事件集合)
  2. readyOps(已经准备好的事件集合)
  3. channel和selector(用于关系对应)
  4. attachment(附件对象)

感兴趣的对象包括以下几个操作:

// 读操作
public static final int OP_READ = 1 << 0;
// 写操作
public static final int OP_WRITE = 1 << 2;
// 连接动作
public static final int OP_CONNECT = 1 << 3;
// 接受连接
public static final int OP_ACCEPT = 1 << 4;

附件的作用:可以将一个或者多个附加对象绑定到SelectionKey上,以便容易的识别给定的通道。通常有两种方式:

// 1.在注册的时候直接绑定
SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);
// 2.在绑定完成之后附加
selectionKey.attach(theObject);//绑定

这是在一个单线程中使用一个Selector处理3个Channel的图示:

实际项目分享

这里分享一个利用Nio实现对大文件(大于10GB的文件)内容截取的功能(按时间段),文件的格式大致如下所示:

[2018-05-12 12:10:00.234] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2018-05-12 12:10:00.530] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2018-05-12 12:10:01.040] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2018-05-12 12:10:01.040] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2018-05-12 12:10:01.045] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
......
......
......
[2018-05-12 20:33:02.100] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2018-05-12 20:33:02.105] XXXX XXXXXX XXXXXX XXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

实现的思路如下所示,首先我们要知道直接内存映射最多只支持2G内存(Integer.MAX_VALUE)。这里我选择了1G内存,通过遍历的方式(每一个G为一个块)确认开始位置在哪一个块中。然后再通过查询算法(比如折半查询等进行查询),这里要注意行的处理。代码如下所示:

public class Searcher {
	/** 默认显示行数 */
	static int defaultRows = SysConfig.getResultShowNum();
	/** 时间format */
	static SimpleDateFormat format1 = new SimpleDateFormat("yyyyMMddHHmmssSSS");
	static SimpleDateFormat format2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
	static DecimalFormat decimalFormat = new DecimalFormat("#.00");
	static {
		format2.setTimeZone(TimeZone.getTimeZone("GMT+16"));
	}
	
	/**
	 * 错误日志提取功能,并提供关键字查询功能
	 * 
	 * @param indexPath 索引目录
	 * @param start 开始
	 * @param end 结束
	 * @param queryString 查询的字符串
	 * @param reverse false 升序 | true 降序
	 * @param save 是否保存到文件
	 * @param savePath 保存路径
	 * @throws Exception 
	 */
	@SuppressWarnings("resource")
	public static List<String> getErrorLogSegment(String indexPath,
			String start, String end, String queryString,
			boolean reverse, boolean save, String savePath) throws Exception{
		List<String> resultList = new ArrayList<String>();
		IndexSearcher searcher = null;
		IndexReader reader = null;
		Analyzer analyzer = new StandardAnalyzer();
		try {
			reader = DirectoryReader
					.open(FSDirectory.open(Paths.get(indexPath)));
			searcher = new IndexSearcher(reader);// 检索工具
			// 查询配置区
			// 多条件查询
			BooleanQuery.Builder builder = new BooleanQuery.Builder();
			// 时间区间
			Query query = new TermRangeQuery("date", new BytesRef(
					format1.format(format2.parse(start))), new BytesRef(
					format1.format(format2.parse(end))), true, true);
			builder.add(query, Occur.MUST);
			// 查询关键词
			if (queryString != null && !queryString.equals("")) {
				QueryParser parser = new QueryParser("content", analyzer);
				Query wordsQuery = parser.parse(queryString);
				builder.add(wordsQuery, Occur.MUST);
			}
			Sort sort = new Sort(
					new SortField("date", SortField.Type.LONG, reverse));
			int searchNum = save ? Integer.MAX_VALUE : defaultRows; // 查询的个数
			TopDocs results = searcher.search(builder.build(), searchNum, sort);
			ScoreDoc[] hits = results.scoreDocs;
			//System.out.println("总命中行数:" + hits.length);
			if (save) {// 保存结果到文件中
				FileWriter fwrite = new FileWriter(savePath, false);
				for (int i = 0; i < hits.length; i++) {
					Document doc = searcher.doc(hits[i].doc);
					fwrite.write(format2.format(format1.parse(doc.get("date")))
							+ " " + doc.get("content") + "\r\n");
					fwrite.flush();
				}
				return null;
			} else {// 显示结果到界面
				int size = hits.length >= defaultRows ? defaultRows : hits.length;
				for (int i = 0; i < size; i++) {
					Document doc = searcher.doc(hits[i].doc);
					resultList.add(format2.format(format1.parse(doc.get("date")))
							+ " " + doc.get("content"));
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return resultList;
	}

	/**
	 * 日志片段截取,并提供关键字查询功能
	 * <p>若选择保存结果到文件,则不返回查询的结果;若不选择保存结果到文件,则返回前指定个数的结果
	 * 
	 * @param filePath 文件路径
	 * @param indexPath 索引目录
	 * @param start 开始
	 * @param end 结束
	 * @param queryRequest 查询的请求
	 * @param save 是否保存
	 * @param savePath 保存路径
	 * @throws Exception 
	 */
	public static List<String> getLogSegment(String filePath, String indexPath, String start,
			String end, String queryRequest, boolean save, String savePath) throws Exception {
		List<String> resultList = new ArrayList<String>();
		File file = new File(filePath);
		try{
			if (!file.exists()) {
				throw new LogAnalysisException("未找到文件!");
			}
			if (queryRequest == null || queryRequest.equals("")) {
				long startPos = getLogPos(file, "[" + start + "]", true);
				startPos = startPos==-1?0:startPos;
				long endPos = getLogPos(file, "[" + end + "]", false);
				endPos = endPos==-1?file.length():endPos;
				//System.out.println("pos:" + startPos + "," + endPos);
				if(save){
					while(true){
						// FileChanel.map的最大长度不能超过Integer.MAX_VALUE(大约一次性最多读取2G内容,大于2G需要分次读取)
						if(endPos - startPos > Integer.MAX_VALUE){
							readFileByMBBAndWriter(file, startPos, (long)(startPos + Integer.MAX_VALUE), savePath);
							startPos = startPos + (long)Integer.MAX_VALUE;
						} else {
							readFileByMBBAndWriter(file, startPos, endPos, savePath);
							break;
						}
					}
				} else {
					resultList = readFileByMappedByteBuffer(file, startPos, endPos);
				}
			} else {
				if(save){
					getRequestLogSegment(indexPath, start, end, queryRequest, null, null, SortType.Default, false, save, savePath);
				}else{
					resultList = getRequestLogSegment(indexPath, start, end, queryRequest, null, null, SortType.Default, false, save, savePath);
				}
			}
		} catch (Exception e) {
			throw e;
		}
		return resultList;
	}
	
	/**
	 * 传入位置列表,并找出所有的行
	 * 
	 * @param file
	 * @param posList 位置列表
	 * @return
	 * @throws Exception 
	 */
	@SuppressWarnings("unused")
	private static List<String> readFileByMappedByteBuffer(File file, List<Long> posList) throws Exception{
		List<String> resultList = new ArrayList<String>();
		MappedByteBuffer mbb = null;
		FileInputStream fis = null;
		FileChannel fChannel = null;
		try {
			fis = new FileInputStream(file);
			fChannel = fis.getChannel();
			for (long pos : posList) {
				mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, pos, 10*1024);
				char ch = (char) mbb.get();
				StringBuilder line = new StringBuilder();
				while (mbb.hasRemaining()) {
					ch = (char) mbb.get();
					line.append(ch);
					if (ch == '\n') {
						resultList.add(line.toString());
						line.delete(0, line.length());
						break;
					}
				}
			}	
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			try {
				if (fChannel != null)
					fChannel.close();
				if (fis != null)
					fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return resultList;
	}
	
	/**
	 * 截取文件中start和end中间数据(读取显示到界面)
	 * 
	 * @param file
	 * @param start
	 * @param end
	 * @return
	 * @throws Exception 
	 */
	private static List<String> readFileByMappedByteBuffer(File file, long start,
			long end) throws Exception {
		List<String> resultList = new ArrayList<String>();
		FileInputStream fis = null;
		FileChannel fChannel = null;
		ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
		Charset charset = Charset.forName("UTF-8");// 解决乱码问题
		CharBuffer charBuffer = null;
		int rows = 0;
		try{
			fis = new FileInputStream(file);
			fChannel = fis.getChannel();
			fChannel.position(start);
			StringBuilder line = new StringBuilder();
			outer:
			while(fChannel.read(buffer) != -1){
				buffer.flip();
				charBuffer = charset.decode(buffer);
				while (charBuffer.hasRemaining()) {
					char ch = charBuffer.get();
					if(ch != '\n'){
						line.append(ch);
					} else {
						rows++;
						line.append(ch);
						resultList.add(line.toString());
						line.delete(0, line.length());
						if(rows >= defaultRows*2)
							break outer;
					}
				}
				buffer.clear();
				charBuffer.clear();
			}
		}catch (IOException e) {
			try {
				if (fChannel != null)
					fChannel.close();
				if (fis != null)
					fis.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
		return resultList;
	}

	/**
	 * 截取文件中start和end中间数据(保存到文件中)
	 * 
	 * @param file
	 * @param start
	 * @param end
	 * @param savePath
	 * @throws Exception 
	 */
	private static void readFileByMBBAndWriter(File file, long start,
			long end, String savePath) throws Exception {
		MappedByteBuffer mbb = null;
		FileInputStream fis = null;
		FileChannel finChannel = null;
		FileChannel foutChanel = null;
		FileOutputStream fout = null;
		try {
			File saveFile = new File(savePath);
			saveFile.getParentFile().mkdirs();
			
			fis = new FileInputStream(file);
			finChannel = fis.getChannel(); // 获取fis的channel
			fout = new FileOutputStream(savePath, true);
			foutChanel = fout.getChannel();
			// 长度最大值有限制不能超过 Size exceeds Integer.MAX_VALUE
			mbb = finChannel.map(FileChannel.MapMode.READ_ONLY, start, end - start);
			//byte[] buffer = new byte[(int) (end-start)];
			foutChanel.write(mbb);
			//mbb.get(buffer);
			//fout.write(buffer);
			fout.flush();
			
			// 清理缓存区
			mbb.clear();
			// 解决mbb内存释放问题
			//clean(mbb);
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			try {
				if (finChannel != null)
					finChannel.close();
				if (foutChanel != null)
					foutChanel.close();
				if (fis != null)
					fis.close();
				if (fout != null)
					fout.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	/*public static void clean(final Object buffer) {
		AccessController.doPrivileged(new PrivilegedAction() {

			@Override
			public Object run() {
				try{
					Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
					getCleanerMethod.setAccessible(true);
					Cleaner cleaner = (Cleaner)getCleanerMethod.invoke(buffer, new Object[0]);
					cleaner.clean();
				}catch (Exception e) {
					e.printStackTrace();
				}
				return null;
			}
			
		});
	}*/
	
	/**
	 * 获取日志中某个请求的详细日志
	 * 
	 * @param datetime
	 * @param sessionAndThread
	 * @param url
	 * @param filePath
	 * @return
	 * @throws Exception 
	 */
	public static List<String> getRequestDetailLog(String datetime, String sessionAndThread, String url, String filePath) throws Exception{
		List<String> list = new ArrayList<String>();
		File file = new File(filePath);
		// 获取时间所在的位置
		long postion = getLogPos(file, datetime, true);
		FileInputStream fis = null;
		FileChannel finChannel = null;
		ByteBuffer buffer = ByteBuffer.allocate(1024);// 1MB
		try{
			fis = new FileInputStream(file);
			finChannel = fis.getChannel();
			finChannel.position(postion);
			StringBuilder line = new StringBuilder();
			outer:
			while(finChannel.read(buffer) != -1){
				buffer.flip();
				while (buffer.hasRemaining()) {
					char ch = (char) buffer.get();
					if(ch != '\n'){
						line.append(ch);
					} else {
						String tmpline = line.toString();
						line.delete(0, line.length());
						if(tmpline.contains(sessionAndThread)){
							list.add(tmpline);
							if(tmpline.contains("After request")){
								break outer;
							}
						}
					}
				}
				buffer.clear();
			}
			buffer.clear();
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			try {
				if(finChannel != null){
					finChannel.close();	
				}
				if(fis != null){
					fis.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return list;
	}
	
	/**
	 * 查询文件中所要查询日期的位置
	 * 
	 * @param file 文件
	 * @param date 查询日期
	 * @param bigOrSmall 当bigOrSmall为true时,pos在查询日期之前 | 当bigOrSmall为false时,pos在查询日志过后
	 * @return
	 * @throws Exception 
	 */
	private static long getLogPos(File file, String date, boolean bigOrSmall) throws Exception {
		String regex = "^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}\\]$";
		String formatStr = "[yyyy-MM-dd HH:mm:ss.SSS]";
		FileInputStream fis = null;
		FileChannel fChannel = null;
		Pattern pattern = Pattern.compile(regex);
		SimpleDateFormat format = new SimpleDateFormat(formatStr);
		// 查询的位置 , 通道直接内存映射大小,默认1GB
		long searchPos = 0L, size = 1073741824L;
		MappedByteBuffer mbb = null;
		try {
			Date start = format.parse(date);
			// 读取的缓存大小,默认1MB
			byte[] buffer = null;
			fis = new FileInputStream(file);
			fChannel = fis.getChannel();
			if(fChannel.size() >= 1048576)
				buffer = new byte[1048576];
			else
				buffer = new byte[(int) fChannel.size()];
			// 获取日志文件最多需要遍历的次数
			int cyclicCount = (int) (fChannel.size() / size + 1);
			long highPosOffset = size;
			// 遍历文件,确定起始位置属于哪个直接内存映射块
			outer: for (int i = 0; i < cyclicCount; i++) {
				if (i == cyclicCount - 1) {
					mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, i * size,
							fChannel.size() - i * size);
					highPosOffset = fChannel.size() - i * size;
				} else {
					mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, i * size,
							size);
				}
				mbb.get(buffer);
				StringBuilder line = new StringBuilder();
				for (byte b : buffer) {
					line.append((char) b);
					if (b == '\n') {
						if (line.length() > 25
								&& pattern.matcher(line.substring(0, 25))
										.matches()) {
							if (format.parse(line.substring(0, 25)).getTime() > start
									.getTime()) {
								break outer;
							}
							searchPos = i * size;
							break;
						}
						line.delete(0, line.length());
					}
				}
			}
			//System.out.println("区间:"+searchPos+","+(searchPos + highPosOffset));
			return bisearchForFile(pattern, format, mbb, fChannel, start,
					searchPos, searchPos + highPosOffset, bigOrSmall);
		} catch (Exception e) {
			//e.printStackTrace();
			throw e;
		} finally {
			try {
				if (fChannel != null)
					fChannel.close();
				if (fis != null)
					fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		//return -1;
	}

	/**
	 * 折半查找时间起始点
	 * 
	 * @param pattern
	 * @param format
	 * @param mbb
	 * @param fChannel
	 * @param startDate
	 * @param lowPos
	 * @param highPos
	 * @return
	 * @throws ParseException
	 * @throws IOException
	 */
	private static long bisearchForFile(Pattern pattern,
			SimpleDateFormat format, MappedByteBuffer mbb,
			FileChannel fChannel, Date start, long lowPos, long highPos, boolean bigOrSmall)
			throws java.text.ParseException, IOException {
		if (lowPos <= highPos) {
			long midPos = (lowPos + highPos) / 2;
			byte[] tmpbuffer = new byte[1024*512];// 512kb
			Date midDate = null;
			//mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, midPos,
			//		(highPos - midPos));
			//System.out.println("L:"+lowPos+", H:"+highPos);
			
			if ((highPos - midPos)*2 > 1024*1024) {
				mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, midPos, highPos - midPos);
				mbb.get(tmpbuffer);
			} else { // 区间达到1M的精确度
				mbb = fChannel.map(FileChannel.MapMode.READ_ONLY, lowPos, highPos - lowPos);
				//System.out.println("lowPos:"+lowPos+", highPos:"+highPos+", midPos:"+midPos);
				midPos = lowPos;
				tmpbuffer = new byte[(int) (highPos - lowPos)];
				mbb.get(tmpbuffer);
				StringBuilder line = new StringBuilder();
				for (byte b : tmpbuffer) {
					line.append((char) b);
					if (b == '\n') {
						if (line.length() > 25
								&& pattern.matcher(line.substring(0, 25))
										.matches()) {
							midDate = format.parse(line.substring(0, 25));
							if (start.getTime() <= midDate.getTime()) {
								if(bigOrSmall){// 之前
									return midPos;
								} else {// 之后
									return lowPos;
								}
							}
							midPos = lowPos;
						}
						line.delete(0, line.length());
					}
					lowPos++;
				}
				return -1;
			}
			StringBuilder line = new StringBuilder();
			for (byte b : tmpbuffer) {
				line.append((char) b);
				if (b == '\n') {
					if (line.length() > 25
							&& pattern.matcher(line.substring(0, 25)).matches()) {
						midDate = format.parse(line.substring(0, 25));
						break;
					}
					line.delete(0, line.length());
				}
			}
			if (start.getTime() == midDate.getTime()) {
				return midPos;
			} else if (start.getTime() > midDate.getTime()) {// 大值半区
				return bisearchForFile(pattern, format, mbb, fChannel, start,
						midPos + 1, highPos, bigOrSmall);
			} else {// 小值半区
				return bisearchForFile(pattern, format, mbb, fChannel, start,
						lowPos, midPos - 1, bigOrSmall);
			}
		}
		return -1;
	}

	/**
	 * 查询某个时间段内某个请求的所有记录,并返回请求的时间信息
	 * 
	 * @param indexPath
	 * @param start
	 * @param end
	 * @param queryRequest
	 * @throws Exception 
	 */
	public static List<String> getAllRequest(String indexPath, String start, String end, String queryRequest) throws Exception {
		IndexSearcher searcher = null;
		IndexReader reader = null;
		Analyzer analyzer = new StandardAnalyzer();
		List<String> requestTimes = new ArrayList<String>();
		try {
			reader = DirectoryReader
					.open(FSDirectory.open(Paths.get(indexPath)));
			searcher = new IndexSearcher(reader);// 检索工具
			// 查询配置区
			// 多条件查询
			BooleanQuery.Builder builder = new BooleanQuery.Builder();
			// 时间区间
			Query query = new TermRangeQuery("date", new BytesRef(
					format1.format(format2.parse(start))), new BytesRef(
					format1.format(format2.parse(end))), true, true);
			builder.add(query, Occur.MUST);
			// 查询关键词
			if (queryRequest != null && !queryRequest.equals("")) {
				QueryParser parser = new QueryParser("content", analyzer);
				Query wordsQuery = parser.parse(queryRequest);
				builder.add(wordsQuery, Occur.MUST);
			}
			TopDocs results = searcher.search(builder.build(), Integer.MAX_VALUE);
			ScoreDoc[] hits = results.scoreDocs;
			//System.out.println("总命中行数:" + hits.length);
			for (int i = 0; i < hits.length; i++) {
				Document doc = searcher.doc(hits[i].doc);
				requestTimes.add(format2.format(format1.parse(doc.get("date"))));
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return requestTimes;
	}
	
	/**
	 * 日志请求提取功能,并提供关键字查询功能 (显示前{@link #defaultRows}条;若选择保存,则会将结果保存到指定目录)
	 * <p>
	 * 选择排序类型包括以下几个类型:
	 * <p>
	 * 1.默认查询排序:如果没有选择请求时间长短排序或者请求次数排序,则默认按时间排序
	 * <p>
	 * 2.请求时间长短排序:如果选择请求时间长短排序,则按照时间的长短进行排序
	 * <p>
	 * 3.请求次数排序:如果选择请求次数排序,则按照请求次数进行排序
	 * 
	 * @param indexPath
	 *            索引目录(必选)
	 * @param start
	 *            开始时间(必选)
	 * @param end
	 *            结束时间(必选)
	 * @param queryString
	 *            查询字符串(可选择)
	 * @param session
	 *            会话(可选择)
	 * @param thread
	 *            线程(可选择)
	 * @param sortType
	 *            选择排序类型
	 * @param reverse
	 *            false 升序 | true 降序
	 * @param save
	 *            是否保存
	 * @param savePath
	 *            保存路径
	 * @return
	 * @throws Exception 
	 */
	public static List<String> getRequestLogSegment(String indexPath,
			String start, String end, String queryString, String session, String thread, 
			SortType sortType, boolean reverse, boolean save, String savePath) throws Exception {
		List<String> resultList = new ArrayList<String>();
		IndexSearcher searcher = null;
		IndexReader reader = null;
		Analyzer analyzer = new StandardAnalyzer();
		try {
			reader = DirectoryReader
					.open(FSDirectory.open(Paths.get(indexPath)));
			searcher = new IndexSearcher(reader);// 检索工具
			// 查询配置区
			// 多条件查询
			BooleanQuery.Builder builder = new BooleanQuery.Builder();
			// 时间区间
			Query query = new TermRangeQuery("date", new BytesRef(
					format1.format(format2.parse(start))), new BytesRef(
					format1.format(format2.parse(end))), true, true);
			builder.add(query, Occur.MUST);
			// 查询关键词
			if (queryString != null && !queryString.equals("")) {
				QueryParser parser = new QueryParser("content", analyzer);
				Query wordsQuery = parser.parse(queryString);
				builder.add(wordsQuery, Occur.MUST);
			}
			if (thread != null && !thread.equals("")){
				Term term = new Term("thread", thread);
				Query query2 = new TermQuery(term);
				builder.add(query2, Occur.MUST);
			}
			if (session != null && !session.equals("")){
				Term term = new Term("session", session);
				Query query2 = new TermQuery(term);
				builder.add(query2, Occur.MUST);
			}
			// 排序, lucene5中对排序的字段,必须是使用NumericDocValuesField字段
			if (sortType == SortType.Default) {
				resultList = defaultRequestLogSegment(searcher, builder, reverse, save,
						savePath);
			} else if (sortType == SortType.RequestTime) {
				resultList = requestTimeLogSegment(searcher, builder, !reverse, save,
						savePath);
			} else if (sortType == SortType.RequestFrequency) {
				resultList = requestFrequencyLogSegment(searcher, builder, !reverse, save,
						savePath);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw e;
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return resultList;
	}

	/**
	 * 默认的查询,默认只显示排名前{@link #defaultRows}的结果
	 * 
	 * @param searcher
	 * @param builder
	 * @param save
	 * @param savePath
	 * @return
	 * @throws Exception
	 */
	private static List<String> defaultRequestLogSegment(
			IndexSearcher searcher, Builder builder, boolean reverse,
			boolean save, String savePath) throws Exception {
		Sort sort = new Sort(
				new SortField("date", SortField.Type.LONG, reverse));
		int searchNum = save ? Integer.MAX_VALUE : defaultRows; // 查询的个数
		TopDocs results = searcher.search(builder.build(), searchNum, sort);
		ScoreDoc[] hits = results.scoreDocs;
		//System.out.println("总命中行数:" + hits.length);
		if (save) {// 保存结果到文件中
			//FileWriter fwrite = new FileWriter(savePath, false);
			List<String[]> data = new ArrayList<String[]>();
			for (int i = 0; i < hits.length; i++) {
				Document doc = searcher.doc(hits[i].doc);
				// {日志级别}%{seesion:线程}%{日志名称}%{url}
				data.add(new String[]{
						"[" + format2.format(format1.parse(doc.get("date"))) + "]",
						doc.get("content").split(" ")[0], 
						doc.get("content").split(" ")[1],
						doc.get("content").split(" ")[2],
						doc.get("content").split(" ")[3]});
			}
			ExcelExport.exportInfo(data, new String[]{"时间", "日志级别", "session:线程", "日志名称", "URL"}, new File(savePath));
			return null;
		} else {// 显示结果到界面
			List<String> list = new ArrayList<String>();
			int size = hits.length >= defaultRows ? defaultRows : hits.length;
			for (int i = 0; i < size; i++) {
				Document doc = searcher.doc(hits[i].doc);
				list.add("[" + format2.format(format1.parse(doc.get("date"))).replace(" ", "_") + "] "
						+ doc.get("content") + "\n");
			}
			return list;
		}
	}

	/**
	 * 按请求时间长短进行查询排序,默认只显示排名前{@link #defaultRows}的结果
	 * 
	 * @param searcher
	 * @param builder
	 * @param reverse
	 * @param save
	 *            false 升序 | true 降序
	 * @param savePath
	 * @return
	 * @throws Exception
	 */
	private static List<String> requestTimeLogSegment(IndexSearcher searcher,
			Builder builder, boolean reverse, boolean save, String savePath)
			throws Exception {
		Sort sort = new Sort(new SortField("interval", SortField.Type.FLOAT,
				reverse));
		int searchNum = save ? Integer.MAX_VALUE : defaultRows; // 查询的个数
		TopDocs results = searcher.search(builder.build(), searchNum, sort);
		ScoreDoc[] hits = results.scoreDocs;
		//System.out.println("总命中行数:" + hits.length);
		if (save) {// 保存到文件
			List<String[]> data = new ArrayList<String[]>();
			for (int i = 0; i < hits.length; i++) {
				Document doc = searcher.doc(hits[i].doc);
				data.add(new String[]{ "[" + format2.format(format1.parse(doc.get("date"))) + "]",
					doc.get("content").split(" ")[1],
					doc.get("content").split(" ")[2],
					doc.get("content").split(" ")[3],
					doc.get("interval")});
			}
			ExcelExport.exportInfo(data, new String[]{"时间", "session:线程", "日志名称", "URL", "执行时间(毫秒)"}, new File(savePath));
			return null;
		} else {// 显示结果到界面
			List<String> list = new ArrayList<String>();
			int size = hits.length >= defaultRows ? defaultRows : hits.length;
			for (int i = 0; i < size; i++) {
				Document doc = searcher.doc(hits[i].doc);
				// {日志级别}%{seesion:线程}%{日志名称}%{url}%{执行时间}
				list.add(format2.format(format1.parse(doc.get("date"))).replace(" ", "_") + " "
						+ doc.get("content").split(" ")[1] + " "
						+ doc.get("content").split(" ")[2] + " "
						+ doc.get("content").split(" ")[3] + " " 
						+ doc.get("interval") + "\n");
			}
			return list;
		}
	}

	/**
	 * 按请求次数进行查询排序,默认只显示排名前{@link #defaultRows}的结果
	 * 
	 * @param searcher
	 * @param builder
	 * @param reverse
	 * @param save
	 *            false 升序 | true 降序
	 * @return
	 * @throws Exception
	 */
	private static List<String> requestFrequencyLogSegment(
			IndexSearcher searcher, Builder builder, boolean reverse,
			boolean save, String savePath) throws Exception {
		Sort sort = new Sort(new SortField("date", SortField.Type.LONG));
		TopDocs results = searcher.search(builder.build(), Integer.MAX_VALUE,
				sort);
		ScoreDoc[] hits = results.scoreDocs;
		//System.out.println("总命中行数:" + hits.length);
		HashMap<String, RequestModel> requestMap = new HashMap<String, RequestModel>();
		// 获取
		for (ScoreDoc scoreDoc : hits) {
			Document doc = searcher.doc(scoreDoc.doc);
			String url = doc.get("content").split(" ")[3];
			if (requestMap.containsKey(url)) {
				requestMap.put(url, new Searcher().new RequestModel(requestMap.get(url).times + 1,
						requestMap.get(url).sumTime + Float.parseFloat(doc.get("interval"))));
			} else {
				requestMap.put(url, new Searcher().new RequestModel(1, Float.parseFloat(doc.get("interval"))));
			}
		}
		// 排序
		List<Map.Entry<String, RequestModel>> retList = new ArrayList<Map.Entry<String, RequestModel>>();
		retList.addAll(requestMap.entrySet());
		MapValueComparator mvComparator = new Searcher().new MapValueComparator(reverse);
		// sort方法使用的是归并排序(合并排序),是稳定排序中最快的一种
		Collections.sort(retList, mvComparator);
		if (save) {// 保存到文件
			List<String[]> data = new ArrayList<String[]>();
			for (Entry<String, RequestModel> entry : retList) {
				data.add(new String[]{entry.getKey(), String.valueOf(entry.getValue().times), 
						String.valueOf((int)(entry.getValue().sumTime/entry.getValue().times))});
			}
			ExcelExport.exportInfo(data, new String[]{"URL", "访问次数", "平均执行时间(毫秒)"}, new File(savePath));
			return null;
		} else {// 显示结果到界面
			List<String> list = new ArrayList<String>();
			int showCount = 0;
			for (Entry<String, RequestModel> entry : retList) {
				showCount++;
				list.add(entry.getKey() + " " + entry.getValue().times + " " + (int)(entry.getValue().sumTime/entry.getValue().times));
				if (showCount >= defaultRows) {
					break;
				}
			}
			return list;
		}
	}

	/**
	 * 对Map类型元素的值进行比较
	 * 
	 * @author wuxinhui
	 *
	 */
	class MapValueComparator implements Comparator<Map.Entry<String, RequestModel>> {

		public MapValueComparator() {
		}

		public MapValueComparator(boolean reverse) {
			this.reverse = reverse;
		}

		boolean reverse = false;// false 升序|true 降序

		public void setReverse(boolean reverse) {
			this.reverse = reverse;
		}

		@Override
		public int compare(Map.Entry<String, RequestModel> map1,
				Map.Entry<String, RequestModel> map2) {
			if (reverse) {
				return map2.getValue().times - map1.getValue().times;
			} else {
				return map1.getValue().times - map2.getValue().times;
			}
		}
	}

	/**
	 * 希尔排序(缩小增量排序):对于数据库不大的排序建议使用,希尔排序是对直接插入排序的优化。
	 * 
	 * @param data
	 */
	@SuppressWarnings("unused")
	private static int[] shellSort(int data[]) {
		int j = 0, temp = 0;
		for (int increment = data.length / 2; increment > 0; increment = increment / 2) {
			//System.out.println("increment:" + increment);
			for (int i = increment; i < data.length; i++) {
				temp = data[i];
				for (j = i - increment; j >= 0; j = j - increment) {
					if (temp < data[j]) {
						data[j + increment] = data[j];
					} else {
						break;
					}
				}
				data[j + increment] = temp;
			}
			for (int i = 0; i < data.length; i++) {
				System.out.print(data[i] + " ");
			}
		}
		return data;
	}

	class RequestModel{
		int times;// 访问的次数
		float sumTime;// 访问的总时间,以便计算平均时间
		public RequestModel(int times, float sumTime) {
			super();
			this.times = times;
			this.sumTime = sumTime;
		}
		public int getTimes() {
			return times;
		}
		public float getSumTime() {
			return sumTime;
		}
	}
}

参考文档

https://www.cnblogs.com/gaotianle/p/3325451.html

http://ifeve.com

https://blog.csdn.net/jeffleo/article/details/54695959

 

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值