springboot集成websocket 实时输出日志到浏览器(一)

前言

 一个小功能,页面实时输出日志信息。

一、首先springboot集成websocket

maven配置

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.sakyoka.test</groupId>
  <artifactId>springboot-websocket-log-test</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>springboot-websocket-log-test</name>
  <url>http://maven.apache.org</url>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.4.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<!-- springboot start -->
		<!-- web -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<!-- <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> 
				<artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> -->
		</dependency>

		<!-- socket -->
		<dependency>
		  <groupId>org.springframework.boot</groupId>
		  <artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
		<!-- springboot end -->        
		
		<!-- utils start -->
		<!-- 日志 logging-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</dependency>

		<!-- 热部署 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<!-- utlis end -->
		
		<dependency> 
		    <groupId>org.projectlombok</groupId> 
		    <artifactId>lombok</artifactId> 
		</dependency>

		<dependency>
		    <groupId>cn.hutool</groupId>
		    <artifactId>hutool-all</artifactId>
		    <version>4.1.19</version>
		</dependency>

		<dependency>
		    <groupId>org.apache.tomcat.embed</groupId>
		    <artifactId>tomcat-embed-jasper</artifactId>
		    <scope>provided</scope>
		</dependency>
	</dependencies>


   <build>
        <finalName>springboot-websocket-log-test</finalName>
        <resources>
            <resource>
                <directory>${basedir}/src/main/webapp</directory>
                <!--注意此次必须要放在此目录下才能被访问到-->
                <targetPath>META-INF/resources</targetPath>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
            <resource>
                <directory>${basedir}/src/main/resources</directory>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

配置ServerEndpointExporter ,开启websoket

package com.sakyoka.test.webscoketlog;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 
 * 描述:开启WebSocket、注册ServerEndpointExporter实例
 * @author sakyoka
 * @date 2022年8月14日 2022
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

二、利用socket事件读取日志信息

websocket读取逻辑实现

package com.sakyoka.test.webscoketlog;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Component;

import lombok.extern.log4j.Log4j;

/**
 * 
 * 描述:读取日志信息
 * @author sakyoka
 * @date 2022年8月14日 上午11:01:14
 */
@ServerEndpoint("/log")
@Log4j
@Component
public class WebSocketLog {

    private Process process;
    
    private InputStream inputStream;

    private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();

    @OnOpen
    public void onOpen(Session session) {

    	Map<String, List<String>> params = session.getRequestParameterMap(); 
    	String logPath = "D:\\system.log";
        if (params.containsKey("logPath")){
        	logPath = params.get("logPath").get(0);
        }
        //window系统 tail命令需要添加tail.exe小工具到system32
        String cmd = "tail -f " +logPath; 
        
        log.debug(String.format("show log cmd >> %s", cmd));
		
		Command command = Command. getBuilder().commandStr(cmd).autoReadStream(false);
		command.exec();
		process = command.getProcess();
		inputStream = process.getInputStream();
		
		EXECUTOR_SERVICE.execute(() -> {
	        String line;
	        BufferedReader reader = null;
	        try {
	        	reader = new BufferedReader(new InputStreamReader(inputStream));
	            while((line = reader.readLine()) != null) {
	                session.getBasicRemote().sendText(line + "<br>");
	            }
	        } catch (IOException e) {
	        	
	        }
		});

    }

	@OnMessage
    public void onMessage(String message, Session session){
		log.debug(String.format("socket onmessage ==> 接收到信息:%s", message));
    }

    @OnClose
    public void onClose(Session session) {
    	this.close();
    	log.debug(String.format("socket已关闭"));
    }

    @OnError
    public void onError(Throwable thr) {
    	this.close();
    	log.debug(String.format("socket异常,errorMessage:%s" , thr.getMessage()));
    }
    
    private void close(){
    	
    	//这里应该先停止命令, 然后再关闭流
        if(process != null){
        	process.destroy();
        }

        try {
			if (Objects.nonNull(inputStream)){
				inputStream.close();
			}
		} catch (Exception e) {}   
    }
}

