企业级Spring Boot应用管理:从零打造生产级启动脚本

企业级Spring Boot应用管理:从零打造生产级启动脚本

摘要:本文深入解析一个完整的企业级Spring Boot应用启动脚本,涵盖多环境配置、JVM调优、进程管理、日志管理等核心功能,帮助开发者构建稳定可靠的生产部署方案。

关键词:Spring Boot, 启动脚本, JVM调优, 多环境配置, 生产部署, Linux运维, 应用管理


目录


为什么需要专业的启动脚本

在生产环境中运行Spring Boot应用时,简单的 java -jar 命令远远不够。企业级应用需要考虑:

  1. 多环境差异化配置:开发、测试、UAT、生产环境的JVM参数完全不同
  2. 进程管理:PID追踪、状态监控、防止重复启动
  3. 资源优化:根据环境合理分配内存、GC策略
  4. 故障诊断:OOM时的堆转储、GC日志记录
  5. 优雅停机:避免数据丢失和连接中断

一个优秀的启动脚本能够显著提升应用的稳定性和可维护性。


脚本架构设计

整体结构

bash 复制代码
start.sh
├── 配置区 (Configuration Section)
│   ├── 应用基本信息
│   ├── 路径定义
│   └── 日志配置
├── JVM参数配置 (JVM Parameters Configuration)
│   ├── dev环境配置
│   ├── test环境配置
│   ├── uat环境配置
│   └── prod环境配置
├── Spring Profile配置 (Profile Configuration)
├── 工具函数 (Utility Functions)
│   ├── 日志输出
│   ├── Java检查
│   └── 目录初始化
├── 核心功能 (Core Functions)
│   ├── start() - 启动应用
│   ├── stop() - 停止应用
│   ├── restart() - 重启应用
│   └── status() - 查看状态
└── 主执行流程 (Main Execution)

设计原则

  1. 单一职责:每个函数只负责一个功能
  2. 环境隔离:不同环境配置完全独立
  3. 防御性编程:充分的错误检查和异常处理
  4. 可观测性:详细的日志记录和状态反馈

核心功能详解

多环境JVM参数配置

开发环境 (dev)
bash 复制代码
JAVA_OPTS="-Xms512m -Xmx1024m \
    -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \
    -XX:+UseG1GC \
    -XX:MaxGCPauseMillis=200 \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
    -Dfile.encoding=UTF-8 \
    -Djava.security.egd=file:/dev/./urandom \
    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"

关键特性

  • 较小内存(512M-1G),适合本地开发
  • 启用远程调试端口5005
  • G1垃圾收集器,平衡吞吐量和延迟
生产环境 (prod)
bash 复制代码
JAVA_OPTS="-Xms4g -Xmx8g \
    -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m \
    -XX:+UseG1GC \
    -XX:MaxGCPauseMillis=200 \
    -XX:InitiatingHeapOccupancyPercent=35 \
    -XX:+ParallelRefProcEnabled \
    -XX:ConcGCThreads=4 \
    -XX:ParallelGCThreads=8 \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
    -XX:ErrorFile=${LOG_DIR}/hs_err_pid%p.log \
    -XX:+PrintGCDetails \
    -XX:+PrintGCDateStamps \
    -Xloggc:${LOG_DIR}/gc.log \
    -XX:+UseGCLogFileRotation \
    -XX:NumberOfGCLogFiles=5 \
    -XX:GCLogFileSize=10M \
    -Dfile.encoding=UTF-8 \
    -Djava.security.egd=file:/dev/./urandom"

关键特性

  • 大内存(4G-8G),满足高并发需求
  • GC日志轮转,避免日志文件过大
  • 并行GC线程优化,提升吞吐量
  • 详细的错误日志,便于问题诊断
JVM参数解读
参数 说明 推荐值
-Xms 初始堆内存 prod: 4g, dev: 512m
-Xmx 最大堆内存 prod: 8g, dev: 1g
-XX:MetaspaceSize 元空间初始大小 prod: 512m, dev: 128m
-XX:MaxGCPauseMillis GC最大暂停时间 200ms
-XX:InitiatingHeapOccupancyPercent 触发并发GC的堆占用百分比 35%
-XX:ConcGCThreads 并发GC线程数 CPU核数/2
-XX:ParallelGCThreads 并行GC线程数 CPU核数

