news 2026/10/1 5:14:00

Linux 下 jar 包 systemd 自启动与守护实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 下 jar 包 systemd 自启动与守护实践

1. 为什么要在 Linux 上给 jar 包做自启动与守护

1.1 一个真实运维场景引发的思考

我第一次遇到这个问题,是在给一家做仓储管理的小公司做部署的时候。服务器上跑着一个 Spring Boot 打包出来的 jar,白天业务在用,晚上我回家睡觉,结果凌晨两点机房那台 Ubuntu 22.04 因为内核更新后自动重启了一遍,第二天早上业务方打电话过来说系统打不开。上去一看,进程没了,服务端口 8080 没有任何监听。那一刻我才意识到,nohup java -jar xxx.jar &这种临时起意的启动方式,在生产环境里基本等于埋雷。

这个问题的本质其实分成两层:第一层是进程级的存活保障,也就是进程意外退出(OOM、代码抛异常导致主线程终止、被误 kill)之后能不能自己拉起来;第二层是系统级的启动保障,即机器重启之后,服务能不能不依赖人工登录就自动跑起来。很多人只做了第一层,或者只做了第二层,结果就是要么重启后服务没了,要么服务挂了没人管。这两件事必须一起做,才算是一个完整的方案。

我把这套东西后来又在好几个项目里复用打磨,包括单体 jar、多实例 jar、带外部配置文件目录的 jar,慢慢总结出一套自己比较顺手的做法。下面这些内容,是我在不同年限的机器、不同发行版上踩过坑之后沉淀下来的,不是官方文档的翻译,而是实际干活时的取舍。

1.2 适合什么基础的人看

这篇内容我尽量写得对新手友好,但确实需要你满足几个前提:会基本的 Linux 命令操作,知道systemctl、ps、ss这类工具怎么用;知道自己那个 jar 包启动时依赖哪些环境变量、哪些外部文件;服务器上你有 root 或者 sudo 权限,因为注册服务这件事离不开它。

如果你现在的状态是"jar 能手动跑起来,但不知道怎么让它常驻",或者"配了 systemd 但 service 一直 failed 找不到原因",那这篇基本能覆盖你的诉求。如果你用的是 Docker、Kubernetes 那一套编排,那 jar 的自愈其实交给编排层更合适,不过 systemd 这套逻辑理解了,对你排查容器内进程信号问题也有帮助。

1.3 几条主流路线先摆出来对比

在动手之前,先把可选方案摆清楚,免得你走了弯路又回头。Linux 下让 jar 常驻,常见的有这么几种:

方案存活保障开机自启适用场景主要缺点
nohup + &无无临时测试关掉终端就没了,重启必丢
screen / tmux 会话弱无手工调试依赖会话存活,不适合无人值守
crontab 定时拉起有(轮询式)可以(@reboot)简易场景检测粒度粗,日志和状态管理乱
supervisor有有多进程托管需额外装组件,部分发行版源里版本旧
systemd service有有生产标配配置项多,出错了排查要经验

我给绝大多数人的建议是直接上systemd。理由很实在:主流的发行版(Ubuntu、Debian、CentOS、Rocky、openEuler、麒麟等)全都自带了,不用额外安装任何东西;它的Restart策略足够细,可以区分正常退出和异常退出;日志走 journald,journalctl一条命令就能看;而且它对资源限制、启动依赖、启动顺序的支持都很完整,等于一个方案覆盖了好几件事。supervisor 我更倾向于在需要托管几十个异构进程、或者团队已经有一套统一脚本体系的时候才用,单体 jar 真没必要引入这个额外依赖。

2. 把 jar 写成 systemd 服务:核心字段逐个拆透

2.1 单元文件放哪里,命名有什么讲究

systemd 读的单元文件,按优先级主要分三个位置:/etc/systemd/system/、/run/systemd/system/、/usr/lib/systemd/system/(部分发行版是/lib/systemd/system/)。自己手写的服务一律放/etc/systemd/system/,这是管理员手动管理的位置,优先级比发行版自带的目录高,升级软件包时也不会被覆盖。

