它跟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方法為例,大緻的執行流程如下所示:
像自定義的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/