进程生命周期管理

PID文件管理
bash 复制代码
PID_FILE="${APP_DIR}/${APP_NAME}.pid"

get_pid() {
    if [ -f "${PID_FILE}" ]; then
        local pid=$(cat "${PID_FILE}")
        if ps -p "${pid}" > /dev/null 2>&1; then
            echo "${pid}"
            return 0
        else
            # 清理过期的PID文件
            rm -f "${PID_FILE}"
        fi
    fi
    
    # 备用方案:通过JAR名称查找进程
    local pid=$(ps aux | grep "${JAR_NAME}" | grep -v grep | awk '{print $2}')
    if [ -n "${pid}" ]; then
        echo "${pid}"
        return 0
    fi
    
    return 1
}

设计亮点

  1. 双重验证:先检查PID文件,再验证进程是否存在
  2. 过期清理:自动删除无效的PID文件
  3. 降级方案:PID文件丢失时,通过进程名查找
防止重复启动
bash 复制代码
start() {
    # 检查是否已在运行
    if is_running; then
        local pid=$(get_pid)
        log_error "Application is already running with PID: ${pid}"
        exit 1
    fi
    
    # ... 启动逻辑
}

日志管理策略

日志分类
bash 复制代码
LOG_DIR="${APP_DIR}/logs"
STDOUT_LOG="${LOG_DIR}/stdout.log"      # 标准输出
ERROR_LOG="${LOG_DIR}/error.log"         # 错误日志
# GC日志(仅生产环境)
-Xloggc:${LOG_DIR}/gc.log
日志分离的好处
  1. 快速定位问题:错误日志单独存储,便于grep分析
  2. 性能监控:GC日志用于分析内存使用趋势
  3. 磁盘空间管理:可以针对不同日志设置不同的轮转策略
启动时日志重定向
bash 复制代码
nohup ${java_cmd} > "${STDOUT_LOG}" 2>"${ERROR_LOG}" &

使用 nohup 确保进程在终端关闭后继续运行,同时分离标准输出和错误输出。


优雅停机机制

bash 复制代码
stop() {
    local pid=$(get_pid)
    
    # 第一步:发送SIGTERM信号(优雅停机)
    kill ${pid}
    
    # 第二步:等待最多60秒
    local count=0
    while is_running && [ ${count} -lt 60 ]; do
        sleep 1
        count=$((count + 1))
        if [ $((count % 10)) -eq 0 ]; then
            log_info "Waiting for application to stop... (${count}s)"
        fi
    done
    
    # 第三步:如果仍未停止,强制kill
    if is_running; then
        log_info "Application did not stop gracefully. Sending SIGKILL..."
        kill -9 ${pid}
        sleep 1
    fi
    
    rm -f "${PID_FILE}"
}

优雅停机的重要性

  • 完成正在处理的请求
  • 关闭数据库连接池
  • 释放文件句柄
  • 清理临时资源

超时保护:60秒后强制终止,避免无限等待。


最佳实践与优化建议

1. 环境变量外部化

将敏感信息(如数据库密码)从脚本中移除,使用环境变量:

bash 复制代码
export DB_PASSWORD="your_password"
export REDIS_HOST="redis.example.com"

JAVA_OPTS="${JAVA_OPTS} \
    -Dspring.datasource.password=${DB_PASSWORD} \
    -Dspring.redis.host=${REDIS_HOST}"

2. 健康检查增强

bash 复制代码
health_check() {
    local max_retries=30
    local retry_count=0
    
    while [ ${retry_count} -lt ${max_retries} ]; do
        if curl -s http://localhost:8080/actuator/health | grep -q "UP"; then
            log_success "Application health check passed"
            return 0
        fi
        
        sleep 2
        retry_count=$((retry_count + 1))
    done
    
    log_error "Health check failed after ${max_retries} retries"
    return 1
}