命名上我用的是应用名.service,比如warehouse-api.service。注意文件名里的短横线和下划线在 systemd 里是有语义差异的,一般用短横线更稳妥,别用空格和中文。改完单元文件之后必须执行systemctl daemon-reload,这一步新手最容易忘,改了配置不 reload,systemd 读的还是旧内容,然后你就会对着一个"明明改了却没生效"的配置怀疑人生。

2.2 三个 Section 到底各自管什么

一个 service 单元文件基本由[Unit]、[Service]、[Install]三段组成,我见过太多人把字段塞错段,然后服务起不来。

[Unit]段管的是这个单元和别的单元的关系:Description是描述,journalctl列表里显示的就是它;After和Before定义启动顺序;Wants定义弱依赖(对方失败我也照常启);Requires定义强依赖(对方挂了我也被停)。这里有个高频误区:After=network.target只表示"在网络目标之后启动",不代表网络真的已经可用。如果你的 jar 启动时要连数据库、要注册到某个注册中心,光写network.target很可能启动太快导致连接失败。后面我会给更稳的写法。

[Service]段是核心,管进程怎么起、怎么死、怎么重启、什么身份跑。

[Install]段一般是WantedBy=multi-user.target,这个字段决定了systemctl enable的时候,往哪个 target 的wants目录里建软链接。multi-user.target对应传统的多用户命令行运行级别,服务器上基本都是它。写了这段,enable才会真正生效;不写[Install],enable命令会报"没有安装信息"。

2.3 启动命令怎么写才不出幺蛾子

ExecStart是最关键的一行,写错了服务直接status=203/EXEC。几个必须注意的点:

  • 必须用绝对路径。ExecStart=java -jar app.jar这种写法在 systemd 里会失败,因为 systemd 不走你的 shell 环境变量,PATH里的 java 它是找不到的。要么写/usr/bin/java,要么先which java确认真实路径,注意有些系统上/usr/bin/java只是个软链接,指向具体的 JDK 目录,反正是哪个你用哪个。
  • 命令里的空格、引号要小心。systemd 的解析规则和 shell 不一样,不会做变量展开、不会做通配符展开。所以-Dspring.profiles.active=prod这类 JVM 参数要一个一个写清楚,不要指望它替你展开${JAVA_HOME}。如果确实需要变量,用Environment=或者EnvironmentFile=显式声明。
  • 不要在命令里加&、nohup、重定向。systemd 自己就负责把进程放到后台、负责接日志,你再加这些反而会破坏它的进程跟踪。Type=simple模式下,systemd 认为ExecStart启动的那个进程就是主进程,如果被&提前 fork 掉,systemd 会以为进程退出了,然后触发重启,形成"启动-退出-重启"死循环。这个坑我踩过,现象是systemctl status里 process 不断变化,日志里全是启动记录。

一个我常用的启动行长这样:

ExecStart=/usr/bin/java -Xms512m -Xmx1024m -Dfile.encoding=UTF-8 -Dspring.profiles.active=prod -jar /opt/warehouse/warehouse-api.jar --spring.config.location=/opt/warehouse/config/

注意我把-jar放在 JVM 参数之后、应用参数之前,这个顺序不能乱,-jar后面第一个参数是 jar 路径,再往后的--spring.config.location才会作为应用参数传进去。应用参数如果包含空格或特殊字符,要用双引号整体包起来。

2.4 Type 选 simple 还是 forking,这是个真实的分叉

Type决定 systemd 怎么判断"服务已经启动完成"。常见值:

  • simple(默认):ExecStart一执行就算启动成功。绝大多数java -jar都该用这个。
  • forking:进程自己 fork 出子进程后父进程退出,systemd 认为才算启动完成。典型的比如 nginx 的 daemon 模式。java 进程一般不要用这个,除非你的启动脚本自身做了 daemon 化。
  • notify:进程通过 sd_notify 主动通知 systemd 启动完成。Spring Boot 默认不会,除非你引入了相关依赖或者写了对应的通知逻辑。
  • oneshot:执行一次就结束的任务,比如初始化脚本。

