前言
MongoDB
作為一個基於分散式檔案儲存的資料庫,在微服務領域中廣泛使用.本篇文章將學習 Spring Boot
程式如何執行 MongoDB
操作以及底層實現方式的原始碼分析,來更好地幫助我們理解Spring程式操作 MongoDB
資料庫的行為.以下兩點是原始碼分析的收穫,讓我們一起來看下這些是怎麼發現的吧.
- Spring 框架操作MongoDB 資料的底層使用的 MongoDBTemplate, 而實際使用時通過JDK 動態代理和
AOP
攔截器方式層層呼叫. - 在自己的DAO物件中自定義查詢方法是要符合
spring-boot-data-mongodb
框架的方法命名規則,才能達到完全自動處理的效果.
正文
本文使用
MongoDB
伺服器版本為4.0.0
MongoDB
伺服器的安裝可以參考我的另一篇部落格:後端架構搭建系列之MonogDB
下載示例工程
首先在SPRING INITIALIZR網站上下載示例工程,Spring Boot 版本為1.5.17,僅依賴一個 MongoDB.
用 IDE 匯入工程後開啟POM 檔案,就可以看到 MongoDB 依賴對應的Maven 座標和對應第三方庫為 spring-boot-starter-data-mongodb
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
複製程式碼
那以後我們要在Spring Boot
專案使用 MongoDB
時就可以在主 POM
檔案中引入這個庫的座標就OK 了.
而 spring-boot-starter-data-mongodb
是 spring-data
的子專案, 其作用就是針對 MongoDB
的訪問提供豐富的操作和簡化.
配置 MongoDB連線
要操作 MongoDB
資料庫, 首先要讓程式連線到 MongoDB
伺服器,由於 Spring Boot
強大的簡化配置特性, 想要連線 MongoDB
伺服器, 我們只需在資原始檔夾下的 application.properties
檔案裡新增一行配置即可.
spring.data.mongodb.uri=mongodb://localhost:27017/test
複製程式碼
如果連線有使用者驗證的 MongoDB 伺服器,則uri 形式為
mongodb://name:password@ip:port/dbName
編寫程式碼
配置後之後,接下來我們先建立一個實體 Post
, 包含屬性: id
,title
,content
,createTime
public class Post {
@Id
private Long id;
private String title;
private String content;
private Date createTime;
public Post() {
}
public Post(Long id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
this.createTime = new Date();
}
// 省略 setter,getter 方法
}
複製程式碼
這裡用 註解@Id
表示該實體屬性對應為資料庫記錄的主鍵.
然後再提供對Post的資料訪問的儲存物件 PostRepository
, 繼承 官方提供的MongoRepository
介面
public interface PostRepository extends MongoRepository<Post,Long> {
void findByTitle(String title);
}
複製程式碼
到這裡 對 Post
實體的 CRUD
操作程式碼就完成. What !!! 我們還沒寫什麼程式碼就結束了麼? 我們現在就來寫個測試用例來看看吧.
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootMongodbApplicationTests {
@Autowired
private PostRepository postRepository;
@Test
public void testInsert() {
Post post = new Post(1L,"sayhi", "hi,mongodb");
postRepository.insert(post);
List<Post> all = postRepository.findAll();
System.out.println(all);
// [Post{id=1, title='sayhi', content='hi,mongodb',
//createTime=Sat Oct 20 20:55:15 CST 2018}]
Assert.assertEquals(all.size(),1); // true
}
}
複製程式碼
執行測試用例,執行結果如下, Post
資料成功地儲存到了 MongoDB
資料庫中,並且能夠查詢出來了.
我們也可以在MongoDB
伺服器裡查到這條記錄:
從記錄中看到多了個 _ class
欄位 的值,其實是由 MongoRepository
自動幫我們設定的,用來表示這條記錄對應的實體型別,但底層是什麼時候操作的呢,期待在我們後續分析的時候揭曉答案.
新增之後,我們再嘗試下更新操作,這裡用的也是用繼承而來的 save
方法,除此之外我們還使用了自己寫的介面方法 findByTitle
來根據 title
欄位查詢出 Post 實體.
@Test
public void testUpdate() {
Post post = new Post();
post.setId(1L);
post.setTitle("sayHi");
post.setContent("hi,springboot");
post.setCreateTime(new Date());
postRepository.save(post); // 更新 post 物件
Post updatedPost = postRepository.findByTitle("sayHi"); // 根據 title 查詢
Assert.assertEquals(updatedPost.getId(),post.getId());
Assert.assertEquals(updatedPost.getTitle(),post.getTitle());
Assert.assertEquals(updatedPost.getContent(),"hi,springboot");
}
複製程式碼
執行這個測試用例,結果也是通過.但這裡也有個疑問: 自己提供的方法,沒有寫如何實現,程式怎麼就能依照我們所想要的:根據title
欄位的值去查詢到匹配到的記錄呢 ? 這樣也在下面實戰分析裡看個明白吧.
到這裡我們對資料的增,改,查都已經試過了,刪除其實也很簡單,只要呼叫 postRepository
的delete
方法即可,現在最主要還是探究 PostRepository
僅通過繼承MongoRepository
如何實現資料增刪改查的呢?
實戰分析
postRepository的執行底層
實現了基本的資料操作之後,我們現在就來看下這一切是怎麼做到的呢? 首先我們對測試用例 testUpdate
中的postRepository#save
進行斷點除錯,觀察程式的執行路徑.在單步進入 save
方法內部,程式碼執行到了JdkDynamicAopProxy
型別下, 此時程式碼呼叫鏈如下圖所示
很顯然這裡是用到 Spring
的 JDK
動態代理,而invoke
方法內這個 proxy
物件十分引人注意, 方法執行時實際呼叫的 proxy
的 save
方法,而這個 proxy
則是 org.springframework.data.mongodb.repository.support.SimpleMongoRepository@8deb645
, 是 SimpleMongoRepository
類的例項.那麼最後呼叫就會落到SimpleMongoRepository# save
方法中,我們在這個方法裡再次進行斷點然後繼續執行.
從這裡可以看出,save 方法內部有兩個操作: 如果是傳入的實體是新紀錄則執行 insert
,否則執行 save
更新操作.顯然現在要執行的是後者.
而要完成操作跟兩個物件 entityInformation
和mongoOperations
有著密切關係,他們又是幹什麼的呢,什麼時候初始化的呢.
首先我們看下 mongoOperations
這個物件,利用IDEA
除錯工具可以看到 mongoOperations
其實就是 MongoTemplate
物件, 類似 JDBCTemplate
,針對MongoDB
資料的增刪改查, Spring
也採用相似的名稱方式和 API
.所以真正操作MongoDB
資料庫底層就是這個MongoTemplate
物件.
至於entityInformation
物件所屬的類 MappingMongoEntityInformation
,儲存著Mongo
資料實體資訊,如集合名稱,主鍵型別,一些所對映的實體後設資料等.
再來看下他們的初始化時機,在SimpleMongoRepository
類, 可以找到他們都在的構造方法中初始化
public SimpleMongoRepository(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {
Assert.notNull(metadata, "MongoEntityInformation must not be null!");
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
this.entityInformation = metadata;
this.mongoOperations = mongoOperations;
}
複製程式碼
以同樣的方式,在SimpleMongoRepository
構造器中進行斷點,重新允許觀察初始化 SimpleMongoRepository
物件時的呼叫鏈.發現整個鏈路如下,從執行測試用例到這裡很長的執行鏈路,這裡只標識出了我們所需要關注的那些類和方法.
從一層層原始碼可以跟蹤到 SimpleMongoRepository
類的建立和初始化是由 工廠類MongoRepositoryFactory
完成,
public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {
RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
Class<?> customImplementationClass = null == customImplementation ? null : customImplementation.getClass();
RepositoryInformation information = getRepositoryInformation(metadata, customImplementationClass);
validate(information, customImplementation);
Object target = getTargetRepository(information); // 獲取初始化後的SimpleMongoRepository物件.
// Create proxy
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(new Class[] { repositoryInterface, Repository.class });
// 對 repositoryInterface介面類進行 AOP 代理
result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
return (T) result.getProxy(classLoader);
}
複製程式碼
下圖就是MongoRepositoryFactory
的類圖,而MongoRepositoryFactory
又是在MongoRepositoryFactoryBean
類裡構造的.
在呼叫鏈的下半截裡,我們再看下發生著一切的來源在哪, 找到 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean
方法,內部建立Bean
例項的 doCreateBean
呼叫引數為postRepository
和MongoRepositoryFactoryBean
例項,也就是在建立postRepository
例項的時候完成的.
而建立postRepository
對應實體物件實際為 MongoRepositoryFactoryBean
這個工廠 Bean
當需要使用 postRepository
物件時,實際就是使用工廠物件的方法MongoRepositoryFactoryBean#getObject
返回的 SimpleMongoRepository
物件,詳見當類AbstractBeanFactory
的doGetBean
方法,當引數 name
為 postRepository
時程式碼呼叫鏈.
好了,到這裡基本說完 postRepository
是如何完成MongoDB
資料庫操作的,還有個問題就是僅定義了介面方法 findByTitle
,如何實現根據 title
欄位查詢的.
findByTitle的查詢實現
斷點到執行 findByTitle
方法的地方,除錯進去跟之前一樣在 JdkDynamicAopProxy
類中執行,而在獲取呼叫鏈時
,這個代理物件的所擁有的攔截器中一個攔截器類org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor
引起了我的注意.從命名上看是專門處理查詢方法的攔截器.我嘗試在這個攔截的invoke
方法進行斷點,果然執行findByTitle
時,程式執行到了這裡.
然後在攔截器方法中判斷該方法是否為查詢方法,如果是就會攜帶引數呼叫 PartTreeMongoQuery
物件繼承而來的AbstractMongoQuery#execute
方法.
// AbstractMongoQuery
public Object execute(Object[] parameters) {
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(method, parameters);
// 構建查詢物件 Query: { "title" : "sayHi"}, Fields: null, Sort: null
Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), accessor));
applyQueryMetaAttributesWhenPresent(query);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
String collection = method.getEntityInformation().getCollectionName();
// 構建查詢執行物件
MongoQueryExecution execution = getExecution(query, accessor,new ResultProcessingConverter(processor, operations, instantiators));
return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
}
複製程式碼
而 MongoQueryExecution#execute
方法裡經過層層地呼叫實際執行而以下程式碼:
// AbstractMongoQuery#execute =>
// MongoQueryExecution.ResultProcessingExecution#execute =>
// MongoQueryExecution.SingleEntityExecution#execute
@Override
public Object execute(Query query, Class<?> type, String collection) {
return operations.findOne(query, type, collection);
}
複製程式碼
這裡的 operations
就是我們之前提到的 MongoDBTemplate
例項.所以當執行 自定義方法findByTitile
查詢時底層呼叫的還是MongoDBTemplate#findOne
.
而這裡也有個疑問:構建Query
物件時能獲取到引數值為sayHi
,如何是獲取對應查詢欄位為title
的呢?
在方法createQuery
是一個模板方法,真正執行在``PartTreeMongoQuery`類上.
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
Query query = creator.createQuery();
//...
return query
}
複製程式碼
這裡在構建MongoQueryCreator
時有個 tree
屬性,這個物件就是構建條件查詢的關係.
而 tree
物件的初始化在PartTreeMongoQuery
這個類的構造器中完成的, 根據方法名, PartTree
又是如何構造出來的呢.
//PartTree.java
public PartTree(String source, Class<?> domainClass) {
Assert.notNull(source, "Source must not be null");
Assert.notNull(domainClass, "Domain class must not be null");
Matcher matcher = PREFIX_TEMPLATE.matcher(source);
if (!matcher.find()) {
this.subject = new Subject(null);
this.predicate = new Predicate(source, domainClass);
} else {
this.subject = new Subject(matcher.group(0));
// 構造查詢欄位的關鍵
this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
}
}
複製程式碼
從上面程式碼可以看到 , 用正則方式匹配方法名,其中 PREFIX_TEMPLATE
表示著 ^(find|read|get|query|stream|count|exists|delete|remove)((\p{Lu}.*?))??By
, 如果匹配到了就將 By 後面緊跟的單詞提取出來,內部再根據該名稱去匹配對應類的屬性,找到構建完成後就會放在一個 ArrayList
集合裡存放,等待後續查詢的時候使用.
所以也可以看出 我們自定義的方法 findByTitle
符合框架預設的正則要求,所以能自動提取到Post
的 title
欄位作為查詢欄位. 除此之外,使用類似queryBy
,getBy
等等也可以達到同樣效果, 這裡體現的就是 Spring Framework
約定由於配置的思想, 如果我們隨意定義方法名,那框架就無法直接識別出查詢欄位了.
好了到這裡, 我們再次總結一下原始碼分析成果:
- 定義
postRepository
實現MongoRepository
介面,操作MongoDB 資料的底層使用的 MongoDBTemplate, 而實際使用時通過JDK 動態代理和AOP
攔截器方式層層呼叫. - 在
postRepository
中自定義查詢方法是要符合spring-boot-data-mongodb
框架的方法命名規則,才能達到完全自動處理的效果.
結語
到這裡,我們的 Spring Boot
與 MongoDB
的實戰分析就結束了,細看內部原始碼,雖然結構層次清晰,但由於模組間複雜呼叫關係,也往往容易迷失於原始碼中,這時候耐心和明確的目標就至關重要.這算也是本次原始碼分析的收穫吧,希望這篇文章能有更多收穫,我們下篇再見吧.???