①首先创建websocket服务器
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Author YMG
* @Date 2021/7/16 11:05
* @Description :
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author YMG
* @Date 2021/7/22 18:37
* @Description :
*/
@SuppressWarnings(value = {"all"})
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/log")
public class WebsocketLoggingServer {
private Process process;
private InputStream inputStream;
/**
* 连接集合
*/
private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
private static final Map<String, Integer> LENGTH_MAP = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
//添加到集合中
SESSION_MAP.put(session.getId(), session);
//默认从第一行开始
LENGTH_MAP.put(session.getId(), 1);
//获取日志信息
new Thread(() -> {
log.info("--------------------log开始--------------------");
boolean first = true;
while (SESSION_MAP.get(session.getId()) != null) {
BufferedReader reader = null;
try {
// 执行 tail -n 命令,填写自己的服务器jar包日志地址,得到流解析
process = Runtime.getRuntime().exec("tail -n 200 nohup.log");
inputStream = process.getInputStream();
//字符流
reader = new BufferedReader(new InputStreamReader(inputStream));
Object[] lines = reader.lines().toArray();
//只取从上次之后产生的日志
Object[] copyOfRange = Arrays.copyOfRange(lines, LENGTH_MAP.get(session.getId()), lines.length);
//对日志关键字加上颜色,可根据自己需求定义
for (int i = 0; i < copyOfRange.length; i++) {
String line = (String) copyOfRange[i];
//先转义
line = line.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """);
//处理等级
line = line.replace("DEBUG", "<span style='color: blue;'>DEBUG</span>");
line = line.replace("INFO", "<span style='color: green;'>INFO</span>");
line = line.replace("WARN", "<span style='color: orange;'>WARN</span>");
line = line.replace("ERROR", "<span style='color: red;'>ERROR</span>");
//处理类名
String[] split = line.split("]");
if (split.length >= 2) {
String[] split1 = split[1].split("-");
if (split1.length >= 2) {
line = split[0] + "]" + "<span style='color: #298a8a;'>" + split1[0] + "</span>" + "-" + split1[1];
}
}
copyOfRange[i] = line;
}
//存储最新一行开始
LENGTH_MAP.put(session.getId(), lines.length);
//截取最新的200行,避免传输的数据太大
if (first && copyOfRange.length > 200) {
copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length);
first = false;
}
String result = StringUtils.join(copyOfRange, "<br/>");
//发送
send(session, result);
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
reader.close();
} catch (IOException ignored) {
}
}
}
log.info("--------------------log结束--------------------");
}).start();
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
//从集合中删除
SESSION_MAP.remove(session.getId());
LENGTH_MAP.remove(session.getId());
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
}
/**
* 封装一个send方法,发送消息到前端
*/
private void send(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
②html页面连接websocket服务器获取日志展示,由于我们用的是thymeleaf模版,请注意静态资源的导入方式(用到了jquery+layui)可以自行操作
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>logging</title>
<!-- 引入公用部分 -->
<script th:src="@{/static/jquery.js}"></script>
<link rel="stylesheet" th:href="@{/static/layui/css/layui.css}">
<script th:src="@{/static/layui/layui.js}"></script>
</head>
<body>
<!-- 显示区 -->
<div id="loggingText" contenteditable="true"
style="width:100%;height: 890px;padding:0 30px;box-sizing:border-box;background-color: ghostwhite; overflow: auto;">
</div>
<!-- 操作栏 -->
<div style="text-align: center;" class="layui-btn-container">
<button class="layui-btn" onclick="$('#loggingText').text('')">清屏
</button>
<button class="layui-btn" onclick="$('#loggingText').animate({scrollTop:$('#loggingText')[0].scrollHeight});">滚动至底部
</button>
<button class="layui-btn" onclick="if(window.loggingAutoBottom){$(this).text('开启自动滚动');}else{$(this).text('关闭自动滚动');}
window.loggingAutoBottom = !window.loggingAutoBottom">开启自动滚动
</button>
</div>
</body>
<script>
//websocket对象
let websocket;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
//服务器IP+端口
websocket = new WebSocket("ws://127.0.0.1:9027/websocket/log");
} else {
console.error("不支持WebSocket");
}
//连接发生错误的回调方法
websocket.onerror = function () {
console.error("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
console.log("WebSocket连接成功")
};
//接收到消息的回调方法
websocket.onmessage = function (event) {
//追加
if (event.data) {
//日志内容
let $loggingText = $("#loggingText");
$loggingText.append(event.data);
//是否开启自动底部
if (window.loggingAutoBottom) {
//滚动条自动到最底部
$loggingText.scrollTop($loggingText[0].scrollHeight);
}
}
}
//连接关闭的回调方法
websocket.onclose = function () {
console.log("WebSocket连接关闭")
};
</script>
</html>
以下是静态资源+HTML页面地址
由于这个项目用到了拦截器,要配置静态资源及页面放行 地址
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @Author YMG
* @Date 2021/6/2 16:35
* @Description :
*/
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**")
/*放行所有请求*/
.excludePathPatterns("/**");
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
/*放行静态资源与页面*/
registry.addResourceHandler("/templates/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX+"/templates/");
registry.addResourceHandler("/static/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX+"/static/");
super.addResourceHandlers(registry);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
/*解决请求跨域*/
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "DELETE")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
③编写访问controller
import com.zh.wisdom.config.security.token.PassToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* @Author YMG
* @Date 2021/7/16 11:18
* @Description :
*/
@RestController
@RequestMapping(value = "/websocket")
public class WebsocketController {
@PassToken
@GetMapping("/log")
public ModelAndView index(){
return new ModelAndView("log");
}
}
④项目打包部署到服务器,然后本地访问controller查看日志
访问地址"服务器ip:端口/websocket/log"
最终呈现效果如下