我见过有人为了"等应用真正 ready 再算启动完成"用了notify,结果应用没发通知,systemd 一直挂在那里等,最后超时失败。除非你明确知道自己在做什么,simple就是最优解。

2.5 重启策略:Restart 和 RestartSec 的配合

这部分是"自动重启"的核心。先看几个关键值:

  • Restart=no:不重启,默认值。
  • Restart=always:不管怎么退出都重启,包括被systemctl stop停掉,这个不太符合直觉,一般不推荐。
  • Restart=on-failure:只有非零退出码、被信号杀死、超时才算失败,才重启。这个是我最推荐的,它能把"人为正常停服"和"进程崩溃"区分开。
  • Restart=on-abnormal:只有被信号终止和超时才重启,退出码非零不算。用途窄一些。

RestartSec是重启前等待多少秒,默认 100ms。这个值太短会有问题:如果应用启动就崩(比如配置写错、端口被占),systemd 会疯狂重启,日志刷屏,CPU 也会被拖起来。我一般设成 5 到 10 秒,给运维一点反应时间,也给外部依赖(比如数据库)一点恢复时间。

还有一对容易被忽略的参数:StartLimitIntervalSec和StartLimitBurst,它们在[Unit]段里。含义是在StartLimitIntervalSec这段时间内,如果启动次数超过StartLimitBurst,systemd 就不再尝试了,把服务标记为 failed,避免无限重启把机器拖垮。默认值大致是 10 秒内 5 次。我在生产环境会放宽一些,比如 60 秒内允许 10 次,因为有些偶发的依赖抖动,重启几次就好了,没必要放弃治疗。

[Unit] StartLimitIntervalSec=60 StartLimitBurst=10

注意这两个参数在较老的 systemd 版本里放在[Service]段,如果你的系统提示未知选项,就挪到[Unit]段试试,这是版本差异。

3. 一份可以直接抄的完整配置与落地步骤

3.1 目录规划:把东西放整齐,后面少受罪

在写配置之前,先把目录定下来。我习惯的布局是:

  • 程序本体:/opt/warehouse/warehouse-api.jar
  • 外部配置:/opt/warehouse/config/(放application-prod.yml之类)
  • 日志输出:/var/log/warehouse/(如果要自己写文件日志)
  • 运行用户:专门建一个appuser,不给它登录 shell

为什么坚持程序不放/root或者/home/xxx?因为这两个目录的权限模型和系统服务运行身份经常冲突,用User=appuser跑起来之后,读不到 root 家目录里的文件是常态。放/opt是 Linux 惯例,权限好控。建用户的命令:

sudo useradd -r -s /sbin/nologin appuser sudo mkdir -p /opt/warehouse/config /var/log/warehouse sudo chown -R appuser:appuser /opt/warehouse /var/log/warehouse

-r表示建系统用户,-s /sbin/nologin表示不允许登录,纯服务账号。这一步别偷懒,直接用 root 跑服务是很多安全事故的起点,也是不规范运维的典型特征。

3.2 完整单元文件示例

下面这份是我实际在用的模板,字段都做了注释:

[Unit] Description=Warehouse API Service Documentation=https://example.invalid/warehouse After=network-online.target Wants=network-online.target StartLimitIntervalSec=60 StartLimitBurst=10 [Service] Type=simple User=appuser Group=appuser WorkingDirectory=/opt/warehouse Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk" Environment="SPRING_PROFILES_ACTIVE=prod" EnvironmentFile=-/opt/warehouse/env.conf ExecStart=/usr/bin/java -Xms512m -Xmx1024m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/log/warehouse/ \ -Dfile.encoding=UTF-8 \ -jar /opt/warehouse/warehouse-api.jar SuccessExitStatus=143 Restart=on-failure RestartSec=8 StandardOutput=journal StandardError=journal SyslogIdentifier=warehouse-api LimitNOFILE=65536 TimeoutStopSec=30 KillMode=control-group [Install] WantedBy=multi-user.target

