最近在写一个接口的时候我盯着IDE里那几十行setter调用脑子里突然冒出一个问题前端传过来一段JSON字符串我一行代码就把它变成了业务对象中间到底是谁在帮我完成键名和字段的匹配顺着这个疑问查下去答案落到了两个词上JSON和反射。这篇内容不是“JSON入门教程”也不是“反射API大全”而是我围绕“java中json和反射的学习和认识”这个主题把从“会调用工具方法”到“能讲清楚底层逻辑”这个过程完整梳理了一遍。如果你已经写过几个增删改查接口但还没认真拆解过这两个基础概念的底层关系这篇内容应该能帮你在脑海里把拼图拼完整。1. 从“会用”到“想明白”JSON和反射到底解决的是哪类问题1.1 一个让我重新审视两者的场景假设前端提交了一段JSON文本{userName:张三,age:25}后端有一个User类类里有userName和age两个字段。我用框架提供的序列化工具一行代码就能拿到一个User对象。这个过程太顺畅了顺畅到几乎所有初学者都不会停下来问一句它怎么知道JSON里的userName要对应User类的userName字段答案被拆成两半JSON负责的是把一段普通文本解析成结构化的键值集合。反射负责的是在运行时查看User类里有哪些字段然后把键值对塞进去。这两件事单独拿出来都不复杂但组合在一起就成了几乎所有Java框架的地基。你要是在这两个概念上只停留在“会用”层面后面读Spring MVC的URL参数绑定、看MyBatis的结果集映射、甚至自己写个RPC调用工具都会感觉每个框架都是黑盒。1.2 JSON解决的是“跨语言数据通道”问题JSON能流行起来不是因为它的性能最好而是因为它的约束足够少。没有类型系统没有编译期检查只有对象、数组、字符串、数字、布尔值和null。它只负责一种职责让两个系统之间能高效地传递结构化数据。那问题就来了Java是一个强类型语言JSON里的25传到Java这边到底应该装进int字段还是long字段JSON里的张三是直接放进String字段还是要被解析成某个Date对象这种“类型失配”是JSON和Java之间最核心的矛盾。所以把Java对象转成JSON字符串序列化是相对容易的对象的字段类型已经在编译期确定遍历所有字段拼字符串就行。但把JSON字符串转回Java对象反序列化就难得多你需要知道目标类型需要知道字段对应关系需要处理嵌套对象、数组、泛型还需要先把字符串解析成一个中间结构。这个中间结构在大多数JSON库里就是Map或者JsonNode。1.3 反射解决的是“运行时发现类结构”问题Java是一个静态语言编译器在编译期就知道User类有哪些字段、这些字段是什么类型。但程序一旦跑起来编译器就退场了。这时候如果你从外部拿到一个运行时才知道的键名userName你能在代码里写user.setUserName(...)吗不能因为写代码的时候你根本不知道将来会传入什么键名。反射做的事情就是让程序在运行时重新“看到”类的结构字段清单、方法清单、构造器、注解信息、泛型信息。拿到这些信息之后再动态地创建对象、调用方法、设置字段。这套能力对Java这种静态语言来说是接近“动态语言感觉”的唯一正统途径。2. JSON部分序列化与反序列化的“不对称武器”2.1 为什么对象转JSON只是“按图索骥”对象转JSON时目标是一段文本而源对象的结构是确定的。我们要做的就是把对象里的字段值读出来按语法规则写成字符串。这个阶段的核心步骤是“遍历”遍历对象的字段表读取每个字段值处理null、转义特殊字符、格式化时间。就算字段是private的也能通过反射读取或者调用getter获取。所以你会发现序列化库的核心工作量其实集中在“怎么把任意对象的所有字段都遍历出来”这件事上。普通链表遍历你怎么写这里就怎么写只是每次读值都多了一个反射调用的成本。2.2 为什么JSON转对象才是真正硬核的半场反序列化的难点在于JSON字符串本质上是一串没有类型约定的文本而Java对象有严格的类型约束。把文本变成对象至少要经历这么几个阶段词法分析把{userName:张三,age:25}拆成一个个token。语法分析把这些token组装成键值结构。类型映射判断JSON里的25是int、long还是Double。字段匹配把键名userName对应到目标类的字段。实例创建创建目标类的一个实例把值放进去。递归处理如果JSON里嵌了对象、数组继续对这层做同样的流程。这六个阶段里第3到第6步都离不开反射。尤其是第5步如果目标类没有合适的构造器很多库只能另想办法直接分配内存。这也是为什么反序列化比序列化更容易暴露出各种诡异问题的原因。2.3 配合代码手写一个最小可用的“JSON→对象”绑定逻辑为了把逻辑讲透我写了一个简化版工具。以下代码只演示“已经解析好的Map映射到Java对象”这个核心环节因为JSON字符串的词法解析需要几千行才能做完整那不是本文的重点public class MiniJsonBinder { public static T T bind(MapString, Object jsonMap, ClassT targetClass) throws Exception { // 第一步通过无参构造器创建目标对象 T instance targetClass.getDeclaredConstructor().newInstance(); // 第二步遍历JSON解析出来的所有键 for (Map.EntryString, Object entry : jsonMap.entrySet()) { String jsonKey entry.getKey(); Object jsonValue entry.getValue(); // 第三步通过反射查找目标类中同名字段 Field field findField(targetClass, jsonKey); if (field null) { continue; // JSON里的键在目标类中不存在忽略或记录警告 } // 第四步处理私有字段访问权限 field.setAccessible(true); // 第五步类型适配 Object converted convertValue(jsonValue, field.getType()); // 第六步给字段赋值 field.set(instance, converted); } return instance; } private static Field findField(Class? clazz, String fieldName) { Class? current clazz; while (current ! null current ! Object.class) { try { return current.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { current current.getSuperclass(); } } return null; } private static Object convertValue(Object value, Class? targetType) { if (targetType Integer.class || targetType int.class) { return Integer.valueOf(value.toString()); } if (targetType Long.class || targetType long.class) { return Long.valueOf(value.toString()); } if (targetType Double.class || targetType double.class) { return Double.valueOf(value.toString()); } return value; // String、Boolean、嵌套对象等情况按原样返回 } }这段代码虽然朴素但把反射的核心动作全部包含了获取构造器、查找字段、设置可访问、按类型转换、赋值。很多成熟框架的早期版本内部逻辑跟这个非常接近只不过在性能、类型适配和注解支持上做了大量扩展。2.4 主流JSON库除了反射还额外做了哪些事我建议你学这个主题时别去背各种框架的API而是去对比它们“额外做了哪些活”。以下表格是主流JSON库在基础反射之外处理的一些点能力说明类型适配器用户可以为某个类型自定义解析逻辑替代默认反射流程注解支持通过注解指定JSON键名、忽略字段、自定义格式泛型恢复通过TypeReference读取List 中的User类型多态处理JSON里存了类型标识反序列化时反序列化成具体的子类循环引用处理通过编号或引用标记防止栈溢出日期格式化在String和Date之间做自定义转换数值精度控制处理BigDecimal、高精度数字避免精度丢失这些额外能力都不是魔法它们最终还是要落到“用反射去读取类结构、用类型系统做匹配”这条主线上。3. 反射部分把“运行时查看类结构”这件事彻底搞懂3.1 Class对象是所有反射操作的入口无论你要查看字段、调用方法还是创建对象第一步永远是拿到一个Class对象。获取Class对象有三种方式// 方式一通过类字面量编译期就确定最直接 ClassUser clazz1 User.class; // 方式二通过实例获取适用于你手上已经有对象的场景 User user new User(); Class? clazz2 user.getClass(); // 方式三通过全限定类名获取这是框架中最常用的方式 Class? clazz3 Class.forName(com.example.model.User);第三种方式值得多说一句。Class.forName接收一个字符串意味着类的名字可以完全由配置或外部输入决定不需要写进代码里。很多框架读取配置文件的className之后就用它动态加载类。代价是编译器帮不了你如果类名拼错运行时才会报ClassNotFoundException。3.2 从Class到字段getField与getDeclaredField的区别反射操作字段时很多初学者分不清这两个方法踩过坑之后才知道差别很大getField只能获取public字段包括从父类继承下来的public字段。getDeclaredField能获取本类声明的所有字段无论public还是private但不包括父类声明字段。JSON字段绑定工具大多采用“从当前类往上逐级查找”的getDeclaredField策略。为什么因为JSON库要尽可能覆盖继承体系里的所有业务字段。如果你直接使用getField遇到一个全字段private的类会返回空数组如果你只使用getDeclaredField又拿不到父类字段。setAccessible(true)是另一个常用动作。它做的事情是把Java语言级别对私有成员访问的控制临时打开。注意是“临时打开”而不是“永久删除”。在JDK 9之前只要不涉及安全管理器这个方法几乎不会报错但在JDK 9加入模块系统之后如果目标类所在的模块没有向调用方开放这个方法就会抛出InaccessibleObjectException。这一点在后面第5章还会单独讲。3.3 反射慢的本质以及现代Java的补救方案很多人都说“反射慢”但如果你问到底慢在哪里很少有人能说明白。编译后的普通方法是直接通过方法表调用JIT还能对内联做优化。反射调用多了几件事每次调用都要做权限检查确认是否有访问权。参数需要包装成Object数组。方法查找过程要去Method对象里解析。因为调用点未知JIT很难做内联和逃逸分析。所以反射在大量循环调用场景下的开销是肉眼可见的。JDK 7之后出现了MethodHandle它像一个轻量级的“方法指针”省略了反射调用中不必要的检查性能接近直接调用。JDK 9之后又有了VarHandle用来代替Field.set这类字段操作。我的建议是写业务代码时不用过度在意反射性能写底层框架、基础工具才是你需要考虑MethodHandle的场景。大多数JSON库在启动时已经做过了一次反射预热把关键调用点转换成句柄运行时的开销已经比早期版本低很多。4. JSON和反射在框架层如何“接头”注解、泛型与工厂4.1 注解不是魔法而是给反射看的元数据很多刚接触Spring或Jackson的开发者会觉得JsonProperty(user_name)是一种魔法。其实注解本身没有行为它只是在类文件里留下了一段元数据。你定一个注解编译器把它写进class文件运行时通过反射的getAnnotation方法读出来程序再根据注解内容调整行为。所以理解注解的关键在于注解加上反射才能产生实际作用。反射负责“读”程序逻辑负责“用”。序列化库在遍历字段时会额外检查字段上有没有注解有的话就按注解指定的名称去匹配JSON键。这在我的MiniJsonBinder里只要加几行public class MiniJsonBinder { Retention(RetentionPolicy.RUNTIME) Target(ElementType.FIELD) public interface JsonField { String name() default ; } private static String resolveKey(Field field) { JsonField ann field.getAnnotation(JsonField.class); if (ann ! null !ann.name().isEmpty()) { return ann.name(); } return field.getName(); } }使用的时候在字段上加注解public class User { JsonField(name user_name) private String userName; private int age; }这个例子虽然简单但足以说明“约定优于配置”的底层实现默认情况下字段名就是JSON键名不一致时用注解补充信息。4.2 泛型信息的“偷看”方式TypeReference原理泛型在Java里存在严重的运行时信息丢失问题。你写ListUser编译完之后字节码里只剩ListUser这个泛型参数在大多数场景下不会保留。可是JSON库反序列化的时候恰恰需要一个准确目标类型否则它只能把每个元素反序列化成Map。TypeReference为什么能解决这个问题关键是一个冷门语言特性匿名内部类会保留父类的泛型参数信息。当你写new TypeReferenceListUser() {}时这个匿名类在字节码里明确写了一行Signature指向ListUser。JSON库再通过getGenericSuperclass()读取这个信息拿到完整的泛型类型。这段代码展示了读取逻辑public abstract class TypeReferenceT { private final Type actualType; protected TypeReference() { Type superClass getClass().getGenericSuperclass(); if (superClass instanceof ParameterizedType) { this.actualType ((ParameterizedType) superClass).getActualTypeArguments()[0]; } else { throw new RuntimeException(TypeReference必须通过匿名类使用); } } public Type getType() { return actualType; } }这里的getClass().getGenericSuperclass()本身就是一个反射操作。所以你看所谓的泛型恢复本质上还是靠反射读取类型元数据。4.3 手写一个支持TypeReference的迷你反序列化器把上面的逻辑拼起来我们可以写一个能处理简单泛型字段的迷你工具。假设目标对象里有一个ListUser字段public class MiniJsonParser { public static Object parse(String json, Type type) throws Exception { // 实际中第一步是把json字符串解析成中间结构这里假设已经拿到Map MapString, Object root fakeParseJson(json); if (type instanceof ParameterizedType) { ParameterizedType pt (ParameterizedType) type; Class? rawType (Class?) pt.getRawType(); if (rawType List.class) { Type elementType pt.getActualTypeArguments()[0]; ListObject result new ArrayList(); if (root instanceof Map root.containsKey(list)) { Object rawList root.get(list); if (rawList instanceof List? items) { for (Object item : items) { result.add(parse(item.toString(), elementType)); } } } return result; } } // 如果是普通对象就走反射字段绑定 return bind(root, (Class?) type); } }这种代码在实际项目中没有任何竞争力它缺少转义处理、数组支持、性能优化。但作为学习素材它能帮你把“JSON解析”和“反射绑定”的逻辑彻底分清楚解析后的中间结构是Map再到Java对象则是反射的领域。5. 学习过程中反复踩到的坑和认知修正5.1 无参构造器是很多反序列化库的默认约定我刚开始学的时候写了一个只有有参构造器的类然后怎么都反序列化不了。底层原因很简单反射在创建实例时不知道应该传什么参数给构造器它只能优先找无参构造器。找不到无参构造器很多库会直接放弃或者转用Unsafe机制直接分配内存绕开构造器。所以我的建议是给实体类保留一个无参构造器。用Lombok的NoArgsConstructor可以但你要理解为什么需要它而不是无脑加注解。有一些不可变对象的库如某些使用Builder模式的类会提供更复杂的实例化策略但普通业务实体没必要增加这种复杂度。5.2 JDK模块化和强封装对反射的限制在JDK 9以前setAccessible(true)几乎是万能的。JDK 9引入模块系统之后情况变了每个模块中的包都可以显式声明是否对外开放。如果目标包没有opens给调用方反射代码即使调了setAccessible(true)也会抛出InaccessibleObjectException。这一点在JDK 17之后更明显因为JDK内部默认强封装。想让反射可以访问某个模块的内部类正确做法是在模块描述文件或启动参数里明确打开对应的包。这不是什么漏洞而是平台特意设计的边界。学习反射时认清这一点可以避免在Java 17环境下写框架代码时一脸茫然。版本环境反射访问私有成员的自然状态注意事项Java 8及以前限制宽松setAccessible可用安全管理器偶尔干扰Java 9到16模块系统引入普通classpath代码默认可用跨模块需要opensJava 17JDK内部默认强封装第三方库需要特殊处理5.3 Lombok、getter命名与反射的顺序问题另一个常见误区是以为反射一定要走getter/setter。实际上很多JSON库默认直接操作字段而不是通过方法。如果你在实体类上用Lombok生成getter/setter它生成的方法名和字段名通常是一一对应的正常使用没问题。麻烦出在手写getter时命名不规范。比如一个boolean active字段很多人写getActive()但JavaBean规范要求写成isActive()。如果某些库选择setter作为字段赋值入口找不到setActive就会报错。更隐蔽的是字段上既有Lombok生成的setActive又有手写的setActive(boolean)签名相同会直接编译报错。理解这个问题的关键是反射有两种“路径”可选一种走字段一种走方法。走字段更稳妥但破坏封装走方法更规范但依赖命名。5.4 循环引用、时间格式和枚举反序列化的典型表现这几个问题是我在实际使用JSON库时最常遇到的统一整理成一张表方便排查问题现象根本原因解决思路无限递归导致StackOverflowError对象A包含对象BB又包含A使用引用标识、JsonIgnore忽略某个方向反序列化后得到一个MapJSON库不知道List里的元素类型使用TypeReference传入完整泛型信息LocalDateTime解析报错JSON库没注册时间模块无法确定格式配置统一的时间格式或注册自定义适配器枚举反序列化失败枚举常量名称与JSON的value不匹配使用JsonValue等注解或自定义枚举转换数字精度丢失大数被解析成Double使用JsonNode或BigDecimal统一处理数值这些坑不是反射本身的错但它们大多发生在“JSON值如何转换成Java类型”这个环节和2.2节讲的类型映射高度相关。学到这里你会发现真正难的不是反射API本身而是“类型转换规则”的设计。6. 从“认识”到“熟练”推荐的学习路线6.1 第一层不依赖第三方库手写一个最简JSON解析器这一层练习的是“JSON字符串如何变成Map”。你去实现词法分析读取花括号、引号、冒号、逗号处理字符串转义处理嵌套对象和数组。做一遍之后你就能体会到为什么JSON解析器几千行很常见也会彻底明白第三方库的价值。建议练习规模不用支持全部语法分两步走。先支持一层的扁平对象再支持嵌套对象和数组。关键不是写得多完美而是亲手处理过\这样的转义序列处理过数字和布尔值的区分。这一步做完再回去看Jackson的解析流程你会发现它只是把同样的逻辑更完整地实现了一遍。6.2 第二层用反射实现一个通用对象映射工具参考我这篇文章里的MiniJsonBinder尝试自己扩展它支持父类字段、支持字段类型自动转换、支持注解别名。这层练习的意义是迫使你频繁使用getDeclaredFields、setAccessible、getGenericSuperclass这些反射API。做完这个练习之后你可以试着把一个接口返回的JSON自动绑定到一个拥有二十个字段的类上。如果你发现要写二十次的field.set能封装成一个循环那你就真正理解了“反射批量操作”的价值。6.3 第三层在开源框架源码里找到“反射的现场”不要急着读整个框架源码那样很容易放弃。挑选几个关键调用点去搜搜索getDeclaredFields看某个Web框架怎么批量读取对象字段。搜索getDeclaredConstructors看它怎么选择构造器创建实例。搜索getAnnotation看它怎么读取第三方自定义注解。搜索getGenericSuperclass看它怎么恢复泛型信息。你不需要看完所有源码只需要看懂“这段反射代码要解决什么问题”。把它记录在自己的笔记里遇到类似需求时能想到去参考这比硬背框架源码有效得多。6.4 一些关于学习顺序的认知调整按我自己的经验学习这两个主题有个比较顺的顺序先写一个最简单的JSON解析器哪怕只支持50行代码。再写一个利用反射的对象映射工具MiniBinder。然后把两者组合起来完成一个从字符串到对象的完整链路。最后再回头去读成熟库的源码补充性能优化和边界情况的知识。这个顺序的本质是“先实现再优化”。你如果一上来就看Jackson源码会迷失在几百个类和复杂的状态机里但你带着自己写过的代码去看就会知道每个类的职责大约是什么。学JSON库的API、学各个注解的用法是知识碎片但“反射遍历字段加TypeReference恢复类型”这条主线可以把碎片串起来。我学这个主题最大的收获不是记住了什么API而是学会了任何时候看到一个能“自动绑定参数”的框架第一反应不再是“它太神奇了”而是“它大概是在用反射做字段映射”。这种思维方式比具体代码更值钱。希望这篇关于java中json和反射的学习与认识能帮你把这条主线搭起来。