在启动后调用此函数,确保应用真正可用。

3. 日志轮转配置

使用 logrotate 管理日志文件:

bash 复制代码
# /etc/logrotate.d/zdqms
/logs/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0644 appuser appgroup
}

4. 监控集成

添加Prometheus JMX Exporter:

bash 复制代码
JAVA_OPTS="${JAVA_OPTS} \
    -javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9090:/opt/jmx_exporter/config.yml"

5. 容器化适配

如果在Docker/K8s环境中运行,简化脚本:

bash 复制代码
# Docker环境下不需要PID管理
exec java ${JAVA_OPTS} -jar ${JAR_PATH}

使用 exec 替换当前shell进程,确保信号正确传递。


常见问题排查

问题1:应用启动失败

症状:脚本显示启动成功,但进程立即退出

排查步骤

bash 复制代码
# 1. 查看错误日志
tail -f logs/error.log

# 2. 检查端口占用
netstat -tlnp | grep 8080

# 3. 手动运行Java命令测试
java -jar zdqms.jar --debug

常见原因

  • 端口被占用
  • 数据库连接失败
  • 配置文件缺失
  • JVM参数不合理(内存不足)

问题2:内存溢出(OOM)

症状:应用运行一段时间后崩溃

解决方案

bash 复制代码
# 1. 分析堆转储文件
jmap -heap <pid>
jhat heapdump.hprof

# 2. 调整JVM参数
-Xms4g -Xmx8g  # 增加堆内存
-XX:MaxMetaspaceSize=1024m  # 增加元空间

# 3. 检查内存泄漏
# 使用MAT (Memory Analyzer Tool) 分析heapdump

问题3:GC频繁导致性能下降

症状:应用响应缓慢,CPU使用率高

排查方法

bash 复制代码
# 1. 分析GC日志
grep "GC pause" logs/gc.log

# 2. 实时监控
jstat -gcutil <pid> 1000

# 3. 调整GC参数
-XX:MaxGCPauseMillis=200  # 降低目标暂停时间
-XX:InitiatingHeapOccupancyPercent=45  # 提高触发阈值

问题4:无法停止应用

症状 :执行 stop 命令后进程仍然存在

解决方案

bash 复制代码
# 1. 手动查找进程
ps aux | grep zdqms.jar

# 2. 强制终止
kill -9 <pid>

# 3. 清理PID文件
rm -f zdqms.pid

# 4. 检查是否有子进程未退出
pstree <pid>

完整脚本代码

以下是经过生产验证的完整启动脚本:

bash 复制代码
#!/bin/bash
################################################################################
# Application Start/Stop Script
# Description: Enterprise-level application management script
# Usage: ./start.sh [start|stop|restart|status] [env]
# Environments: dev|test|uat|prod
################################################################################

# ==================== Configuration Section ====================

# Application name (used for process identification)
APP_NAME="zdqms"

# JAR file path (relative to script directory)
JAR_NAME="zdqms.jar"

# Script directory (absolute path)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Application directory
APP_DIR="${SCRIPT_DIR}"

# Full JAR path
JAR_PATH="${APP_DIR}/${JAR_NAME}"

# PID file location
PID_FILE="${APP_DIR}/${APP_NAME}.pid"

# Log directory
LOG_DIR="${APP_DIR}/logs"

# Standard output log file
STDOUT_LOG="${LOG_DIR}/stdout.log"

# Error log file
ERROR_LOG="${LOG_DIR}/error.log"

# Environment configuration directory
CONFIG_DIR="${APP_DIR}/config"

# ==================== JVM Parameters Configuration ====================

# Default JVM options (will be overridden by environment-specific settings)
JAVA_OPTS=""