这里有几个点值得单独说。After=network-online.target配合Wants=network-online.target是我推荐的网络等待组合,它比network.target更严格,表示"网络接口已经配置好并且有地址"。不过要注意,network-online.target本身的就绪判定依赖NetworkManager-wait-online或systemd-networkd-wait-online服务是否启用,如果这两个没启用,这个 target 可能立刻就被认为满足,等于没等。如果你确实需要严格等网络,先去确认这两个服务是 enabled 状态。

EnvironmentFile=-/opt/warehouse/env.conf前面的那个减号表示"文件不存在也不报错",这是个很实用的小技巧,方便把敏感配置(数据库密码)单独放一个文件、从版本控制里排除掉。env.conf 的格式是KEY=VALUE一行一个。

SuccessExitStatus=143这行是专门给 Java 加的。143 是 128+15,也就是进程收到 SIGTERM 正常退出的退出码。有些 Java 应用在收到停止信号后是以这个码退出的,如果不声明成成功状态,systemd 会认为它"异常退出"从而触发重启,结果就是你systemctl stop之后它自己又起来了,非常迷惑。加上这行就能避免。

KillMode=control-group保证systemctl stop时把整个进程组都干掉,包括 Java 线程里 fork 出来的子进程。默认值在新版 systemd 里就是 control-group,但显式写出来更清楚。

3.3 从零开始的落地命令序列

配置写好后,按这个顺序操作,一步都别跳:

# 1. 写入单元文件 sudo vim /etc/systemd/system/warehouse-api.service # 2. 重载 systemd 配置,让它认识新文件 sudo systemctl daemon-reload # 3. 先启动,观察状态 sudo systemctl start warehouse-api sudo systemctl status warehouse-api # 4. 跟着看实时日志,确认应用真的起来了 sudo journalctl -u warehouse-api -f # 5. 测试无误后设置开机自启 sudo systemctl enable warehouse-api # 6. 双向确认:既看 enable 状态,又检查软链接有没有建出来 sudo systemctl is-enabled warehouse-api ls -l /etc/systemd/system/multi-user.target.wants/ | grep warehouse

第 6 步为什么要额外看软链接?因为我遇到过一次is-enabled显示 enabled,但实际重启后没起来的情况。原因是[Install]段写成了WantedBy=multi-user.target之外的值,或者别的单元把它冲突屏蔽了。看一眼软链接是最直接的验证方式。

enable的时候还可以用--now参数,等于enable加start一气呵成:sudo systemctl enable --now warehouse-api。

3.4 验证自动重启真的生效了

配完不能就这么算完,得实测。我的验证方法是手工模拟崩溃:

# 找到主进程 PID sudo systemctl status warehouse-api | grep "Main PID" # 模拟被信号杀死(等同 OOM killer 干掉进程的场景) sudo kill -9 <PID> # 立刻 watch 状态变化,应该能看到它自己起来 watch -n 1 'systemctl status warehouse-api --no-pager | head -15'

正常情况下,8 秒左右(你设的 RestartSec)服务会重新变成 active。如果一直没起来,先看journalctl -u warehouse-api -n 50的报错,八成是启动命令路径或者权限问题。

第二个验证是重启机器:sudo reboot,等机器起来后登录,systemctl status warehouse-api应该已经是 active。这一步一定要在正式上线前做,别指望"应该没问题"。我见过太多"以为配好了"然后在真重启时翻车的案例。

4. 排查与避坑:那些只有踩过才知道的细节

4.1 高频故障速查表

先把最常见的几类问题整理成表,照着对号入座基本能解决八成故障:

现象可能原因排查方向
status=203/EXECExecStart 路径不对或无执行权限ls -l看 java 路径,确认绝对路径
status=200/CHDIRWorkingDirectory 不存在或无权限检查目录是否创建、属主是否匹配
反复重启,日志全是启动记录应用启动即崩,或命令带了 & 提前 fork去掉后台符号,手工执行同一命令验证
停止后自己又起来未声明 SuccessExitStatus 或 Restart=always加 SuccessExitStatus=143,改用 on-failure
启动时报找不到配置文件相对路径问题、WorkingDirectory 不对改绝对路径,或显式指定 config.location
日志里中文乱码未指定 file.encoding加-Dfile.encoding=UTF-8
enable 成功但重启不生效WantedBy 写错或被其他单元屏蔽检查软链接、systemctl list-dependencies
数据库连接失败启动太早,网络未就绪用 network-online.target,或加重试逻辑

4.2 关于"启动太早连不上数据库"这件事

这个坑我印象太深了。那个仓储系统用的 MySQL 和 jar 在同一台机器上,我写了After=network-online.target和After=mysql.service,以为万无一失。结果偶发性地,服务起来之后日志里报连接池初始化失败,重启一下又好了。后来才想明白:After=mysql.service只保证 MySQL 的 systemd 单元"启动动作完成了",但 MySQL 单元本身是Type=notify还是simple、它什么时候真正能接受连接,是两回事。MySQL 初始化 InnoDB 可能还要几秒。

我的解决方案是双保险:一是在 systemd 层面加After=mysql.service,二是在应用层面配置连接池的初始化重试(比如 HikariCP 的initializationFailTimeout设为正值,让它启动时容忍一段时间的连接失败)。因为 systemd 层面再怎么调顺序,也无法百分之百保证应用层依赖的中间件真的 ready,把重试逻辑放到应用里才是最可靠的。

4.3 内存溢出的处理思路

如果 jar 挂掉的原因是 OOM,光靠自动重启是治标不治本。我在配置里加了-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath,这样每次 OOM 都会落一个堆转储文件出来,方便事后用 MAT 之类的工具分析到底是哪个对象在膨胀。配合-Xmx限制堆上限,同时用 systemd 的MemoryMax限制整个服务的物理内存,防止它把机器拖垮:

[Service] MemoryMax=1.5G MemoryHigh=1.2G

MemoryHigh是软限制,超过会触发内存回收并节流;MemoryMax是硬限制,超过会被 OOM killer 干掉。有了这两个,即使你的 jar 有内存泄漏,崩的也只是它自己,不会把整台服务器一起带走。这也是把内存限制交给 cgroup(systemd 底层就是 cgroup)而不是只靠 JVM 参数的意义所在。

4.4 日志去哪了:journald 的取舍

systemd 服务的标准输出默认进 journald,用journalctl -u xxx看。好处是集中、有时间戳、可以按时间过滤。坏处是默认策略下日志存在/run/log/journal,内存盘,重启就没了。如果你的合规需求或者排障习惯需要日志持久化,得去改 journald 配置:

# 编辑 /etc/systemd/journald.conf,设置 Storage=persistent sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal sudo systemctl restart systemd-journald

改了Storage=persistent并且建了/var/log/journal目录之后,日志才会真正落盘到/var/log/journal/。同时记得配SystemMaxUse=限制一下总占用,不然日积月累会把磁盘写满,这个也是常见事故。

另外,如果应用自身用 logback 写了文件日志,比如输出到/var/log/warehouse/app.log,那 journald 里就只有启动阶段的东西,运行日志要去文件里看。两种方式各有场景,我的习惯是:启动阶段看 journalctl,运行日志看应用自己的文件,System.out打出来的东西尽量少,因为那些最终都会进 journal。

4.5 一个容易被忽略的点:jar 里读不到外部配置

