news 2026/10/24 1:07:11

Spring Boot项目Linux快速启动全流程:环境准备到systemd托管

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot项目Linux快速启动全流程:环境准备到systemd托管

我在这几年部署 Spring Boot 项目的实践中,最常被问到的问题就是“怎么在 Linux 上把项目快速跑起来”。很多刚接触部署的同学以为java -jar app.jar就算完事,实际上一关关过完才发现,启动脚本、进程管理、日志处理、开机自启这些环节没处理好,后面运维会非常被动。这篇内容我按照自己的实操经验,把 springBoot项目在Linux中快速启动这件事从零到一拆开讲透,涵盖环境准备、打包技巧、启动脚本、systemd 托管、常见坑排查,希望能给正在折腾部署的同学一份可以照着抄的作业。

这篇内容的受众很明确:已经写得出 Spring Boot 接口,但没系统搞过 Linux 部署的开发者,也包括刚接手公司测试环境部署任务的运维新人。读完你至少能实现以下三个目标:二次启动项目不再靠复制粘贴命令;关掉终端后项目依然稳定运行;服务器重启后服务能自动恢复。

1. 启动前的整体规划与基础环境准备

1.1 先搞清楚“快速启动”真正的瓶颈在哪

很多人觉得启动快就是把 jar 丢上去执行就行,其实项目的启动速度由两阶段决定:服务进程启动时间和业务就绪时间。前者是 JVM 从加载字节码到执行 main 方法的时间,后者是你项目里 DataSource、Redis、MQ 等组件完成初始化的时间。

我自己做过一次实测,一个依赖了配置中心、数据库连接池、消息队列的常规微服务,java -jar之后大概 8 秒输出Started Application日志,但端口真正能被外部访问往往要到第 12 秒左右,因为内嵌 Tomcat 的端口监听和 Actuator 的就绪探针是不同时机注册的。理解这个差异对后面配置健康检查、滚动发布很有用。

另一个经常被忽略的瓶颈是构建阶段。如果每次部署都在服务器上执行mvn package,不仅慢,还会把服务器变成一台构建机,占用大量 CPU 和内存。所以我在本文中推荐的部署流程是“本地或 CI 构建,产物上传,服务器只负责运行”,这才是真正意义上的快速启动。

1.2 Linux 环境准备清单:JDK、用户与目录规划

一台干净的 CentOS 7.9 或 Ubuntu 20.04 服务器,装好 JDK 后基本就能跑 Spring Boot 项目。但为了长期运维方便,我强烈建议做一些基础规划,这些是我踩过不少坑之后沉淀下来的习惯。

项目推荐配置说明
JDK版本OpenJDK 8 / 11 / 17根据项目 pom.xml 里 java.version 选择,别盲目装最新版
部署用户独立账号,如 app禁止用 root 跑业务服务,降低安全风险和误操作代价
目录结构/opt/app/bin、/opt/app/logsbin 放启动脚本,logs 放运行时日志,后续排查方便
端口规划固定业务端口和端口段如 8080 业务端口,避免随机端口导致防火墙规则失效

JDK 安装其实没什么花头,解压即用。但我提醒一点:装完后一定执行java -version确认默认版本,Linux 服务器上常有多套 JDK,JAVA_HOME配错会导致明明装了新版本,跑起来还是老的,排查起来很浪费时间。

创建目录和账号两条命令就能完成:

useradd -m -s /bin/bash app mkdir -p /opt/app/bin /opt/app/logs chown -R app:app /opt/app

这个步骤很多人觉得啰嗦,但目录规划越早做,后面部署多套环境时就越轻松。你可以同时在 /opt/app 下分项目名建子目录,比如 /opt/app/order-service,每个服务一套独立目录,互不干扰。

2. 打包到首次启动:最小可行的快速启动路径

2.1 打包配置决定启动效率:Fat Jar 与瘦包的选择

Spring Boot 的spring-boot-maven-plugin在 package 阶段会把应用代码和所有依赖打进同一个可执行 Jar,也就是常说的 Fat Jar。这种包的优点是对部署环境要求最低,服务器上只需要 JDK,不需要装 Maven,也不需要准备依赖仓库。

pom.xml 中核心配置长这样:

<build> <finalName>app</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.Application</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

finalName指定了打包产物名称,执行mvn clean package后会在 target 目录生成app.jar。我建议固定服务名,比如 app.jar、order-service.jar 这种带业务含义的名字,配合后面的启动脚本,版本更新时只需要替换相同的 jar 文件名,启动命令完全不用变。

