java實現實時展示運行日誌(1)-springboot + websocket 實現後臺主動將日誌推送到前端頁面

最近有一個需求,要將任務運行的日誌實時顯示在前端頁面上,讓用戶及時瞭解該任務的執行情況。分兩部分:後臺如何獲取日誌信息?獲取日誌信息後,如何實時展示在前端頁面上?

本文先討論下實時展示到頁面,可採取的方案有兩種:①ajax輪詢,隔3-5秒訪問後臺,獲取日誌信息響應;②無需前端請求,後臺主動將日誌信息推送到前端頁面展示。以上兩者都是可行的,沒有絕對的優劣。本文就第二種方案使用websocket實現做了一個樣例,說明:這裏只有乾貨,websocket 的詳細介紹可問度娘。。。。


1. 使用 swagger 接口文檔進行調試(生產環境應關閉swagger訪問配置), 若不習慣使用 swagger 接口調試,可忽略下面的swagger 的部分。

2. 使用 websocket 主動、實時推送信息到前端(注意事項:使用 websocket 前確定環境支持websocket;demo爲本地環境,使用的谷歌瀏覽器;websocket 不兼容低版本的IE,若使用,請確認線上用戶是否有瀏覽器的限制)


項目結構:


配置部分--

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.RELEASE</version>
	</parent>
	<groupId>com.zlf</groupId>
	<artifactId>websocket-demo</artifactId>
	<version>1.0-SNAPSHOT</version>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<lombok.version>1.16.6</lombok.version>
		<commons.version>5.0.7</commons.version>
		<junit.version>4.12</junit.version>
		<springfox-swagger.version>2.7.0</springfox-swagger.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.11</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.system.commons</groupId>
			<artifactId>commons</artifactId>
			<version>${commons.version}</version>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
		</dependency>
		<!-- websocket  -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>
		<!-- Swagger API文檔 -->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>${springfox-swagger.version}</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>${springfox-swagger.version}</version>
		</dependency>
		<!-- thymeleaf 頁面模板-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<configuration>
					<skip>true</skip>
					<testFailureIgnore>true</testFailureIgnore>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>
application.yml
server:
  port: 8086

spring:
  application:
    name: websocket-demo
  thymeleaf:
    prefix: classpath:/templates/

swagger:
  title: WebSocketDemo API接口文檔
  description: WebSocketDemo Api Documentation
  version: 1.0.0
  termsOfServiceUrl: https://blog.csdn.net/daoerZ
  contact:
    name: daoerZ
    url: https://blog.csdn.net/daoerZ
    email: [email protected]

SpringBootWebSocketApplication.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 啓動類
 *
 * @version 1.0
 **/
@Slf4j
@SpringBootApplication(scanBasePackages = {"com.zlf.websocket"})
@RestController
public class SpringBootWebSocketApplication {

	public static void main(String[] args){
		SpringApplication.run(SpringBootWebSocketApplication.class, args);
        // 爲了使用swagger方便,這裏把swagger的訪問地址打印出來,啓動項目後,在控制檯點擊此鏈接,即可自動打開swagger頁面
		log.info("swagger url:  http://localhost:8086/swagger-ui.html");
	}

	@RequestMapping(value = "/helloWebSocket",method = RequestMethod.GET)
	public String helloWebSocket(){
		return "helloWebSocket";
	}

}

swagger部分--

Swagger2Config.java
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * Swagger2 的配置類
 *
 **/
@Slf4j
@Configuration
@EnableSwagger2
public class Swagger2Config {

	@Value("${swagger.title}")
	private String title;

	@Value("${swagger.description}")
	private String description;

	@Value("${swagger.version}")
	private String version;

	@Value("${swagger.termsOfServiceUrl}")
	private String termsOfServiceUrl;

	@Value("${swagger.contact.name}")
	private String name;

	@Value("${swagger.contact.url}")
	private String url;

	@Value("${swagger.contact.email}")
	private String email;


