背景
由于Spring boot安全漏洞,须将项目中Spring boot升级至2.3.4版本,2.3.4版本集成了spring-data-mongodb 3.x版本,项目中原spring-data-mongodb 2.x版本被替换成spring-data-mongodb 3.x版本,须进行兼容性适配。
spring-data-mongodb从2.x升级至3.x版本部分api变化见官网项目链接
问题描述
适配过程中发现,项目中存在大量aggregate group操作,例如:
TypedAggregation<A> aggregation = Aggregation.newAggregation(A.class,Aggregation.match(Criteria.and("q").in(...)),Aggregation.group("w", "e").sum("totalcount").as("SumTotal").sum("coveragecount").as("SumCoverage"));
AggregationResults<BasicDBObject> aggregate = mongoTemplateProvince.aggregate(aggregation, BasicDBObject.class);
List<BasicDBObject> mappedResults = aggregate.getMappedResults();
获取group结果返回给前端页面。
spring-data-mongodb 2.x版本中,mappedResults结果会对group的属性进行处理,去除结果集group属性之前的_id前缀,例如上述代码中,返回结果集字段中是"w",“e"而非”_id.w","_id.e"。
spring-data-mongodb 3.x版本中则不会进行处理,直接返回"_id.w","_id.e"。
排查过程
查看源码发现在MongoTemplate#doAggregate(Aggregation aggregation, String collectionName, Class outputType,AggregationOperationContext context)方法中,结果集返回时有做Callback处理。
spring-data-mongodb 2.x版本中使用UnwrapAndReadDocumentCallback内部类,代码如下:
class UnwrapAndReadDocumentCallback<T> extends ReadDocumentCallback<T> {
public UnwrapAndReadDocumentCallback(EntityReader<? super T, Bson> reader, Class<T> type, String collectionName) {
super(reader, type, collectionName);
}
@Override
public T doWith(@Nullable Document object) {
if (object == null) {
return null;
}
Object idField = object.get(Fields.UNDERSCORE_ID);
if (!(idField instanceof Document)) {
return super.doWith(object);
}
Document toMap = new Document();
Document nested = (Document) idField;
toMap.putAll(nested);
for (String key : object.keySet()) {
if (!Fields.UNDERSCORE_ID.equals(key)) {
toMap.put(key, object.get(key));
}
}
return super.doWith(toMap);
}
}
其中对_id为Document类的实例对象进行处理,将_id对象中的属性拷贝到一个新Document中,从而去除结果集中_id前缀。
spring-data-mongodb 3.x版本中使用ReadDocumentCallback内部类,代码如下:
private class ReadDocumentCallback<T> implements DocumentCallback<T> {
private final @NonNull EntityReader<? super T, Bson> reader;
private final @NonNull Class<T> type;
private final String collectionName;
@Nullable
public T doWith(@Nullable Document document) {
T source = null;
if (document != null) {
maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
source = reader.read(type, document);
}
if (source != null) {
maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
source = maybeCallAfterConvert(source, document, collectionName);
}
return source;
}
}
基于Spring Application Event方式对返回结果进行处理,继续查看AfterConvertEvent源码:
public class AfterLoadEvent<T> extends MongoMappingEvent<Document> {
private static final long serialVersionUID = 1L;
private final Class<T> type;
/**
* Creates a new {@link AfterLoadEvent} for the given {@link Document}, type and collectionName.
*
* @param document must not be {@literal null}.
* @param type must not be {@literal null}.
* @param collectionName must not be {@literal null}.
* @since 1.8
*/
public AfterLoadEvent(Document document, Class<T> type, String collectionName) {
super(document, document, collectionName);
Assert.notNull(type, "Type must not be null!");
this.type = type;
}
/**
* Returns the type for which the {@link AfterLoadEvent} shall be invoked for.
*
* @return never {@literal null}.
*/
public Class<T> getType() {
return type;
}
}
其中调用父类构造方法,父类构造方法源码如下
public MongoMappingEvent(T source, @Nullable Document document, @Nullable String collectionName) {
super(source);
this.document = document;
this.collectionName = collectionName;
}
可看出直接进行对象赋值,未对结果集进行特殊处理——返回结果集中group属性包含_id前缀。
解决方法
综上,发现问题出现原因,由于项目中存在大量aggregate group查询逻辑,全部更改工作量太大。考虑兼容性适配。
因为spring-data-mongodb 3.x版本ReadDocumentCallback类中使用Application Event方式对结果集进行处理,考虑注入一个自定义的Bean,重写其AfterLoadEvent方法,在方法中提取出_id对象的属性放入结果对象中,保证原业务处理代码逻辑不变。自定义Application Event如下,继承AfterConvertCallback类,重写onAfterConvert方法:
@Component
public class CzxMongoAfterConvertCallback implements AfterConvertCallback {
@Override
public Object onAfterConvert(Object entity, Document document, String collection) {
if(entity == null) {
return null;
}
if (entity instanceof BasicDBObject) {
BasicDBObject bs = (BasicDBObject) entity;
Map id = MapUtils.getMap(document, Fields.UNDERSCORE_ID);
if (MapUtil.isNotEmpty(id) && id.size() > 1) {
bs.putAll(id);
return bs;
}
} else if (entity instanceof Document) {
Map id = MapUtils.getMap(document, Fields.UNDERSCORE_ID);
if (MapUtil.isNotEmpty(id) && id.size() > 1) {
document.putAll(id);
return document;
}
}
return entity;
}
}
onAfterConvert方法中提取_id的属性赋值到新的实体对象中。
Spring boot在启动时会自动加载ApplicationContext中的CzxMongoAfterConvertCallback Bean覆盖原AfterLoadEvent实例。至此spring-data-mongodb从2.x升级至3.x版本,aggregate group操作,分组字段出现_id前缀的兼容性问题得到解决。