有些同学优化过头,想用thin方式把依赖包放到服务器上,项目启动时从固定目录加载。这么做确实能让每次发布包从 120MB 缩到几千 KB,但如果依赖项版本升级了,忘记把新依赖上传到服务器,启动就会直接报ClassNotFoundException,这种问题排查起来比较难受。我的意见是:要么全用 Fat Jar,简单可靠优先;要么用 Docker 镜像分层,把改动频率低的依赖层缓存起来。生产环境扛不住折腾,能少一个变数就少一个。

2.2 首次手动启动的前台与后台模式

打包产物拿到手后,第一次启动我建议用前台模式跑一次,目的不是追求后台化,而是快速验证最小闭环。直接执行:

cd /opt/app/bin java -jar ../app.jar --server.port=8080

看到Started Application in 7.91 seconds这类日志后,项目就完成了启动。此时打开另一个终端窗口验证接口:

curl http://127.0.0.1:8080/actuator/health

如果你配了 Spring Boot Actuator,这个接口会返回{"status":"UP"}。

确认没问题后,把前台进程 Ctrl+C 停掉,进入真正适合运行的 nohup 方式:

nohup java -jar ../app.jar --spring.profiles.active=prod > ../logs/app.log 2>&1 &

这条命令里每个部分都有明确含义。nohup让进程忽略终端挂断信号,即使你关闭 SSH 窗口,进程依然存活;> ../logs/app.log把标准输出重定向到日志文件;2>&1把标准错误也合并到同一个文件;最后的&是让命令在后台执行。这样组合的含义就是“启动一个不受终端影响的后台进程,并把所有输出存到指定日志”。

注意:很多人以为用了 nohup 进程就一定能存活,其实如果直接用 root 部署,之后用普通账号去 kill 进程会有权限问题;另外 nohup 只解决了终端关闭的问题,进程崩溃后不会自动拉起,这一层要靠后面讲的 systemd 或守护脚本来兜底。

2.3 JVM 内存参数和运行日志之间的取舍

Spring Boot 应用是 Java 进程,默认 JVM 堆内存是物理内存的 1/4。如果一台 4G 服务器上同时跑两三个服务,默认配置很可能产生内存竞争,导致整机频繁 Swap。我一般在启动命令里固定堆大小,比如:

java -Xms512m -Xmx512m -XX:+UseG1GC -jar ../app.jar

-Xms是初始堆大小,-Xmx是最大堆大小,两者设成一致能避免运行时频繁扩容带来的性能抖动。至于堆大小定多少,我的粗略估法是:业务接口以数据查询为主给 512MB 到 1GB;涉及大量文件处理或复杂计算给 1GB 到 2GB;如果服务要支撑高并发,就得结合压测结果去压榨合适的值。关键是别让-Xmx超过容器或物理机的可用内存,否则系统会直接被 OOM Killer 盯上,出现进程莫名消失的诡异问题。

第一次跑起来后,打开日志文件看几行输出,确认没有异常堆栈。如果日志里出现OutOfMemoryError,优先调整-Xmx,而不是急着加机器。

2.4 结合热词趋势做一个部署代理选型的提醒

最近很多团队提到“轻量部署”,实际是想在简化运维和资源成本之间找平衡。除了裸机跑 jar,还有两种常见方案可参考。一种是直接用 Docker 跑容器,构建一次镜像到处运行,环境一致性最好;另一种是 K8s 编排,适合服务数量多、需要弹性伸缩的场景。如果服务数量没超过 5 个,且没有自动扩缩容需求,用 systemd 跑裸 jar 反而是成本最低、最容易排查问题的方式。这几种方式的对比可以看下面:

方式启动速度环境一致性运维复杂度适合规模
nohup + 脚本最快依赖服务器环境最低简单服务、临时测试
systemd 服务快依赖服务器环境低生产单机服务
Docker 容器快好中中小团队多服务
K8s中最好高大规模微服务

我个人的经验法则是,公司基础设施没有容器化平台时,老老实实用 systemd,别为了追新而引入整套容器方案。

3. 启动脚本编写:从手动操作走向半自动化

3.1 一套完整可复用的启停脚本拆解

手动敲启动命令的问题在于不仅慢,还容易因为环境变量、参数状态不同导致线上服务配置漂移。比如说你昨天启动时加了--server.port=8081,今天忘了加,服务就默认占了 8080 端口,排查半天才发现是启动命令不一致。所以部署的第二步就是把启动逻辑固化到脚本里。

下面是一套我常用的 deploy.sh,功能覆盖启动、停止、重启、状态检查:

