场景
将terms
聚合的结果直接返给前端处理,如果bucket的key为字符串时,在mvc层jackson进行json序列化处理会报类型转换错误。
原因分析
terms
聚合的结果中有keyAsNumber字段,是将桶key转为number类型,但是当key为字符串类型的数据时,是无法转为numebr类型的。
jackson在序列化的时候,当terms聚合的key为字符串类型时,则会调用ParsedStringTerms
类来转换处理字段,源码如下:
public class ParsedStringTerms extends ParsedTerms {
@Override
public String getType() {
return StringTerms.NAME;
}
private static ObjectParser<ParsedStringTerms, Void> PARSER =
new ObjectParser<>(ParsedStringTerms.class.getSimpleName(), true, ParsedStringTerms::new);
static {
declareParsedTermsFields(PARSER, ParsedBucket::fromXContent);
}
public static ParsedStringTerms fromXContent(XContentParser parser, String name) throws IOException {
ParsedStringTerms aggregation = PARSER.parse(parser, null);
aggregation.setName(name);
return aggregation;
}
public static class ParsedBucket extends ParsedTerms.ParsedBucket {
private BytesRef key;
@Override
public Object getKey() {
return getKeyAsString();
}
@Override
public String getKeyAsString() {
String keyAsString = super.getKeyAsString();
if (keyAsString != null) {
return keyAsString;
}
if (key != null) {
return key.utf8ToString();
}
return null;
}
public Number getKeyAsNumber() {
if (key != null) {
return Double.parseDouble(key.utf8ToString());
}
return null;
}
@Override
protected XContentBuilder keyToXContent(XContentBuilder builder) throws IOException {
return builder.field(CommonFields.KEY.getPreferredName(), getKey());
}
static ParsedBucket fromXContent(XContentParser parser) throws IOException {
return parseTermsBucketXContent(parser, ParsedBucket::new, (p, bucket) -> {
CharBuffer cb = p.charBufferOrNull();
if (cb == null) {
bucket.key = null;
} else {
bucket.key = new BytesRef(cb);
}
});
}
}
}
可以看见它在对bucket做处理的时候,会调用getKey
,getKeyAsString
,getKeyAsNumber
方法处理相关字段,其中getKeyAsNumber
是直接将key(BytesRef
类型)处理为Number
型,直接将字符串的字节强行转换为number,必然是行不通的。
public Number getKeyAsNumber() {
if (key != null) {
return Double.parseDouble(key.utf8ToString());
}
return null;
}
解决方案
从上述分析看来,要解决此问题需要从序列化入手,让其在json序列化的时候直接过滤keyAsNumber字段。
首先想到的是通过重写ParsedStringTerms
,让es遇到terms聚合的key是字符串时,即处理聚合结果为StringTerms
的时候,用自定义的parsed方式,但是此路行不通,因为在使用RestHighLevelClient
API的时候,其限制了我们不能定制自己的parsed方式。
所以只能在外部解决,这里的解决方案是控制jackson在序列化的时候,对ParsedStringTerms.ParsedBucket
类做特殊处理,也就是忽略keyAsNumber
字段。
对ParsedStringTerms.ParsedBucket
类自定义序列化如下:
public class ParsedStringTermsBucketSerializer extends StdSerializer<ParsedStringTerms.ParsedBucket> {
public ParsedStringTermsBucketSerializer(Class<ParsedStringTerms.ParsedBucket> t) {
super(t);
}
@Override
public void serialize(ParsedStringTerms.ParsedBucket value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeObjectField("aggregations", value.getAggregations());
gen.writeObjectField("key", value.getKey());
gen.writeStringField("keyAsString", value.getKeyAsString());
gen.writeNumberField("docCount", value.getDocCount());
gen.writeEndObject();
}
}
将自定义的序列化方式设置到jackson的mapper中:
@Configuration
public class ObjectMapperConfigure {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(simpleModule());
return objectMapper;
}
private SimpleModule simpleModule() {
ParsedStringTermsBucketSerializer serializer = new ParsedStringTermsBucketSerializer(ParsedStringTerms.ParsedBucket.class);
SimpleModule module = new SimpleModule();
module.addSerializer(serializer);
return module;
}
}