springboot推送实时日志到前端的两种方式

springboot推送实时日志到前端的两种方式

环境:

后端框架:springboot

前端框架:layui(没有太大影响)

通信方式:websocket

导入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
第一种方式

说明:通过WebSocket的方法,来传输日志
前提:spring boot配置logback

配置WebSocketController
@ServerEndpoint(value = "/websocket/logging")
@Slf4j
@RestController
public class WebSocketController {
    private static Session session;

    /**
     * 连接集合
     */
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
    private static Map<String, Integer> lengthMap = new ConcurrentHashMap<String, Integer>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        //添加到集合中
        sessionMap.put(session.getId(), session);
        lengthMap.put(session.getId(), 1);//默认从第一行开始

        //获取日志信息
        new Thread(() -> {
            log.info("LoggingWebSocketServer 任务开始");
            boolean first = true;
            while (sessionMap.get(session.getId()) != null) {
                BufferedReader reader = null;
                try {
                    //日志文件路径,获取最新的
                    String filePath = "D:/mylogs/debug.log";

                    //字符流
                    reader = new BufferedReader(new FileReader(filePath));
                    Object[] lines = reader.lines().toArray();

                    //只取从上次之后产生的日志
                    Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);

                    //对日志进行着色,更加美观  PS:注意,这里要根据日志生成规则来操作
                    for (int i = 0; i < copyOfRange.length; i++) {
                        String line = (String) copyOfRange[i];
                        //先转义
                        line = line.replaceAll("&", "&amp;")
                                .replaceAll("<", "&lt;")
                                .replaceAll(">", "&gt;")
                                .replaceAll("\"", "&quot;");

                        //处理等级
                        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;
                    }

                    //存储最新一行开始
                    lengthMap.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("LoggingWebSocketServer 任务结束");
        }).start();
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        //从集合中删除
        sessionMap.remove(session.getId());
        lengthMap.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();
        }
    }
}
注入Bean
@Configuration
public class WebSocketConfig {
    /**
     * 服务器节点
     * 如果使用独立的servlet容器,而不是直接使用springboot 的内置容器,就不要注入ServerEndPoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
前端引用

使用websocket的方法

<script th:inline="javascript">
    //websocket对象
    let websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:8088/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);
            //是否开启自动底部
            if (window.loggingAutoBottom) {
                //滚动条自动到最底部
                $loggingText.scrollTop($loggingText[0].scrollHeight);
            }
        }
    }
    //连接关闭的回调方法
    websocket.onclose = function () {
        console.log("WebSocket连接关闭")
    };
</script>
第二种方式

说明:通过配置logback.xml的filter,来直接获取输出到控制台的日志(这里对日志几级别不做处理,一般都是获取全部,然后展示时处理),然后获取的内容用一个实体类存,再将实体类放入队列,websocketconfig配置消息代理端点,即stomp服务端(spring boot自带的webSocket模块提供stomp的服务端),通过stomp来作为服务端去发送log。

首先在项目的日志配置文件配置filter(在你的appender中加入就好)

<!-- 控制台设置 -->
    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="com.unismc.springbootudcap.powersecurity.util.LogFilter"></filter>(加入这个)
    </appender>
	<root level="info">
        <appender-ref ref="consoleAppender"/>
        <!--<appender-ref ref="STDOUT" />-->
    </root>

还可以这样

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--encoder 默认配置为PatternLayoutEncoder-->
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="com.xxxx.LogFilter"></filter>
</appender>

日志过滤器都一致

@Service
public class LogFilter extends Filter<ILoggingEvent> {
    //获取logback的日志,塞入日志队列中
    @Override
    public FilterReply decide(ILoggingEvent event) {
        String exception = "";
        IThrowableProxy iThrowableProxy1 = event.getThrowableProxy();
        if(iThrowableProxy1!=null){
            exception = "<span class='excehtext'>"+iThrowableProxy1.getClassName()+" "+iThrowableProxy1.getMessage()+"</span></br>";
            for(int i=0; i<iThrowableProxy1.getStackTraceElementProxyArray().length;i++){
                exception += "<span class='excetext'>"+iThrowableProxy1.getStackTraceElementProxyArray()[i].toString()+"</span></br>";
            }
        }
        LoggerMessage loggerMessage = new LoggerMessage(
                event.getMessage()
                , DateFormat.getDateTimeInstance().format(new Date(event.getTimeStamp())),
                event.getThreadName(),
                event.getLoggerName(),
                event.getLevel().levelStr,
                exception,
                ""
        );
        LoggerQueue.getInstance().push(loggerMessage);
        return FilterReply.ACCEPT;
    }
}

创建一个阻塞队列,作为日志系统输出的日志的一个临时载体,同样一致

public class LoggerQueue {
    //队列大小
    public static final int QUEUE_MAX_SIZE = 10000;
    private static LoggerQueue alarmMessageQueue = new LoggerQueue();
    //阻塞队列
    private BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);

    private LoggerQueue() {
    }

    public static LoggerQueue getInstance() {
        return alarmMessageQueue;
    }

    /**
     * @Description: 消息入队
     * @Return: boolean
     * @Author: leijun
     * @Date: 2019/11/26
    **/
    public boolean push(LoggerMessage log) {
        //System.out.println("消息入队的信息===="+log);
        return this.blockingQueue.add(log);//队列满了就抛出异常,不阻塞
    }

