1.问题来源
有的时候,我们可能要查看控制台的输出信息,用来排查可能出现的问题,但是又不方便连接到服务器进行查看,这个时候如果有个网页版的日志信息查看功能就好了,于是这个功能就有存在的必要了。
这个例子我们采用
Springboot2.4.6
+websocket
的方式来实现,如果采用其他框架,则只需要做相应的调整就好了,毕竟核心是不变的。项目基本配置参考SpringBoot入门一,使用myEclipse新建一个SpringBoot项目,使用MyEclipse新建一个SpringBoot项目即可.
2.功能实现
2.1 pom.xml添加Websocket支持
<!-- 引入websocket支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 application.properties设定配置信息
主要是设置一下 项目路径,如果采用默认的信息,不设定也行
# ----------------项目基本配置---------------
## 端口号
server.port=80
## 设置项目路径.默认是"/"
#server.servlet.context-path=/qfxWebConsoleInfoDemo
2.3 添加Websocket服务
想要将控制台的信息实时输出到网页上,比较好的方式就是采用Websocket来实现,可以更高效的将信息进行实时传递。
Websocket的添加可以参考《SpringBoot入门二十,添加Websocket支持》,这里简单说一下.
2.3.1 添加配置文件
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* <h5>描述:如果使用外部Tomcat部署的话,则不需要此配置</h5>
*
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.3.1 添加Websocket服务
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* <h5>描述:WebSocket服务端</h5>
* WebSocket是类似客户端服务端的形式(采用ws协议),
* 所以 WebSocketServer其实就相当于一个ws协议的 Controller,
* 可以在里面实现 @OnOpen、@onClose、@onMessage等方法
*/
@ServerEndpoint("/websocket/{cid}")
@Component
public class WebSocketSer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketSer.class);
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketSer> webSocketSet = new CopyOnWriteArraySet<WebSocketSer>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收cid
private String cid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("cid") String cid) {
this.session = session;
webSocketSet.add(this); // 加入set中
addOnlineCount(); // 在线数加1
LOG.info("客户端: " + cid + " 连接成功, 当前在线人数为:" + getOnlineCount());
this.cid = cid;
try {
sendMessage("连接成功");
} catch (IOException e) {
LOG.error("发送消息异常:", e);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); // 从set中删除
subOnlineCount(); // 在线数减1
//LOG.info("有一个连接关闭,当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到来自客户端 " + cid + " 的信息: " + message);
// 群发消息
for (WebSocketSer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 群发自定义消息
*/
public static void sendInfo(String message, @PathParam("cid") String cid) {
// LOG.info("推送消息到客户端:" + cid + ",内容: " + message);
for (WebSocketSer item : webSocketSet) {
try {
// 这里可以设定只推送给这个cid的,为null则全部推送
if (cid == null) {
item.sendMessage(message);
} else if (item.cid.equals(cid)) {
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketSer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketSer.onlineCount--;
}
}
2.4 自定义输出流(核心)
想要将控制台的信息实时输出到网页上,除了采用Websocket来进行消息的实时传递,更重要的是要能够获取到控制台的输出信息并通过Websocket发送到页面上,并且不能影响控制台本身的输出显示。
2.4.1 创建自定义的TeePrintStream
创建一个自定义的输出流,
继承PrintStream
,并重写write方法
/**
* 继承自 `PrintStream`,可以同时将输出内容通过WebSocket发送到页面上。
*/
public class TeePrintStream extends PrintStream {
/**
* 构造函数,用于创建 `TeePrintStream` 对象。
*
* @param mainStream 主要的输出流
*/
public TeePrintStream(OutputStream mainStream) {
super(mainStream); // 调用父类 PrintStream 的构造函数,传入主要的输出流
}
/**
* 将字节数组输出到主要输出流。
*
* @param buf 输出的字节数组
* @param off 数组的起始偏移量
* @param len 要写入的字节数
*/
@Override
public void write(byte[] buf, int off, int len) {
super.write(buf, off, len); // 将字节数组输出到主要输出流
// 检测换行符
String str = new String(buf, off, len);
if ("\r\n".equals(str) || "\r".equals(str)) {
return;
}
WebSocketSer.sendInfo(str, null);
}
}
2.4.1 启用自定义的输出流
//import java.io.PrintStream;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import com.qfx.common.bean.TeePrintStream;
@Component
public class ConsolePrint {
/**
* 重写标准输出流,支持正常输出的同时将信息通过websocket信息发送的页面上
*/
@PostConstruct
public void setPrintStream() {
System.out.println("开始重新定义输出流...");
// 保存原来的标准输出流
// PrintStream originalOut = new PrintStream(System.out);
TeePrintStream teePrintStream = new TeePrintStream(System.out);
// 设定新的输出流
System.setOut(teePrintStream);
// 恢复原始的标准输出流
// System.setOut(originalOut);
System.out.println("重新定义输出流完毕...");
}
}
2.5 创建web页面
创建一个web页面用来输出控制台信息,本示例是创建的一个静态页面,文件放在了
/resources/static/
目录下,无需经过controller即可直接访问,如有需要也可以做成动态页面,根据自己项目情况而定即可。对了,这里引用了jquery-3.4.1.min.js
,也可以根据自己的情况调整。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>控制台信息展示</title>
<style type="text/css">
/* 日志页面样式 */
.console {
background-color: #363636 !important;
color: #00c200;
font-size: 14px;
}
</style>
</head>
<body class="console">
<button onclick="closeWebSocket()">关闭日志</button>
<button onclick="openWebSocket()">打开日志</button>
<button onclick="clearMessages()">清理日志</button>
<div id="message"></div>
</body>
<!-- jquery控件 -->
<script type="text/javascript" charset="utf-8" src="../js/common/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
var websocket = null;
var messageList = []; // 存储消息的数组
var maxMessages = 20; // 最大存储消息数量
var isWebSocketOpen = false; // WebSocket是否已打开的标志
window.onload = function() {
setTimeout(function() {
var winHeight = $(window).height(); //浏览器时下窗口可视区域高度
var showHeight = winHeight - 22.8;
// 根据页面高度来自动计算最大存储消息数量
maxMessages = parseInt(showHeight/19.2);
}, 1000);
};
// 获取当前项目IP信息
var host = window.location.hostname;
// 获取当前项目端口号信息
var port = window.location.port;
// 设定当前项目名称
var url = window.location.href;
var projectName = url.split('/')[3];
// 如果是使用的springboot且没有指定名称,需将projectName设定为空串:""
projectName = "";
// 打开WebSocket连接
openWebSocket();
//连接发生错误的回调方法
function onError(event) {
setMessageInnerHTML("error");
}
//连接成功建立的回调方法
function onOpen(event) {
isWebSocketOpen = true;
setMessageInnerHTML(event.data);
}
//接收到消息的回调方法
function onMessage(event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
function onClose() {
isWebSocketOpen = false;
setMessageInnerHTML("连接关闭");
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
if (undefined != innerHTML) {
messageList.push(innerHTML); // 将新消息添加到数组末尾
if (messageList.length > maxMessages) {
messageList.shift(); // 如果超过最大存储数量,则移除数组开头的消息
}
document.getElementById('message').innerHTML = messageList.join('<br>'); // 将数组中的消息连接成字符串并显示在页面上
}
}
// 打开WebSocket连接
function openWebSocket() {
if (isWebSocketOpen) {
setMessageInnerHTML("连接已打开");
return;
}
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://" + host + ":" + port + "/" + projectName + "/websocket/0001");
websocket.onerror = onError;
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onclose = onClose;
} else {
alert('Not support websocket')
}
}
//关闭连接
function closeWebSocket() {
if (websocket) {
websocket.close();
}
}
//清除消息列表
function clearMessages() {
messageList = []; // 清空消息列表
document.getElementById('message').innerHTML = ""; // 清空页面消息显示区域
}
</script>
</html>
其实到了这里整个功能就都配置完成了,启动服务访问指定的web页面即可。
3.测试
我们写一个定时任务来输出一些信息,看看是否能正确获取到控制台信息
3.1 开启定时任务服务
Springboot添加
@EnableScheduling
注解,开启定时任务支持
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class RunApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(RunApp.class, args);
Environment environment = context.getBean(Environment.class);
String port = environment.getProperty("server.port");
String ctxPath = environment.getProperty("server.servlet.context-path");
ctxPath = ctxPath == null ? "/" : ctxPath;
System.out.println("系统 已启动!请访问 http://localhost:" + port + ctxPath);
}
}
3.2 添加定时任务
添加了一个测试的定时任务,每两秒钟会输出一条信息
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class TestTask {
//输出时间格式
private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:sss");
@Scheduled(cron = "0/2 * * * * ? ")
private void sayHello(){
String dateTime = format.format(new Date());
System.out.println(dateTime + " 向宇宙发出了一声问候: Hello World!");
}
}
3.3 页面效果查看
使用Springboot项目,输入 http://127.0.0.1/websocket/consoleinfo.html ,即可查看到日志信息,可以看到控制台与页面输出的信息都是一致的。