Java八股实战:动态代理、AQS、深拷贝与数据一致性解析
我先简单说下背景。做后端这些年我发现一个很有意思的现象网上把“八股”说成贬义词好像背面试题就等于没实战。但真要拿这件事杠一下我反倒觉得八股里藏着大量真实业务的底层逻辑关键是你会不会背、背完会不会用。比如你调一个接口慢脑子里有没有JVM内存模型、有没有MySQL索引结构、有没有GC日志分析的基本概念完全就是两码事。今天这篇Day16咱们继续沿着Java八股这条线往下走把最近高频出现、但网上讲得比较散的几个点串一遍动态代理、AQS、深度拷贝、数据一致性还有几个实际项目中踩过的坑。文章会按照“思路拆解—核心细节—实操过程—问题排查”的顺序来走最后我再补一段自己真实的体会。保证不是纯背概念每个点都尽量落到可执行的代码和排查动作上。1. 内容整体设计与思路拆解1.1 为什么“八股”其实是系统的浓缩先把话说通透。Java的知识体系太庞大了从语法、集合、并发、JVM到Spring、MySQL、Redis、消息队列再到分布式、微服务如果没有一条主线你很难形成有效记忆。而八股文的本质就是把这条主干线上最关键的节点挑出来用问答形式帮你反复强化。它不是知识的终点而是索引。好比你要去一个陌生的城市八股先给你一张地图你说地图不够精细没问题实战是逐条街道去走但地图的价值在于你先有了全局方向不会迷路。我每天安排一个Day主题就是在做这种地图索引的工作。Day16这期我挑了几个在近期热词里出现频率很高、而且面试和日常开发都离不开的硬核点动态代理、面向对象设计在并发场景里的延伸AQS、对象深度拷贝、以及数据一致性这个话题。这四个点分开看是四个技术连着看其实是一条主线怎么让Java代码更灵活、更安全、更可复用地处理复杂业务。1.2 这个Day的核心定位拒绝“背完就忘”我做这种学习笔记的时候最反感的就是背完就忘所以每期的内容都刻意加了一些“非标准答案”的东西为什么这样设计、实际项目里会在哪里用到、踩坑场景是什么。这也是我写这系列最大的初衷让八股变成你项目排查时的思维工具而不是躺在收藏夹里吃灰的文档。比如动态代理很多人知道JDK动态代理和CGLIB的区别张口就能背“JDK基于接口CGLIB基于继承”。但真让你在一个老项目里给某个Service加个日志切面或者排查为什么Spring的Transactional不生效时你会不会想到动态代理是这一切的幕后操盘手如果想不到那这个知识点就只是个“背过”的状态。今天我希望把它变成一个你能实际调用的常识。再比如AQS听起来很高大上其实你天天在用的ReentrantLock、Semaphore、CountDownLatch都是它的儿子。搞懂AQS的state状态、CLH队列、独占与共享模式你再去看Java并发编程视线基本就通了。我们常说“并发编程的底层是AQS”这句话说对了一半更准确的说法是理解AQS是理解Java显式锁的一把钥匙。1.3 主线串法以“数据一致性”收口最后一个重头戏是数据一致性。这个东西八股里考得也很多比如分布式事务、本地消息表、最终一致性、CAP理论。这些概念背后其实都离不开“状态变更的可靠传递”这一命题。我发现很多初级同学把数据一致性问题单纯等同于分布式事务好像不做Seata就是不一致这是典型的被框架绑架。真实场景下可能你改一个订单状态先更新数据库再发MQ这两步之间怎么保证一致性你缓存和数据库都写了一个失败怎么办这些问题恰恰是动态代理也帮不上忙AQS也管不了需要你在架构层面设计兜底策略的地方。所以Day16的内容不是四个孤立知识点而是一条从“代码灵活生成”到“并发安全控制”再到“数据可靠落地”的链条。下面开始逐个拆。2. 核心细节解析与实操要点2.1 动态代理JDK与CGLIB“物尽其用”动态代理这块网上的标准答案太多我只说面试里容易翻车、项目中容易踩坑的几个细节。JDK动态代理的实现本质JDK动态代理要求目标必须实现接口本质是运行时生成一个实现相同接口的代理类方法调用经由InvocationHandler#invoke转发。热词里出现java invocationhandler()说明这块关注度一直很高。我建议你写出这样的代码来验证自己的理解interface UserService { void create(String name); } class UserServiceImpl implements UserService { Override public void create(String name) { System.out.println(创建用户: name); } } class LogInvocationHandler implements InvocationHandler { private final Object target; LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(调用前: method.getName()); Object result method.invoke(target, args); System.out.println(调用后: method.getName()); return result; } } // 使用 UserService userService (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogInvocationHandler(new UserServiceImpl()) ); userService.create(张三);这段代码麻雀虽小五脏俱全。你要是能不看任何资料把这个写出来并且解释清楚Proxy.newProxyInstance的三个参数各自的作用动态代理这一块基本就稳了。我之前面试别人最喜欢用这个小demo去区分“真会”和“背过”。CGLIB的底层是继承CGLIB则不需要接口它直接生成目标类的子类通过继承并重写方法来实现增强。正因为走的是继承路线所以它有两个天然限制目标类不能是final的目标方法也不能是final的。同时CGLIB无法代理final方法本质上是字节码层面生成子类override如果方法被final修饰JVM层面就不允许override。Spring是怎么选的呢标准答案是如果bean实现了接口默认用JDK动态代理如果没有实现接口就自动切换到CGLIB。但从Spring Boot 2.x开始spring.aop.proxy-target-class默认是true也就是强制走CGLIB不再因为“有接口”就自动选JDK。这个变化很多人没注意看到项目里明明是接口实现类却生成了CGLIB代理就开始怀疑是不是配错了。其实不是这是Spring Boot的默认设置演变。实际项目里的经典坑在旧项目改造或排查事务失效时动态代理的坑往往出在“内部方法自调用”上。比如在一个Service内部methodA()里直接调用同类里的methodB()如果methodB()上有Transactional或Async这些注解大概率不生效。因为走的是this调用而不是代理对象调用代理逻辑直接被绕过了。解决办法很简单注入自身代理比如Autowired自己或者用AopContext.currentProxy()或者拆分到另一个Bean里。这类报错不会给你任何红色异常业务也能正常执行只是悄悄失效属于最阴的坑之一。排查技巧是日志里看不到事务连接的变化或者明确打印代理类名看看是不是cglib代理类。2.2 AQSJava显式锁的统一底座再来聊AQS也就是AbstractQueuedSynchronizer。热词里aqs java出现说明这个点依然是复习重点。我一直建议想真正理解Java并发的人把AQS当作一个“状态机等待队列”的组合来理解而不是死记源码。AQS的核心三点state一个volatile修饰的int字段代表同步状态。比如ReentrantLock里state0表示无锁state1表示有人持有锁state1表示重入次数。CLH队列一个FIFO双向链表当拿不到锁时线程封装成Node挂到队列尾部进入等待。等持有锁的线程释放时唤醒队列头部的线程去抢锁。acquire/release模式模板方法模式的应用子类通过实现tryAcquire、tryRelease等方法定义自己的获取和释放规则。模板方法模式在AQS里的体现我看源码有个习惯先找哪些方法是final的哪些是留给子类实现的。AQS就很典型acquire()方法是final的整个排队流程固定不变但tryAcquire()由子类决定比如ReentrantLock的公平锁和非公平锁差异只在tryAcquire这一个方法里。这就叫模板方法模式——把不变的流程固定把可变的策略留出接口。常见子类体验ReentrantLock独占锁基于AQS的独占模式。Semaphore共享锁acquire时state减少release时state增加。CountDownLatch也是共享模式state初始为NcountDown一次减一到0唤醒所有等待者。ReentrantReadWriteLock一个AQS里同时维护读锁和写锁状态高16位和低16位分别记录。这些类如果你日常只是用其实不会感知到AQS但一旦遇到性能问题或死锁排查就需要理解它们底层的等待队列和唤醒机制。比如ReentrantLock和synchronized的区别很多人都知道“ReentrantLock可以中断、可以超时、可以公平”但你得再追问一句为什么它可以因为AQS提供了中断响应和超时获取的实现路径。从热词里延伸java invocationhandler()与AQS没关系关于动态代理的InvocationHandler和AQS有初学者容易混淆以为它们都属于“代理/处理”类机制。其实完全两码事InvocationHandler是方法调用的拦截分发AQS是并发同步队列的底层架构。硬要说有联系两者都体现了Java里“流程与应用解耦”的设计思想但应用场景一个在AOP切面一个在并发控制别搞混。2.3 深度拷贝别让对象引用“复制了个寂寞”热词里有java对象深度拷贝这也是个容易迷糊的点。很多人写代码用clone()或BeanUtils.copyProperties()结果发现改了副本原对象也跟着变了才意识到自己做了个浅拷贝。概念区分直接用代码说事class Address { String city; // getter/setter... } class Person { String name; Address address; // getter/setter... }如果只做浅拷贝copyPerson的address和原person的address指向同一个对象你改copyPerson.address.city原对象也变了这种“复制”在很多业务场景会引发隐蔽bug尤其是订单、合同这类快照类数据。深度拷贝则要求所有引用类型的属性也都复制一份独立对象。几种实现深度拷贝的方式方式原理优点缺点手动set/get自己创建新对象逐个赋值最简单、可控层次深了代码量大容易漏字段序列化拷贝对象转字节再反序列化实现简单通用性能开销大要求所有类实现SerializableJackson/Gson序列化JSON中转无需实现Serializable对特殊类型如某些时间类需要配置Apache BeanUtils反射属性复制使用方便性能一般且默认浅拷贝MapStruct编译期生成映射代码性能极好类型安全需要引入编译期注解处理器项目实战里我比较推荐用MapStruct做对象属性转换尤其是DTO、VO、Entity之间的互转。它是在编译期生成转换代码性能好而且类型不匹配会在编译期直接报错省去运行时才发现问题的痛苦。不过要提醒一点序列化拷贝有陷阱。如果你的对象里藏着像Thread、Socket、InputStream这类不可序列化或不该序列化的属性反序列化时会直接失败或产生脏状态。正确的做法是指定transient排除或者手写拷贝逻辑。总的来说深度拷贝没有银弹关键是你要清楚业务里哪些属性需要快照哪些可以共用。2.4 StringBuilder、数组越界与正则小坑这几个虽然都是基础题但复习的时候容易一眼略过面试官最喜欢在这种看似简单的地方加问一层。StringBuilder与StringBuffer的区别要能答到线程安全性StringBuilder非线程安全速度更快适合单线程拼接场景StringBuffer加了Synchronized线程安全但性能略低。这个答案大多数人会背但你要能接着讲为什么单线程拼接推荐StringBuilder而不是直接用因为在循环里拼接会产生大量中间String对象触发频繁GC。虽然现代编译器会对简单的做优化变成StringBuilder但循环拼接场景下优化不一定到位建议手动用StringBuilder。数组越界异常怎么排查热词里的java中数组越界异常表面上是ArrayIndexOutOfBoundsException但排查的真正功夫在于“为什么下标会越界”。常见原因循环边界写错、多线程并发读写集合、从0开始还是从1开始的认知错乱。我见过一个生产事故SQL分页查出来给前端返回数据时某段代码用list.get(i 1)最后一轮直接越界。这个就是典型的边界没考虑到。排查手段无外乎第一步看异常堆栈定位到行号第二步打印当前下标、数组/集合长度第三步回归边界条件。基本能解决九成问题。正则背不背重点是理解正则不展开讲但我劝你别死记各种元字符表而是记几个高频模式判断邮箱、手机号、身份证、版本号以及字符串提取。真正写代码时建议多用Pattern预编译避免每轮都重新编译正则性能差异在循环里非常明显。3. 实操过程与核心环节实现这一章节我们挑三个与实际编码强相关的场景一步一步走通写一个自定义注解配合动态代理做简易审计日志用ReentrantLock实现一个带超时的资源访问控制再实操一把深度拷贝在选择方案时的对比。3.1 实操一自定义注解动态代理实现审计日志场景老项目里有一套Service需要在每个方法入口和出口打印操作日志但不想在业务代码里到处写logger.info。Step 1定义一个注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface AuditLog { String action() default ; }Step 2创建代理处理逻辑这里简单点我们直接通过JDK动态代理来演示调用链路处理public class AuditProxy implements InvocationHandler { private final Object target; public AuditProxy(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { AuditLog auditLog method.getAnnotation(AuditLog.class); if (auditLog ! null) { System.out.println([审计] action auditLog.action() , args Arrays.toString(args)); } Object result method.invoke(target, args); if (auditLog ! null) { System.out.println([审计] action auditLog.action() , 执行成功); } return result; } }Step 3为目标Service生成代理UserService userService new UserServiceImpl(); UserService proxyInstance (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new AuditProxy(userService) ); proxyInstance.create(李四);这种写法放在框架里就是Spring AOP的雏形。你自己动手走一遍这个流程再看Spring的Around注解、切面表达式就没有黑盒感觉。必要时补充AspectJ的织入时机包括编译期、加载期和运行期Spring AOP默认就是运行期代理所以代理的具体形态绕不开JDK或CGLIB这两个实现。3.2 实操二ReentrantLock实现带超时的库存扣减场景电商秒杀里需要保证同一商品的库存扣减在同一时刻只有一个线程能执行但等待时间不能太长。用synchronized无法直接实现“等不到就放弃”此时就轮到ReentrantLock的超时锁登场。public class StockService { private final ReentrantLock lock new ReentrantLock(); private int stock 10; public boolean deduct(int count) { boolean locked false; try { locked lock.tryLock(1, TimeUnit.SECONDS); if (!locked) { System.out.println(获取锁超时当前线程 Thread.currentThread().getName()); return false; } if (stock count) { System.out.println(库存不足); return false; } stock - count; System.out.println(扣减成功剩余库存 stock); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { if (locked) { lock.unlock(); } } } }这段代码的价值不在几行API而在于几个细节tryLock必须放在try外面否则拿到锁但后续异常时locked变量可能还没初始化finally里如果去unlock一个没有获得的锁会抛IllegalMonitorStateException。捕获到InterruptedException后要重新设置中断标志位Thread.currentThread().interrupt()这行不能丢。这是处理中断的标准姿势很多人会忽略这个恢复动作。finally里判断locked才unlock保证只有真正拿到锁的线程能释放锁。对比synchronizedsynchronized拿不到锁时只能阻塞等待没法超时放弃。而在高并发场景下线程大量阻塞反而容易引发线程堆积超时放弃策略往往更优雅。这正好呼应前面AQS里讲到的“中断响应、超时获取”这些能力它们不是空谈是真能在业务代码里救命的。3.3 实操三深度拷贝方案对比我们用一个实际可运行的示例展示序列化方式和MapStruct方式的差别。先看序列化方式public class DeepCopyUtils { SuppressWarnings(unchecked) public static T T deepCopyBySerialization(T source) { try { ByteArrayOutputStream bos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(bos); oos.writeObject(source); oos.close(); ByteArrayInputStream bis new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois new ObjectInputStream(bis); return (T) ois.readObject(); } catch (Exception e) { throw new RuntimeException(深拷贝失败, e); } } }要求目标对象和所有嵌套对象都实现Serializable。这个方法的好处是通用、隔离彻底坏处是性能差对象内部牵扯到不可序列化属性时直接歇菜。真实项目里如果你只是需要拷贝某个实体而这个实体已经实现了Serializable用它可以快速搞定。再看MapStruct方式需要定义一个转换器接口Mapper public interface PersonMapper { PersonMapper INSTANCE Mappers.getMapper(PersonMapper.class); Person copy(Person source); }这个会在编译期生成一个PersonMapperImpl里面手动new出新对象并逐个赋值包括嵌套对象里的字段相当于替你生成了最笨但最可靠的手写拷贝逻辑。所以如果要追求性能和可控性MapStruct更稳。实操到这里你会发现“深度拷贝”从来不是一个技术选型问题而是业务边界问题你需要快照到什么层级哪些字段要共用哪些必须独立想清楚再动手选工具事半功倍。4. 常见问题与排查技巧实录4.1 动态代理相关事务注解不生效、代理Object方法异常现象1同类内部调用导致Transactional失效业务在methodA里调methodB后者有事务注解数据库操作后主动抛异常却发现数据没有被回滚。排查步骤为确认Spring管理的Bean类型是不是代理类。方式是在启动时打印bean class或者直接用System.out.println(bean.getClass())。如果打出来是xxxService$$EnhancerBySpringCGLIB说明走的是CGLIB代理理论上切面是生效的。检查调用方式如果是this.methodB()就是内部自调用。解决方案注入ApplicationContext取代理Bean来调或用Lazy注入自身或用AopContext.currentProxy()。这个坑出现频率极高属于典型的“不会报错但结果不对”的问题。所以排查时要先怀疑代码路径而不是一上来就查数据库。现象2JDK动态代理时Object的equals/hashCode也会被拦截吗会。代理对象所有公开方法调用都会经过InvocationHandler包括equals、hashCode、toString等Object方法。面试或开发时如果不注意这个细节可能出现“代理对象和原对象equals比较结果诡异”的情况。解决办法是在InvocationHandler里手动区分方法名或直接对Object方法做特殊处理。4.2 AQS与并发锁问题死锁和中断恢复场景多把锁加锁顺序不一致导致死锁写过购票、转账、库存类逻辑的人应该都知道加锁顺序的重要性。比如线程A持有锁1等待锁2线程B持有锁2等待锁1双双卡死。用AQS的tryLock加超时可以缓解但不能完全预防。更根本的解法是统一加锁顺序比如所有转账都按账户ID升序加锁。排查死锁的方式先jps找到进程ID再用jstack pid导出线程快照看到Found one Java-level deadlock字样后会明确标出两个线程各自持有的锁和等待的锁。学会用jstack排查死锁是并发编程的基本功。场景InterruptedException吞掉中断状态在try/catch里捕获了InterruptedException却什么都不做是个非常常见的毛病。实际代码里这会导致上层无法感知线程中断任务可能继续跑下去甚至产生逻辑错误。正确处理方式就是我上面写的Thread.currentThread().interrupt()恢复中断标志。这个细节也常被面试官拿来考察候选人对并发中断语义的理解。4.3 深度拷贝与数据一致性常见坑位速查深度拷贝的坑位不多常见的有三个忘写Serializable序列化抛NotSerializableException。解决办法检查嵌套对象是否都实现序列化集合也要看元素类型。使用clone()但没重写深克隆逻辑。Object自带的clone是浅拷贝重写时必须自己处理引用类型字段。复制大对象时性能开销非常大。一次性拷贝一个几十MB的大对象内存和CPU都可能飙起来。数据一致性问题则更多是宏观设计层面问题场景推荐方案说明双写数据库缓存Cache Aside Pattern 延迟双删更新数据库后删除缓存防止缓存与数据库短暂不一致数据库MQ消息发送本地消息表 / 事务消息保证数据库操作与消息发送的原子性多服务跨库事务最终一致性 本地消息表避免强分布式事务带来的性能损耗和复杂度订单支付状态同步状态机 对账补偿通过定时任务扫描待处理订单推动最终一致热词里提到java怎么保证数据一致性这是很多中高级面试必问的开放题。我的建议是不要一上来就讲Seata、TCC这些重型方案。先讲清楚单体应用里的事务边界在哪里分布式下强一致不可行时要怎么取舍再讲最终一致性的落地组合拳。4.4 环境与构建相关版本号警告问题热词里有一条很具体java: 警告: 源发行版 17 需要目标发行版 17。这个是JDK版本不匹配的典型报错本质是编译器的--release、--source、--target三者不统一。多数情况是IntelliJ IDEA里Project SDK设成17但Maven编译器插件还指定JDK 8或者反过来。解决办法很直接检查Project Structure里的SDK版本。检查pom.xml里maven.compiler.source和maven.compiler.target。检查Spring Boot父依赖里java.version属性。把这几处版本调整一致重新build问题基本消失。这种问题技术含量不高但卡住很多人我把它列在这里也算给正在折腾环境的人省点时间。同样属于环境问题的还有java环境变量配置。虽然现在IDE都能自动识别JDK但命令行跑Maven或Java时环境变量尤其是JAVA_HOME和PATH里指到错误版本的情况依然时有发生。Windows上排查思路也一样命令行里java -version、javac -version先看结果确认是不是同一个JDK。很多时候是系统里装了多个JDKPATH顺序把旧版本顶到前面了。5. 额外延展把POI生成Word图表和Java通配等点串进来热词里有两个相对冷门但有意思的点java poi word能生成图表吗和java聚合。我不占用太长的篇幅但值得一一回应。POI是操作Office文档的老牌工具库。POI可以生成Word里的基础段落、表格、图片但图表Chart这块支持相对弱官方对Word图表的API并不像Excel那么完善。实际项目的常见方案是先用POI生成Word并在需要图表的位置插入一个占位图片图表本身用JFreeChart或ECharts服务端渲染成图片再塞进Word。或者更直接的做法是用模板引擎比如Freemarker配合docx XML模板预置图表结构POI负责填充数据。java聚合这个词在面向对象范畴里更多指组合关系比如一个班级Class包含多个学生Student这是“has-a”关系和“继承”的“is-a”区分开。聚合通常也是面试里考察面向对象设计时的一个基础概念配合前端、接口这些热词说明大家最近在复习设计基础挺扎实。还有热词里的列车调度java看起来像某个课程设计选题。这个题目如果用Java实现核心其实是“栈”的应用列车进站出站需要遵循后进先出的结构很多经典算法题就是拿列车调度来模拟栈的操作顺序。如果把这个选题扩展开你应该先定义栈的容量再模拟入栈出栈序列判断一个出站序列是否合法。顺便一提Java的Deque接口比Stack类更推荐因为Stack继承自Vector带同步开销且有历史包袱。写代码时直接声明DequeInteger stack new ArrayDeque();这句替代本身也是个小八股知识点但非常有实际意义。6. 结合热词快览Java面试与自学路线的碎片化补充热词里还有java面试题、java面试宝典pdf、java自学路线图、java基础面试题这类偏检索类的关键词。这类东西本身内容不比知识点重要关键是别被“搜集资料”绑架。我见过太多人收藏夹里几百篇PDF和脑图真正静下心读的没几篇。慢即是快能系统把一个主题吃透比囤资料有价值得多。如果你正在学Java不管是为了跳槽还是项目开发我的实用建议是基础阶段Java语法、面向对象、集合、异常、IO、反射这部分推荐配合官方文档和一本不错的入门书不必深抠源码。进阶阶段JVM、并发、MySQL、Redis这部分要动手写代码分析日志亲眼看GC日志、锁竞争、索引优化。框架阶段Spring、Spring Boot、MyBatis要多问“为什么这样设计”。项目阶段独立做两三个完整项目从前端到后端到部署全链路走通才谈得上有“全局观”。这条路看起来很长但每一段都能沉淀出直接可用的经验。比如你现在看到这篇文章如果你能把动态代理的demo敲一遍、把AQS的源码跟一遍、把深度拷贝的几种方式跑一遍今天这波Day16就算没白学。热词里的java基础题目的网站、java面试大全及答案随便找一个都行关键是消化不是收藏。7. 最后分享两件实操中的小事我在实际学习和排查问题中有一个习惯凡是涉及“为什么”的知识点都会顺手写一个小demo验证不验证不放心。比如今天写的动态代理demo、ReentrantLock超时锁demo都是我排查线上问题时抽象出来的精简版。这个习惯帮了我很多因为它让我对框架的行为不再是“猜”而是“验证过”。再分享一个小技巧读Java并发源码时不要一开始就扎进细节。先看注释尤其AQS类上那段英文注释把设计意图读明白。然后只盯着三个关键状态state、等待队列的头尾节点、以及线程的等待状态。剩下就是流程推演你作为线程进队、阻塞、唤醒、抢锁、释放这五步走一遍AQS的骨架就刻在脑子里了。Day16的内容到这里主线已经很清楚了动态代理让代码增强透明化AQS让并发控制标准化深度拷贝让对象复制可靠化数据一致性让系统落到终态稳态。每一块都足够你再掏出源码、再写两段代码去夯实。八股只是一个入口真正的功夫还是那句老话动手实战踩坑复盘。下一次Day更新见。