    /**
     * @Description: 消息出队
     * @Return: com.unismc.springbootudcap.powersecurity.entity.LoggerMessage
     * @Author: leijun
     * @Date: 2019/11/26
    **/
    public LoggerMessage poll() {
        LoggerMessage result = null;
        try {
            //System.out.println("输出:"+this.blockingQueue);
            result = (LoggerMessage) this.blockingQueue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

然后是日志实体

public class LoggerMessage {
    private String body;
    private String timestamp;
    private String threadName;
    private String className;
    private String level;
    private String exception;
    private String cause;
}

重要的是WebSocketConfig

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    //配置WebSocket消息代理端点,即stomp服务端;spring boot自带的webSocket模块提供stomp的服务端
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    /**
     * 推送日志到/topic/pullLogger
     */
    @PostConstruct
    public void pushLogger(){
        ExecutorService executorService= Executors.newFixedThreadPool(2);
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        LoggerMessage log = LoggerQueue.getInstance().poll();
                        if(log!=null){
                            if(messagingTemplate!=null)
                                //服务端发送
                                messagingTemplate.convertAndSend("/topic/pullLogger",log);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        executorService.submit(runnable);
    }
}

需要主义的是:在对应的类上面加入@EnableWebSocketMessageBroker注解,如果不加该注解在运行程序的时候SimpMessagingTemplate类将抛出空指针异常,无法注入。

给一个后端程序猿一个简单的模板,之前看前面一些博主的文章到了前端自己做起来还是有些小问题

<!DOCTYPE>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="js/jquery-v1.12.4.min.js"></script>
</head>
<body>
<!-- 标题 -->
<h1 style="text-align: center;">实时日志</h1>

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

<!-- 操作栏 -->
<div style="text-align: center;">
    <button onclick="openSocket()" style="color: black; height: 35px;">连接</button>
    <button onclick="closeSocket()" style="color: black; height: 35px;">关闭</button>
    <button onclick="$('#log-container').text('')" style="color: green; height: 35px;">清屏</button>
</div>
<!--开启并使用SockJS后,它会优先选用Websocket协议作为传输协议-->
<script type="application/javascript" src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script type="application/javascript" src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>

<script type="text/javascript">
    var stompClient = null;

    function openSocket() {
        if (stompClient == null) {
            if($("#log-container").find("span").length==0){
                $("#log-container").append("<div style='color: #18d035;font-size: 14px'>通道连接成功,静默等待....</div>");
            }
            var socket = new SockJS('http://localhost:8088/websocket?token=kl');
            stompClient = Stomp.over(socket);
            stompClient.connect({token: "kl"}, function (frame) {
                //订阅/topic/pullLogger的消息
                stompClient.subscribe('/topic/pullLogger', function (event) {
                    var content = JSON.parse(event.body);
                    var leverhtml = '';
                    var className = '<span class="classnametext">' + content.className + '</span>';
                    switch (content.level) {
                        case 'INFO':
                            leverhtml = "<span style='color: #90ad2b'>" +content.level + "</span>";
                            break;
                        case 'DEBUG':
                            leverhtml = "<span style='color: #A8C023'>" +content.level + "</span>";
                            break;
                        case 'WARN':
                            leverhtml = "<span style='color: #fffa1c'>" +content.level + "</span>";
                            break;
                        case 'ERROR':
                            leverhtml = "<span style='color: #e3270e'>" +content.level + "</span>";
                            break;
                    }
                    $("#log-container").append("<p class='logp'>" + content.timestamp + " " + leverhtml + " --- [" + content.threadName + "] " + className + " :" + content.body + "</p>");
                    if (content.exception != "") {
                        $("#log-container").append("<p class='logp'>" + content.exception + "</p>");
                    }
                    if (content.cause != "") {
                        $("#log-container").append("<p class='logp'>" + content.cause + "</p>");
                    }
                    //自适应高度
                    //$("#log-container").scrollTop($("#log-container").height() - $("#log-container").height());
                }, {
                    token: "kltoen"
                });
            });
        }
    }

    function closeSocket() {
        if (stompClient != null) {
            stompClient.disconnect();
            stompClient = null;
        }
    }

</script>

</body>
</html>

页面就不展示了

参考地址:
stomp.js客户端:http://jmesnil.net/stomp-websocket/doc/
scok.js客户端:https://github.com/sockjs/sockjs-client
spring webSocket:https://docs.spring.io/spring/docs/

### 回答1: Spring Boot 2.0 是一套快速开发框架,其中包含了 WebSocket 模块,能够轻松地集成 WebSocket,实现服务端主动向前端推送数据。 首先,在pom.xml文件中引入Spring Boot的Starter Websocket依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> 然后,在Spring Boot的启动类上使用@EnableWebSocket注解开启 WebSocket: @SpringBootApplication @EnableWebSocket public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } } 接着,编写一个 WebSocketEndpoint 类用于处理 WebSocket 连接和消息的收发: @Component public class WebSocketEndpoint implements WebSocketHandler { private static final List<WebSocketSession> SESSIONS = new CopyOnWriteArrayList<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { SESSIONS.add(session); } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { // 接收到消息 } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { SESSIONS.remove(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { SESSIONS.remove(session); } @Override public boolean supportsPartialMessages() { return false; } // 服务器向前端推送消息 public static void sendMessage(String message) throws IOException { for (WebSocketSession session : SESSIONS) { if (session.isOpen()) { session.sendMessage(new TextMessage(message)); } } } } 最后,当服务端需要向前端推送数据时,可以调用 WebSocketEndpoint 中的 sendMessage 方法: WebSocketEndpoint.sendMessage("Hello, websocket!"); 前端则需要开启 WebSocket 连接,并在 onmessage 方法中接收服务端推送的数据: var socket = new WebSocket("ws://localhost:8080/websocket"); socket.onmessage = function(event) { var data = event.data; // 处理服务端推送的数据 }; 总之,Spring Boot 2.0 整合 WebSocket 实现服务端主动向前端推送数据非常简单,只需要几行代码即可实现。 ### 回答2: Spring Boot 2.0 提供了与 WebSocket 相关的依赖和注解,可以方便地实现与前端实时通信。下面介绍如何使用 Spring Boot 2.0 整合 WebSocket 实现服务器主动推送数据到前端。 首先,在 pom.xml 文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 然后,创建一个 WebSocket 配置类,使用 `@EnableWebSocket` 注解启用 WebSocket: ``` @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler(), "/websocket").setAllowedOrigins("*"); } @Bean public WebSocketHandler myWebSocketHandler() { return new MyWebSocketHandler(); } } ``` 其中 `MyWebSocketHandler` 是自己实现的 WebSocket 消息处理类,可以在其中实现处理客户端发送过来的消息以及向客户端发送消息的逻辑。 下面是 `MyWebSocketHandler` 的一个示例: ``` public class MyWebSocketHandler extends TextWebSocketHandler { private Set<WebSocketSession> sessions = new CopyOnWriteArraySet<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { sessions.add(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessions.remove(session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 处理客户端发送过来的消息 } public void pushMessage(String message) { for (WebSocketSession session : sessions) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 发送消息失败 } } } } ``` 在 `pushMessage` 方法中,可以遍历所有连接的客户端会话,向它们发送消息。 最后,在需要推送数据的地方,注入 `MyWebSocketHandler`,调用 `pushMessage` 方法即可: ``` @Autowired private MyWebSocketHandler webSocketHandler; public void sendData() { // 处理数据 webSocketHandler.pushMessage(data); } ``` 至此,我们就成功地在 Spring Boot 2.0 中整合了 WebSocket,并实现了服务器主动向前端推送数据的功能。 ### 回答3: 随着现代web应用的流行,实时数据交换变得越来越重要,而websocket作为实时双向通信的技术,成为了重要的实时数据传输协议。Spring Boot2.0整合websocket可以让我们通过服务器主动向前端推送数据,实现实时数据交换,满足现代web应用的高实时性需求。 WebSocket是一种基于HTTP协议的双向通信协议,在通信过程中,浏览器和服务器相互发送数据,实现实时数据交互。Spring Boot2.0已经内置了对WebSocket协议的支持,通过使用Spring WebSocket API可以很容易地配置WebSocket端点和处理程序。 服务器端可通过定义一个WebSocketConfig的配置类,配置WebSocket的Endpoint和Handler,并注册到拦截器链中,如下所示: @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler(), "/ws").addInterceptors(new HttpSessionHandshakeInterceptor()); } @Bean public WebSocketHandler webSocketHandler() { return new MyWebSocketHandler(); } } 在MyWebSocketHandler中,通过实现WebSocketHandler接口的handleMessage方法,可以处理客户端发送来的消息。如下所示: public class MyWebSocketHandler implements WebSocketHandler { @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { //处理客户端发送来的消息 } } 服务器端推送消息到前端,可以通过WebSocketSession的sendMessage方法实现,如下所示: session.sendMessage(new TextMessage("Hello,World!")); 客户端需要使用WebSocket API接收服务器推送来的数据,并处理数据,如下所示: var socket = new WebSocket("ws://localhost:8080/ws"); socket.onmessage = function(event) { //处理服务器推送过来的数据 }; 综上所述,Spring Boot2.0整合websocket可以通过配置WebSocket的Endpoint和Handler,在服务器端主动向前端推送数据,实现实时数据交换。对于现代web应用的高实时性需求,该技术具有重要意义。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值