#!/bin/bash APP_NAME=app APP_HOME=/opt/app JAR_NAME=$APP_HOME/app.jar LOG_DIR=$APP_HOME/logs LOG_FILE=$LOG_DIR/${APP_NAME}.log PID_FILE=$LOG_DIR/${APP_NAME}.pid JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC" SPRING_OPTS="--spring.profiles.active=prod" start() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if ps -p "$PID" > /dev/null 2>&1; then echo "服务已经在运行中, PID=$PID" exit 1 fi fi mkdir -p "$LOG_DIR" nohup java $JAVA_OPTS -jar $JAR_NAME $SPRING_OPTS > "$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" echo "服务启动中, PID=$(cat "$PID_FILE")" } stop() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if ps -p "$PID" > /dev/null 2>&1; then kill "$PID" echo "已发送停止信号给 $PID" else echo "进程不存在,清理 PID 文件" fi rm -f "$PID_FILE" else echo "未找到 PID 文件" fi } status() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if ps -p "$PID" > /dev/null 2>&1; then echo "运行中, PID=$PID" return 0 fi fi echo "未运行" return 1 } case "$1" in start) start ;; stop) stop ;; restart) stop; sleep 2; start ;; status) status ;; *) echo "用法: $0 {start|stop|restart|status}" ;; esac

使用方式:

chmod +x deploy.sh ./deploy.sh start ./deploy.sh status ./deploy.sh restart

这套脚本的核心思想是用 PID 文件记录进程号,操作前先检查进程是否存活。$!在 bash 里代表上一个后台进程的进程号,启动后立刻写入 PID 文件。stop 函数的kill默认发送的是 TERM 信号,这给了 Spring Boot 一个优雅停机窗口,可以完成未处理请求的收尾和资源释放。

3.2 脚本设计里的两个关键细节

第一个细节是等待进程真正退出。kill是异步信号,进程收到后还需要时间完成清理。立刻执行start可能导致新进程抢占旧进程还没释放的端口。我在 restart 里先sleep 2再启动,这个时间窗口在绝大多数情况下够用了。如果你在重启后主动刷新接口,发现前几次请求偶尔失败,多半是旧进程还没完全释放端口,可以把 sleep 时间拉长到 5 秒,或者做一个等待端口释放的轮询循环。

第二个细节是 PID 文件的存放位置。我放在 logs 目录下,不跟脚本混在一起,这样即使脚本文件被更新或误删,运行状态不受影响。另外一个容易踩的坑是 PID 文件里的进程号过期了,系统里那个 PID 已经被完全不同的进程占用,所以我在 start 里加了ps -p存活检查,就是为了避免“PID 文件存在且对应进程还活着”这种误判导致重复启动。

3.3 环境变量和配置项的注入方式

Spring Boot 的外部化配置优先级规则是 命令行参数 > Java 系统属性 > application.yml。利用这个特性,我经常把一些私有配置写成环境变量,避免敏感信息写进配置文件。脚本支持这种用法:

export DB_PASSWORD='实际密码' java -jar ../app.jar --spring.datasource.password=${DB_PASSWORD}

application.yml 里对应写成:

spring: datasource: password: ${DB_PASSWORD}

用环境变量注入的另一个好处是,同一个 jar 包在不同环境(dev、test、prod)之间传递时,不需要为各环境重新打包,只传不同环境的变量值就行。比如 prod 环境的数据库地址、缓存地址都通过SPRING_OPTS传入,配置的可移植性就体现出来了。

4. 进程托管升级:用 systemd 实现开机自启和崩溃自愈

4.1 从 nohup 到 systemd:为什么推荐这一步

nohup 解决了后台运行问题,但没法解决“服务器重启后服务不会自动恢复”和“进程意外崩溃后没人拉起”这两个生产环境核心诉求。手动重启听起来简单,但在凌晨三点发生问题时就很难受了。systemd 是 Linux 下标准的服务管理工具,用起来并不复杂,对 Spring Boot 的优势体现在三点:

  • 开机自启:设置systemctl enable后,机器重启自动拉起服务。
  • 崩溃自愈:通过Restart=on-failure配置,进程异常退出自动重新拉起。
  • 统一管理:systemctl start/stop/status可以替代自定义脚本,对运维人员友好。

有人可能觉得上了 systemd 就没必要保留 deploy.sh 了,我的习惯是两边都保留。systemd 负责守护和自启,deploy.sh 仍然是手动快速部署时顺手可用的应急工具,两套路线各有用途。

4.2 编写 service 单元文件并启用服务

以创建/etc/systemd/system/app.service为例,文件内容如下:

[Unit] Description=Spring Boot Application After=network.target [Service] User=app Group=app WorkingDirectory=/opt/app/bin Environment="JAVA_HOME=/usr/local/jdk17" Environment="SPRING_PROFILES_ACTIVE=prod" ExecStart=/usr/local/jdk17/bin/java -Xms512m -Xmx512m -XX:+UseG1GC -jar /opt/app/app.jar SuccessExitStatus=143 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

逐个解释几个关键配置。User和Group指定运行账号,让进程以普通账号身份运行,这是安全基线。WorkingDirectory指定工作目录,有些框架(比如读取相对路径文件的操作)依赖这个目录。Environment可以注入环境变量,这里既指定了JAVA_HOME,也设了SPRING_PROFILES_ACTIVE,后续不用再在启动命令里加--spring.profiles.active参数。

ExecStart是实际启动命令,必须写绝对路径。有个容易出错的点:如果 JDK 是通过系统包管理器安装的,路径一般是/usr/bin/java;手动解压安装则要写实际路径。我用/usr/local/jdk17/bin/java是因为服务器上装了解压版 JDK,这样做的好处是服务绑定到明确版本,升级 JDK 时不会影响运行中的服务。

SuccessExitStatus=143是 Spring Boot 配合 systemd 的经典参数。原因是 systemd 停止服务时默认发送 SIGTERM(信号值 15),JVM 收到后退出,shell 的退出码为 143(128+15)。如果不声明这个状态码,systemd 会认为进程异常退出,从而触发 Restart 逻辑,出现“停止服务后又自己启动”的问题。

文件创建后执行:

systemctl daemon-reload systemctl enable app systemctl start app systemctl status app

daemon-reload必须执行,否则系统不感知新增的 unit 文件。执行enable后开机自启就生效了,start立即拉起服务。

注意:如果你的 Java 应用进程启动后会 fork 子进程或者执行一些初始化脚本,需要设置Type=forking并配合PIDFile。但 Spring Boot 的 Fat Jar 运行在单进程模式,用默认的Type=simple最合适,不要去画蛇添足。

4.3 优雅停止与超时控制

上面提到 systemd 默认发送 SIGTERM,这对应 Spring Boot 的优雅停机。默认行为是立即关闭,不等待进行中的请求完成。如果希望服务处理完当前请求再退出,在 application.yml 中配置:

server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s

timeout-per-shutdown-phase表示 graceful 停机最多等待 30 秒。如果超过这个时间还没完成,systemd 会先发 SIGTERM,再在TimeoutStopSec超时后发 SIGKILL 强制结束,避免停机过程卡死导致资源泄漏。实践中我习惯把系统侧TimeoutStopSec=30和 Spring 侧超时配成一致,防止信号发送顺序混乱。

另外提一个很多人会踩的坑:如果你用kill -9强制杀掉 Spring Boot 进程,PID 文件或 systemd 状态会变成异常。尤其在生产环境要控制使用kill -9的频率,它不会触发 Spring 的清理逻辑,极端情况下可能造成数据库连接池没有正常释放或本地临时文件残留。正常停止一律用systemctl stop或kill(不带-9)命令即可。

5. 常见问题与快速排查速查表

5.1 端口被占用导致启动失败

表现:日志末尾出现Web server failed to start. Port 8080 was already in use。这种问题的原因一般是旧进程没有完全退出,或者有别的服务占用了端口。

排查命令:

ss -lntp | grep 8080

输出里会显示占用进程的 PID,再用ps -ef | grep 该PID确认是什么进程。注意ss比netstat查询更快,而且新版系统更常用。如果是自己的服务,用kill 该PID结束进程后再启动;如果是别的服务,要么改端口,要么在 systemd 文件里给当前服务换一个没被占用的端口。

5.2 启动秒退但无明确错因

有时候执行 java -jar 后,两三秒就退回 shell,日志也没显示明显报错。我第一眼看的是环境变量是否完整,尤其是数据库密码或配置中心地址是否注入正确。环境变量缺失往往表现为启动过程中连接超时,然后应用创建 Spring 容器失败退出。

其次要查 JVM 参数是否有非法值。比如-Xmx设得比物理内存还大或写错单位(写成512MB而不是512m),JVM 会直接退出。这种启动失败在日志里通常有明确 Error 行,只是被淹没在大量 INFO 里,搜索ERROR或Exception关键词是高效做法:

grep -E "ERROR|Exception|Caused by" ../logs/app.log | tail -50

如果日志文件里完全是空的,优先确认日志目录的写权限。之前遇到过一次服务以 root 启动时创建了 root 属主的日志文件,后来切换到 app 用户,服务一直起不来,就是因为没权限追加内容,查了半天才发现文件权限问题。