# Environment-specific JVM configurations
configure_jvm_by_env() {
    local env=$1
    
    case $env in
        dev)
            # Development environment - minimal settings, enable debug
            JAVA_OPTS="-Xms512m -Xmx1024m \
                -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \
                -XX:+UseG1GC \
                -XX:MaxGCPauseMillis=200 \
                -XX:+HeapDumpOnOutOfMemoryError \
                -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
                -Dfile.encoding=UTF-8 \
                -Djava.security.egd=file:/dev/./urandom \
                -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
            ;;
        test)
            # Test environment - moderate settings
            JAVA_OPTS="-Xms1g -Xmx2g \
                -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
                -XX:+UseG1GC \
                -XX:MaxGCPauseMillis=200 \
                -XX:+ParallelRefProcEnabled \
                -XX:+HeapDumpOnOutOfMemoryError \
                -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
                -Dfile.encoding=UTF-8 \
                -Djava.security.egd=file:/dev/./urandom"
            ;;
        uat)
            # UAT environment - production-like settings
            JAVA_OPTS="-Xms2g -Xmx4g \
                -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
                -XX:+UseG1GC \
                -XX:MaxGCPauseMillis=200 \
                -XX:InitiatingHeapOccupancyPercent=35 \
                -XX:+ParallelRefProcEnabled \
                -XX:+HeapDumpOnOutOfMemoryError \
                -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
                -Dfile.encoding=UTF-8 \
                -Djava.security.egd=file:/dev/./urandom"
            ;;
        prod)
            # Production environment - optimized settings
            JAVA_OPTS="-Xms4g -Xmx8g \
                -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m \
                -XX:+UseG1GC \
                -XX:MaxGCPauseMillis=200 \
                -XX:InitiatingHeapOccupancyPercent=35 \
                -XX:+ParallelRefProcEnabled \
                -XX:ConcGCThreads=4 \
                -XX:ParallelGCThreads=8 \
                -XX:+HeapDumpOnOutOfMemoryError \
                -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
                -XX:ErrorFile=${LOG_DIR}/hs_err_pid%p.log \
                -XX:+PrintGCDetails \
                -XX:+PrintGCDateStamps \
                -Xloggc:${LOG_DIR}/gc.log \
                -XX:+UseGCLogFileRotation \
                -XX:NumberOfGCLogFiles=5 \
                -XX:GCLogFileSize=10M \
                -Dfile.encoding=UTF-8 \
                -Djava.security.egd=file:/dev/./urandom"
            ;;
        *)
            # Default configuration
            JAVA_OPTS="-Xms1g -Xmx2g \
                -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
                -XX:+UseG1GC \
                -XX:MaxGCPauseMillis=200 \
                -XX:+HeapDumpOnOutOfMemoryError \
                -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof \
                -Dfile.encoding=UTF-8 \
                -Djava.security.egd=file:/dev/./urandom"
            ;;
    esac
}

# ==================== Spring Boot Profile Configuration ====================

# Active profile (will be set based on environment)
ACTIVE_PROFILE=""

configure_profile_by_env() {
    local env=$1
    
    case $env in
        dev)
            ACTIVE_PROFILE="dev"
            ;;
        test)
            ACTIVE_PROFILE="test"
            ;;
        uat)
            ACTIVE_PROFILE="uat"
            ;;
        prod)
            ACTIVE_PROFILE="prod"
            ;;
        *)
            ACTIVE_PROFILE="default"
            ;;
    esac
}

# ==================== Utility Functions ====================

# Get current timestamp
get_timestamp() {
    date '+%Y-%m-%d %H:%M:%S'
}

# Print info message
log_info() {
    echo "[$(get_timestamp)] [INFO] $1"
}

# Print error message
log_error() {
    echo "[$(get_timestamp)] [ERROR] $1" >&2
}

# Print success message
log_success() {
    echo "[$(get_timestamp)] [SUCCESS] $1"
}

# Check if Java is installed
check_java() {
    if ! command -v java &> /dev/null; then
        log_error "Java is not installed or not in PATH"
        exit 1
    fi
    
    JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
    log_info "Java version: ${JAVA_VERSION}"
}

# Check if JAR file exists
check_jar() {
    if [ ! -f "${JAR_PATH}" ]; then
        log_error "JAR file not found: ${JAR_PATH}"
        exit 1
    fi
}