三、日志访问页面

application.properties配置视图、当前测试日志路径(根据实际配置就好)

#系统热部署
spring.devtools.restart.enabled=true
spring.devtools.restart.additional-paths=src/main/java

#端口号
server.port=9000

#视图配置
spring.mvc.view.prefix=/WEB-INF/views
spring.mvc.view.suffix=.jsp

#系统日志重新数据到这个路径文件
logging.file=D:\\system.log

控制层,添加访问页面及测试接口(包含数字打印信息测试)

package com.sakyoka.test.webscoketlog;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import lombok.extern.log4j.Log4j;

/**
 * 
 * 描述:  log控制层
 * @author sakyoka
 * @date 2022年8月14日 上午11:28:25
 */
@RequestMapping("/log")
@Controller
@Log4j
public class LogController {

	private int count = 0;
	
	@RequestMapping("/logconsole")
	public ModelAndView logPage() {
		return new ModelAndView("/logconsole/logconsole");
	}
	
	@RequestMapping("/testlog")
	@ResponseBody
	public String testlog() {
		String string = "测试数据:" + count;
		log.info(string);
		count++;
		return string;
	}
}

页面代码

<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head> 
    <title>jarLog</title>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
	<meta name="renderer" content="webkit">
    <jsp:include page="/WEB-INF/views/common/commonstatic.jsp" flush="true" />
</head>
<body>
<div style="background-color:black;width:99%; height:500px; padding: 10px" id="console-parent">
    <div id="console" style="width:99%; height:95%; color:white;overflow-y: auto; overflow-x:hidden;"></div>
</div>
</body>
<script type="text/javascript" src="${root}/js/console.js"></script>
<script type="text/javascript" src="${root}/js/console-websocket.js"></script>
<script type="text/javascript">
//var port = window.location.port;//如果经过代理?这个经过网关是网关的端口
//var port = "${pageContext.request.serverPort}";//这个才是后端端口
var jarWebSocket;
var jarConsole;
//webscoket访问地址 对应@ServerEndpoint("/log")
var wsurl = 'ws://'+ ip +':'+ port + root +'/log';
$(function(){
	//添加console-parent内容变化,调整滚动条位置,自动滚动最下面
	$("#console").bind("DOMNodeInserted",function(e){
	 	var height = $(this).prop("scrollHeight");
	 	$(this).animate({scrollTop: height},10);		
	});
	
	jarConsole = new JarConsole();
	jarConsole.load('console');
    jarWebSocket= new JarWebSocket({
		url: wsurl,
		//获取后台返回信息
		onmessage: function(event){
			jarConsole.fill(event.data);
		}
	
	}).addEventListener();
});
</script>
</html>	

console-websocket.js 封装socket,socket自动连接

/**
 * WebSocket定义
 * add 2022-01-27 sakyoka
 * ws.readyState ==>
    CONNECTING:值为0,表示正在连接。
    OPEN:值为1,表示连接成功,可以通信了。
    CLOSING:值为2,表示连接正在关闭。
    CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
 */
