Skip to content

Commit

Permalink
feat: add get real-time memory usage sse endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
AkagiYui committed May 17, 2024
1 parent d258ed1 commit 9ea289d
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 66 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
- [ ] WebDAV
- [ ] 搜索引擎
- [x] 事务管理
- [x] [WebSocket](app/src/main/kotlin/com/akagiyui/drive/controller/persist/MemoryWebSocketHandler.kt)
- [x] [SSE(Server-Sent Events)](app/src/main/kotlin/com/akagiyui/drive/controller/persist/MemorySseController.kt)

## RoadMap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ abstract class BroadcastWebSocketHandler : AbstractWebSocketHandler() {
}
}
}

/**
* 关闭所有连接
*/
fun closeAll() {
val sessions = this.sessions.toList()
for (webSocketSession in sessions) {
webSocketSession.close()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter

/**
* 请求日志 切面
Expand Down Expand Up @@ -109,6 +110,9 @@ class RequestLogAspect {
if ((any is Collection<*>) && any.isNotEmpty() && (any.first() is MultipartFile)) {
return "Collection<MultipartFile>[${any.size}]"
}
if (any is SseEmitter) {
return any.toString()
}
return objectMapper.writeValueAsString(any)
}

Expand Down
40 changes: 25 additions & 15 deletions app/src/main/kotlin/com/akagiyui/drive/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.akagiyui.drive.config

import com.akagiyui.drive.filter.CustomPasswordHandleFilter
import com.akagiyui.drive.filter.TokenAuthenticationFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand Down Expand Up @@ -30,7 +29,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@EnableMethodSecurity
class SecurityConfig(
private val tokenAuthenticationFilter: TokenAuthenticationFilter,
private val customPasswordHandleFilter: CustomPasswordHandleFilter,
private val authenticationEntryPoint: AuthenticationEntryPoint,
private val accessDeniedHandler: AccessDeniedHandler,
) {
Expand All @@ -51,23 +49,35 @@ class SecurityConfig(
return http
.authorizeHttpRequests {
it // 允许指定路径通过
.requestMatchers(HttpMethod.GET, "/system/version").permitAll()
.requestMatchers(HttpMethod.GET, "/system/setting/register").permitAll()
.requestMatchers(HttpMethod.GET, "/file/*/download/**").permitAll()
.requestMatchers(HttpMethod.GET, "/captcha/**").permitAll()
.requestMatchers(HttpMethod.POST, "/user/sms").permitAll()
.requestMatchers(HttpMethod.POST, LOGIN_URL).anonymous()
.requestMatchers(HttpMethod.GET, "/user/token/sms").permitAll()
.requestMatchers("/user/register/**").permitAll()
.requestMatchers("/sse").permitAll()
// 允许匿名 GET 请求访问
.requestMatchers(
HttpMethod.GET,
"/system/version", // 获取系统版本
"/system/setting/register", // 是否开放注册
"/file/*/download/**", // 下载文件
"/user/token/sms", // 短信登录
).permitAll()
// 允许匿名 POST 请求访问
.requestMatchers(
HttpMethod.POST,
"/user/sms", // 发送短信验证码
).permitAll()
// 仅允许匿名 POST 访问
.requestMatchers(
HttpMethod.POST,
LOGIN_URL, // 获取 Token
"/user/register/**", // 注册
).anonymous()
.anyRequest().authenticated() // 其他请求需要认证
}
.csrf { it.disable() } // 关闭 CSRF
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // 关闭 Session
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) // 添加 JWT 过滤器
// .addFilterBefore(customPasswordHandleFilter, UsernamePasswordAuthenticationFilter::class.java) // 添加密码处理过滤器
.formLogin { it.disable() }
.logout { it.disable() }
.addFilterBefore(
tokenAuthenticationFilter,
UsernamePasswordAuthenticationFilter::class.java
) // 添加 Token 过滤器
.formLogin { it.disable() } // 禁用内置的表单登录
.logout { it.disable() } // 禁用内置的登出
.exceptionHandling {
it
.authenticationEntryPoint(authenticationEntryPoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.akagiyui.drive.config

import com.akagiyui.common.WebSocketPermissionChecker
import com.akagiyui.common.model.WebSocketHandlerWithPermissions
import com.akagiyui.drive.controller.websocket.MemoryWebSocketHandler
import com.akagiyui.drive.controller.persist.MemoryWebSocketHandler
import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.akagiyui.drive.controller.persist

import com.akagiyui.drive.component.permission.RequirePermission
import com.akagiyui.drive.model.Permission
import com.akagiyui.drive.service.SystemService
import org.springframework.beans.factory.DisposableBean
import org.springframework.beans.factory.InitializingBean
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.context.request.async.AsyncRequestNotUsableException
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
import java.util.concurrent.CopyOnWriteArrayList


/**
* 内存信息 Server-Sent Events 处理器
* @author AkagiYui
*/
@Controller
class MemorySseController(private val systemService: SystemService) : InitializingBean, DisposableBean {
val sseEmitters = CopyOnWriteArrayList<SseEmitter>()

@GetMapping("/system/memory/sse")
@RequirePermission(Permission.SYSTEM_INFO_GET)
fun sse(): SseEmitter {
val sseEmitter = SseEmitter()
sseEmitters.add(sseEmitter)
sseEmitter.onCompletion { sseEmitters.remove(sseEmitter) }
sseEmitter.send(systemService.getMemoryInfoHistory()) // 发送历史数据
return sseEmitter
}

private fun broadcast(obj: Any) {
sseEmitters.forEach {
try {
it.send(obj)
} catch (e: AsyncRequestNotUsableException) {
// 客户端已断开连接
}
}
}

override fun afterPropertiesSet() {
systemService.addRealTimeInfoCallback(::broadcast)
}

override fun destroy() {
systemService.removeRealTimeInfoCallback(::broadcast)
val sseEmitters = this.sseEmitters.toList()
for (sseEmitter in sseEmitters) {
sseEmitter.complete()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.akagiyui.drive.controller.persist

import com.akagiyui.common.BroadcastWebSocketHandler
import com.akagiyui.common.model.WebSocketHandlerWithPermissions
import com.akagiyui.drive.model.Permission
import com.akagiyui.drive.service.SystemService
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.DisposableBean
import org.springframework.beans.factory.InitializingBean
import org.springframework.stereotype.Component
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession


/**
* 内存信息 WebSocket 处理器
* @author AkagiYui
*/
@Component
class MemoryWebSocketHandler(private val systemService: SystemService) : BroadcastWebSocketHandler(),
WebSocketHandlerWithPermissions, InitializingBean, DisposableBean {
override val permissions = setOf(Permission.SYSTEM_INFO_GET)
private val objectMapper = ObjectMapper()

override fun afterConnectionEstablished(session: WebSocketSession) {
// 发送历史数据
session.sendMessage(objectEncode(systemService.getMemoryInfoHistory()))
super.afterConnectionEstablished(session)
}

private fun objectEncode(obj: Any): TextMessage {
return TextMessage(objectMapper.writeValueAsString(obj))
}

private fun callback(memoryInfo: Map<String, Any>) {
broadcast(objectEncode(memoryInfo))
}

override fun afterPropertiesSet() {
systemService.addRealTimeInfoCallback(::callback)
}

override fun destroy() {
systemService.removeRealTimeInfoCallback(::callback)
closeAll()
}
}

This file was deleted.

25 changes: 25 additions & 0 deletions app/src/main/kotlin/com/akagiyui/drive/service/SystemService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.akagiyui.drive.service

/**
* 系统服务接口
* @author AkagiYui
*/

interface SystemService {

/**
* 添加实时信息回调
*/
fun addRealTimeInfoCallback(callback: (Map<String, Any>) -> Unit)

/**
* 删除实时信息回调
*/
fun removeRealTimeInfoCallback(callback: (Map<String, Any>) -> Unit)

/**
* 获取历史内存信息
*/
fun getMemoryInfoHistory(): List<Map<String, Number>>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.akagiyui.drive.service.impl

import cn.hutool.core.collection.ConcurrentHashSet
import com.akagiyui.common.collection.CircularBuffer
import com.akagiyui.drive.service.SystemService
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service

/**
* 系统服务实现
* @author AkagiYui
*/
@Service
class SystemServiceImpl : SystemService {

private val realTimeInfoCallbacks = ConcurrentHashSet<(Map<String, Any>) -> Unit>()
private val memoryInfoBuffer: CircularBuffer<Map<String, Number>> = CircularBuffer(60)

/**
* 定时收集内存信息
*/
@Scheduled(fixedDelay = 1000)
private fun collectInfo() {
val runtime = Runtime.getRuntime()
val totalMemory = runtime.totalMemory()
val freeMemory = runtime.freeMemory()

val memoryInfo = mapOf(
"time" to System.currentTimeMillis(), // 时间戳
"totalMemory" to runtime.totalMemory(),
"freeMemory" to runtime.freeMemory(),
"usedMemory" to totalMemory - freeMemory,
"maxMemory" to runtime.maxMemory()
)
memoryInfoBuffer.append(memoryInfo)
realTimeInfoCallbacks.forEach { it(memoryInfo) }
}

override fun addRealTimeInfoCallback(callback: (Map<String, Any>) -> Unit) {
realTimeInfoCallbacks.add(callback)
}

override fun removeRealTimeInfoCallback(callback: (Map<String, Any>) -> Unit) {
realTimeInfoCallbacks.remove(callback)
}

override fun getMemoryInfoHistory(): List<Map<String, Number>> {
return memoryInfoBuffer.getAll()
}

}

0 comments on commit 9ea289d

Please sign in to comment.