# Create necessary directories
init_directories() {
    mkdir -p "${LOG_DIR}"
    mkdir -p "${CONFIG_DIR}"
}

# ==================== Core Functions ====================

# Get process ID
get_pid() {
    if [ -f "${PID_FILE}" ]; then
        local pid=$(cat "${PID_FILE}")
        if ps -p "${pid}" > /dev/null 2>&1; then
            echo "${pid}"
            return 0
        else
            # Stale PID file
            rm -f "${PID_FILE}"
        fi
    fi
    
    # Fallback: find by JAR name
    local pid=$(ps aux | grep "${JAR_NAME}" | grep -v grep | awk '{print $2}')
    if [ -n "${pid}" ]; then
        echo "${pid}"
        return 0
    fi
    
    return 1
}

# Check if application is running
is_running() {
    get_pid > /dev/null 2>&1
    return $?
}

# Start application
start() {
    local env=${1:-"dev"}
    
    log_info "Starting application with environment: ${env}"
    
    # Check if already running
    if is_running; then
        local pid=$(get_pid)
        log_error "Application is already running with PID: ${pid}"
        exit 1
    fi
    
    # Configure environment
    configure_jvm_by_env "${env}"
    configure_profile_by_env "${env}"
    
    # Build startup command
    local java_cmd="java ${JAVA_OPTS} \
        -Dspring.profiles.active=${ACTIVE_PROFILE} \
        -Dapp.name=${APP_NAME} \
        -Dapp.env=${env} \
        -jar ${JAR_PATH}"
    
    log_info "Environment: ${env}"
    log_info "Profile: ${ACTIVE_PROFILE}"
    log_info "JVM Options: ${JAVA_OPTS}"
    log_info "JAR Path: ${JAR_PATH}"
    
    # Start application in background
    nohup ${java_cmd} > "${STDOUT_LOG}" 2>"${ERROR_LOG}" &
    
    local pid=$!
    echo ${pid} > "${PID_FILE}"
    
    log_info "Application started with PID: ${pid}"
    log_info "Standard output log: ${STDOUT_LOG}"
    log_info "Error log: ${ERROR_LOG}"
    
    # Wait a moment and check if process is still running
    sleep 2
    if is_running; then
        log_success "Application is running successfully"
    else
        log_error "Application failed to start. Check logs for details:"
        log_error "  - ${STDOUT_LOG}"
        log_error "  - ${ERROR_LOG}"
        exit 1
    fi
}

# Stop application
stop() {
    log_info "Stopping application..."
    
    if ! is_running; then
        log_info "Application is not running"
        rm -f "${PID_FILE}"
        return 0
    fi
    
    local pid=$(get_pid)
    log_info "Sending SIGTERM to PID: ${pid}"
    
    # Graceful shutdown
    kill ${pid}
    
    # Wait for process to terminate (max 60 seconds)
    local count=0
    while is_running && [ ${count} -lt 60 ]; do
        sleep 1
        count=$((count + 1))
        if [ $((count % 10)) -eq 0 ]; then
            log_info "Waiting for application to stop... (${count}s)"
        fi
    done
    
    # Force kill if still running
    if is_running; then
        log_info "Application did not stop gracefully. Sending SIGKILL..."
        kill -9 ${pid}
        sleep 1
    fi
    
    rm -f "${PID_FILE}"
    
    if ! is_running; then
        log_success "Application stopped successfully"
    else
        log_error "Failed to stop application"
        exit 1
    fi
}

# Restart application
restart() {
    local env=${1:-"dev"}
    
    log_info "Restarting application with environment: ${env}"
    
    # Stop if running
    if is_running; then
        stop
        sleep 2
    fi
    
    # Start
    start "${env}"
}

# Show application status
status() {
    if is_running; then
        local pid=$(get_pid)
        log_success "Application is running"
        log_info "PID: ${pid}"
        
        # Show memory usage
        if command -v jmap &> /dev/null; then
            log_info "Memory usage:"
            jmap -heap ${pid} 2>/dev/null | grep -E "(used|capacity)" | head -5
        fi
        
        # Show uptime
        local uptime=$(ps -o etime= -p ${pid})
        log_info "Uptime: ${uptime}"
    else
        log_info "Application is not running"
    fi
}