var JarWebSocket = function(config){
	
	var config = config || {};//配置对象
	//是否连接
	var isConnect = false;
	//WebSocket对象
	var ws = config.ws;
	//请求地址
	var wsurl = config.url; 
	//onmessage 事件
	var onmessage = config.onmessage;
	//是否主动关闭
	var driving = false;
	//是否自动连接,不传默认是
	var autoConnect = config.autoConnect || true;
	//尝试重新连接次数
	var defaultFailTryConnTimes = config.failTryConnTimes || 5;
	var tryConnTimes = 0;
	//心跳失败次数
	var defaulFailtHeartCheckTimes = config.failHeartCheckTimes || 5;
	var tryHeartCheckTimes = 0;
	
	//心跳定时器
	var heartCheckInterval;
	
	//当前对象
	var jarWs = this;

	/**
	 * 创建
	 */
	this.create = function(url){
		
		if (isConnect === true &&ws != undefined){
			return this;
		}
		
		wsurl = url;
		ws = new WebSocket(wsurl);
		return this;
	}
	
	/**
	 * 事件处理
	 */
	this.addEventListener = function(){
		
		if (ws == undefined){
			this.create(wsurl);
		}
		
		if (ws == undefined){
			throw '获取WebSocket失败.';
		}
		
		ws.onopen = function(){
			isConnect = true;
			heartCheckInterval = heartCheck();
			console.log('连接websocket服务成功.');
		}
		
		ws.onerror = function(){
			//清除保持连接定时器
			if (heartCheckInterval){
				clearInterval(heartCheckInterval)
			}
			console.log('连接websocket服务失败.');
		}
		
		ws.onclose = function(){
			console.log('websocket连接服务关闭.');
			//标识连接失败
			isConnect = false;
			//清除保持连接定时器
			if (heartCheckInterval){
				clearInterval(heartCheckInterval)
			}
			//重新连接
			if (!driving && autoConnect === true){
				console.log('websocket尝试连接...');
				jarWs.reconnect();
			}
		}
		
		/**
		 * 接收信息
		 */
		ws.onmessage = function(event){
			if (onmessage){
				onmessage(event);
			}else{
				console.log(event.data);
			}
		}
		
		/**
		 * 浏览器刷新,关闭ws
		 */
		window.onbeforeunload = function(){
			jarWs.close();
		}		
		
		return this;
	}
	
	/**
	 * 主动关闭连接
	 */
	this.close = function(){
		
		//浏览器刷新也算是主动关闭,手动调用也是
		driving = true;
		
		//清除保持连接定时器
		if (heartCheckInterval){
			clearInterval(heartCheckInterval)
		}
		
		//关闭连接
		ws.close();
		
		isConnect = false;
		
		return this;
	}
	
	/**
	 * 重新连接
	 */
	this.reconnect = function(){
		tryConnTimes += 1;
		if (defaultFailTryConnTimes < tryConnTimes){
			console.log('已超过最大尝试连接次数失败,不再重连.times:' + tryConnTimes);
			return ;
		}
		
		if (isConnect === true){
			return ;
		}
		setTimeout(function(){
			if (heartCheckInterval){
				clearInterval(heartCheckInterval);
			}
			jarWs.create(wsurl).addEventListener();
	    }, 3000);
	}
	
	/**
	 * 清除
	 */
	this.reset = function(){
		//重置重连次数
		tryConnTimes = 0;
		//重置心跳次数
		tryHeartCheckTimes = 0;
		//清空定时
		if (heartCheckInterval){
			clearInterval(heartCheckInterval);
		}
		//设置没有主动关闭
		driving = false;
		return this;
	}
	
	/**
	 * 获取WebSocket
	 */
	this.getWebSocket = function(){
		return ws;
	}
	
	/**
	 * 获取连接状态true/false
	 */
	this.isConnect = function(){
		return isConnect;
	}
	
	/**
	 * 心跳连接,10秒发送一次
	 */
	var heartCheck = function(){
		
		var heartCheckInterval = setInterval(function(){
			
			if (defaulFailtHeartCheckTimes < tryHeartCheckTimes){
				console.log('已超过最大尝试心跳发送失败次数,不再发送.times:' + tryHeartCheckTimes);
				clearInterval(this);
				return ;
			}
			
			try{
				ws.send('HEART_CHECK');
			}catch(e){
				console.log("readyState:" + ws.readyState);
				tryHeartCheckTimes += 1;
			}
			
		}, 10 * 1000);
		
		return heartCheckInterval;
	}
}  

console.js,封装日志数据数据到控制台

