场景
将terms
聚合的结果直接返给前端处理,如果bucket的key为字符串时,在mvc层jackson进行json序列化处理会报类型转换错误。
原因分析
terms
聚合的结果中有keyAsNumber字段,是将桶key转为number类型,但是当key为字符串类型的数据时,是无法转为numebr类型的。
jackson在序列化的时候,当terms聚合的key为字符串类型时,则会调用ParsedStringTerms
类来转换处理字段,源码如下:
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 56 57 58 59 60 61 62 63 64
| 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,必然是行不通的。
1 2 3 4 5 6
| 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
类自定义序列化如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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; } }
|