news 2026/9/8 15:54:28

Java原型模式实战:从浅拷贝到深拷贝,高效复制对象副本

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java原型模式实战:从浅拷贝到深拷贝,高效复制对象副本

如果要在23种设计模式里选一个“看起来鸡肋、用起来真香”的角色,我投原型模式一票。很多同行对它的印象停留在面试题里的Cloneable和clone方法,真到业务代码里,还是老老实实new一个对象再挨个set。这个习惯本身没什么不对,可一旦对象的创建链路很长、初始化很重、又需要反复生成相似副本时,new出来的状态往往不是你要的那份“半成品”,而是又走了一遍漫长构造过程的新品。原型模式解决的就是这件事:把一个已经造好、处于理想状态的对象当模板,通过复制快速生成新对象,再在新副本上做差异化调整。这篇文章就把原型模式从概念、Java实现、深拷贝雷区到真实业务案例一次性讲透。

1. 原型模式不是“再new一个”,它的核心是复印半成品

1.1 为什么创建型模式里偏偏它走“复制”路线

说到创建型模式,大家最熟悉的往往是单例、工厂、建造者。这些模式解决的核心问题都是“怎么把对象的创建过程封装起来”:工厂把复杂构建逻辑收拢到一处,建造者把一大堆必填参数和可选参数打散成链式调用。它们的共同点是,每次要对象时,仍然是从零开始创建。

原型模式的想法不一样。它不再关心“怎么从零搭一个对象”,而是直接关心“怎么把一个已经搭好的对象复制一份”。这个差异看起来很微小,实际影响很大。比如一个营销规则对象,构造时要根据活动类型去加载若干条规则、去远程接口拉黑白名单、还要做一堆默认值校验。你用工厂每新建一次,这些开销就跑一遍。可如果你已经有一个配置正确、数据完整、状态刚刚好的模板对象,直接复制它,就能拿到一个状态几乎一样的对象,再改其中一两个字段就能用于新场景。

这就像你写一份合同。工厂方式是把所有字段从空开始填;原型方式是把现成的标准合同复印一份,然后在复印件上做修订。显然,模板越复杂、越成熟,复印的收益越明显。

1.2 原型模式的三个角色:模板、复印机、客户

从设计模式的角度看,原型模式通常有三个参与角色:

  • 原型接口:声明一个用于复制自身的方法,Java中往往对应Cloneable标记加clone()
  • 具体原型:实现这个复制方法,真正完成对象拷贝。
  • 客户:持有一个原型实例,通过调用复制方法获得新对象,而不是通过new

用最简代码表示,大概是这个样子:

public interface Prototype { Prototype copy(); } public class SearchCondition implements Prototype { private String keyword; private List<String> categories; public SearchCondition(String keyword, List<String> categories) { this.keyword = keyword; this.categories = categories; } @Override public SearchCondition copy() { return new SearchCondition(keyword, categories); } }

这里我写的copy()方法其实只是借用了原型的思想,用构造器复制了一遍。Java里更“正统”的做法是让类实现Cloneable并重写clone(),这样能利用JVM原生的对象复制能力,不用手动拷贝每个字段。

1.3 理解clone绕过构造函数的真正含义

很多人第一次接触clone()时,最不理解的一条规则是:它不会调用构造函数。这一条往深了想很有意思。

普通new一个对象,会经过类加载、内存分配、构造函数执行这一连串过程。构造函数里可能会做参数校验、状态初始化、联动其他对象。而Object.clone()是native方法,它直接在内存层面复制对象结构,把所有实例字段的值原封不动地搬过去。也就是说,哪怕你的构造函数里写了非常重的初始化逻辑,克隆对象时这段逻辑也不会重新跑。

这本来是原型模式的一大优势:复制对象“快且真”,快在省去了构造函数开销,真在复制对象和原对象状态完全一致。但反过来也提醒你,原型对象必须是“真正可用”的成品。如果你拿一个还没完成初始化、或者依赖构造函数补数据的对象去克隆,克隆出来的对象也会同样处于残缺状态,因为构造函数不会再帮你兜底。

所以,用原型模式前,请先确认模板对象的状态是可靠、完整的。一旦建立这个认知,后面所有坑都容易理解。

2. 在Java里写原型模式,先学会clone()这个祖宗留下的暗坑

2.1 Cloneable和Object.clone到底是干嘛的

Java里要实现原型模式,绕不开一个历史包袱:Cloneable接口本身是空的,它没有声明任何方法。真正干活的是Object.clone(),一个protected native方法。

Object.clone()的逻辑是:先判断当前对象是否实现了Cloneable,没有就抛CloneNotSupportedException;实现了就分配一块新内存,把对象各个实例字段的值按位复制到新对象里。复制出来的对象,基本类型字段是全新值,引用类型字段只是复制了引用,两个对象仍指向同一个底层对象。这就是“浅拷贝”。

由于Object.clone()是protected方法,用户自己的代码不能直接调obj.clone()。要暴露克隆能力,必须在子类里重写,把可见性改成public,并调用super.clone()

public class User implements Cloneable { private String name; private Address address; @Override public User clone() { try { return (User) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } }

类声明时必须加上Cloneable,否则super.clone()直接抛异常。clone()返回的Object类型,在Java里支持协变返回类型,所以这里可以写成User而不是Object,客户端不用强转,代码干净很多。

2.2 浅拷贝翻车现场:一个引用字段改崩两个对象

浅拷贝最典型的翻车场景,是对象里包含一个可变引用字段。很多第一次写原型的人会以为,调用clone()就万事大吉了,结果业务上一改,连原对象都被改了。来看个例子。

public class Address { private String city; public Address(String city) { this.city = city; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } } public class User implements Cloneable { private String name; private Address address; // 省略构造方法和getter/setter @Override public User clone() { try { return (User) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } } // 测试 User u1 = new User("张三", new Address("北京")); User u2 = u1.clone(); u2.getAddress().setCity("上海"); System.out.println(u1.getAddress().getCity()); // 竟然输出上海

问题就出在u1.addressu2.address两个引用指向同一个Address对象。super.clone()只是把address引用本身复制了一份,没有创建一个新的Address。结果通过u2修改城市,u1也跟着变。线上如果出现这种“一个活动配置改完,另一个活动同步被改”的诡异现象,多半就是浅拷贝引起的共享可变对象。

2.3 手写深拷贝的通用姿势

要解决上面的问题,就不能只依赖super.clone(),必须在重写的clone()方法里把引用字段也逐个复制,并且嵌套对象最好也支持克隆。

public class Address implements Cloneable { private String city; @Override public Address clone() { try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } } public class User implements Cloneable { private String name; private Address address; private List<String> tags; @Override public User clone() { try { User copy = (User) super.clone(); // 先处理可空引用 copy.address = address != null ? address.clone() : null; // String本身不可变,新建容器即可 copy.tags = tags != null ? new ArrayList<>(tags) : null; return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } }

这里有几个细节值得注意。第一,Address必须自己实现Cloneable并重写clone(),否则父类User.clone()里根本没法深拷贝它。第二,如果tags里放的也是可变对象,比如List<Rule>,只做new ArrayList<>(tags)依然不够,需要遍历并逐个克隆元素。第三,处理引用字段时一定要判空,否则原对象某个引用字段为null,克隆时直接NPE。

深拷贝的核对标准是:新对象与原对象之间,不能再有任何一条可达路径能改到同一块可变内存。只要这个标准没达到,就不能说自己完成了深拷贝。

2.4 深拷贝还是浅拷贝,取决于你希望共享什么

深拷贝不是在所有场景里都是最优解。有些字段本来就不应该被复制,或者复制了反而浪费。比如一个全局路由表、一个缓存客户端、一个Spring Bean,这类字段通常是单例或者不可变配置,原对象和克隆对象共享同一个引用反而更合理。如果你盲目把每个字段都递归深拷贝一遍,不但性能变差,甚至可能把本该全局共享的连接池复制成多个,引发资源问题。

我自己习惯先把对象里的字段分三类:

  • 不可变或可安全共享的:String、基本类型包装类、枚举、单例Bean,浅拷贝共用没问题。
  • 对象内有嵌套可变对象、且业务上每个副本都需要独立修改的:必须深拷贝。
  • 和外部资源强相关的:比如SocketConnection、锁对象,一般不建议用原型复制,要么保持共享,要么在自定义clone()里重新初始化。

想清楚这一点,比背深拷贝代码更重要。

3. 从浅拷贝到深拷贝:四种复制方案实测对比

3.1 手动重写clone:最费手但最可控

如果要拷贝的对象层级不深、字段数量有限,手动重写clone()是最直白、最可控的方案。代码写起来没有黑魔法,新增了字段也容易记得在clone()里补上,因为它就写在同一个类里。

但层级一深,手动方案就变得很痛苦。比如一个订单对象里嵌套了多个明细,明细里又嵌套商品快照和优惠信息。你需要在每一层对象上实现clone(),还要在父对象里逐字段调用,一旦漏了一个可变对象,线上就埋雷。

我建议在两种情况优先考虑手动重写:

  • 这个对象就是原型模式的核心对象,并且结构相对稳定。
  • 你要做的特殊处理很多,比如克隆后强制重置自增ID、修改状态位、忽略某些上下文字段。手动写可以准确表达这些规则。

3.2 用Java序列化偷懒:省事的前提是类都Serializable

序列化复制是很多人偷懒的首选。思路是先把对象写进字节流,再从字节流里反序列化出一个新对象,因为整个过程和原对象的内存引用链无关,天然就是深拷贝。

一个通用的序列化克隆工具类可以这样写:

import java.io.*; public class SerializationUtils { @SuppressWarnings("unchecked") public static <T> T clone(T source) { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(source); try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis)) { return (T) ois.readObject(); } } catch (IOException | ClassNotFoundException e) { throw new RuntimeException("克隆对象失败", e); } } }

用起来倒是很省心:

Order copy = SerializationUtils.clone(order);

但在真实项目里,这条路有很多门槛。

  • 所有涉及的类都必须实现Serializable,而且它们的字段类型也要可序列化。
  • transient修饰的字段不会被复制,反序列化后这些字段默认走类型的零值,比如null
  • static字段同样不会被复制,因为静态字段根本不属于某个对象。
  • 反序列化不调用构造函数,所以依赖构造函数做初始化的对象,copy回来可能不完整。
  • 每次克隆的序列化和反序列化开销不小,在高频调用场合要慎重。

如果对象内部包含ConnectionThreadLock这类不可序列化的资源,序列化方案直接失败。

3.3 JSON往返拷贝:业务项目里的“土办法”

还有一种在业务项目里非常常见的深拷贝方案:先把对象转成JSON字符串,再从JSON字符串反序列化成新对象。用Jackson写大概长这样:

ObjectMapper mapper = new ObjectMapper(); Order copy = mapper.readValue(mapper.writeValueAsString(order), Order.class);

这个方案最大的优点是:只要对象是普通DTO,几乎不用加什么额外接口,用起来太方便了。有些团队的代码里甚至把“JSON深拷贝”做成了工具方法,需要快速复制对象时直接调用。

但它的缺点同样明显。

  • 如果对象有循环引用,比如A引用了BB又引用了A,直接序列化会栈溢出。
  • 多态类型容易丢。父类字段实际存的是子类对象,如果没有配置@JsonTypeInfo,反序列化时很可能变成父类实例,子类字段白白丢失。
  • 日期类型、泛型嵌套、LocalDateTime这类复杂类型,在反序列化时容易出现格式兼容问题。
  • 序列化过程中会丢失部分对象语义,比如对象的equals方法可能依赖的内存地址特征,经过字符串绕一圈后不可恢复。

所以JSON方案更适合“对外传输的DTO”复制,不适合内部复杂领域对象的深拷贝。

3.4 工具类copyProperties:你以为省事,其实是半拷贝

前端传参给后端时,我见过很多人用Apache Commons或Spring的BeanUtils.copyProperties来做对象属性拷贝。这个工具确实能在两个对象之间“按属性名复制字段”,但它的复制是浅拷贝。对象里的ListMap字段,复制后两个对象依然指向同一个集合引用。

如果原来的对象本身是新建的,浅拷贝也能凑合;但如果用在前文那种原型复制场景,极大概率出事。我见过最经典的事故是:用BeanUtils.copyProperties把一个配置对象复制给多个子对象,结果子对象内部共用了同一个规则List,业务人员在界面改了一个活动规则,其他活动全跟着变了。

工具类本身没有错,错在很多人把它当成“深拷贝API”用。要记住,属性拷贝只是“给一个已经存在的目标对象赋值”,它不负责创建目标对象,也不负责展开嵌套引用。

3.5 四种方案选型对照

方案拷贝深度是否需要额外接口性能主要风险
手动重写clone可控实现Cloneable层级深时容易漏字段
Java序列化深拷贝实现Serializable较低外部资源字段、transient字段丢失
JSON往返深拷贝基本无较低循环引用、多态丢失、类型转换问题
BeanUtils属性拷贝浅拷贝中等内部引用共享,容易改一个动全部

我自己在项目里的原则是:核心领域对象用手动clone(),把复制逻辑显式写在类里;DTO对外传输或跨服务复制,可以用JSON方案;一涉及外部资源、锁、线程上下文,干脆放弃“通用深拷贝”的念头,老老实实按业务语义重建。

4. 你以为它冷门,源码和框架里却到处有它的影子

4.1 从ArrayList到HashMap:容器里的暗送秋波

有些人对原型模式的理解停留在自己写的业务类里,觉得实际项目用得少。其实只要翻开JDK源码,就会发现容器类对这个模式很偏爱。比如ArrayList就实现了Cloneable,并重写了clone()。它的复制逻辑并不是把所有元素对象都new一遍,而是复制一份新的内部数组,数组里的元素引用还是和原集合共享。

这样做的好处是,复制出来的ArrayList结构上完全独立,你在新List里addremove元素,不会影响原List;但如果修改某个元素对象内部的字段,两个List都能看到。Java容器这种“浅外壳、深共享”的复制策略,在大部分场景下是合理的,因为它兼顾了性能和语义。

HashMap也有类似的clone()实现,不过要注意,即使两个Map实例内部数组互不影响,键和值对象本身仍然是共享引用。你曾以为复制了Map就能完全隔离数据,实际上只是隔离了Map的桶结构,并没有隔离里面的业务对象。

4.2 Android源码里经常被拿出来讲的“复制状态”

相关热搜词里频繁出现《Android源码设计模式解析与实战》这本书,确实,Android开发中有很多地方体现了原型思想。最典型的例子是Intent,它实现了Cloneable,并且提供了clone()方法,允许通过复制已有Intent快速生成一个新Intent,然后在副本上补上不同的ExtraFlag。这比每次重新new Intent()后逐个putExtra要方便,也能避免遗漏原本已经组装好的那些公共参数。

除了Intent,Android里的很多“快照”类对象也有类似需求。比如页面状态恢复时,系统会把已保存的Bundle或Parcel数据作为模板,重新构造一个对象。Parcelable机制本质上是把一个对象的状态“写到缓冲区”,然后从缓冲区恢复出一个新对象。这和序列化深拷贝的思想同源,只是Android把它用在跨进程传参上。

读源码时不见得每个类都写着“Prototype”字样,但只要看到“一个对象如何复制自己”的设计,基本就是在运用原型模式或其变体。

4.3 原型注册中心:从模板库中取模版

原型模式在实践中还会演化出一个很有用的配套结构:原型注册中心。简单说,你用Map维护一堆原型对象,给每个原型一个唯一key,调用方只需要传入key,就能从注册中心拿到对应原型的副本。

public class PrototypeRegistry { private static final Map<String, Prototype> PROTOTYPES = new ConcurrentHashMap<>(); public static void register(String key, Prototype prototype) { PROTOTYPES.put(key, prototype); } public static Prototype create(String key) { Prototype prototype = PROTOTYPES.get(key); if (prototype == null) { throw new IllegalArgumentException("未找到原型: " + key); } return prototype.copy(); } }

在配置中心或者规则引擎里,这种模式特别实用。把每种默认模板预先创建好注册进去,业务方不用关心模板是怎么构造出来的,只需要说“我要A类型模板的副本”,语义非常清晰。它本质上是工厂模式和原型模式的结合,但底层的“深拷贝能力”仍然来自原型对象自己。

5. 原型模式别硬套:四个边界条件用错就翻车

5.1 final字段会让clone很尴尬

Java里如果某个引用字段被声明为final,在clone()方法里想把它重新指向一个新的深拷贝对象,是做不到的,因为final字段一旦赋值就不能再变。比如:

public class User implements Cloneable { private final Address address; // final字段 @Override public User clone() { try { User copy = (User) super.clone(); // 编译报错,无法给final字段重新赋值 // copy.address = address.clone(); return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } }

这种情况下,copy.address和原对象的address仍然共享同一个可变引用,深拷贝策略直接失效。解决方案有三种:第一,把需要深拷贝的字段改成非final;第二,不依赖Object.clone(),改用专门的拷贝构造函数或工厂,在新对象构造时传入副本;第三,从设计上让这个字段指向不可变对象,共享引用本身也没问题。

所以设计原型类时,不要把所有字段都习惯性加final。不可变性虽好,但和Java原生的克隆机制之间有一道天然的裂缝。

5.2 单例类不该被clone出分身

单例模式和原型模式在目标上是冲突的:一个强调全局只有一份,一个强调复制出多份。如果一个单例类不小心实现了Cloneable,并且没有重写clone(),调用者完全可以通过克隆拿到第二个、第三个实例,单例约束直接崩溃。

如果要让一个单例类同时支持克隆语义,要么在clone()里直接返回this,让所有克隆操作都拿到同一个实例;要么明确抛出CloneNotSupportedException,从接口层面阻断克隆。

很多框架里的配置中心对象本身是单例,但需要给业务返回副本,这种场景通常不会在单例类本身上实现clone(),而是由注册中心持有原型,对外暴露“按key生成副本”的方法。这样既保证了注册中心单例,又让业务方拿到独立副本。

5.3 并发环境下,clone快照也不是万无一失

原型对象的复制过程本身,是把一个“当前状态”固化下来。如果原型对象在A线程执行clone()的同时,被B线程修改了某些字段,那么A线程拿到的可能是修改前和修改后的混合状态。尤其浅拷贝时,共享引用字段被改,所有副本都会收到影响。

面对这种并发问题,我常用的手段是:

  • 尽量把需要长期复用的原型设计成不可变对象,要变更时直接替换整个原型,而不是原地修改。
  • 如果一定要原地修改,加锁或使用CopyOnWrite思路快照化。
  • 复制完成后,新副本如果要被多个线程同时修改,注意不要让各线程直接改共享的嵌套引用字段。

在异步任务、线程池场景里,很多人以为“反正复制出来了,后续随便改”,结果一调试发现打印出来的字段值飘忽不定。别怀疑,基本是原型对象本身还被别的线程共用。

5.4 这几种情况我劝你别用原型模式

原型模式好用,但不是万能药。遇到下面几种情况,我更倾向不用:

  • 对象创建成本极低,就两三个字段,用new比克隆更直观,没必要引入Cloneable和维护成本。
  • 对象里带有明确的业务标识字段,比如数据库自增ID、全局唯一编号。如果直接复制,两个对象会带着同一个ID,必须手工重置,一旦忘记,可能出现“同一份配置更新了两个活动”的事故。
  • 对象和外部上下文强关联,比如持有某个用户的Session、请求TraceId、当前租户信息。直接复制容易把上下文也带过去,这种业务语义用工厂或建造者重建更干净。

记住一个判断标准:如果你复制完对象后,还要额外写一堆“修正”代码来重置字段、修复关联关系,那说明复制思路不一定适合当前场景,至少不该无脑套clone()

6. 一个完整业务场景:从模板克隆出营销活动

6.1 需求背景和类设计

假设这样一个场景:运营系统里有“活动模板”,模板里配置好了默认的营销规则、屏蔽用户名单、扩展参数。每来一个新的活动,运营不想从空页面开始填,而是选择一套模板,一键生成活动草稿,再微调几个参数保存。

这里的核心难点不是复制几个字符串,而是模板内部有多个List和Map,这些嵌套结构必须是独立的。活动A改了规则,绝不能影响模板和其他从同一模板生成的活动。

两个核心类可以设计为:

public class Rule implements Cloneable { private String type; private int threshold; @Override public Rule clone() { try { return (Rule) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } } public class ActivityTemplate implements Cloneable { private Long id; private String name; private List<Rule> rules; private Set<String> blockedUserIds; private Map<String, ConfigItem> extraConfig; @Override public ActivityTemplate clone() { try { ActivityTemplate copy = (ActivityTemplate) super.clone(); copy.id = null; // 新生成的草稿要有新ID copy.rules = rules != null ? rules.stream().map(Rule::clone).collect(Collectors.toList()) : null; copy.blockedUserIds = blockedUserIds != null ? new HashSet<>(blockedUserIds) : null; copy.extraConfig = new HashMap<>(); if (extraConfig != null) { for (Map.Entry<String, ConfigItem> entry : extraConfig.entrySet()) { copy.extraConfig.put(entry.getKey(), entry.getValue().clone()); } } return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } }

id字段我选择直接置为null,因为新活动复制的是模板的“内容”,而不是模板的主键。rules里的每个Rule都是可变对象,所以逐个调用clone()blockedUserIds里是String,不可变,新建一个HashSet即可。extraConfig里的ConfigItem如果内部还有可变字段,它本身也需要实现深拷贝。

6.2 实现细节绕不开的两个问题

第一,集合字段的类型要尽量使用接口类型。比如字段声明为List<Rule>,但实际存储是ArrayList。在深拷贝时,用流的collect(Collectors.toList())会得到一个ArrayList,但如果原集合是LinkedList,复制结果和原集合类型不一致会带来后续隐患。更严谨的写法是判断原集合类型,或者保证原对象创建时统一使用同一实现类。

第二,调用关系要清晰。模板生成草稿,草稿生成正式活动,不要只有一套clone()方法到处复用。处理规则可能不同,模板的clone()可以保留id=null的语义,但正式活动再次复制时,可能需要保留活动ID,也可能需要清空审批时间等状态。把不同复制需求拆成不同方法,比在同一个clone()里加一堆if要安全得多。

6.3 我在这个场景里真实踩过的坑

第一次实现这个功能时,图省事,模板的clone()只写了super.clone(),以为模板内部那些List不会有人直接改。结果运营同学在活动A的规则列表里删了一条规则,模板和其他活动里的规则也被删了。排查时最迷惑的是:数据库里模板数据明明没变,但页面展示的数据像“无规则”一样。后来定位到是对象在内存里共享了同一个ArrayList引用,所有从模板复制出来的活动,在Spring容器的Bean作用域内都指向同一个集合。

改成深拷贝之后又踩了第二个坑:忘记重置id字段。新的活动草稿继承了模板id,落库时主键冲突,但因为代码里id是手动set进去的,报错信息非常具有迷惑性,一开始还以为是缓存问题。

这两个问题都让我更确信,原型模式不是“override一个clone方法”那么简单。它需要你认真梳理对象里每个字段的复制语义:哪些要深拷,哪些要重置,哪些要共享。这个梳理过程,才是原型模式真正的价值所在。

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

3 步自查搞定 res-downloader 下载失败:文件完整性校验排查指南

3 步自查搞定 res-downloader 下载失败&#xff1a;文件完整性校验排查指南 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader r…

作者头像 李华
网站建设 2026/9/8 15:51:59

3步跑通全网视频资源嗅探|res-downloader 从安装到存片的完整实操

3步跑通全网视频资源嗅探&#xff5c;res-downloader 从安装到存片的完整实操 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader …

作者头像 李华
网站建设 2026/9/8 15:50:29

测试工程师职业岔路口:管理岗还是技术专家?

测试工程师这个岗位&#xff0c;做了几年之后几乎都会被同一个问题堵在胸口&#xff1a;到底往哪儿走&#xff1f;往管理走&#xff0c;怕丢了手艺、卷不进办公室政治&#xff1b;往技术走&#xff0c;又怕自己钻了牛角尖&#xff0c;到头来位置尴尬。我有段时间满脑子都在想这…

作者头像 李华