5.3 systemd 状态显示 active 但接口不通

systemctl status显示active (running),但 curl 访问 8080 端口失败。这种问题先别怀疑服务,大概率是防火墙或网络策略。在新环境检查:

curl -v http://127.0.0.1:8080/actuator/health # 本机访问 ssh 其他机器 "curl http://这台机器IP:8080/actuator/health" # 跨机器访问

如果本机能通、跨机器不通,那就是防火墙。CentOS 7 上用:

firewall-cmd --add-port=8080/tcp --permanent firewall-cmd --reload

云服务器还需要在安全组放行,这一步虽然不涉及具体厂商策略,但很多生产问题都是安全组忘开端口导致的。

5.4 PID 文件残留导致启动脚本误判

deploy.sh 在停止服务后把 PID 文件删掉,但如果进程被外部误杀且 PID 文件来不及清理,就会出现 pid 文件存在但进程不存在的情况。很多部署脚本不检查这一点,误判服务还在运行,拒绝重新拉起。我的脚本里已经通过ps -p解决了这个问题,如果你是自己写的脚本,务必加上这个判断。

5.5 日志文件快速定位与切割

运行久了日志文件会越来越大,定位问题也不方便。我习惯用 logback 配置按天滚动:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> </appender>

同时在启动参数里加上--logging.path=/opt/app/logs,日志就能落到规划好的目录里,按天切割并保留 30 天。这样排错时直接看当天的日志文件,不用在几十 GB 的单个文件里痛苦地grep。

5.6 常见问题速查表

现象可能原因解决方向
启动秒退,无日志环境变量缺失或 JVM 参数非法检查 SPRING_PROFILES_ACTIVE、数据库密码,搜索 ERROR 日志
端口被占用旧进程未退出或多个服务抢同一端口ss -lntp 定位占用进程再处理
服务 active 但接口不通防火墙、安全组、监听地址只有 127.0.0.1检查 server.address,确认防火墙规则
systemctl stop 后自动重启缺少 SuccessExitStatus=143systemd 文件补齐该配置并 daemon-reload
日志不滚动logback 未配置 RollingFileAppender按天滚动,并设置 maxHistory
内存持续上涨堆大小设置不当或代码内存泄漏配合 -Xmx 限制,并用 jstat 观察堆内存走势
服务随着终端关闭退出没用 nohup 或 systemd改用 systemd 管理,或 nohup + & 运行

6. 根据经验总结的快速启动参考流程

下面是一套我在新服务器上部署 Spring Boot 项目时的标准操作序列,按照这个顺序执行可以少走弯路:

  1. 检查 JDK 是否可用,执行java -version确认版本。
  2. 创建专用账号和固定目录,调整目录属主。
  3. 上传 jar 包到 /opt/app 目录。
  4. 初始化脚本 deploy.sh,赋予执行权限。
  5. 首次前台或脚本启动,确认日志输出与接口可访问。
  6. 编写 systemd unit 文件,执行 daemon-reload 和 enable。
  7. 通过systemctl start app重新拉起服务,确认状态稳定。
  8. 配置日志按天滚动和保留策略。
  9. 压测或试跑一段时间,观察内存和接口响应,再调整 JVM 参数。

我实际部署一套服务时,前两步约十分钟,后续步骤熟悉后五分钟左右就能全部搞定。真正麻烦的一般是环境依赖的差异,比如 Nacos 地址、数据库白名单这类外部因素。

在动手写脚本前,想清楚进程为什么要后台运行、为什么用 PID 文件、为什么 systemd 要配SuccessExitStatus,你会发现启动这件事再也不神秘。还有个小习惯:每次更新配置或脚本后,都在一个新终端窗口执行一次完整启停流程,确认没问题后再离开终端。这样线上马上要用时,你不用一边翻历史命令一边焦虑。

最后再多说一句我自己的体会。启动脚本和 systemd 文件这种东西,平时配置量不大,也很少出彩,但它属于“配置一次、受益很久”的工程投资。与其每次部署都重复敲命令、手动查端口,不如花一小时把这套东西沉淀下来,之后每加一个新服务就是复制粘贴改个名字的事。按这个思路去推进,你会感受到“快速启动”真正落在实处。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 1:06:43

Apache Calcite 深度解读:从 SQL 解析到优化器与适配器开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:06:16

TM500基站测试:从TMA偏好设置到RCI自动化回归测试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:05:07

芋道管理后台本地部署与企业级外部访问全链路指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:05:04

ADB offline设备删除全攻略:从原理到实战清理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:04:07

VHDL顺序语句详解:signal/variable、if/case及综合避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华