	private List<ApiKey> securitySchemes() {
		List<ApiKey> apiKeys = new ArrayList<>();
		apiKeys.add(new ApiKey("Authorization", "accessToken", "header"));
		return apiKeys;
	}

	private List<SecurityContext> securityContexts() {
		List<SecurityContext> securityContexts = new ArrayList<>();
		securityContexts.add(SecurityContext.builder()
				.securityReferences(defaultAuth())
				.forPaths(PathSelectors.regex("^(?!auth).*$")).build());
		return securityContexts;
	}

	private List<SecurityReference> defaultAuth() {
		AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
		AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
		authorizationScopes[0] = authorizationScope;
		List<SecurityReference> securityReferences = new ArrayList<>();
		securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
		return securityReferences;
	}

	@Bean
	public Docket createRestApi() {

		log.info("加載Swagger2");

		return new Docket(DocumentationType.SWAGGER_2)
				.apiInfo(apiInfo()).select()
            // 掃描所有有註解的api,用這種方式更靈活
				.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
				.paths(PathSelectors.any())
				.build()
				.securitySchemes(securitySchemes())
				.securityContexts(securityContexts());
	}

	/**
	 * 創建API的基本信息,會展示在文檔頁面中
	 * @return
	 */
	private ApiInfo apiInfo() {
		return new ApiInfoBuilder()
				.title(title)
				.description(description)
				.termsOfServiceUrl(termsOfServiceUrl)
				.contact(new Contact(name, url, email))
				.version(version)
				.build();
	}

}
SwaggerTestController.java
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * Swagger 測試控制類
 **/
@Slf4j
@RestController
@Api(description = "Swagger測試控制接口")
@RequestMapping("/swaggerTest")
public class SwaggerTestController {

	@RequestMapping(value = "/hello/{param}", method = RequestMethod.POST)
	@ApiOperation(value = "測試hello")
	public Integer helloSwagger(@PathVariable("param") Integer param) {
		return param;
	}

}

==至此,可以啓動項目,測試下swagger的功能了。

控制檯日誌有swagger訪問路徑,http://localhost:8086/swagger-ui.html


websocket部分--

WebSocketConfig.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 開啓 WebSocket:使用 springboot 內嵌的tomcat容器啓動 websocket
 **/
@Slf4j
@Configuration
public class WebSocketConfig {

	/**
	 * 服務器節點
	 *
	 * 如果使用獨立的servlet容器,而不是直接使用springboot 的內置容器,就不要注入ServerEndPoint
	 *
	 * @return
	 */
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		log.info("啓動 WebSocket ...");
		return new ServerEndpointExporter();
	}

}
WebSocketServer.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

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

/**
 * WebSocket 服務
 *
 **/
@Slf4j
@ServerEndpoint(value = "/websocket")
@Component
public class WebSocketServer {

	// 用來記錄當前連接數的變量
	private static volatile int onlineCount = 0;

	// 線程安全Set,用來存放每個客戶端對應的 WebSocket 對象
	private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

	// 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
	private Session session;

	/**
	 * 連接建立成功調用的方法
	 * @param session 連接會話
	 */
	@OnOpen
	public void onOpen(Session session) {
		this.session = session;
		// 加入set中
		webSocketSet.add(this);
		// 連接數加1
		addOnlineCount();
		log.info("有新連接,當前連接數爲" + getOnlineCount());
		try {
			sendMessage("連接成功");
		} catch (IOException e) {
			log.error("websocket IO異常");
		}
	}

	/**
	 * 連接關閉調用的方法
	 */
	@OnClose
	public void onClose() {
		// 從set中刪除
		webSocketSet.remove(this);
		// 連接數減1
		subOnlineCount();
		log.info("有一連接關閉!當前連接數爲:" + getOnlineCount());
	}

	/**
	 * 收到客戶端消息後調用的方法
	 *
	 * @param message 消息
	 * @param session 連接會話
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		log.info("收到來自客戶端的消息:" + message);

		// 羣發消息
		for (WebSocketServer 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();
	}

	/**
	 * 實現服務器主動推送消息
	 *
	 * @param message 消息
	 * @throws IOException
	 */
	public void sendMessage(String message) throws IOException {
		this.session.getBasicRemote().sendText(message);
	}