有人把配置文件放在 jar 同级目录,然后用相对路径./config/去读,手工java -jar跑没问题,配成 systemd 服务就读不到了。原因就是 systemd 服务的WorkingDirectory默认是/(或者没设置),相对路径解析的基准变了。两个解法:设WorkingDirectory=/opt/warehouse,或者干脆在ExecStart里用--spring.config.location指定绝对路径。我通常两个都做,双保险,因为线上出问题的时候你不想再去猜路径。

5. 进阶:多实例、平滑重启与替代方案

5.1 同一台机器跑多个 jar 实例

有时候一台机器要跑同一个应用的两个实例,绑不同端口。最干净的做法是写模板单元文件,而不是复制两份。在/etc/systemd/system/下建warehouse@.service,里面的端口、配置路径用%i占位:

[Unit] Description=Warehouse API Instance %i After=network-online.target [Service] Type=simple User=appuser ExecStart=/usr/bin/java -jar /opt/warehouse/warehouse-api.jar --server.port=80%i Restart=on-failure RestartSec=8 [Install] WantedBy=multi-user.target

然后启动的时候用systemctl start warehouse@81、warehouse@82,@后面的就是%i的值。enable也是一样systemctl enable warehouse@81 warehouse@82。这个写法省去了维护多份几乎相同的配置文件的麻烦,改一处全生效。模板实例的日志可以用SyslogIdentifier=warehouse-%i区分开。

5.2 平滑重启:不想让用户掉线怎么办

systemctl restart是暴力重启,先停后起,中间有服务不可用窗口。对于要求不高的内部系统可以接受,但对于对外接口,几十秒的窗口也是事故。我的做法是让应用支持优雅停机:Spring Boot 2.3 之后有server.shutdown=graceful配置,配合spring.lifecycle.timeout-per-shutdown-phase=30s,收到 SIGTERM 之后先停止接新请求、等已有请求处理完再退出。systemd 侧配TimeoutStopSec=30与之呼应,给足退出时间。

再进一步,如果有多个实例在负载均衡后面,可以做滚动重启:逐个restart,每次等健康检查通过再动下一个。这个通常要写一点脚本或者交给上层编排,但原理就这么简单,理解了这个你就能自己搭。

另外提醒一句,systemctl reload能不能用,取决于服务是否实现了ExecReload。Java 应用一般没有实现这个,你写了也会报"没有 ExecReload 命令"。想要 reload 语义,得应用自己监听信号或者提供管理接口,不能指望 systemd 凭空变出来。

5.3 什么情况下我会放弃 systemd 改用别的

systemd 不是万能的,有两种情况我会考虑别的方案。一是需要跨机器统一调度、需要健康检查、需要自动扩缩容,那 Kubernetes 的探针和重启策略更专业,systemd 只能管单机。二是团队有强制的进程管理平台,比如统一的 supervisor 体系或者自研的守护进程工具,那遵循团队规范比个人偏好更重要。

还有一类特殊情况:应用需要按非常复杂的条件判断是否重启(比如检查某个外部健康接口的返回内容再决定),这时候 systemd 的Restart策略表达不了,可以用一个外部的守护脚本或者专门的健康检查工具,通过 systemd 的 timer 定期执行来判断。但这种设计复杂度和收益要权衡,多数时候还是把健康判断做进应用自己的探针接口更简单。

5.4 关于安全的一点实践经验

最后说点运维习惯上的东西。服务账号不用 root、配置文件权限收紧到640并且属主是appuser、数据库密码走EnvironmentFile而不写死在单元文件里(单元文件本身权限设成644就够了,因为里面不该有明文密码)、jar 包和配置的目录不允许其他用户写。这几条做到位,已经能挡掉相当一部分常见风险。我自己吃过亏的一次是偷懒把配置放在/tmp下调试,权限开得比较宽,虽然没造成实际损失,但事后复盘时后背发凉。