# Show help
show_help() {
    cat << EOF
================================================================================
Usage: $(basename $0) [command] [environment]

Commands:
  start       Start the application
  stop        Stop the application
  restart     Restart the application
  status      Show application status
  help        Show this help message

Environments:
  dev         Development environment (default)
  test        Test environment
  uat         User Acceptance Testing environment
  prod        Production environment

Examples:
  $(basename $0) start              # Start with dev environment
  $(basename $0) start prod         # Start with production environment
  $(basename $0) stop               # Stop the application
  $(basename $0) restart test       # Restart with test environment
  $(basename $0) status             # Check application status

Configuration:
  Edit this script to customize:
  - APP_NAME: Application name
  - JAR_NAME: JAR file name
  - JVM parameters for each environment
  - Log directory location

Logs:
  Standard output: ${LOG_DIR}/stdout.log
  Error output:    ${LOG_DIR}/error.log
  GC log:          ${LOG_DIR}/gc.log (production only)
================================================================================
EOF
}

# ==================== Main Execution ====================

main() {
    local command=${1:-"help"}
    local env=${2:-"prod"}
    
    # Validate environment
    case "${env}" in
        dev|test|uat|prod)
            ;;
        *)
            log_error "Invalid environment: ${env}"
            log_error "Valid environments: dev, test, uat, prod"
            exit 1
            ;;
    esac
    
    # Initialize
    init_directories
    check_java
    check_jar
    
    # Execute command
    case "${command}" in
        start)
            start "${env}"
            ;;
        stop)
            stop
            ;;
        restart)
            restart "${env}"
            ;;
        status)
            status
            ;;
        help|--help|-h)
            show_help
            ;;
        *)
            log_error "Unknown command: ${command}"
            show_help
            exit 1
            ;;
    esac
}

# Execute main function with all arguments
main "$@"

总结

一个优秀的Spring Boot启动脚本应该具备以下特点:

环境隔离 :不同环境使用不同的JVM参数

健壮性 :完善的错误处理和状态检查

可观测性 :详细的日志记录和监控支持

易用性 :简洁的命令接口和清晰的帮助信息

安全性:防止重复启动、优雅停机

通过本文介绍的脚本,您可以:

  • 快速部署应用到不同环境
  • 有效管理应用生命周期
  • 快速定位和解决运行时问题
  • 提升系统的稳定性和可维护性

下一步优化方向

  1. 集成CI/CD流水线(Jenkins/GitLab CI)
  2. 添加自动化健康检查和自愈机制
  3. 对接集中式日志系统(ELK/Loki)
  4. 实现蓝绿部署或滚动更新策略

参考资料


关于作者:资深Java开发工程师,专注于企业级应用架构设计和DevOps实践。

版权声明:本文为原创文章,转载请注明出处。

更新日期:2026-05-20

相关推荐
砍材农夫3 小时前
物联网 基于netty构建mqtt协议规范(三种 QoS 等级)
java·开发语言·物联网
NiceCloud喜云3 小时前
Claude API 流式输出(SSE)实战:从打字机效果到工具调用全流程
java·前端·ide·人工智能·chrome·intellij-idea·状态模式
甲方大人请饶命3 小时前
Java-IO流
java·开发语言
SimonKing3 小时前
别再死磕 Elasticsearch 了,这个轻量级搜索引擎更香
java·后端·程序员
asdfg12589633 小时前
一文理解“工程化思维”
java·编程思想
阿昌喜欢吃黄桃3 小时前
并发线程工具类分享
java·线程池·多线程·并发·juc
Gopher_HBo3 小时前
阻塞队列之ArrayBlockingQueue
后端
Rsun045513 小时前
try-with-resources跟try-catch-finally的区别
java
随身数智备忘录3 小时前
从点检到全生命周期:设备管理体系能解决哪些场景痛点?一套设备管理体系的实战应用
java·网络·数据库