尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java异常处理实战:线上排查、最佳实践与设计模式融合
Java异常处理是被讨论得最多、又最容易流于表面的知识点。我见过不少能把继承结构倒背如流的人真到线上排查时却连Caused by那一行都不看直接把整个堆栈甩到群里然后问“这啥意思”。下面要聊的内容我不想讲八股而是把那些真正坑过我的典型问题、排查链路、沉淀下来的最佳实践以及设计模式在异常处理里的融合用法一次性说透。内容偏向后端开发真实场景适合准备Java面试的中级工程师、正在为线上异常挠头的开发也想给想建异常处理规范的团队做参考。1. 线上高频异常现场NoClassDefFoundError 与 Redis 自增我都被坑过1.1 空指针看起来简单定位起来要花半天NPE可能是Java里出现频率最高的异常但很多人对它的定位思路还停留在Java 8时代看到NullPointerException然后去代码里猜是哪个变量为null。JDK 14之后JVM默认会在NPE的异常消息里带上具体信息比如Cannot invoke String.length() because name is null这大大缩短了定位时间。问题在于很多老项目还在JDK 8上跑线上打出来的堆栈依然是冷冰冰的NullPointerException不带任何message。这时候我的做法是先从堆栈行号反推代码再看那一行有几次方法调用判断哪个对象最可能是null。如果方法调用链特别长可以在对应行前面临时把每个中间对象打印出来或者用条件断点。更彻底的办法是升级到JDK 14让NPE消息自带答案但在老版本上就只能靠代码层面排查。另外有一种NPE很容易被忽略就是包装类型自动拆箱。比如从缓存里拿到一个Long类型的countcount为null时直接执行count 1JVM在自动拆箱的瞬间抛出NPE。统计类场景里这种问题最常见因为缓存未命中时返回null是常态。排查时不要只看“哪一行”还要看那一行有没有隐藏的拆箱动作。1.2 NoClassDefFoundError堆栈上一闪而过的类加载事故NoClassDefFoundError我在一个老项目里真实遇过应用启动阶段一切正常某个接口第一次被调用时直接抛异常堆栈最前面是uncaught exception java.lang.NoClassDefFoundError: java/applet/Applet。看到java.applet.Applet这个类名心里基本就有数了——这是JDK 9模块化之后被移除的老类项目里某个库还在引用它。这类问题的迷惑性在于应用能正常启动只有运行到某个方法时才炸。原因是JVM的类加载是懒加载的启动阶段只会加载启动路径上必需的类等到运行期new某个对象或访问某个静态字段才会去加载对应类而此时发现这个类引用了不存在的类型就会抛NoClassDefFoundError。要是你对类加载的时机不敏感很容易误判成“代码写错了”或者“环境有问题”然后在错误的方向上浪费一晚上。1.3 RedisTemplate.increment 报错的真实原因还有一个我见过很多次的运行时异常场景是在Spring Boot项目里用RedisTemplate做自增Long count redisTemplate.opsForValue().increment(order:count:20250101, 1L);结果运行时抛异常报错信息类似not integer or out of range或者直接ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long。很多人第一反应是Redis里存了奇怪的值其实用命令行查一下keyvalue很可能是正常的数字。真正的问题往往出在序列化器配置上。默认的RedisTemplate使用JdkSerializationRedisSerializerkey和value在Redis里都是二进制形式。increment操作要求key对应的value在Redis服务端可以被解析成整数而jdk序列化后的value是一段二进制服务端根本没法把它当整数解析于是抛错。这个问题在“只跑通了Spring Boot demo但没细看RedisTemplate默认配置”的项目里特别容易出因为它不报编译错误运行期才炸。2. 把JVM异常机制讲透才知道该不该catch2.1 Throwable体系Error、检查型异常与运行时异常的边界Java的异常体系以Throwable为根往下分Error和Exception。Error表示JVM层面的严重问题比如OutOfMemoryError、StackOverflowError、NoClassDefFoundError这类异常通常不期望应用去捕获Exception下面又分检查型异常和RuntimeException非检查型前者编译器强制你处理后者可以完全不管。不过“Error不能catch”不是绝对的。我见过一个缓存模块会捕获OutOfMemoryError在catch块里先清理一级缓存和软引用对象再尝试重新分配如果内存能释放服务就继续跑。这里的关键不是“能不能catch”而是“catch之后有没有能力让程序恢复”。如果catch完只是打个日志然后继续往深层走那跟没catch没区别只是把崩溃延后了几秒钟。用一个生活类比来记Error像小区停电Exception像家里某个电器出故障。停电时正确做法是关掉大功率设备等待恢复而不是假装没停电继续开空调但也不是说停电就一定不能处理比如你能切到备用电源那这个“catch”就是有意义的。2.2 为什么现在的框架都在抛弃checked exception检查型异常是Java设计里争议最大的一环。早期JDBC、IO API大量使用检查型异常比如IOException、SQLException强制调用方try-catch或throws。听起来很安全但实际上带来了成片的模板代码而且很多检查型异常在调用方根本没有恢复能力。比如文件不存在你让业务代码catch之后做什么大部分时候只能往上抛。Spring给了一个反向示范把数据访问层的检查型异常统一包装成RuntimeException的子类DataAccessException让上层按需处理而不是被迫处理。Java 8之后检查型异常和函数式接口的兼容性也暴露了问题在Lambda里调用一个声明throws检查型异常的方法时函数式接口的抽象方法不允许抛出检查型异常所以你只能在Lambda内部try-catch或者包一层RuntimeException再抛。这导致现在的新项目普遍在领域层只抛自定义运行时异常再由全局异常处理器统一兜底。2.3 try-with-resources与suppressed exception的设计亮点try-with-resources是Java 7引入的底层原理是编译器自动在finally里按逆序调用close()并且用addSuppressed方法保存被压制suppressed的异常。为什么需要suppressed因为在传统finally里关闭资源如果又抛一个异常最外层catch到的是finally里的新异常真正的业务异常反而丢了。try-with-resources不会这样主异常会被保留关闭资源的异常以suppressed异常的形式附加在主异常上日志一打出来整条链路清清楚楚。try (FileInputStream in new FileInputStream(a.txt); FileOutputStream out new FileOutputStream(b.txt)) { byte[] buffer new byte[4096]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } }如果你用等价的老写法就得在finally里再嵌套try-catch去关资源然后手动addSuppressed代码又长又容易漏。所以现在写文件、网络连接、数据库连接时我基本只用try-with-resources只有资源类实在没实现AutoCloseable时才会退回finally手动关闭。3. 一次异常排查的完整链路从堆栈到根因再到修复3.1 案例一NoClassDefFoundError的定位五步回到那个NoClassDefFoundError我把完整的排查过程拆成五步以后你遇到类似问题可以直接照着走。第一步区分异常类别。如果堆栈是java.lang.ClassNotFoundException: java.applet.Applet通常是Class.forName或自定义类加载器主动去查类路径找不到才抛如果是java.lang.NoClassDefFoundError: java/applet/Applet则是代码在运行期通过new或访问静态字段触发了某个类的加载而那个类引用的类型找不到。虽然结果都是类没加载到但排查方向完全不同。第二步看堆栈里第一个业务代码类。NoClassDefFoundError的堆栈往往会指向真正调用出问题的代码位置去那个位置看方法依赖了哪些类。Applet这个例子通常是老库在某个工具类里引用了Applet或者是反射代码里出现了java.applet.Applet字符串。第三步查依赖树。用mvn dependency:tree或者IDEA的依赖分析找到引用java.applet.Applet的库。如果是直接依赖还好替换如果是传递依赖需要一层一层往上找。第四步用javap验证类签名。如果源码里找不到引用可以反编译或者用javap -c查看字节码确认类引用。这一步能避免“凭感觉猜依赖”。第五步给修复方案。按优先级来升级或替换掉依赖该类的库如果只是反射字符串直接去掉这段逻辑如果SDK按Java 9以上编译就同步升级运行JDK。改完重新打包用下面的命令验证启动阶段不再加载这个类java -verbose:class YourMainClass 21 | grep -i applet没有输出说明那个类已经完全退出加载路径。整个过程里最花时间的往往不是修复而是确认“谁在引用它”所以不要跳过第四步。3.2 案例二RedisTemplate自增失败的定位步骤Redis自增的问题我也按步骤拆一遍。第一步先确认Redis里的实际value类型。用命令行GET key如果是正常数字说明数据本身没问题。第二步看堆栈。如果是ClassCastException那重点基本可以锁定到序列化器。第三步打印当前RedisTemplate的valueSerializer配置没配置时默认是JdkSerializationRedisSerializervalue以二进制形式存在Redis里increment操作在服务端自然无法把它当整数解析。修复思路分两种情况。如果业务上只存字符串和数字最简单的是直接注入StringRedisTemplate它内部已经配好StringRedisSerializeropsForValue().increment()返回Long不会踩类型坑Long count stringRedisTemplate.opsForValue().increment(order:count:20250101, 1L);如果业务上确实需要RedisTemplateString, Object那就在Bean里明确指定序列化器Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }这样key保持字符串可读value用JSON序列化既不影响increment也能存对象。改完之后建议先删除掉之前被jdk序列化过的旧key再重新set一个数字用increment验证一次。这个案例的本质是泛型擦除RedisTemplateString, Object在编译期看着没问题运行期RedisConnection返回的实际类型由序列化器决定类型约定一旦和代码不一致ClassCastException就来了。3.3 排查异常时容易被忽略的隐藏信息排查异常时有几个信息经常被忽略但往往藏着根因。第一个是Caused by链。日志里第一行只是入口真正的原因可能在下面好几层一定要把完整堆栈拉出来再看。第二个是suppressed信息。try-with-resources里关闭资源时抛出的异常会出现在Suppressed: 后面如果我没注意这个遇到“业务异常”和“关闭异常”叠加的情况就会非常拧巴。第三个是线程名。同一段代码在请求线程和异步线程里抛异常处理策略完全不一样异步线程的异常往往不会经过全局异常处理器。另外在日志平台里排查线上异常时不要只用message去搜因为message里的业务参数可能每次都不同。更好的办法是用“堆栈第一行的类名方法名异常类型”做聚合先看这类异常的整体频次再挑一条具体堆栈展开分析。这个习惯能帮你快速判断是偶发问题还是系统性故障而不是一上来就被一屏日志淹没。4. 异常处理的最佳实践怎么落地才不空谈4.1 先定规矩什么时候catch什么时候抛什么时候包装团队里如果没有统一的异常处理约定代码很快就变成各种try-catch大杂烩。我一般定三条规矩第一能往上抛就让统一出口处理业务代码不要到处catch第二只有在当前层可以补偿时才catch比如回滚、重试、清理资源否则继续抛第三包装异常时务必把原始异常作为cause传进去少了cause排查链路就断了。反例很常见try { orderService.create(order); } catch (Exception e) { throw new BizException(创建订单失败); }这样抛出的BizException里没有cause线上看到报错只能知道“创建订单失败”至于底层是数据库超时还是字段校验问题完全不知道。正确写法是try { orderService.create(order); } catch (Exception e) { throw new BizException(创建订单失败, e); }一行之差排查难度是两种量级。4.2 业务异常与系统异常的分层设计更完整的做法是把异常分成两层业务异常和系统异常。业务异常用自定义BizException继承RuntimeException内部带错误码和提示信息系统异常包括NPE、数据库异常、IO异常等不应该直接暴露给用户而是由全局异常处理器统一转换成通用错误码。我可以给一个经典的骨架。定义错误码枚举public enum ErrorCode { SUCCESS(0, ok), PARAM_ERROR(400, 参数错误), BIZ_ERROR(500, 业务处理失败), SYSTEM_ERROR(500, 系统繁忙请稍后再试); private final int code; private final String message; // 构造器和getter省略 }定义业务异常public class BizException extends RuntimeException { private final int code; public BizException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code errorCode.getCode(); } public BizException(ErrorCode errorCode, Throwable cause) { super(errorCode.getMessage(), cause); this.code errorCode.getCode(); } }再配一个RestControllerAdvice统一出口Controller里就不用到处try-catch了。这里有一个容易踩的细节业务异常最好不要做成检查型异常哪怕编译期也不会强制你处理否则Service接口签名上全是throws既难看又难以和Java 8的Stream/Lambda配合。4.3 日志规范别让异常信息变成失忆现场日志里打异常最常见的坏习惯是只打e.getMessage()。message往往只是一句描述没有堆栈也没有上下文参数出了事什么都看不出来。正确姿势是把整个异常对象作为最后一个参数传进去让日志框架打印完整堆栈log.error(处理订单失败orderId{}, orderId, e);同时要避免同一个异常打两遍有的代码在catch里先log.error然后往外throw全局异常处理器又log一次一条异常在日志里出现两条相似堆栈告警平台还会重复通知。我的约定是异常只在“真正兜底的地方”打一次中间层包装时可以不打最多在异常对象里带上上下文信息。至于printStackTrace()线上环境基本不会输出到日志文件看到这种代码直接改掉。4.4 资源关闭、Optional与防御式编程资源关闭优先用try-with-resources这条前面已经说透了。Optional则更适合在“返回值可能不存在”的场景里用比如根据ID查用户查不到就返回Optional.empty()调用方显式处理空值。但我不建议把Optional用在方法参数上那会让每个调用方都要包一层Optional.ofNullable很别扭。关键入参可以用Objects.requireNonNull做快速失败在入口就把null拦截住。防御式编程并不意味着到处判空。我见过有人每个方法开头写三行if (obj null) return结果真的出问题时完全不知道是哪个环节漏了。更好的做法是把判空放在边界位置外部API入口、DB查询结果、RPC响应转换处内部流转的数据一旦进入对象包里就默认它是非null的。这样异常不会在中间某个角落悄悄出现而是会在一个可预期的位置暴露出来排起来也快。5. 设计模式融合把异常处理做成一套可扩展的机制5.1 模板方法统一处理流程的骨架模板方法非常适合用来固化异常处理流程。异常处理的典型步骤是固定的判断是否该处理、收集上下文、记录日志、决定是否告警、给用户返回结果。这些步骤的先后顺序不会变变的只是每一步的具体实现。把骨架写进抽象类把变化点留给子类就是模板方法模式。public abstract class AbstractExceptionHandlerT extends Throwable { public final void handle(T ex) { if (!shouldHandle(ex)) return; ExceptionContext context buildContext(ex); recordLog(context); doHandle(context); if (context.needNotify()) { notify(context); } } protected abstract boolean shouldHandle(T ex); protected abstract ExceptionContext buildContext(T ex); protected abstract void doHandle(ExceptionContext context); protected void recordLog(ExceptionContext context) { log.error(异常处理errorCode{}, message{}, context.getErrorCode(), context.getMessage(), context.getCause()); } protected void notify(ExceptionContext context) { // 默认不通知子类按需覆盖 } }子类只需要实现shouldHandle、buildContext、doHandle这三个方法流程不会乱。比如BizExceptionHandler继承这个抽象类doHandle里构造统一的错误响应其他异常走SystemExceptionHandler。模板方法的好处是把“每新加一种异常处理都要重新写一遍日志和告警”这件事彻底终结。5.2 策略模式按异常类型选择降级方案策略模式适合处理“同一种异常有多种处理方式”的场景。比如数据库连接异常可能需要重试上游服务超时可能需要返回兜底数据业务参数异常只需要直接提示。每种策略定义一个实现由选择器决定当前异常走哪个策略。public interface ExceptionStrategy { boolean supports(Throwable throwable); void handle(Throwable throwable); }public class RetryStrategy implements ExceptionStrategy { Override public boolean supports(Throwable throwable) { return throwable instanceof DataAccessException; } Override public void handle(Throwable throwable) { // 记录重试次数按指数退避重试 } }选择器可以是一个Map把异常类型映射到对应策略也可以遍历全部策略用supports判断。策略模式的核心价值是以后要加一种新的降级方案不需要改已有的if-else链新增一个实现类注册进去就行。这和模板方法正好互补——模板方法管“流程骨架”策略模式管“某个步骤的可替换实现”。5.3 责任链模式异常处理器的链式路由当多个处理器都可能处理某个异常时用责任链更顺手。每个处理器先判断自己能不能处理能处理就处理并结束否则把异常传给下一个。Spring MVC的HandlerExceptionResolver本质上也有这个味道多个解析器按优先级依次尝试。public interface ExceptionHandler { boolean canHandle(Throwable throwable); void handle(Throwable throwable); }public class ExceptionHandlerChain { private final ListExceptionHandler handlers new ArrayList(); public void addHandler(ExceptionHandler handler) { handlers.add(handler); } public void handle(Throwable throwable) { for (ExceptionHandler handler : handlers) { if (handler.canHandle(throwable)) { handler.handle(throwable); return; } } throw new GlobalException(ErrorCode.SYSTEM_ERROR, throwable); } }这个模式的威力在于扩展性。今天链上是BizHandler、ValidateHandler、SystemHandler明天要加一个RateLimitHandler只需要实现ExceptionHandler并加到链里其它代码一行都不用动。相比之下大型if-else链每加一种异常都要小心翼翼可能影响其它异常的分支责任链天然规避了这个问题。5.4 观察者模式异常事件与告警联动异常发生之后的副操作比如发邮件、钉钉通知、记录监控指标这些不应该写死在异常处理主流程里。这时候观察者模式最好用。在Spring环境里可以直接用ApplicationEventPublisher发布一个异常事件由不同的监听器处理各自的逻辑。public class ExceptionOccurredEvent extends ApplicationEvent { private final Throwable cause; private final String requestId; public ExceptionOccurredEvent(Object source, Throwable cause, String requestId) { super(source); this.cause cause; this.requestId requestId; } // getter省略 }发布点放在全局异常处理器里监听器可以写多个Component public class AlarmListener { EventListener public void onException(ExceptionOccurredEvent event) { // 发送告警注意不要因为网络问题影响主流程 } }观察者模式的好处是主流程只做“发布事件”这一件事后面的告警和分析由监听器异步处理主流程不会被通知的延迟或失败拖住。这里要留意如果监听器是同步的告警网络超时还是会影响主流程所以生产环境建议用Async或者消息队列解耦。这正好也是“设计模式融合”的实际体验——模式不是靠背的是在一次次线上事故里长出来的。6. 异常处理里的反模式这些坑我劝你别踩6.1 空catch与无脑return null空catch是异常处理里最危险的反模式。一段代码如果catch住异常却不记录任何信息线上出了问题就像拔掉了烟雾报警器看似安静实际上故障在悄悄蔓延。return null更致命它把异常变成了另一个NPE而且原来的根因已经丢了。如果实在觉得某个异常可以忽略至少写清楚为什么try { cache.put(key, value); } catch (Exception ignore) { // 缓存失效场景可以容忍降级为下一次全量加载 }注释里的“为什么可以忽略”比代码本身更有价值它告诉后来的人这个决定是有意识的而不是随手一写。6.2 用异常控制业务流程用异常做流程控制是我最反对的做法。异常对象的创建要填充堆栈开销远高于一个if判断在高频循环里可以直接把性能拖垮。更麻烦的是它会误导阅读代码的人让人以为这里真的出了什么错。用户输入校验、状态判断、循环终止这类场景应该用if、Optional、自定义结果码异常只留给真正的异常情况。6.3 在循环里抓异常与重复打日志在循环里逐条try-catch并打印日志是我见过线上日志量暴涨的头号原因。一个10万次循环即使异常率是1%也会瞬间产生1000条堆栈日志。正确处理是在循环外层catch或者把出错项的ID收集起来循环结束后一并上报。还有一个容易被忽略的问题try块范围过大。有些方法从头到尾包在一个try里一旦异常出现定位范围是整段方法。try块尽量缩小到最容易出错的语句附近别让异常排查变成大海捞针。6.4 全局异常处理器兜不住的异步场景全局异常处理器不是万能的它只能兜住Controller抛上来的异常。异步线程、MQ消费线程里发生的异常不会自动走进RestControllerAdvice这些地方需要单独加兜底逻辑和监听器否则异常会直接打到控制台线上日志平台根本收集不到。比如CompletableFuture异步任务里抛了异常如果不显式处理异常会静默丢失。常用的做法是加exceptionallyCompletableFutureOrder future CompletableFuture.supplyAsync(() - orderService.get(orderId)); future.exceptionally(ex - { log.error(异步查询订单失败, orderId{}, orderId, ex); return defaultOrder(); });Async方法里同理要么在方法内部try-catch要么配置一个自定义AsyncUncaughtExceptionHandler去收集异常。MQ消费线程更要小心如果异常没被捕获消息可能一直重试直到阻塞队列如果异常被无脑吞掉消息又可能被确认消费但业务没执行。消费逻辑里要明确区分“可重试异常”和“业务不可恢复异常”前者抛出触发重试后者记录日志并走死信。这些反模式往往不是一开始就存在而是项目迭代到中后期大家为了“快速解决问题”随手加try-catch时慢慢长出来的。等到线上告警变成噪音再想回头清理成本就高了。所以从一开始就要把这些规矩定下来比我事后苦口婆心劝代码要有效得多。最后分享一个我自己养成的排查习惯线上看到异常先别急着打开编辑器第一件事是把Caused by链路、suppressed信息、当时的入参日志截图存下来。很多时候问题不是代码写错而是数据、依赖、环境三者的组合堆栈和数据一起看才能避免在错误的方向上折腾。我现在写业务代码时异常处理的优先级永远是保留完整上下文 优雅降级 及时告警。代码里宁可少几个try-catch也要把异常出口设计得统一。这个习惯帮我省下了大量深夜排查的时间希望也能帮到你。
RELATED

相关推荐

STM32F103RBT6 CAN总线开发调通:HAL库配置、过滤器与中断实战

STM32F103RBT6 CAN总线开发调通:HAL库配置、过滤器与中断实战

简介:一套已调通的 STM32F103RBT6 CAN 总线开发代码,基于 HAL 库与 STM32CubeMX 配置,面向嵌入式初学者及需要快速落地 CAN 通信的开发者,解决从 CubeMX 初始化、Keil 工程移植到消息收发调试的全流程问题。压缩包共 586 个文件&a…

📅 2026/9/9 20:02:58
G4900/G5400核显装Win7失败?UHD610/630魔改驱动安装全指南

G4900/G5400核显装Win7失败?UHD610/630魔改驱动安装全指南

简介:面向Windows 7平台的Intel UHD Graphics 610/620/630/P630显卡驱动,同时特别优化奔腾G4900、G5400处理器集成显卡的兼容性与稳定性。该驱动重点解决旧系统下可能出现的花屏、闪烁、图像失真及显示异常问题,适合仍在使用Win7且配备上述核…

📅 2026/9/9 19:57:58
行列式怎么算?从几何意义到初等变换全解析

行列式怎么算?从几何意义到初等变换全解析

如果让我选线性代数里“最劝退”的一个考点,我会把票投给行列式的计算。这一节在教材里的位置多半是第5章第2节,前面刚讲完行列式的定义和性质,后面就要开始连着一整章的习题。看起来内容不多,可实际做题的时候,同一个…

📅 2026/9/9 19:57:58
MORE NEWS

更多资讯

📰

Skills不是函数,而是智能体的动作契约

1. 这不是编程语言,而是智能体的“肌肉记忆”——Skills 的本质重新定义你打开一个智能体项目文档,看到 SKILL.md 文件,第一反应可能是:“哦,又一个配置文件?”接着翻到目录页,发现 Skills 目录…

📰

多智能体协作框架怎么落地?拆解TradingAgents的投研辩论机制

先说我看到 TradingAgents 这项目的第一反应:GitHub 上头这类“AI 智能体炒股”的开源项目多了去了,但真正把开会辩论这套流程做完整的很少。它模拟了一个真实投资机构里的投委会——几个研究员分别从基本面、技术面、市场情绪这些角度去分析同一只股票&…

📰

AI Agent记忆系统:从跨会话连续性到工程化落地

1. 为什么“让 Agent 记住你”不是功能升级,而是范式切换? “走进AI Agent第三篇:让 Agent 记住你”——这个标题乍看像一个普通功能点,但实际踩中了当前Agent落地最深的断层带。我从2022年第一批用LangChain搭客服Bot开始&#x…

📰

羽毛球教学如何用好智能陪伴与数据反馈?一套让进步清晰的训练方法

在吴忠这几年的羽毛球培训圈里,我常被人问到同一个问题:明明球馆里的场地一直很抢手,为什么很多人打了两年球,还是“只会发球接球,一打比赛就乱”?我的答案是,大多数人并不缺场地、不缺时间&…

📰

Airi Vue 组件测试最佳实践:采用黑盒测试思路,聚焦行为而非内部实现

Airi Vue 组件测试最佳实践:采用黑盒测试思路,聚焦行为而非内部实现 【免费下载链接】airi 💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing…

📰

在 airi 项目中正确处理 Vue 异步测试:nextTick、trigger 与 flushPromises 实战指南

在 airi 项目中正确处理 Vue 异步测试:nextTick、trigger 与 flushPromises 实战指南 【免费下载链接】airi 💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wi…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