还有一点值得强调的是daemon-reload之外的变更纪律:任何对单元文件的修改,都应该走"改文件 → daemon-reload → 重启服务 → 验证"这个固定流程,并且做好记录。线上出现"配置改了但行为和预期不一致"的问题,十有八九是有人改了之后忘了 reload,或者改了 A 文件实际生效的是 B 文件。这类问题查起来非常耗时间,不如一开始就把流程固定下来。

我后来把上面这套东西做成了一个内部的部署脚本:检查目录、建用户、渲染 service 模板、daemon-reload、enable、启动、跑一次自检。执行完输出一份检查报告,包含服务状态、监听端口、软链接、最近 20 行日志。这样接手的人不需要理解每个字段的含义,照着跑就行,出错的时候报告里也直接能看出卡在哪一步。这套东西用了两年多,后面新上的几个 jar 服务都是照着改改名字就上,基本没再为"服务起不来"这类问题浪费过整个上午。如果你也在维护多个类似的 Java 服务,真的值得花半天时间把这套模板固化下来,回报率比想象中高得多。

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

导弹姿态控制与MATLAB仿真:从气动模型到闭环调参全流程

1. 项目缘起&#xff1a;先搞清楚这个仿真到底在做什么1.1 为什么姿态控制是绕不开的坎搞飞行器姿态控制的人都有一个共同感受&#xff1a;模型很多、符号很乱&#xff0c;真正能跑起来、还敢拿去给控制器设计参考的仿真&#xff0c;反而最难得。这个项目叫“基于气动力学的导弹…

作者头像 李华
网站建设 2026/10/1 5:13:31

情人节反套路:如何写好一场毕业季分手故事

情人节发一篇《毕业季分手的女友》&#xff0c;乍看是故意跟节日气氛唱反调——满屏玫瑰和巧克力的时候&#xff0c;偏要讲一个以散场收尾的故事。其实这个选题一点不叛逆。毕业季分手几乎是几代年轻人共享的情感数据库&#xff0c;谁身边没有一对在六月各奔东西的情侣&#xf…

作者头像 李华
网站建设 2026/10/1 5:13:18

Spring AI工具调用实战:从订单查询到库存校验的完整落地指南

Spring AI 的工具调用&#xff08;Tool Calling&#xff09;这块&#xff0c;我前后踩了小半个月的坑才算是真正玩明白。网上现在的资料基本都停在“Hello World”级别的示例&#xff1a;定义一个加法工具、让模型算一下 11&#xff0c;然后就没有然后了。但真实项目里压根不是…

作者头像 李华
网站建设 2026/10/1 5:13:13

AI Agent生产落地四道坎:稳定、并发、记忆与安全

开头先泼盆冷水。我见过太多这样的项目&#xff1a;Demo 演示的时候&#xff0c;Agent 在台上侃侃而谈、把工具调用得行云流水&#xff0c;客户当场拍板。结果一上线&#xff0c;不是答非所问&#xff0c;就是卡在某个工具调用里出不来&#xff0c;要不就是并发一上来直接超时&…

作者头像 李华
网站建设 2026/10/1 5:12:33

Madeira兼容层解析:FEX-Emu与Wine如何实现x86-64应用跨平台运行

1. 从“Madeira”这个名字说起&#xff1a;一个跨平台兼容层的野心第一次看到“Madeira”这个项目名&#xff0c;很多人会以为是某个旅游项目或者葡萄酒品牌。但结合热搜词里的 FEX-Emu、Wine、DXMT、x86-64 这些关键词&#xff0c;方向就很清楚了——这是一个围绕x86-64 应用在…

作者头像 李华
网站建设 2026/10/1 5:12:17

Spring Boot + Vue电影院购票系统:从选座并发到部署上线

1. 项目定位与技术选型&#xff1a;为什么是 Spring Boot Vue 这对组合先聊聊这个项目到底是个什么东西。电影院购票管理系统&#xff0c;说白了就是一套完整的线上售票解决方案&#xff0c;覆盖了用户从浏览影片、查看排片、选座下单到支付取票的全流程&#xff0c;同时给影院…

作者头像 李华