天天看點

SpringData ES中一些底層原理的分析

它跟Spring Data一樣,提供了Repository接口,我們隻需要定義一個新的接口并繼承這個Repository接口,然後就可以注入這個新的接口使用了。

定義接口:

注入接口進行使用:

Repository接口的代理生成

上面的例子中TaskRepository是個接口,而我們卻直接注入了這個接口并調用方法;很明顯,這是錯誤的。

其實SpringData ES内部基于這個TaskRepository接口構造一個SimpleElasticsearchRepository,真正被注入的是這個SimpleElasticsearchRepository。

這個過程是如何實作的呢?  來分析一下。

ElasticsearchRepositoriesAutoConfiguration自動化配置類會導入ElasticsearchRepositoriesRegistrar這個ImportBeanDefinitionRegistrar。

ElasticsearchRepositoriesRegistrar繼承自AbstractRepositoryConfigurationSourceSupport,是個ImportBeanDefinitionRegistrar接口的實作類,會被Spring容器調用registerBeanDefinitions進行自定義bean的注冊。

ElasticsearchRepositoriesRegistrar委托給RepositoryConfigurationDelegate完成bean的解析。

整個解析過程可以分3個步驟:

找出子產品中的org.springframework.data.repository.Repository接口的實作類或者org.springframework.data.repository.RepositoryDefinition注解的修飾類,并會過濾掉org.springframework.data.repository.NoRepositoryBean注解的修飾類。找出後封裝到RepositoryConfiguration中

周遊這些RepositoryConfiguration,然後構造成BeanDefinition并注冊到Spring容器中。需要注意的是這些RepositoryConfiguration會以beanClass為ElasticsearchRepositoryFactoryBean這個類的方式被注冊,并把對應的Repository接口當做構造參數傳遞給ElasticsearchRepositoryFactoryBean,還會設定相應的屬性比如elasticsearchOperations、evaluationContextProvider、namedQueries、repositoryBaseClass、lazyInitqueryLookupStrategyKey

ElasticsearchRepositoryFactoryBean被執行個體化的時候設定對應的構造參數和屬性。設定完畢以後調用afterPropertiesSet方法(實作了InitializingBean接口)。在afterPropertiesSet方法内部會去建立RepositoryFactorySupport類,并進行一些初始化,比如namedQueries、repositoryBaseClass等。然後通過這個RepositoryFactorySupport的getRepository方法基于Repository接口建立出代理類,并使用AOP添加了幾個MethodInterceptor

ElasticsearchRepositoryFactoryBean是一個FactoryBean接口的實作類,getObject方法傳回的上面提到的getRepository方法傳回的代理對象;getObjectType方法傳回的是對應Repository接口類型。

我們文章一開始提到的注入TaskRepository的時候,實際上這個對象是ElasticsearchRepositoryFactoryBean類型的執行個體,隻不過ElasticsearchRepositoryFactoryBean實作了FactoryBean接口,是以注入的時候會得到一個代理對象,這個代理對象是由jdk内置的代理生成的,并且它的target對象是SimpleElasticsearchRepository(主鍵是String類型)。

SpringData ES中ElasticsearchOperations的介紹

ElasticsearchTemplate實作了ElasticsearchOperations接口。

ElasticsearchOperations接口是SpringData對Elasticsearch操作的一層封裝,比如有建立索引createIndex方法、擷取索引的設定資訊getSetting方法、查詢對象queryForObject方法、分頁查詢方法queryForPage、删除文檔delete方法、更新文檔update方法等等。

ElasticsearchTemplate是具體的實作類,它有這些屬性:

Client接口在ElasticsearchAutoConfiguration自動化配置類裡被構造:

ElasticsearchTemplate、ElasticsearchConverter以及SimpleElasticsearchMappingContext在ElasticsearchDataAutoConfiguration自動化配置類裡被構造:

 需要注意的是這個bean被自動化配置類構造的前提是它們在Spring容器中并不存在。

Repository的調用過程

以自定義的TaskRepository的save方法為例,大緻的執行流程如下所示:

SpringData ES中一些底層原理的分析

像自定義的Repository查詢方法,或者Repository接口的自定義實作類的操作這些底層,可以去QueryExecutorMethodInterceptor中檢視,大家有興趣的可以自行檢視源碼。

http://spring4all.com/article/17

Document對應的POJO的屬性跟es裡面文檔的字段名字不一樣,這樣Repository裡面編寫自定義的查詢方法就會查詢不出結果。

比如有個Person類,它有2個屬性goodFace和goodAt。這2個屬性在es的索引裡對應的字段表為good_face和good_at:

1

2

3

4

5

6

7

8

9

10

11

@Document(replicas = 1, shards = 1, type = "person", indexName = "person")

