玩命加载中🤣🤣🤣

基于websocket实现实时日志输出


由于本demo是在原有服务进行拓展, 为减少耦合故新建模块

依赖及配置文件

pom文件

<artifactId>publish-plat-log</artifactId>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <!-- springboot websocket -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <!-- thymeleaf模板 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

注意:

由于此模块没有启动类, 故本依赖及父依赖不能不能有打包插件

spring-boot-maven-plugin

application文件

此处取名为application-wslog.yml

spring:
  thymeleaf:
    prefix: classpath:/static/view/
    check-template: true
  config:
    activate:
      on-profile: wslog

主模块添加配置引用改模块

spring:
  profiles:
    active: dev
    include: wslog

资源包

resources 下新建 static

jquery js工具包

放在static.js, 用于页面引用

HTML页面

static.view 下新建 logging.html

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>实时日志</title>
    <!-- jquery -->
    <script th:src="@{/js/jquery-1.9.1.min.js}"></script>
</head>
<body>
<!-- 标题 -->
<h1 style="text-align: center;">实时日志</h1>

<!-- 显示区 -->
<div id="loggingText" contenteditable="true"
     style="width:100%;height: 600px;background-color: ghostwhite; overflow: auto;"></div>

<!-- 操作栏 -->
<div style="text-align: center;">
    <button onclick="$('#loggingText').text('')" style="color: green; height: 35px;">清屏</button>
    <button onclick="$('#loggingText').animate({scrollTop:$('#loggingText')[0].scrollHeight});"
            style="color: green; height: 35px;">滚动至底部
    </button>
    <button onclick="if(window.loggingAutoBottom){$(this).text('开启自动滚动');}else{$(this).text('关闭自动滚动');};window.loggingAutoBottom = !window.loggingAutoBottom"
            style="color: green; height: 35px; ">开启自动滚动
    </button>
</div>
</body>
<script th:inline="javascript">
    //websocket对象
    let websocket = null;

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:28822/websocket/logging");
    } else {
        console.error("不支持WebSocket");
    }

    //连接发生错误的回调方法
    websocket.onerror = function (e) {
        console.error("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
        console.log("WebSocket连接成功")
    };

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        //追加
        if (event.data) {

            //日志内容
            let $loggingText = $("#loggingText");
            $loggingText.append(event.data + '<br/>');

            // //是否开启自动底部
            if (window.loggingAutoBottom) {
                //滚动条自动到最底部
                $loggingText.scrollTop($loggingText[0].scrollHeight);
            }
        }
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        console.log("WebSocket连接关闭")
    };
</script>
</html>

实现代码

Endpoint

WebSocket获取实时日志并输出到Web页面

import ch.qos.logback.classic.LoggerContext;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Random;

/**
 * WebSocket获取实时日志并输出到Web页面
 */
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/logging")
public class LoggingWSServer {
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    private Integer sessionId;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        this.sessionId = (new Random()).nextInt(100000);
        LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
        // 第二步:获取日志对象 (日志是有继承关系的,关闭上层,下层如果没有特殊说明也会关闭)
        ch.qos.logback.classic.Logger rootLogger = lc.getLogger("root");
        MyAppender myAppender = new MyAppender(this);
        myAppender.setContext(lc);
        // 自定义Appender设置name
        myAppender.setName("myAppender" + sessionId);
        myAppender.start();
        rootLogger.addAppender(myAppender);
        System.out.println("====注入成功====");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
        ch.qos.logback.classic.Logger rootLogger = lc.getLogger("root");
        // 通过name移除Appender
        rootLogger.detachAppender("myAppender" + sessionId);
        System.out.println("====移除成功====");
    }

    /**
     * 服务器主动发送消息
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }
}

WebSocketConfig

此处为了偷懒将测试controller也放在此处

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * WebSocket配置
 */
@Slf4j
@RestController
@Configuration
public class WebSocketConfig {


    /**
     * 用途:扫描并注册所有携带@ServerEndpoint注解的实例。 @ServerEndpoint("/websocket")
     * PS:如果使用外部容器 则无需提供ServerEndpointExporter。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 端口
     */
    @Value("${server.port}")
    private String port;
    @Bean
    public ApplicationRunner applicationRunner() {
        return applicationArguments -> {
            try {
                InetAddress ia = InetAddress.getLocalHost();
                //获取本机内网IP
                log.info("启动成功:" + "http://" + ia.getHostAddress() + ":" + port + "/");
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            }
        };
    }

    /**
     * 跳转实时日志
     */
    @GetMapping("/logging")
    public ModelAndView logging() {
        return new ModelAndView("logging.html");
    }

    /**
     * 测试日志输出
     */
    @GetMapping("/testLog")
    public String testLog() throws Exception {
        log.info("测试日志输出");
        if (true) {
            throw new Exception("异常测试");
        }
        return "testLog";
    }
}

日志格式拓展

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MyAppender extends AppenderBase<ILoggingEvent> {

	private final LoggingWSServer webSocketServer;

	public MyAppender(LoggingWSServer webSocketServer) {
		this.webSocketServer = webSocketServer;
	}

	/**
	 * 添加日志
	 * @param iLoggingEvent
	 */
	@Override
	protected void append(ILoggingEvent iLoggingEvent) {
		try {
			webSocketServer.sendMessage(doLayout(iLoggingEvent));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 格式化日志
	 * @param event
	 */
	public String doLayout(ILoggingEvent event) {
		StringBuilder sbuf = new StringBuilder();
		if (null != event && null != event.getMDCPropertyMap()) {
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");

			sbuf.append(simpleDateFormat.format(new Date(event.getTimeStamp())));
			sbuf.append("\t");

			sbuf.append(event.getLevel());
			sbuf.append("\t");

			sbuf.append(event.getThreadName());
			sbuf.append("\t");

			sbuf.append(event.getLoggerName());
			sbuf.append("\t");

			sbuf.append(event.getFormattedMessage().replace("\"", "\\\""));
			sbuf.append("\t");
		}

		return sbuf.toString();
	}
}

完整层级结构


文章作者: 👑Dee👑
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 👑Dee👑 !
  目录