前言
一个小功能,页面实时输出日志信息。
一、首先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
补充命令执行类
public class Command {
public static Command getBuilder() {
return new Command();
}
private String contents;
private List<String> listContents;
private boolean isPrint = false;
private String commandStr;
private Process process;
private boolean autoReadStream = true;
private String[] commandArr;
private boolean useStrCommand = true;
public Command commandArr(String ...commandArr) {
this.commandArr = commandArr;
this.useStrCommand = false;
return this;
}
public Command commandStr(String commandStr) {
this.commandStr = commandStr;
this.useStrCommand = true;
return this;
}
public Command isPrint(boolean isPrint) {
this.isPrint = isPrint;
return this;
}
public Command autoReadStream(boolean autoReadStream) {
this.autoReadStream = autoReadStream;
return this;
}
/**
*
* 描述:执行返回信息:字符串
* @author sakyoka
* @date 2020 上午12:32:54
* @return
*/
public String toStringContents() {
StringBuilder stringBuilder = new StringBuilder();
this.process = new RuntimeExecutor() {
@Override
protected void doWath(String line) {
print(line);
stringBuilder.append(line).append("\n");
}
}.exec();
this.contents = stringBuilder.toString();
return this.contents;
}
/**
*
* 描述:执行返回信息:集合存储
* @author sakyoka
* @date 2020 上午12:33:18
* @return
*/
public List<String> toListContets(){
List<String> contents = new ArrayList<String>();
this.process = new RuntimeExecutor() {
@Override
protected void doWath(String line) {
print(line);
contents.add(line);
}
}.exec();
this.listContents = contents;
return this.listContents;
}
/**
* 描述:普通执行
* @author sakyoka
* @date 2022年1月12日 上午10:07:00
*/
public void exec(){
this.process = new RuntimeExecutor() {
private final String preffixName = Command.class.getPackage().getName() + ".Command.exec";
@Override
protected void doWath(String line) {
print(preffixName + ":"+ line);
}
}.exec();
}
abstract class RuntimeExecutor{
public Process exec() {
try {
Process process = (useStrCommand ? Runtime.getRuntime().exec(commandStr)
: Runtime.getRuntime().exec(commandArr));
if (autoReadStream){
streamPrint(process.getInputStream());
streamPrint(process.getErrorStream());
}
return process;
} catch (IOException e) {
throw new RuntimeException("执行命令出错 ====>>>> " , e);
}
}
private void streamPrint(InputStream inputStream) {
if (inputStream == null){
return ;
}
BufferedReader reader = null;
String line;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "gb2312"));
while ((line = reader.readLine()) != null) {
this.doWath(line);
}
} catch (IOException e) {
}finally {
if (reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
}
}
}
}
protected abstract void doWath(String line);
}
public String getContents() {
return contents;
}
public List<String> getListContents() {
return listContents;
}
public String getCommandStr() {
return commandStr;
}
public Process getProcess() {
return process;
}
public boolean isPrint() {
return isPrint;
}
public boolean isAutoReadStream() {
return autoReadStream;
}
public String[] getCommandArr() {
return commandArr;
}
public boolean isUseStrCommand() {
return useStrCommand;
}
/**
*
* 描述:控制台打印
* @author sakyoka
* @date 2020 下午5:26:02
* @param content
*/
private void print(String content) {
if (this.isPrint)
System.out.println(content);
}
}
其它
websocket日志读取日志输出-Java文档类资源-CSDN下载
windowtail命令支持-WindowsServer文档类资源-CSDN下载
springboot集成websocket 清空日志后消息广播通知前端重新连接(二)_sakyoka的博客-CSDN博客