解决k8s的javaSDK与jackson不兼容问题
k8s的java sdk中json序列化使用了google自家的gosn,但是springboot中默认使用Jackson,我们的业务代码里面也习惯上使用了Jackson。所以直接用k8s sdk会出现各种序列化/反序列化错误,需要做适配。
解决方案
其实主要就是修改默认objectMapper的配置:
@Configuration
@AutoConfigureBefore(MvcConfigPlus.class)
public class SpringMvcConf {
private static void configK8sMapper(ObjectMapper mapper) {
mapper.addMixIn(V1ListMeta.class, V1ListMetaMixIn.class);
SimpleModule simpleModule = new SimpleModule();
IEnumSerializer iEnumSerializer = new IEnumSerializer();
simpleModule.addSerializer(iEnumSerializer);
//截断用户输入中的头尾部空格
simpleModule.addDeserializer(String.class, new StringWithoutSpaceDeserializer());
simpleModule.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer());
//k8s中OffsetDateTime需要适配两种格式
simpleModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer());
simpleModule.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer());
//字节数组与base64之间的转换
simpleModule.addDeserializer(byte[].class, new ByteArrayDeserializer());
simpleModule.addSerializer(byte[].class, new ByteArraySerializer());
//注册k8s object的特殊类
simpleModule.addSerializer(IntOrString.class, new IntOrStringSerializer());
simpleModule.addDeserializer(IntOrString.class, new IntOrStringDeserializer());
simpleModule.addSerializer(Quantity.class, new QuantitySerializer());
simpleModule.addDeserializer(Quantity.class, new QuantityDeserializer());
mapper.registerModule(simpleModule);
}
public static ObjectMapper yamlMapper() {
ObjectMapper objectMapper = JsonUtils.genYamlObjectMapper();
configK8sMapper(objectMapper);
//如果输出yaml,就忽略null字段,减少输出体积
objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
public static ObjectMapper k8sObjectMapper() {
ObjectMapper objectMapper = JsonUtils.genCustomObjectMapper();
configK8sMapper(objectMapper);
return objectMapper;
}
@Bean
@Primary
public ObjectMapper objectMapper() {
return k8sObjectMapper();
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
ObjectMapper mapper = objectMapper();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(mapper);
JacksonTypeHandler.setObjectMapper(mapper);
return converter;
}
}
因为我们这边的基础库里已经自定义了一个objectMapper
的bean,所以这里需要用AutoConfigureBefore
,来保证这边的先初始化(那边的bean上有@ConditionalOnMissingBean
)。
关键字冲突
其中V1ListMetaMixIn
主要解决continue
这个字段是java关键字的问题:
@Data
public class V1ListMetaMixIn {
@JsonIgnore
private String _continue;
@JsonProperty("continue")
private String cursor;
}
k8s的sdk里,使用了_continue
字段来反序列化,这里将其ignore掉,改为cursor
。
特殊类型
yaml中同一个key,value可以是int也可以是字符串,所以k8s有一个IntOrString
类,需要做特殊处理才能正常序列化/反序列化:
public class IntOrStringSerializer extends StdSerializer<IntOrString> {
public IntOrStringSerializer() {
super(IntOrString.class);
}
@Override
public void serialize(IntOrString intOrString, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (intOrString.isInteger()) {
jsonGenerator.writeNumber(intOrString.getIntValue());
} else {
jsonGenerator.writeString(intOrString.getStrValue());
}
}
}
public class IntOrStringDeserializer extends JsonDeserializer<IntOrString> {
@Override
public IntOrString deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonToken currentToken = jsonParser.currentToken();
if (currentToken == JsonToken.VALUE_NUMBER_INT) {
return new IntOrString(jsonParser.getIntValue());
} else if (currentToken == JsonToken.VALUE_STRING) {
return new IntOrString(jsonParser.getText());
} else {
throw new IllegalStateException("Could not deserialize to IntOrString. Was " + currentToken);
}
}
}
同样,还有一个Quantity
是用来处理单位转换的:
public class QuantitySerializer extends StdSerializer<Quantity> {
public QuantitySerializer() {
super(Quantity.class);
}
@Override
public void serialize(Quantity value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.toSuffixedString());
}
}
public class QuantityDeserializer extends JsonDeserializer<Quantity> {
@Override
public Quantity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Quantity.fromString(p.getValueAsString());
}
}
实际上V1Patch
也需要做类似的处理,不过一般打patch都是直接用字符串的,所以也可以不管。
另外还有一个Base64与字节数组之间的转换,这个比较简单,从略。
忽略null字段
k8s api object中字段非常多,输出到yaml时一般需要忽略掉null字段,所以这里对yaml单独做了配置:
public static ObjectMapper yamlMapper() {
ObjectMapper objectMapper = JsonUtils.genYamlObjectMapper();
configK8sMapper(objectMapper);
//如果输出yaml,就忽略null字段,减少输出体积
objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
兼容yaml和json
springboot可以让同一个API既可以返回json也可以返回yaml,只需要在Controller层设置返回为ResponseEntity<String>
即可:
public static ObjectMapper genYamlObjectMapper() {
ObjectMapper mapper = new ObjectMapper(
new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
configObjectMapper(mapper);
return mapper;
}
private static void configObjectMapper(ObjectMapper mapper) {
// 对于空的对象转json的时候不抛出错误
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 禁用遇到未知属性抛出异常
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
mapper.enable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
mapper.registerModule(defaultLocalTimeModule());
mapper.registerModule(defaultSimpleModule());
}
public static final String ACCEPT_YAML = "application/x-yaml";
@SneakyThrows
public ResponseEntity<String> jsonOrYaml(Object data, boolean yaml) {
String body = null;
HttpHeaders headers = new HttpHeaders();
if (yaml) {
body = YAML_MAPPER.writeValueAsString(data);
headers.add(HttpHeaders.CONTENT_TYPE, ACCEPT_YAML);
} else {
body = JSON_MAPPER.writeValueAsString(data);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
return new ResponseEntity<>(body, headers, HttpStatus.OK);
}
通过Content-Type
的header来表明返回的格式。
输入的话兼容起来也很简单:
public class YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public YamlHttpMessageConverter() {
super(JsonUtils.genYamlObjectMapper(), MediaType.parseMediaType("application/x-yaml"));
}
}
然后将其注册到converter里:
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new YamlHttpMessageConverter());
}
}
这样用户的请求header中Content-Type
如果是application/x-yaml
,就会调用Yaml解析器了。当然上面的代码需要一个额外的依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
这样才能让Jackson兼容yaml格式。