k8s的java sdk 中json序列化使用了google自家的gosn,但是springboot中默认使用Jackson,我们的业务代码里面也习惯上使用了Jackson。所以直接用k8s sdk会出现各种序列化/反序列化错误,需要做适配。
解决方案
其实主要就是修改默认objectMapper的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@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关键字的问题:
1
2
3
4
5
6
7
@Data
public class V1ListMetaMixIn {
@JsonIgnore
private String _continue ;
@JsonProperty ( "continue" )
private String cursor ;
}
k8s的sdk里,使用了_continue
字段来反序列化,这里将其ignore掉,改为cursor
。
特殊类型
yaml中同一个key,value可以是int也可以是字符串,所以k8s有一个IntOrString
类,需要做特殊处理才能正常序列化/反序列化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 ());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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
是用来处理单位转换的:
1
2
3
4
5
6
7
8
9
10
11
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 ());
}
}
1
2
3
4
5
6
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单独做了配置:
1
2
3
4
5
6
7
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>
即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 ());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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来表明返回的格式。
输入的话兼容起来也很简单:
1
2
3
4
5
public class YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public YamlHttpMessageConverter () {
super ( JsonUtils . genYamlObjectMapper (), MediaType . parseMediaType ( "application/x-yaml" ));
}
}
然后将其注册到converter里:
1
2
3
4
5
6
7
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters ( List < HttpMessageConverter <?>> converters ) {
converters . add ( new YamlHttpMessageConverter ());
}
}
这样用户的请求header中Content-Type
如果是application/x-yaml
,就会调用Yaml解析器了。当然上面的代码需要一个额外的依赖:
1
2
3
4
<dependency>
<groupId> com.fasterxml.jackson.dataformat</groupId>
<artifactId> jackson-dataformat-yaml</artifactId>
</dependency>
这样才能让Jackson兼容yaml格式。