1. 为什么我要自己封装一个 JsonUtil
做后端开发的朋友大概率都经历过这样的场景:接口返回一个 JSON 字符串,你需要把它转成对象;前端传过来一个 List,你得把它序列化成 JSON 存进缓存;从数据库里读出来的 Map 结构,又要转成特定格式的字符串发给第三方。这些操作本身不复杂,但每次都要写一遍ObjectMapper或者Gson的调用代码,时间长了就会发现——重复代码到处都是,异常处理五花八门,空值判断各写各的。
我所在的项目组维护着十几个微服务模块,早期大家各用各的序列化方式,有人用 Jackson,有人用 Fastjson,还有人直接用字符串拼接。结果就是线上时不时冒出NullPointerException、JsonParseException、日期格式错乱这些问题。排查起来特别头疼,因为每个模块的序列化行为都不一致。
后来我花了两天时间,把项目里所有 JSON 相关的操作梳理了一遍,发现核心需求其实就集中在几个点上:String 与对象互转、String 与 List 互转、String 与 Map 互转、List 与 String 互转、Map 与 String 互转,再加上一些嵌套场景的处理。于是我就动手封装了一个JsonUtil工具类,统一了整个项目组的序列化行为。
这篇文章我会把这个工具类的设计思路、核心实现、踩过的坑、以及实际使用中的经验完整地分享出来。不管你是刚接触 JSON 处理的新手,还是想统一团队序列化规范的老手,应该都能从中找到有用的东西。文章里涉及的所有代码都是可以直接复制使用的,参数选择我也会解释清楚背后的原因。
2. 工具类的整体设计与技术选型
2.1 序列化库的选择:为什么最终选了 Jackson
在 Java 生态里,做 JSON 序列化可选的库不少,主流的有 Jackson、Gson、Fastjson 这几个。我在选型的时候主要考虑了以下几个维度:
| 对比维度 | Jackson | Gson | Fastjson |
|---|---|---|---|
| Spring 生态集成度 | 原生支持,Spring Boot 默认 | 需要额外配置 | 需要额外配置 |
| 性能表现 | 优秀 | 良好 | 优秀 |
| 安全性记录 | 稳定 | 稳定 | 历史漏洞较多 |
| 功能丰富度 | 非常丰富 | 中等 | 丰富 |
| 社区活跃度 | 高 | 中等 | 中等 |
| 学习曲线 | 略陡 | 平缓 | 平缓 |
最终选 Jackson 的原因很直接:Spring Boot 默认就用它,项目里已经有依赖了,不需要额外引入包。而且 Jackson 的ObjectMapper提供了非常灵活的配置能力,可以精细控制序列化和反序列化的行为。虽然它的 API 相比 Gson 稍微复杂一点,但封装成工具类之后,这些复杂度都被隐藏了。
注意:如果你用的是老版本的 Spring Boot(1.x),默认的 JSON 库可能不是 Jackson,需要确认一下项目依赖。另外,如果项目里已经大量使用了 Fastjson,不建议为了封装工具类而强行切换,迁移成本可能比收益更大。
2.2 工具类的设计原则
我在设计这个JsonUtil的时候,给自己定了几个原则:
第一,静态方法为主。工具类不需要实例化,所有方法都是static的,调用方便。ObjectMapper作为静态成员变量,全局共享一个实例。
第二,异常统一处理。所有受检异常都在工具类内部捕获,转换成运行时异常抛出,调用方不需要写一堆 try-catch。但异常信息要保留原始原因,方便排查。
第三,空值安全。传入 null 或者空字符串的时候,不能直接抛异常,要有合理的默认行为。比如toObject(null, Xxx.class)应该返回 null,而不是报错。
第四,泛型支持完善。List、Map 这些集合类型的转换,必须支持泛型,不能因为类型擦除就丢失元素类型信息。这里需要用到TypeReference。
第五,配置可定制。日期格式、空值处理策略、未知字段处理策略这些,要能根据项目需要调整,而不是写死在代码里。
2.3 核心依赖与基础配置
工具类的基础配置直接决定了序列化和反序列化的行为。下面是我实际使用的配置:
private static final ObjectMapper MAPPER = new ObjectMapper(); static { // 序列化时忽略未知字段,避免接口新增字段导致反序列化失败 MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 空对象不抛异常 MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 日期统一使用 yyyy-MM-dd HH:mm:ss 格式 MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 允许序列化没有 getter 的字段(需要配合字段可见性配置) MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); // 处理 Long 类型精度丢失问题(前端 JS 精度只有 53 位) MAPPER.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); }这里有几个配置值得展开说说。
FAIL_ON_UNKNOWN_PROPERTIES设置为 false 是最重要的一个配置。默认情况下,如果 JSON 字符串里有一个字段在目标类里不存在,Jackson 会直接抛异常。这在微服务场景下简直是灾难——上游服务加了个字段,下游服务就崩了。设置为 false 之后,未知字段会被忽略,兼容性好很多。
setVisibility这个配置让 Jackson 可以直接访问私有字段,不需要每个字段都写 getter/setter。这在处理一些第三方库的类或者 DTO 的时候特别有用。但要注意,这样做可能会暴露一些不该序列化的字段,需要配合@JsonIgnore注解使用。
USE_LONG_FOR_INTS这个配置是为了解决前端精度丢失问题。JavaScript 的 Number 类型最大安全整数是 2^53-1,超过这个范围的 Long 值传到前端就会失真。开启这个配置后,Jackson 会把 JSON 中的整数反序列化为 Long 而不是 Integer,减少精度问题的发生。
3. 核心方法的实现与细节拆解
3.1 对象与字符串互转:最基础也最容易出问题
对象转字符串和字符串转对象是使用频率最高的两个方法。看起来简单,但细节很多。
/** * 对象转 JSON 字符串 */ public static String toJson(Object obj) { if (obj == null) { return null; } try { return MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("对象转 JSON 失败: " + e.getMessage(), e); } } /** * JSON 字符串转对象 */ public static <T> T toObject(String json, Class<T> clazz) { if (json == null || json.trim().isEmpty()) { return null; } try { return MAPPER.readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 转对象失败: " + e.getMessage(), e); } }这两个方法看起来平平无奇,但有几个点需要特别注意。
空字符串的处理。我见过很多工具类只判断了 null,没判断空字符串。结果传入""的时候,Jackson 会抛MismatchedInputException。所以这里必须同时判断 null 和 trim 后的空字符串。
异常信息的保留。捕获异常后重新抛出 RuntimeException 的时候,一定要把原始异常 e 作为 cause 传进去。否则线上出问题的时候,堆栈信息会断在工具类这一层,看不到真正的原因。这个坑我踩过,排查了半天才发现是原始异常被吞了。
泛型方法的类型推断。toObject方法用了泛型<T>,调用的时候可以直接User user = JsonUtil.toObject(json, User.class),不需要强制类型转换。这是 Java 泛型的基本用法,但很多人封装的时候会忘记加。
3.2 List 与字符串互转:泛型擦除的坑
List 的转换比对象转换复杂,因为 Java 的泛型擦除机制。你不能直接写List<User>.class,编译都过不了。必须用TypeReference来保留泛型信息。
/** * JSON 字符串转 List */ public static <T> List<T> toList(String json, Class<T> clazz) { if (json == null || json.trim().isEmpty()) { return new ArrayList<>(); } try { CollectionType type = MAPPER.getTypeFactory() .constructCollectionType(List.class, clazz); return MAPPER.readValue(json, type); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 转 List 失败: " + e.getMessage(), e); } } /** * List 转 JSON 字符串 */ public static String toJson(List<?> list) { if (list == null || list.isEmpty()) { return "[]"; } return toJson((Object) list); }这里有个设计决策:List 转字符串的时候,空 List 返回"[]"而不是 null。为什么?因为在前端处理的时候,"[]"可以直接JSON.parse成空数组,而 null 需要额外判断。返回"[]"对调用方更友好。
反序列化的时候,空字符串返回空 List 而不是 null,也是同样的考虑。调用方拿到结果后可以直接遍历,不需要判空。
实操心得:如果你的项目里 List 元素类型经常变化,可以考虑再加一个方法,接收
TypeReference参数,让调用方自己指定类型。这样灵活性更高,但用起来稍微麻烦一点。
3.3 Map 与字符串互转:键类型的选择
Map 的转换和 List 类似,但多了一个键类型的问题。JSON 的键必须是字符串,所以 Map 的键类型通常是 String。但有时候也会遇到Map<Long, String>这种结构,需要特殊处理。
/** * JSON 字符串转 Map */ public static <K, V> Map<K, V> toMap(String json, Class<K> keyClass, Class<V> valueClass) { if (json == null || json.trim().isEmpty()) { return new HashMap<>(); } try { MapType type = MAPPER.getTypeFactory() .constructMapType(Map.class, keyClass, valueClass); return MAPPER.readValue(json, type); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 转 Map 失败: " + e.getMessage(), e); } } /** * 简化版:转成 Map<String, Object> */ public static Map<String, Object> toMap(String json) { return toMap(json, String.class, Object.class); }实际使用中,Map<String, Object>是最常见的场景。比如从缓存里读出来的用户信息,或者第三方接口返回的原始数据。这种情况下,值类型是 Object,Jackson 会自动推断成 String、Integer、Boolean、List、Map 等类型。
但这里有个坑:数字类型会被反序列化成 Integer 或 Long,而不是你期望的 Double。比如 JSON 里的{"price": 19.9},反序列化后map.get("price")得到的是 Double。但如果 JSON 里是{"count": 10},得到的就是 Integer。如果你直接强转成 Double,会抛ClassCastException。
解决办法是取值的时候用Number接收,然后调用doubleValue()或intValue()转换:
Map<String, Object> map = JsonUtil.toMap(json); Number price = (Number) map.get("price"); double priceValue = price.doubleValue();3.4 嵌套结构的处理:TypeReference 的实战用法
实际业务中,经常遇到嵌套的泛型结构,比如List<Map<String, Object>>、Map<String, List<User>>这种。这时候用Class参数就不够了,必须用TypeReference。
/** * 通用转换方法,支持复杂泛型 */ public static <T> T toObject(String json, TypeReference<T> typeReference) { if (json == null || json.trim().isEmpty()) { return null; } try { return MAPPER.readValue(json, typeReference); } catch (JsonProcessingException e) { throw new RuntimeException("JSON 转换失败: " + e.getMessage(), e); } }使用的时候这样调用:
// 转成 List<Map<String, Object>> List<Map<String, Object>> list = JsonUtil.toObject(json, new TypeReference<List<Map<String, Object>>>() {}); // 转成 Map<String, List<User>> Map<String, List<User>> map = JsonUtil.toObject(json, new TypeReference<Map<String, List<User>>>() {});TypeReference的原理是利用匿名内部类在编译期保留泛型信息。new TypeReference<List<Map<String, Object>>>() {}这个匿名类会携带完整的泛型签名,Jackson 通过反射读取这个签名,就能知道要构造什么类型。
注意:
TypeReference必须用匿名内部类的形式,不能写成new TypeReference<List<User>>()然后赋值给变量再传递。因为泛型信息是在编译期确定的,赋值给变量后类型信息就丢失了。
4. 完整实操流程与关键环节实现
4.1 工具类的完整代码结构
把前面讲的内容整合起来,完整的JsonUtil类结构如下:
public final class JsonUtil { private static final ObjectMapper MAPPER = new ObjectMapper(); static { MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); MAPPER.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); } private JsonUtil() { // 私有构造,防止实例化 } // 对象与字符串互转 public static String toJson(Object obj) { ... } public static <T> T toObject(String json, Class<T> clazz) { ... } // List 与字符串互转 public static <T> List<T> toList(String json, Class<T> clazz) { ... } public static String toJson(List<?> list) { ... } // Map 与字符串互转 public static <K, V> Map<K, V> toMap(String json, Class<K> keyClass, Class<V> valueClass) { ... } public static Map<String, Object> toMap(String json) { ... } // 通用转换 public static <T> T toObject(String json, TypeReference<T> typeReference) { ... } // 判断是否为合法 JSON public static boolean isValidJson(String json) { ... } }私有构造函数是一个好习惯,防止有人不小心new JsonUtil()。虽然加了final和私有构造,但工具类的本质还是靠约定,团队规范里要明确写清楚。
4.2 判断合法 JSON 的实现
有时候我们需要判断一个字符串是不是合法的 JSON,比如在做数据校验的时候。实现方式有两种:
/** * 方式一:尝试解析,捕获异常 */ public static boolean isValidJson(String json) { if (json == null || json.trim().isEmpty()) { return false; } try { MAPPER.readTree(json); return true; } catch (JsonProcessingException e) { return false; } }这种方式简单直接,但性能一般,因为要完整解析一遍。如果只是判断格式,可以用更轻量的方式:
/** * 方式二:只检查首尾字符(快速但不严谨) */ public static boolean isJsonLike(String str) { if (str == null) { return false; } String trimmed = str.trim(); return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); }方式二只检查首尾字符,速度快但不严谨。比如"{abc}"会被误判为合法 JSON。实际使用中,如果对准确性要求高,还是用方式一;如果只是做初步筛选,方式二够用了。
4.3 日期格式化的处理
日期格式是 JSON 处理中最容易出问题的部分之一。Jackson 默认会把Date序列化成时间戳(毫秒数),但大多数前端期望的是"2024-01-15 10:30:00"这种格式。
我在工具类里通过setDateFormat统一设置了格式。但这种方式有个局限:它只对java.util.Date生效,对LocalDateTime、LocalDate这些 Java 8 的时间类型无效。
要支持 Java 8 时间类型,需要额外注册模块:
MAPPER.registerModule(new JavaTimeModule()); MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);JavaTimeModule是jackson-datatype-jsr310包提供的,需要在 pom.xml 里引入依赖:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>引入之后,LocalDateTime会被序列化成 ISO 格式"2024-01-15T10:30:00"。如果想去掉中间的T,可以在字段上加@JsonFormat注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;实操心得:如果项目里同时存在
Date和LocalDateTime,建议统一用LocalDateTime,然后在工具类里全局配置格式。Date是线程不安全的,而且 API 设计有很多问题,新项目不建议再用。
4.4 大数字精度问题的处理
前面提到了USE_LONG_FOR_INTS配置,但这只是解决了反序列化的问题。序列化的时候,如果 Long 值超过 JavaScript 的安全整数范围,前端还是会丢精度。
常见的解决方案有两种:
方案一:把 Long 序列化成字符串。在字段上加@JsonSerialize(using = ToStringSerializer.class)注解:
@JsonSerialize(using = ToStringSerializer.class) private Long id;这样序列化出来的 JSON 里,id 是字符串"1234567890123456789",前端拿到后不会丢精度。
方案二:全局配置 Long 转 String。在工具类里注册一个自定义的序列化器:
SimpleModule module = new SimpleModule(); module.addSerializer(Long.class, ToStringSerializer.instance); module.addSerializer(Long.TYPE, ToStringSerializer.instance); MAPPER.registerModule(module);方案二更彻底,但会影响所有 Long 字段。如果项目里有些 Long 字段确实需要保持数字类型(比如参与计算的金额),就不能用全局配置。
我个人的做法是:ID 类字段用方案一单独加注解,其他 Long 字段保持默认。这样既解决了精度问题,又不会影响其他业务逻辑。
5. 常见问题与排查技巧实录
5.1 反序列化失败的常见原因
在实际使用中,反序列化失败是最常见的问题。我整理了一个排查表:
| 异常信息 | 常见原因 | 解决方法 |
|---|---|---|
UnrecognizedPropertyException | JSON 中有目标类不存在的字段 | 配置FAIL_ON_UNKNOWN_PROPERTIES=false |
MismatchedInputException | JSON 结构与目标类型不匹配 | 检查 JSON 格式和类定义是否一致 |
InvalidFormatException | 字段类型转换失败 | 检查日期格式、数字格式是否正确 |
NullPointerException | 传入 null 或空字符串 | 工具类内部判空处理 |
StackOverflowError | 对象之间存在循环引用 | 使用@JsonIgnore或@JsonBackReference |
UnrecognizedPropertyException是最常见的。比如上游服务返回的 JSON 里多了一个"extInfo"字段,但你的 DTO 里没有定义,默认就会抛这个异常。配置FAIL_ON_UNKNOWN_PROPERTIES=false之后,未知字段会被静默忽略。
但这里有个隐患:如果字段名拼写错误,也会被静默忽略。比如 DTO 里定义的是userName,JSON 里是username,反序列化后userName会是 null,但不会报错。这种问题排查起来很麻烦,因为没有任何异常信息。
我的建议是:在开发环境开启FAIL_ON_UNKNOWN_PROPERTIES=true,生产环境关闭。这样开发阶段能及时发现字段不匹配的问题,生产环境又能保证兼容性。
5.2 循环引用导致的栈溢出
循环引用是另一个容易踩的坑。比如User类里有一个List<Order>,Order类里又有一个User字段,序列化的时候就会无限递归,最终StackOverflowError。
解决办法有三种:
第一种,用@JsonIgnore忽略其中一个字段。比如在Order的user字段上加@JsonIgnore,序列化Order的时候就不会包含User信息。
第二种,用@JsonBackReference和@JsonManagedReference。这是一对注解,@JsonManagedReference标注的字段会被正常序列化,@JsonBackReference标注的字段会被忽略。
第三种,用@JsonIdentityInfo。这个注解会给每个对象生成一个 ID,第二次出现的时候只输出 ID,不再展开。适合处理复杂的对象图。
我一般用第一种,简单直接。如果确实需要双向引用,用第二种。
5.3 性能优化的几个技巧
ObjectMapper的创建成本比较高,所以一定要复用。我把它作为静态成员变量,全局共享一个实例。ObjectMapper本身是线程安全的,可以放心在多线程环境下使用。
但如果你的项目对性能要求极高,可以考虑用ObjectReader和ObjectWriter。这两个类是不可变的,创建成本低,而且可以预配置一些参数:
private static final ObjectReader USER_READER = MAPPER.readerFor(User.class); public static User parseUser(String json) { try { return USER_READER.readValue(json); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }ObjectReader可以复用解析配置,避免每次解析都重新初始化。在高频调用的场景下,性能提升比较明显。
另外,如果只是读取 JSON 中的某个字段,不需要完整反序列化,可以用readTree方法:
JsonNode node = MAPPER.readTree(json); String name = node.get("name").asText();这种方式比完整反序列化快很多,适合只需要部分字段的场景。
5.4 空值处理的策略选择
Jackson 默认会把 null 值也序列化到 JSON 里,比如{"name": null, "age": 25}。但有时候我们希望 null 字段直接不输出,减少传输体积。
配置方式:
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);这样配置之后,null 字段会被忽略,输出变成{"age": 25}。
但要注意,反序列化的时候,如果 JSON 里缺少某个字段,对应的属性会是 null。如果这个字段是基本类型(int、boolean 等),会使用默认值(0、false)。如果业务逻辑依赖这些字段,需要额外处理。
JsonInclude.Include还有几个可选值:
ALWAYS:默认值,总是包含NON_NULL:忽略 null 值NON_EMPTY:忽略 null、空字符串、空集合NON_DEFAULT:忽略默认值(比如 int 的 0)
选择哪个取决于业务需求。我一般用NON_NULL,因为NON_EMPTY会把空字符串也忽略掉,有时候空字符串是有意义的。
6. 工具类的扩展与团队落地经验
6.1 如何推动团队统一使用
封装好工具类只是第一步,让团队所有人都用起来才是关键。我在推动落地的过程中总结了几个经验:
第一,先在小范围试点。不要一上来就要求所有人改,先在自己负责的模块用起来,跑一段时间没问题了,再逐步推广。
第二,提供迁移指南。把常见的替换场景整理成文档,比如new ObjectMapper().writeValueAsString(obj)替换成JsonUtil.toJson(obj),让同事能快速上手。
第三,加代码检查规则。在 CI 流程里加一条检查,禁止直接使用ObjectMapper和JSON.toJSONString,必须通过JsonUtil。这样能从机制上保证统一。
第四,保留扩展点。工具类不可能覆盖所有场景,要留出扩展的余地。比如提供getMapper()方法返回底层的ObjectMapper,特殊场景可以自己处理。
6.2 单元测试的编写要点
工具类的单元测试要覆盖各种边界情况。我列一下我写的测试用例:
@Test public void testToJsonWithNull() { assertNull(JsonUtil.toJson(null)); } @Test public void testToObjectWithEmptyString() { assertNull(JsonUtil.toObject("", User.class)); } @Test public void testToListWithEmptyJson() { List<User> list = JsonUtil.toList("[]", User.class); assertNotNull(list); assertTrue(list.isEmpty()); } @Test public void testToMapWithNestedStructure() { String json = "{\"user\":{\"name\":\"test\"},\"count\":10}"; Map<String, Object> map = JsonUtil.toMap(json); assertEquals(10, map.get("count")); }边界情况主要包括:null 输入、空字符串、空集合、嵌套结构、特殊字符、超长字符串。这些场景在实际使用中都可能遇到,测试覆盖到了,上线才放心。
6.3 后续可以扩展的方向
这个工具类目前覆盖了最常用的场景,但还有一些可以扩展的方向:
JSON 与 XML 互转。有些老系统还在用 XML 接口,可以基于 Jackson 的 XML 模块扩展。
JSON Patch 支持。如果需要做部分更新,可以引入json-patch库,实现 JSON 的差异比较和合并。
敏感字段脱敏。在序列化的时候自动对手机号、身份证号等敏感字段做脱敏处理,可以通过自定义序列化器实现。
多版本兼容。如果接口有多个版本,不同版本的 JSON 结构不一样,可以在工具类里加版本参数,根据版本选择不同的序列化策略。
这些扩展不一定都要做,根据项目实际需求来。工具类的原则是够用就好,不要过度设计。我见过有些工具类封装了几十个方法,结果常用的就那几个,维护成本反而很高。
6.4 一个真实的使用场景复盘
最后分享一个我在实际项目中遇到的场景。当时有一个订单导出功能,需要把订单数据转成 JSON 发给第三方系统。订单对象结构比较复杂,包含用户信息、商品列表、支付记录等多个嵌套对象。
最初直接用JsonUtil.toJson(order)序列化,结果发现两个问题:一是日期格式不对,第三方要求yyyyMMdd格式;二是金额字段需要从元转成分,乘以 100。
我的解决方案是创建一个专门的 DTO 类,用@JsonFormat注解控制日期格式,用自定义的getter方法处理金额转换:
public class OrderExportDTO { @JsonFormat(pattern = "yyyyMMdd") private Date orderDate; private Long amountInCents; public Long getAmountInCents() { return this.amount != null ? this.amount * 100 : null; } }然后在导出的时候,把Order对象转换成OrderExportDTO,再序列化。这样既保证了格式正确,又不会影响其他场景下的序列化行为。
这个经验告诉我:工具类解决的是通用问题,特殊场景还是需要专门的 DTO 来处理。不要试图用一个工具类解决所有问题,那样只会让工具类越来越臃肿。
另外一个体会是,序列化配置一定要在项目初期就统一。等到项目中期再改,涉及的面太广,很容易出问题。如果实在要改,建议先加新方法,逐步迁移,最后再删除旧方法,给团队一个过渡期。