@Getter

@Setter

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)

public class Person {

@Id

private String id;

private String name;

private boolean goodFace;

private String goodAt;

}

Repository中的自定義查詢:

@Repository

public interface PersonRepository extends ElasticsearchRepository<Person, String> {

List<Person> findByGoodFace(boolean isGoodFace);

List<Person> findByName(String name);

方法findByGoodFace是查詢不出結果的,而findByName是ok的。

為什麼findByGoodFace不行而findByName可以呢,來探究一下。

Person類的name屬性跟ES中的字段名是一模一樣的,而goodFace字段在ES中的字段是good_face(因為我們使用了SnakeCaseStrategy政策)。

是以産生這個問題的原因在于ES中文檔的字段名跟POJO中的字段名不統一造成的。

但是我們使用PersonRepository的save方法儲存文檔的時候屬性和字段是可以對上的。

那為什麼使用repository的save方法能對應上文檔和字段,而自定義的find方法卻不行呢?

在Person類上使用@JsonNaming注解完成POJO和json的映射,我們使用了SnakeCaseStrategy政策,這個政策會把屬性從駝峰方式改成小寫帶下劃線的方式。

比如goodAt屬性映射的時候就會變成good_at,good_face變成good_face,name變成name。

Spring Data Elasticsearch把對ES的操作封裝成了一個ElasticsearchOperations接口。比如queryForObject、queryForPage、count、queryForList方法。

ElasticsearchOperations接口目前有一個實作類ElasticsearchTemplate。

ElasticsearchTemplate内部有個ResultsMapper屬性,這個ResultsMapper目前隻有一個實作類DefaultResultMapper,DefaultResultMapper内部使用DefaultEntityMapper完成映射。DefaultEntityMapper是個EntityMapper接口的實作類,它的定義如下:

public interface EntityMapper {

public String mapToString(Object object) throws IOException;

public <T> T mapToObject(String source, Class<T> clazz) throws IOException;

方法很明白:對象到json字元串的轉換和json字元串倒對象的轉換。

DefaultEntityMapper内部使用jackson的ObjectMapper完成。

自定義的Repository繼承自ElasticsearchRepository,最後會使用代理映射成SimpleElasticsearchRepository。

SimpleElasticsearchRepository内部有個屬性ElasticsearchOperations用于完成與ES的互動。

我們看下SimpleElasticsearchRepository的save方法:

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

@Override

public <S extends T> S save(S entity) {

Assert.notNull(entity, "Cannot save 'null' entity.");

// createIndexQuery方法會構造一個IndexQuery,然後調用ElasticsearchOperations的index方法

elasticsearchOperations.index(createIndexQuery(entity));

elasticsearchOperations.refresh(entityInformation.getIndexName());

return entity;

// ElasticsearchTemplate的index方法

public String index(IndexQuery query) {

// 調用prepareIndex方法構造一個IndexRequestBuilder

String documentId = prepareIndex(query).execute().actionGet().getId();

// 設定儲存文檔的id

if (query.getObject() != null) {

setPersistentEntityId(query.getObject(), documentId);

return documentId;

private IndexRequestBuilder prepareIndex(IndexQuery query) {

try {

// 從@Document注解中得到索引的名字

String indexName = isBlank(query.getIndexName()) ? retrieveIndexNameFromPersistentEntity(query.getObject()

.getClass())[0] : query.getIndexName();

// 從@Document注解中得到索引的類型

String type = isBlank(query.getType()) ? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0]

: query.getType();

IndexRequestBuilder indexRequestBuilder = null;

if (query.getObject() != null) { // save方法這裡儲存的object就是POJO

// 得到id字段

String id = isBlank(query.getId()) ? getPersistentEntityId(query.getObject()) : query.getId();

if (id != null) { // 如果設定了id字段

indexRequestBuilder = client.prepareIndex(indexName, type, id);

} else { // 如果沒有設定id字段

indexRequestBuilder = client.prepareIndex(indexName, type);

// 使用ResultsMapper映射POJO到json字元串

indexRequestBuilder.setSource(resultsMapper.getEntityMapper().mapToString(query.getObject()));

} else if (query.getSource() != null) { // 如果自定義了source屬性,直接指派

indexRequestBuilder = client.prepareIndex(indexName, type, query.getId()).setSource(query.getSource());

} else { // 沒有設定object屬性或者source屬性,抛出ElasticsearchException異常

throw new ElasticsearchException("object or source is null, failed to index the document [id: " + query.getId() + "]");

if (query.getVersion() != null) { // 設定版本

indexRequestBuilder.setVersion(query.getVersion());

indexRequestBuilder.setVersionType(EXTERNAL);

if (query.getParentId() != null) { // 設定parentId

indexRequestBuilder.setParent(query.getParentId());

return indexRequestBuilder;

} catch (IOException e) {

throw new ElasticsearchException("failed to index the document [id: " + query.getId() + "]", e);

save方法使用ResultsMapper完成了POJO到json的轉換,是以save方法儲存成功對應的文檔資料:

自定義的findByGoodFace方法:

由于是Repository中的自定義方法,會被Spring Data通過代理進行構造,内部還是用了AOP,最終在QueryExecutorMethodInterceptor中并解析成ElasticsearchPartQuery這個RepositoryQuery接口的實作類,然後調用execute方法:

public Object execute(Object[] parameters) {

ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);

CriteriaQuery query = createQuery(accessor);

if(tree.isDelete()) { // 如果是删除方法

Object result = countOrGetDocumentsForDelete(query, accessor);

elasticsearchOperations.delete(query, queryMethod.getEntityInformation().getJavaType());

return result;

} else if (queryMethod.isPageQuery()) { // 如果是分頁查詢

query.setPageable(accessor.getPageable());

return elasticsearchOperations.queryForPage(query, queryMethod.getEntityInformation().getJavaType());

} else if (queryMethod.isStreamQuery()) { // 如果是流式查詢

Class<?> entityType = queryMethod.getEntityInformation().getJavaType();

if (query.getPageable() == null) {

query.setPageable(new PageRequest(0, 20));

return StreamUtils.createStreamFromIterator((CloseableIterator<Object>) elasticsearchOperations.stream(query, entityType));

} else if (queryMethod.isCollectionQuery()) { // 如果是集合查詢

if (accessor.getPageable() == null) {

int itemCount = (int) elasticsearchOperations.count(query, queryMethod.getEntityInformation().getJavaType());

query.setPageable(new PageRequest(0, Math.max(1, itemCount)));

} else {

return elasticsearchOperations.queryForList(query, queryMethod.getEntityInformation().getJavaType());

} else if (tree.isCountProjection()) { // 如果是count查詢

return elasticsearchOperations.count(query, queryMethod.getEntityInformation().getJavaType());

// 單個查詢

return elasticsearchOperations.queryForObject(query, queryMethod.getEntityInformation().getJavaType());

findByGoodFace方法是個集合查詢,最終會調用ElasticsearchOperations的queryForList方法:

public <T> List<T> queryForList(CriteriaQuery query, Class<T> clazz) {

// 調用queryForPage方法

return queryForPage(query, clazz).getContent();

public <T> Page<T> queryForPage(CriteriaQuery criteriaQuery, Class<T> clazz) {

// 查詢解析器進行文法的解析

QueryBuilder elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria());

QueryBuilder elasticsearchFilter = new CriteriaFilterProcessor().createFilterFromCriteria(criteriaQuery.getCriteria());

SearchRequestBuilder searchRequestBuilder = prepareSearch(criteriaQuery, clazz);

if (elasticsearchQuery != null) {

searchRequestBuilder.setQuery(elasticsearchQuery);

searchRequestBuilder.setQuery(QueryBuilders.matchAllQuery());

if (criteriaQuery.getMinScore() > 0) {

searchRequestBuilder.setMinScore(criteriaQuery.getMinScore());

if (elasticsearchFilter != null)

searchRequestBuilder.setPostFilter(elasticsearchFilter);

if (logger.isDebugEnabled()) {

logger.debug("doSearch query:\n" + searchRequestBuilder.toString());

SearchResponse response = getSearchResponse(searchRequestBuilder

.execute());

// 最終的結果是用ResultsMapper進行映射

return resultsMapper.mapResults(response, clazz, criteriaQuery.getPageable());

自定義的方法使用ElasticsearchQueryCreator去建立CriteriaQuery,内部做一些詞法的分析,有了CriteriaQuery之後,使用CriteriaQueryProcessor基于Criteria構造了QueryBuilder,最後使用QueryBuilder去做rest請求得到es的查詢結果。這些過程中是沒有用到ResultsMapper,而隻是用反射得到POJO的屬性,隻有在得到查詢結果後才會用ResultsMapper去做映射。

如果出現了這種情況,解決方案目前有兩種:

1.使用repository的search方法,參數可以是QueryBuilder或者SearchQuery

personRepository.search(

QueryBuilders.boolQuery()

.must(QueryBuilders.termQuery("good_face", true))

)

2.使用@Query注解

@Query("{\"bool\" : {\"must\" : {\"term\" : {\"good_face\" : \"?0\"}}}}")

暫時發現這兩種解決方法,不知還有否更好的解決方案。http://fangjian0423.github.io/2017/05/24/spring-data-es-query-problem/