	/**
	 * 羣發自定義消息
	 *
	 * @param message 自定義消息
	 * @throws IOException
	 */
	public static void sendInfo(String message) throws IOException {
		log.info("推送消息內容:"+message);
		for (WebSocketServer item : webSocketSet) {
			try {
				item.sendMessage(message);
			} catch (IOException e) {
				continue;
			}
		}
	}

	/**
	 * 獲取連接數
	 * @return 連接數
	 */
	private static synchronized int getOnlineCount() {
		return onlineCount;
	}

	/**
	 * 連接數加1
	 */
	private static synchronized void addOnlineCount() {
		WebSocketServer.onlineCount++;
	}

	/**
	 * 連接數減1
	 */
	private static synchronized void subOnlineCount() {
		WebSocketServer.onlineCount--;
	}

}
WebSocketController.java
import com.zlf.websocket.service.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket 控制類
 *
 **/
@Slf4j
@Controller
@RequestMapping("/webSocketCtrl")
public class WebSocketController {

	@ResponseBody
	@RequestMapping(value = "/pushMessageToWeb", method = RequestMethod.POST, consumes = "application/json")
	public String pushMessageToWeb(@RequestBody String message) {
		try {
			WebSocketServer.sendInfo("有新內容:" + message);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return message;
	}

	@RequestMapping("/hello")
	public String helloHtml(HashMap<String, Object> map) {
		map.put("hello", "這是一個thymeleaf頁面");
		return "MyHtml";
	}

}
MyHtml.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
	<head>
		<meta charset="UTF-8">
		<title>MyHtml頁面</title>
	</head>
	<body>
		<h1>WebSocket測試頁面</h1>
		<p th:text="${hello}"></p>

		<script type="text/javascript">
			function clickHello(){
				console.log("=========開始========");
				var socket;
				if(typeof(WebSocket) == "undefined") {
					console.log("您的瀏覽器不支持WebSocket");
				}else{
					console.log("您的瀏覽器支持WebSocket");
					socket = new WebSocket("ws://localhost:8086/websocket");
					//打開事件
					socket.onopen = function() {
						console.log("Socket 已打開");
						//socket.send("這是來自客戶端的消息" + location.href + new Date());
					};
					//獲得消息事件
					socket.onmessage = function(msg) {
						console.log(msg.data);
						//發現消息進入 調後臺獲取
					};
					//關閉事件
					socket.onclose = function() {
						console.log("Socket已關閉");
					};
					//發生了錯誤事件
					socket.onerror = function() {
						alert("Socket發生了錯誤");
					}
				}
			}

		</script>
		<input type="button" value="測試WebSocket" onclick="clickHello()"/>
	</body>
</html>

測試:

訪問 http://localhost:8086/webSocketCtrl/hello,打開MyHtml頁面

點擊【測試WebSocket】按鈕,查看後臺控制檯日誌和前端頁面控制檯(F12):

後臺日誌:

前端頁面日誌:

再來一次:

可以看到連接數變爲2了。。。

刷新或關閉 MyHtml 頁面,後臺日誌:

下面來測下消息的主動推送功能,消息的獲取有多種,可以從 kafka 獲取,也可以從 ES 獲取日誌。這裏爲測試 websocket 簡單起見,使用 http 請求發送消息(WebSocketController 的 pushMessageToWeb 方法)。可以使用postman,也可以使用swagger接口。使用 swagger 時,只需爲 WebSocketController.java類 添加註解 @Api、同時爲方法 pushMessageToWeb() 添加 @ApiOperation 註解即可,修改後如下:

訪問swagger,輸入 message 參數(注意要先建立websocket連接,點擊 MyHtml 頁面的測試按鈕即可重新連接):

Try it out !效果如下:

後臺日誌:

-----------------------------------

使用 Win10系統自帶的 Microsoft Edge 瀏覽器也是支持的:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章