com.netflix.discovery.converters.EurekaJacksonCodec Maven / Gradle / Ivy
package com.netflix.discovery.converters;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.netflix.appinfo.AmazonInfo;
import com.netflix.appinfo.DataCenterInfo;
import com.netflix.appinfo.DataCenterInfo.Name;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.ActionType;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.appinfo.InstanceInfo.PortType;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
import com.netflix.discovery.util.DeserializerStringCache;
import com.netflix.discovery.util.DeserializerStringCache.CacheScope;
import vlsi.utils.CompactHashMap;
/**
* @author Tomasz Bak
* @author Spencer Gibb
*/
public class EurekaJacksonCodec {
private static final Logger logger = LoggerFactory.getLogger(EurekaJacksonCodec.class);
private static final Version VERSION = new Version(1, 1, 0, null);
public static final String NODE_LEASE = "leaseInfo";
public static final String NODE_METADATA = "metadata";
public static final String NODE_DATACENTER = "dataCenterInfo";
public static final String NODE_APP = "application";
protected static final String ELEM_INSTANCE = "instance";
protected static final String ELEM_OVERRIDDEN_STATUS = "overriddenStatus";
protected static final String ELEM_HOST = "hostName";
protected static final String ELEM_INSTANCE_ID = "instanceId";
protected static final String ELEM_APP = "app";
protected static final String ELEM_IP = "ipAddr";
protected static final String ELEM_SID = "sid";
protected static final String ELEM_STATUS = "status";
protected static final String ELEM_PORT = "port";
protected static final String ELEM_SECURE_PORT = "securePort";
protected static final String ELEM_COUNTRY_ID = "countryId";
protected static final String ELEM_IDENTIFYING_ATTR = "identifyingAttribute";
protected static final String ELEM_HEALTHCHECKURL = "healthCheckUrl";
protected static final String ELEM_SECHEALTHCHECKURL = "secureHealthCheckUrl";
protected static final String ELEM_APPGROUPNAME = "appGroupName";
protected static final String ELEM_HOMEPAGEURL = "homePageUrl";
protected static final String ELEM_STATUSPAGEURL = "statusPageUrl";
protected static final String ELEM_VIPADDRESS = "vipAddress";
protected static final String ELEM_SECVIPADDRESS = "secureVipAddress";
protected static final String ELEM_ISCOORDINATINGDISCSOERVER = "isCoordinatingDiscoveryServer";
protected static final String ELEM_LASTUPDATEDTS = "lastUpdatedTimestamp";
protected static final String ELEM_LASTDIRTYTS = "lastDirtyTimestamp";
protected static final String ELEM_ACTIONTYPE = "actionType";
protected static final String ELEM_ASGNAME = "asgName";
protected static final String ELEM_NAME = "name";
protected static final String DATACENTER_METADATA = "metadata";
protected static final String VERSIONS_DELTA_TEMPLATE = "versions_delta";
protected static final String APPS_HASHCODE_TEMPTE = "apps_hashcode";
public static EurekaJacksonCodec INSTANCE = new EurekaJacksonCodec();
/**
* XStream codec supports character replacement in field names to generate XML friendly
* names. This feature is also configurable, and replacement strings can be provided by a user.
* To obey these rules, version and appsHash key field names must be formatted according to the provided
* configuration, which by default replaces '_' with '__' (double underscores).
*/
private final String versionDeltaKey;
private final String appHashCodeKey;
private final ObjectMapper mapper;
private final Map, Supplier> objectReaderByClass;
private final Map, ObjectWriter> objectWriterByClass;
static EurekaClientConfig loadConfig() {
return com.netflix.discovery.DiscoveryManager.getInstance().getEurekaClientConfig();
}
public EurekaJacksonCodec() {
this(formatKey(loadConfig(), VERSIONS_DELTA_TEMPLATE), formatKey(loadConfig(), APPS_HASHCODE_TEMPTE));
}
public EurekaJacksonCodec(String versionDeltaKey, String appsHashCodeKey) {
this.versionDeltaKey = versionDeltaKey;
this.appHashCodeKey = appsHashCodeKey;
this.mapper = new ObjectMapper();
this.mapper.setSerializationInclusion(Include.NON_NULL);
SimpleModule module = new SimpleModule("eureka1.x", VERSION);
module.addSerializer(DataCenterInfo.class, new DataCenterInfoSerializer());
module.addSerializer(InstanceInfo.class, new InstanceInfoSerializer());
module.addSerializer(Application.class, new ApplicationSerializer());
module.addSerializer(Applications.class, new ApplicationsSerializer(this.versionDeltaKey, this.appHashCodeKey));
module.addDeserializer(LeaseInfo.class, new LeaseInfoDeserializer());
module.addDeserializer(InstanceInfo.class, new InstanceInfoDeserializer(this.mapper));
module.addDeserializer(Application.class, new ApplicationDeserializer(this.mapper));
module.addDeserializer(Applications.class, new ApplicationsDeserializer(this.mapper, this.versionDeltaKey, this.appHashCodeKey));
this.mapper.registerModule(module);
Map, Supplier> readers = new HashMap<>();
readers.put(InstanceInfo.class, ()->mapper.reader().forType(InstanceInfo.class).withRootName("instance"));
readers.put(Application.class, ()->mapper.reader().forType(Application.class).withRootName("application"));
readers.put(Applications.class, ()->mapper.reader().forType(Applications.class).withRootName("applications"));
this.objectReaderByClass = readers;
Map, ObjectWriter> writers = new HashMap<>();
writers.put(InstanceInfo.class, mapper.writer().forType(InstanceInfo.class).withRootName("instance"));
writers.put(Application.class, mapper.writer().forType(Application.class).withRootName("application"));
writers.put(Applications.class, mapper.writer().forType(Applications.class).withRootName("applications"));
this.objectWriterByClass = writers;
}
protected ObjectMapper getMapper() {
return mapper;
}
protected String getVersionDeltaKey() {
return versionDeltaKey;
}
protected String getAppHashCodeKey() {
return appHashCodeKey;
}
protected static String formatKey(EurekaClientConfig clientConfig, String keyTemplate) {
String replacement;
if (clientConfig == null) {
replacement = "__";
} else {
replacement = clientConfig.getEscapeCharReplacement();
}
StringBuilder sb = new StringBuilder(keyTemplate.length() + 1);
for (char c : keyTemplate.toCharArray()) {
if (c == '_') {
sb.append(replacement);
} else {
sb.append(c);
}
}
return sb.toString();
}
public T readValue(Class type, InputStream entityStream) throws IOException {
ObjectReader reader = DeserializerStringCache.init(
Optional.ofNullable(objectReaderByClass.get(type)).map(Supplier::get).orElseGet(()->mapper.readerFor(type))
);
try {
return reader.readValue(entityStream);
}
finally {
DeserializerStringCache.clear(reader, CacheScope.GLOBAL_SCOPE);
}
}
public T readValue(Class type, String text) throws IOException {
ObjectReader reader = DeserializerStringCache.init(
Optional.ofNullable(objectReaderByClass.get(type)).map(Supplier::get).orElseGet(()->mapper.readerFor(type))
);
try {
return reader.readValue(text);
}
finally {
DeserializerStringCache.clear(reader, CacheScope.GLOBAL_SCOPE);
}
}
public void writeTo(T object, OutputStream entityStream) throws IOException {
ObjectWriter writer = objectWriterByClass.get(object.getClass());
if (writer == null) {
mapper.writeValue(entityStream, object);
} else {
writer.writeValue(entityStream, object);
}
}
public String writeToString(T object) {
try {
ObjectWriter writer = objectWriterByClass.get(object.getClass());
if (writer == null) {
return mapper.writeValueAsString(object);
}
return writer.writeValueAsString(object);
} catch (IOException e) {
throw new IllegalArgumentException("Cannot encode provided object", e);
}
}
public static EurekaJacksonCodec getInstance() {
return INSTANCE;
}
public static void setInstance(EurekaJacksonCodec instance) {
INSTANCE = instance;
}
public static class DataCenterInfoSerializer extends JsonSerializer {
@Override
public void serializeWithType(DataCenterInfo dataCenterInfo, JsonGenerator jgen,
SerializerProvider provider, TypeSerializer typeSer)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
// XStream encoded adds this for backwards compatibility issue. Not sure if needed anymore
if (dataCenterInfo.getName() == Name.Amazon) {
jgen.writeStringField("@class", "com.netflix.appinfo.AmazonInfo");
} else {
jgen.writeStringField("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo");
}
jgen.writeStringField(ELEM_NAME, dataCenterInfo.getName().name());
if (dataCenterInfo.getName() == Name.Amazon) {
AmazonInfo aInfo = (AmazonInfo) dataCenterInfo;
jgen.writeObjectField(DATACENTER_METADATA, aInfo.getMetadata());
}
jgen.writeEndObject();
}
@Override
public void serialize(DataCenterInfo dataCenterInfo, JsonGenerator jgen, SerializerProvider provider) throws IOException {
serializeWithType(dataCenterInfo, jgen, provider, null);
}
}
public static class LeaseInfoDeserializer extends JsonDeserializer {
enum LeaseInfoField {
DURATION("durationInSecs"),
EVICTION_TIMESTAMP("evictionTimestamp"),
LAST_RENEW_TIMESTAMP("lastRenewalTimestamp"),
REG_TIMESTAMP("registrationTimestamp"),
RENEW_INTERVAL("renewalIntervalInSecs"),
SERVICE_UP_TIMESTAMP("serviceUpTimestamp")
;
private final char[] fieldName;
private LeaseInfoField(String fieldName) {
this.fieldName = fieldName.toCharArray();
}
public char[] getFieldName() {
return fieldName;
}
}
private static EnumLookup fieldLookup = new EnumLookup<>(LeaseInfoField.class, LeaseInfoField::getFieldName);
@Override
public LeaseInfo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
LeaseInfo.Builder builder = LeaseInfo.Builder.newBuilder();
JsonToken jsonToken;
while ((jsonToken = jp.nextToken()) != JsonToken.END_OBJECT) {
LeaseInfoField field = fieldLookup.find(jp);
jsonToken = jp.nextToken();
if (field != null && jsonToken != JsonToken.VALUE_NULL) {
switch(field) {
case DURATION:
builder.setDurationInSecs(jp.getValueAsInt());
break;
case EVICTION_TIMESTAMP:
builder.setEvictionTimestamp(jp.getValueAsLong());
break;
case LAST_RENEW_TIMESTAMP:
builder.setRenewalTimestamp(jp.getValueAsLong());
break;
case REG_TIMESTAMP:
builder.setRegistrationTimestamp(jp.getValueAsLong());
break;
case RENEW_INTERVAL:
builder.setRenewalIntervalInSecs(jp.getValueAsInt());
break;
case SERVICE_UP_TIMESTAMP:
builder.setServiceUpTimestamp(jp.getValueAsLong());
break;
}
}
}
return builder.build();
}
}
public static class InstanceInfoSerializer extends JsonSerializer {
// For backwards compatibility
public static final String METADATA_COMPATIBILITY_KEY = "@class";
public static final String METADATA_COMPATIBILITY_VALUE = "java.util.Collections$EmptyMap";
protected static final Object EMPTY_METADATA = Collections.singletonMap(METADATA_COMPATIBILITY_KEY, METADATA_COMPATIBILITY_VALUE);
@Override
public void serialize(InstanceInfo info, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
if (info.getInstanceId() != null) {
jgen.writeStringField(ELEM_INSTANCE_ID, info.getInstanceId());
}
jgen.writeStringField(ELEM_HOST, info.getHostName());
jgen.writeStringField(ELEM_APP, info.getAppName());
jgen.writeStringField(ELEM_IP, info.getIPAddr());
@SuppressWarnings("deprecation")
String sid = info.getSID();
if (!("unknown".equals(sid) || "na".equals(sid))) {
jgen.writeStringField(ELEM_SID, sid);
}
jgen.writeStringField(ELEM_STATUS, info.getStatus().name());
jgen.writeStringField(ELEM_OVERRIDDEN_STATUS, info.getOverriddenStatus().name());
jgen.writeFieldName(ELEM_PORT);
jgen.writeStartObject();
jgen.writeNumberField("$", info.getPort());
jgen.writeStringField("@enabled", Boolean.toString(info.isPortEnabled(PortType.UNSECURE)));
jgen.writeEndObject();
jgen.writeFieldName(ELEM_SECURE_PORT);
jgen.writeStartObject();
jgen.writeNumberField("$", info.getSecurePort());
jgen.writeStringField("@enabled", Boolean.toString(info.isPortEnabled(PortType.SECURE)));
jgen.writeEndObject();
jgen.writeNumberField(ELEM_COUNTRY_ID, info.getCountryId());
if (info.getDataCenterInfo() != null) {
jgen.writeObjectField(NODE_DATACENTER, info.getDataCenterInfo());
}
if (info.getLeaseInfo() != null) {
jgen.writeObjectField(NODE_LEASE, info.getLeaseInfo());
}
Map metadata = info.getMetadata();
if (metadata != null) {
if (metadata.isEmpty()) {
jgen.writeObjectField(NODE_METADATA, EMPTY_METADATA);
} else {
jgen.writeObjectField(NODE_METADATA, metadata);
}
}
autoMarshalEligible(info, jgen);
jgen.writeEndObject();
}
protected void autoMarshalEligible(Object o, JsonGenerator jgen) {
try {
Class> c = o.getClass();
Field[] fields = c.getDeclaredFields();
Annotation annotation;
for (Field f : fields) {
annotation = f.getAnnotation(Auto.class);
if (annotation != null) {
f.setAccessible(true);
if (f.get(o) != null) {
jgen.writeStringField(f.getName(), String.valueOf(f.get(o)));
}
}
}
} catch (Throwable th) {
logger.error("Error in marshalling the object", th);
}
}
}
public static class InstanceInfoDeserializer extends JsonDeserializer {
private static char[] BUF_AT_CLASS = "@class".toCharArray();
enum InstanceInfoField {
HOSTNAME(ELEM_HOST),
INSTANCE_ID(ELEM_INSTANCE_ID),
APP(ELEM_APP),
IP(ELEM_IP),
SID(ELEM_SID),
ID_ATTR(ELEM_IDENTIFYING_ATTR),// nothing
STATUS(ELEM_STATUS),
OVERRIDDEN_STATUS(ELEM_OVERRIDDEN_STATUS),
PORT(ELEM_PORT),
SECURE_PORT(ELEM_SECURE_PORT),
COUNTRY_ID(ELEM_COUNTRY_ID),
DATACENTER(NODE_DATACENTER),
LEASE(NODE_LEASE),
HEALTHCHECKURL(ELEM_HEALTHCHECKURL),
SECHEALTHCHECKURL(ELEM_SECHEALTHCHECKURL),
APPGROUPNAME(ELEM_APPGROUPNAME),
HOMEPAGEURL(ELEM_HOMEPAGEURL),
STATUSPAGEURL(ELEM_STATUSPAGEURL),
VIPADDRESS(ELEM_VIPADDRESS),
SECVIPADDRESS(ELEM_SECVIPADDRESS),
ISCOORDINATINGDISCSERVER(ELEM_ISCOORDINATINGDISCSOERVER),
LASTUPDATEDTS(ELEM_LASTUPDATEDTS),
LASTDIRTYTS(ELEM_LASTDIRTYTS),
ACTIONTYPE(ELEM_ACTIONTYPE),
ASGNAME(ELEM_ASGNAME),
METADATA(NODE_METADATA)
;
private final char[] elementName;
private InstanceInfoField(String elementName) {
this.elementName = elementName.toCharArray();
}
public char[] getElementName() {
return elementName;
}
public static EnumLookup lookup = new EnumLookup<>(InstanceInfoField.class, InstanceInfoField::getElementName);
}
enum PortField {
PORT("$"), ENABLED("@enabled");
private final char[] fieldName;
private PortField(String name) {
this.fieldName = name.toCharArray();
}
public char[] getFieldName() { return fieldName; }
public static EnumLookup lookup = new EnumLookup<>(PortField.class, PortField::getFieldName);
}
private final ObjectMapper mapper;
private final ConcurrentMap> autoUnmarshalActions = new ConcurrentHashMap<>();
private static EnumLookup statusLookup = new EnumLookup<>(InstanceStatus.class);
private static EnumLookup actionTypeLookup = new EnumLookup<>(ActionType.class);
static Set globalCachedMetadata = new HashSet<>();
static {
globalCachedMetadata.add("route53Type");
globalCachedMetadata.add("enableRoute53");
globalCachedMetadata.add("netflix.stack");
globalCachedMetadata.add("netflix.detail");
globalCachedMetadata.add("NETFLIX_ENVIRONMENT");
globalCachedMetadata.add("transportPort");
}
protected InstanceInfoDeserializer(ObjectMapper mapper) {
this.mapper = mapper;
}
final static Function self = s->s;
@SuppressWarnings("deprecation")
@Override
public InstanceInfo deserialize(JsonParser jp, DeserializationContext context) throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new JsonParseException(jp, "processing aborted");
}
DeserializerStringCache intern = DeserializerStringCache.from(context);
InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(self);
JsonToken jsonToken;
while ((jsonToken = jp.nextToken()) != JsonToken.END_OBJECT) {
InstanceInfoField instanceInfoField = InstanceInfoField.lookup.find(jp);
jsonToken = jp.nextToken();
if (instanceInfoField != null && jsonToken != JsonToken.VALUE_NULL) {
switch(instanceInfoField) {
case HOSTNAME:
builder.setHostName(intern.apply(jp));
break;
case INSTANCE_ID:
builder.setInstanceId(intern.apply(jp));
break;
case APP:
builder.setAppNameForDeser(
intern.apply(jp, CacheScope.APPLICATION_SCOPE,
()->{
try {
return jp.getText().toUpperCase();
} catch (IOException e) {
throw new RuntimeJsonMappingException(e.getMessage());
}
}));
break;
case IP:
builder.setIPAddr(intern.apply(jp));
break;
case SID:
builder.setSID(intern.apply(jp, CacheScope.GLOBAL_SCOPE));
break;
case ID_ATTR:
// nothing
break;
case STATUS:
builder.setStatus(statusLookup.find(jp, InstanceStatus.UNKNOWN));
break;
case OVERRIDDEN_STATUS:
builder.setOverriddenStatus(statusLookup.find(jp, InstanceStatus.UNKNOWN));
break;
case PORT:
while ((jsonToken = jp.nextToken()) != JsonToken.END_OBJECT) {
PortField field = PortField.lookup.find(jp);
switch(field) {
case PORT:
if (jsonToken == JsonToken.FIELD_NAME) jp.nextToken();
builder.setPort(jp.getValueAsInt());
break;
case ENABLED:
if (jsonToken == JsonToken.FIELD_NAME) jp.nextToken();
builder.enablePort(PortType.UNSECURE, jp.getValueAsBoolean());
break;
default:
}
}
break;
case SECURE_PORT:
while ((jsonToken = jp.nextToken()) != JsonToken.END_OBJECT) {
PortField field = PortField.lookup.find(jp);
switch(field) {
case PORT:
if (jsonToken == JsonToken.FIELD_NAME) jp.nextToken();
builder.setSecurePort(jp.getValueAsInt());
break;
case ENABLED:
if (jsonToken == JsonToken.FIELD_NAME) jp.nextToken();
builder.enablePort(PortType.SECURE, jp.getValueAsBoolean());
break;
default:
}
}
break;
case COUNTRY_ID:
builder.setCountryId(jp.getValueAsInt());
break;
case DATACENTER:
builder.setDataCenterInfo(DeserializerStringCache.init(mapper.readerFor(DataCenterInfo.class), context).readValue(jp));
break;
case LEASE:
builder.setLeaseInfo(mapper.readerFor(LeaseInfo.class).readValue(jp));
break;
case HEALTHCHECKURL:
builder.setHealthCheckUrlsForDeser(intern.apply(jp.getText()), null);
break;
case SECHEALTHCHECKURL:
builder.setHealthCheckUrlsForDeser(null, intern.apply(jp.getText()));
break;
case APPGROUPNAME:
builder.setAppGroupNameForDeser(intern.apply(jp, CacheScope.GLOBAL_SCOPE,
()->{
try {
return jp.getText().toUpperCase();
} catch (IOException e) {
throw new RuntimeJsonMappingException(e.getMessage());
}
}));
break;
case HOMEPAGEURL:
builder.setHomePageUrlForDeser(intern.apply(jp.getText()));
break;
case STATUSPAGEURL:
builder.setStatusPageUrlForDeser(intern.apply(jp.getText()));
break;
case VIPADDRESS:
builder.setVIPAddressDeser(intern.apply(jp));
break;
case SECVIPADDRESS:
builder.setSecureVIPAddressDeser(intern.apply(jp));
break;
case ISCOORDINATINGDISCSERVER:
builder.setIsCoordinatingDiscoveryServer(jp.getValueAsBoolean());
break;
case LASTUPDATEDTS:
builder.setLastUpdatedTimestamp(jp.getValueAsLong());
break;
case LASTDIRTYTS:
builder.setLastDirtyTimestamp(jp.getValueAsLong());
break;
case ACTIONTYPE:
builder.setActionType(actionTypeLookup.find(jp.getTextCharacters(), jp.getTextOffset(), jp.getTextLength()));
break;
case ASGNAME:
builder.setASGName(intern.apply(jp));
break;
case METADATA:
Map metadataMap = null;
while ((jsonToken = jp.nextToken()) != JsonToken.END_OBJECT) {
char[] parserChars = jp.getTextCharacters();
if (parserChars[0] == '@' && EnumLookup.equals(BUF_AT_CLASS, parserChars, jp.getTextOffset(), jp.getTextLength())) {
// skip this
jsonToken = jp.nextToken();
}
else { // For backwards compatibility
String key = intern.apply(jp, CacheScope.GLOBAL_SCOPE);
jsonToken = jp.nextToken();
String value = intern.apply(jp, CacheScope.APPLICATION_SCOPE );
metadataMap = Optional.ofNullable(metadataMap).orElseGet(CompactHashMap::new);
metadataMap.put(key, value);
}
};
builder.setMetadata(metadataMap == null ? Collections.emptyMap() : Collections.synchronizedMap(metadataMap));
break;
default:
autoUnmarshalEligible(jp.getCurrentName(), jp.getValueAsString(), builder.getRawInstance());
}
}
else {
autoUnmarshalEligible(jp.getCurrentName(), jp.getValueAsString(), builder.getRawInstance());
}
}
return builder.build();
}
void autoUnmarshalEligible(String fieldName, String value, Object o) {
if (value == null || o == null) return; // early out
Class> c = o.getClass();
String cacheKey = c.getName() + ":" + fieldName;
BiConsumer
© 2015 - 2025 Weber Informatics LLC | Privacy Policy