需求说明
有一个这样的需求,一个类MongoEntity 继承了 BaseEntity,他们的结构是这样的
其中core项目是不支持任何MongoDB注解的。server项目支持MongoDB注解。
现在我要给parentId字段加索引。但是@Indexed注解仅支持在类的字段上标注,并不支持和hibernate在is、get、set方法上标注。所以期望做一个在is、get、set方法上的注解,通过字段对应的is、get、set方法来生成索引。
像上面的需求,只需要重写对应的get方法并标注注解就可以完成功能。
做法
MongoDB @Indexed实现原理
通过跟踪源码,我发现spring-data-mongodb内有一个类 MongoPersistentEntityIndexCreator
其大概结构是这样的
看的出来这是一个spring的事件监听器,在spring容器启动的时候,就会调用监听方法 onApplicationEvent()
,而参数event,我们可以通过断点来看看其结构
可以发现监听条件就是每个有@Document注解标注的实体。跟踪源码最终会来到这个方法
红框中的就是创建索引的代码。要注意的是这个 crateIndex()
是包私有方法,外部没有权限访问。
参数IndexDefinitionHolder的结构是一个静态内部类
其中IndexDefinition的实现类其实就是一个叫Index的类
总结一下spring-mongo-data的做法就是:
- 扫描带了@Document的实体封装成PersistentEntity,里面将@Indexed标注的字段转换成了Index类型。
- 调用监听器。
- 根据PersistentEntity创建索引信息。
而本做法的思路:
- 写一个用在is、get、set方法上的注解@CustomIndexed,最终能通过注解信息生成Index对象。
- 写一个继承监听器的类,同时监听创建索引方法。
- 通过参数获取Spring AnnotationMetadata对象。获取有@CustomIndexed注解的方法信息。
- 根据方法名称获取字段名
- 照葫芦画瓢创建索引。
自定义索引@CustomIndexed
```java import org.springframework.data.domain.Sort;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
/**
- 作用在方法上的MongoDB索引注解
- @author zhy
@date 2021/4/1419:11 */ @Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CustomIndexed {
String name() default “”;
Sort.Direction direction() default Sort.Direction.ASC;
}
这里要注意的是:
- name默认为空字符串,目的是想直接通过is、get、set方法转化字段值,例如getName 字段为name。当然也可以通过给name赋值写上对应的名称
- 观察上面Index类的构造方法,初始化index对象需要一个Sort.Direction枚举,所以注解提供一个direction值,默认为ASC
<a name="sWT5M"></a>
### 自定义监听器
```java
import com.example.demo.entity.CustomIndexed;
import com.example.demo.util.ClassMetadataUtil;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.core.index.*;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
/**
* 自定义MongoDB索引生成器
* @author zhy
* @date 2021/4/1420:00
*/
@Component
public class CustomMongoDBIndexCreator extends MongoPersistentEntityIndexCreator {
private static final Class<?> CUSTOM_INDEX_ANNOTATION_CLASS = CustomIndexed.class;;
private static final String INDEX_NAME_ATTRIBUTE = "name";
private static final String INDEX_DIRECTION_ATTRIBUTE = "direction";
private final IndexOperationsProvider indexOperationsProvider;
@Autowired
private MetadataReaderFactory metadataReaderFactory;
public CustomMongoDBIndexCreator(MongoMappingContext mappingContext, IndexOperationsProvider indexOperationsProvider) {
super(mappingContext, indexOperationsProvider);
this.indexOperationsProvider = indexOperationsProvider;
}
@SneakyThrows
@Override
public void onApplicationEvent(MappingContextEvent<?, ?> event) {
super.onApplicationEvent(event);
PersistentEntity<?, ?> persistentEntity = event.getPersistentEntity();
MongoPersistentEntity root = (MongoPersistentEntity) persistentEntity;
//获取metadata对象
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(root.getName());
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
//获取@CustomIndexed 对应的方法信息
Set<MethodMetadata> annotatedMethods = annotationMetadata.getAnnotatedMethods(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
for (MethodMetadata annotatedMethod : annotatedMethods) {
String indexField = getIndexField(annotatedMethod);
Map<String, Object> annotationAttributes = annotatedMethod.getAnnotationAttributes(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
//创建索引
Index index = new Index(indexField, (Sort.Direction)annotationAttributes.get(INDEX_DIRECTION_ATTRIBUTE)).named(indexField);
MongoPersistentEntityIndexResolver.IndexDefinitionHolder indexDefinitionHolder = new MongoPersistentEntityIndexResolver.IndexDefinitionHolder(indexField, index, root.getCollection());
IndexOperations indexOperations = this.indexOperationsProvider.indexOps(root.getCollection());
indexOperations.ensureIndex(indexDefinitionHolder);
}
}
/**
* 根绝is、get、set方法获取需要加索引的字段
* @param methodMetadata
* @throws
* @return java.lang.String
* @author zhy
* @date 2021/4/15 10:07
*/
private String getIndexField(MethodMetadata methodMetadata){
Map<String, Object> annotationAttributes = methodMetadata.getAnnotationAttributes(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
String indexName;
if (annotationAttributes.get(INDEX_NAME_ATTRIBUTE) == null || StringUtils.isBlank(annotationAttributes.get(INDEX_NAME_ATTRIBUTE).toString())){
//获取方法名截取字段名称
indexName = ClassMetadataUtil.methodNameToFieldName(methodMetadata.getMethodName());
}else{
indexName = annotationAttributes.get(INDEX_NAME_ATTRIBUTE).toString();
}
return indexName;
}
}
这里要注意的点:
- 父类创建索引的方法
crateIndex()
是包私有方法,所以在这里不能使用,只能照葫芦画瓢,按照父类的写一套差不多的代码。
工具类ClassMetadataUtil代码
import java.util.Locale;
/**
* 类元数据工具类
* @author zhy
* @date 2021/4/159:56
*/
public class ClassMetadataUtil {
private ClassMetadataUtil(){}
/**
* 通过方法名称获取字段名称
* <p>方法名称必须是,is,get,set开头的方法</p>
* <p>转换出来的字段例如getName 返回 name</p>
* @param methodName 方法名称
* @throws
* @return java.lang.String
* @author zhy
* @date 2021/1/28 10:51
*/
public static String methodNameToFieldName(String methodName) {
if (methodName.startsWith("is")) {
methodName = methodName.substring(2);
} else {
if (!methodName.startsWith("get") && !methodName.startsWith("set")) {
throw new RuntimeException("Error parsing property name '" + methodName + "'. Didn't start with 'is', 'get' or 'set'.");
}
methodName = methodName.substring(3);
}
if (methodName.length() == 1 || methodName.length() > 1 && !Character.isUpperCase(methodName.charAt(1))) {
methodName = methodName.substring(0, 1).toLowerCase(Locale.ENGLISH) + methodName.substring(1);
}
return methodName;
}
}
遇到的问题
如果项目不引入spring-boot-starter-actuator包则我们注入自定义监听器的时候会报循环依赖问题
具体原因不知道,所以要在项目内引入spring-boot-starter-actuator包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>