需求说明

有一个这样的需求,一个类MongoEntity 继承了 BaseEntity,他们的结构是这样的 spring-data-mongodb 自定义注解生成索引 - 图1其中core项目是不支持任何MongoDB注解的。server项目支持MongoDB注解。
现在我要给parentId字段加索引。但是@Indexed注解仅支持在类的字段上标注,并不支持和hibernate在is、get、set方法上标注。所以期望做一个在is、get、set方法上的注解,通过字段对应的is、get、set方法来生成索引。
像上面的需求,只需要重写对应的get方法并标注注解就可以完成功能。

做法

MongoDB @Indexed实现原理

通过跟踪源码,我发现spring-data-mongodb内有一个类 MongoPersistentEntityIndexCreator 其大概结构是这样的 spring-data-mongodb 自定义注解生成索引 - 图2看的出来这是一个spring的事件监听器,在spring容器启动的时候,就会调用监听方法 onApplicationEvent() ,而参数event,我们可以通过断点来看看其结构
image.png
可以发现监听条件就是每个有@Document注解标注的实体。跟踪源码最终会来到这个方法
image.png
红框中的就是创建索引的代码。要注意的是这个 crateIndex() 是包私有方法,外部没有权限访问。
参数IndexDefinitionHolder的结构是一个静态内部类
image.png
其中IndexDefinition的实现类其实就是一个叫Index的类
image.png

总结一下spring-mongo-data的做法就是:

  1. 扫描带了@Document的实体封装成PersistentEntity,里面将@Indexed标注的字段转换成了Index类型。
  2. 调用监听器。
  3. 根据PersistentEntity创建索引信息。

而本做法的思路:

  1. 写一个用在is、get、set方法上的注解@CustomIndexed,最终能通过注解信息生成Index对象。
  2. 写一个继承监听器的类,同时监听创建索引方法。
  3. 通过参数获取Spring AnnotationMetadata对象。获取有@CustomIndexed注解的方法信息。
  4. 根据方法名称获取字段名
  5. 照葫芦画瓢创建索引。

    自定义索引@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;

}

  1. 这里要注意的是:
  2. - name默认为空字符串,目的是想直接通过isgetset方法转化字段值,例如getName 字段为name。当然也可以通过给name赋值写上对应的名称
  3. - 观察上面Index类的构造方法,初始化index对象需要一个Sort.Direction枚举,所以注解提供一个direction值,默认为ASC
  4. <a name="sWT5M"></a>
  5. ### 自定义监听器
  6. ```java
  7. import com.example.demo.entity.CustomIndexed;
  8. import com.example.demo.util.ClassMetadataUtil;
  9. import lombok.SneakyThrows;
  10. import org.apache.commons.lang3.StringUtils;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.core.type.AnnotationMetadata;
  13. import org.springframework.core.type.MethodMetadata;
  14. import org.springframework.core.type.classreading.MetadataReader;
  15. import org.springframework.core.type.classreading.MetadataReaderFactory;
  16. import org.springframework.data.domain.Sort;
  17. import org.springframework.data.mapping.PersistentEntity;
  18. import org.springframework.data.mapping.context.MappingContextEvent;
  19. import org.springframework.data.mongodb.core.index.*;
  20. import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
  21. import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
  22. import org.springframework.stereotype.Component;
  23. import java.util.Map;
  24. import java.util.Set;
  25. /**
  26. * 自定义MongoDB索引生成器
  27. * @author zhy
  28. * @date 2021/4/1420:00
  29. */
  30. @Component
  31. public class CustomMongoDBIndexCreator extends MongoPersistentEntityIndexCreator {
  32. private static final Class<?> CUSTOM_INDEX_ANNOTATION_CLASS = CustomIndexed.class;;
  33. private static final String INDEX_NAME_ATTRIBUTE = "name";
  34. private static final String INDEX_DIRECTION_ATTRIBUTE = "direction";
  35. private final IndexOperationsProvider indexOperationsProvider;
  36. @Autowired
  37. private MetadataReaderFactory metadataReaderFactory;
  38. public CustomMongoDBIndexCreator(MongoMappingContext mappingContext, IndexOperationsProvider indexOperationsProvider) {
  39. super(mappingContext, indexOperationsProvider);
  40. this.indexOperationsProvider = indexOperationsProvider;
  41. }
  42. @SneakyThrows
  43. @Override
  44. public void onApplicationEvent(MappingContextEvent<?, ?> event) {
  45. super.onApplicationEvent(event);
  46. PersistentEntity<?, ?> persistentEntity = event.getPersistentEntity();
  47. MongoPersistentEntity root = (MongoPersistentEntity) persistentEntity;
  48. //获取metadata对象
  49. MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(root.getName());
  50. AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
  51. //获取@CustomIndexed 对应的方法信息
  52. Set<MethodMetadata> annotatedMethods = annotationMetadata.getAnnotatedMethods(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
  53. for (MethodMetadata annotatedMethod : annotatedMethods) {
  54. String indexField = getIndexField(annotatedMethod);
  55. Map<String, Object> annotationAttributes = annotatedMethod.getAnnotationAttributes(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
  56. //创建索引
  57. Index index = new Index(indexField, (Sort.Direction)annotationAttributes.get(INDEX_DIRECTION_ATTRIBUTE)).named(indexField);
  58. MongoPersistentEntityIndexResolver.IndexDefinitionHolder indexDefinitionHolder = new MongoPersistentEntityIndexResolver.IndexDefinitionHolder(indexField, index, root.getCollection());
  59. IndexOperations indexOperations = this.indexOperationsProvider.indexOps(root.getCollection());
  60. indexOperations.ensureIndex(indexDefinitionHolder);
  61. }
  62. }
  63. /**
  64. * 根绝is、get、set方法获取需要加索引的字段
  65. * @param methodMetadata
  66. * @throws
  67. * @return java.lang.String
  68. * @author zhy
  69. * @date 2021/4/15 10:07
  70. */
  71. private String getIndexField(MethodMetadata methodMetadata){
  72. Map<String, Object> annotationAttributes = methodMetadata.getAnnotationAttributes(CUSTOM_INDEX_ANNOTATION_CLASS.getName());
  73. String indexName;
  74. if (annotationAttributes.get(INDEX_NAME_ATTRIBUTE) == null || StringUtils.isBlank(annotationAttributes.get(INDEX_NAME_ATTRIBUTE).toString())){
  75. //获取方法名截取字段名称
  76. indexName = ClassMetadataUtil.methodNameToFieldName(methodMetadata.getMethodName());
  77. }else{
  78. indexName = annotationAttributes.get(INDEX_NAME_ATTRIBUTE).toString();
  79. }
  80. return indexName;
  81. }
  82. }

这里要注意的点:

  • 父类创建索引的方法 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包则我们注入自定义监听器的时候会报循环依赖问题
image.png
具体原因不知道,所以要在项目内引入spring-boot-starter-actuator包

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>