最近有一個需求,要將任務運行的日誌實時顯示在前端頁面上,讓用戶及時瞭解該任務的執行情況。分兩部分:後臺如何獲取日誌信息?獲取日誌信息後,如何實時展示在前端頁面上?
本文先討論下實時展示到頁面,可採取的方案有兩種:①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 瀏覽器也是支持的: