Quarkus Jackson无反射优化下带JsonView类的序列化行为疑问
问题背景
我在项目中启用了配置quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true,开启Quarkus的Jackson无反射优化功能。现有两个实体类:CustomerWithoutJsonView未使用JsonView注解,CustomerWithJsonView的部分属性标注了@JsonView。二者生成的字节码序列化器存在明显差异——CustomerWithJsonView的序列化器里完全没有属性序列化逻辑,但实际运行时接口输出却和预期一致。想搞明白:
- 为什么生成的序列化器不同,但序列化行为却一致?
- 这类场景下接口的预期表现是什么?
相关代码
无JsonView的实体类
// Without json view enabled package org.acme.model; import java.util.*; public final class CustomerWithoutJsonView { int id; String name; List<Address> addresses; public CustomerWithoutJsonView(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } }
带JsonView的实体类
// With Json view enabled package org.acme.model; import com.fasterxml.jackson.annotation.*; import java.util.*; public final class CustomerWithJsonView { int id; String name; List<Address> addresses; public CustomerWithJsonView(int id, String name, List<Address> addresses) { this.id = id; this.name = name; this.addresses = addresses; } @JsonView({Views.Search.class}) public int getId() { return id; } public String getName() { return name; } @JsonView({Views.Read.class}) public List<Address> getAddresses() { return addresses; } }
Address实体类
package org.acme.model; public final class Address { String streetName; public Address(String streetName) { this.streetName = streetName; } public String getStreetName() { return streetName; } }
接口资源类
// And the following resource class package org.acme; import com.fasterxml.jackson.annotation.*; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.acme.model.*; import java.util.*; @Path("") public class GreetingResource { // All the attributes annotated with search are only included @GET @Path("with-json-view-search") @Produces(MediaType.APPLICATION_JSON) @JsonView({Views.Search.class}) public CustomerWithJsonView withJsonViewSearch() { return new CustomerWithJsonView(1, "John Doe", List.of(new Address("123 Main St"), new Address("456 Elm St"))); } // All the attributes annotated with either search or view are included @GET @Path("with-json-view-read") @Produces(MediaType.APPLICATION_JSON) @JsonView({Views.Read.class}) public CustomerWithJsonView withJsonViewRead() { return new CustomerWithJsonView(1, "John Doe", List.of(new Address("123 Main St"), new Address("456 Elm St"))); } // no json view is applicable so all attributes are provided @GET @Path("without-json-view") @Produces(MediaType.APPLICATION_JSON) public CustomerWithoutJsonView withoutJsonView() { return new CustomerWithoutJsonView(2, "John Doe"); } }
生成的序列化器字节码
package org.acme.model; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil.SerializationInclude; import java.io.IOException; // $VF: synthetic class public class CustomerWithoutJsonView$quarkusjacksonserializer extends StdSerializer { public CustomerWithoutJsonView$quarkusjacksonserializer() { super(CustomerWithoutJsonView.class); } public void serialize(Object var1, JsonGenerator var2, SerializerProvider var3) throws IOException { CustomerWithoutJsonView var4 = (CustomerWithoutJsonView)var1; SerializationInclude var5 = SerializationInclude.decode(var1, var3); var2.writeStartObject(); int var6 = var4.getId(); if (var5.shouldSerialize(var6)) { SerializedString var7 = SerializedStrings$quarkusjacksonserializer.id; var2.writeFieldName((SerializableString)var7); var2.writeNumber(var6); } String var8 = var4.getName(); if (var5.shouldSerialize(var8)) { SerializedString var9 = SerializedStrings$quarkusjacksonserializer.name; var2.writeFieldName((SerializableString)var9); var2.writeString(var8); } var2.writeEndObject(); } }
package org.acme.model; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil.SerializationInclude; import java.io.IOException; // $VF: synthetic class public class CustomerWithJsonView$quarkusjacksonserializer extends StdSerializer { public CustomerWithJsonView$quarkusjacksonserializer() { super(CustomerWithJsonView.class); } public void serialize(Object var1, JsonGenerator var2, SerializerProvider var3) throws IOException { SerializationInclude.decode(var1, var3); var2.writeStartObject(); var2.writeEndObject(); } }
问题解析
1. 序列化器差异的原因
Quarkus的Jackson无反射优化是编译期生成序列化器,但@JsonView属于动态场景:运行时激活的View由接口方法上的注解决定(比如你两个接口分别激活了Search和Read),编译期无法提前预知所有可能的View组合,也就没办法生成适配所有场景的硬编码序列化逻辑。
这种情况下,Quarkus会生成一个空实现的序列化器作为占位符,实际运行时会自动降级到Jackson的动态序列化逻辑(基于提前解析的注解元数据,而非完全的反射)。
2. 运行时行为一致的逻辑
虽然CustomerWithJsonView的编译期序列化器是空的,但Jackson在运行时会检测到这个情况,转而使用自身的BeanSerializer处理:
- 第一步:根据当前接口方法的
@JsonView注解,确定激活的View类型 - 第二步:遍历实体类的所有属性,检查属性的
@JsonView是否与激活View匹配 - 第三步:序列化所有匹配的属性,以及没有标注
@JsonView的属性(这类属性默认在任何View下都序列化)
而CustomerWithoutJsonView没有@JsonView,编译期可以确定所有属性都需要序列化,所以生成了完整的硬编码序列化逻辑,直接在运行时使用,不需要降级。
3. 场景预期表现
完全符合你代码中的注释逻辑:
/with-json-view-search:仅返回id属性(标注了SearchView)/with-json-view-read:返回id(Search View)、addresses(Read View)和name(无View注解,默认序列化)/without-json-view:返回CustomerWithoutJsonView的id和name所有属性
内容的提问来源于stack exchange,提问作者Gautam Lihala