//jarId对应的JarConsole对象
var JarConsoleRecordObject = {};
var JarConsole = function(){
	
	var consoleStr = "";
	
	var id = "";
	
	var consoleEleObj;
	
	var autoClearRef = false;
	
	var consoleParams = {};
	
    /** 
     * 生成对应控制台div字符串 
    */
    this.createConsoleDivStr = function(id){
	    var historyObject = JarConsoleRecordObject[id];
        if (historyObject != undefined){
	        return historyObject.getConsoleStr();
        }
	    JarConsoleRecordObject[id] = this;
        id = id;
        consoleStr = '<div style="width:99%;height:99%;background-color:black;" id="console-'+ id +'"></div>';
        consoleEleObj = $(consoleStr);
        return consoleStr;
    }
    
    /**
     * 加载元素
     */
    this.load = function(idOrEle){
    	var ele = (typeof(idOrEle) == 'string' ? $('#' + idOrEle): idOrEle);
    	var id = $(ele).attr('id');
	    var historyObject = JarConsoleRecordObject[id];
        if (historyObject != undefined){
	        return historyObject;
        }   	
    	consoleStr = $(ele).html();
    	consoleEleObj = $(ele);
    	JarConsoleRecordObject[id] = this;
    	return this;
    }
    
    /**
     * 填充字符
     */
    this.fill = function(str, extraParams){
    	
    	if (str == undefined || str == '' || consoleEleObj == undefined){
    		return ;
    	}
    	extraParams = extraParams || {};
    	for (var k in extraParams){
	        consoleParams[k] = extraParams[k];
        }
    	var splitStr = consoleParams.splitStr || '\n';
    	var contents = str.split(splitStr);
    	if (contents.length == 0){
    		return ;
    	}
    	
    	var index = 0;
    	var timeoutObj ;
    	var time = consoleParams.time || 100;
    	var allowShowMaxRows = consoleParams.allowShowMaxRows || 200;
        var setTimeoutFillContent = function(){
        	
        	//控制控制台最大展示行数,以免文本内容过大
        	var length = consoleEleObj.children().length;
        	if (length > allowShowMaxRows){
	            consoleEleObj.children().eq(0).remove();
            }
        	
        	consoleEleObj.append('<p>'+ contents[index] +'</p>');
        	index += 1;
        	timeoutObj = setTimeout(function(){setTimeoutFillContent();}, time);
        	
        	if (contents.length <= index){
        		clearTimeout(timeoutObj);
        		return ;
        	}
        }
    	
        setTimeoutFillContent();
        
    	if (autoClearRef === true){
    		this.autoClearRef();
    	}
    	
    	return this;
    }
    
    /**
     * 是否自动清除
     */
    this.autoClearRef = function(clear){
    	autoClearRef = clear;
    	return this;
    }
    
    /**
     * 清空内容
     */
    this.clear = function(){
    	consoleEleObj.empty();
    }
    
    /**
     * 获取对应控制台div字符串
    */
    this.getConsoleStr = function(){
	    return consoleStr;
    }
    
    /**
     * 清除关联
     */
    this.clearRef = function(){
	    //移除其它 
      
        //清除关联
	    delete JarConsoleRecordObject[id];
    }
}

四、打印效果

好了,上面工作做好,就可以测试日志实时输出到页面的效果了

首先,启动项目访问http://127.0.0.1:9000/log/logconsole

 可以看到,刚启动的日志信息,然后在访问几次接口http://127.0.0.1:9000/log/testlog试试

 到此测试日志实时输出完毕,有兴趣可以了解下。

问题发现

    1、发现一个有趣事情,webscoket(@ServerEndpoint)和controller放在同一个目录,被一个aop拦截时候,异常as it is not annotated with @ServerEndpoint

    2、

其它

    websocket日志读取日志输出-Java文档类资源-CSDN下载

   windowtail命令支持-WindowsServer文档类资源-CSDN下载

springboot集成websocket 清空日志后消息广播通知前端重新连接(二)_sakyoka的博客-CSDN博客


版权声明:本文为qq_16430873原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。