MongoDB
做爲一個基於分佈式文件存儲的數據庫,在微服務領域中普遍使用.本篇文章將學習 Spring Boot
程序如何執行 MongoDB
操做以及底層實現方式的源碼分析,來更好地幫助咱們理解Spring程序操做 MongoDB
數據庫的行爲.如下兩點是源碼分析的收穫,讓咱們一塊兒來看下這些是怎麼發現的吧.java
AOP
攔截器方式層層調用.spring-boot-data-mongodb
框架的方法命名規則,才能達到徹底自動處理的效果.本文使用
MongoDB
服務器版本爲4.0.0git
MongoDB
服務器的安裝能夠參考個人另外一篇博客:後端架構搭建系列之MonogDBgithub
首先在SPRING INITIALIZR網站上下載示例工程,Spring Boot 版本爲1.5.17,僅依賴一個 MongoDB.spring
用 IDE 導入工程後打開POM 文件,就能夠看到 MongoDB 依賴對應的Maven 座標和對應第三方庫爲 spring-boot-starter-data-mongodb
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
服務器,因爲 Spring Boot
強大的簡化配置特性, 想要鏈接 MongoDB
服務器, 咱們只需在資源文件夾下的 application.properties
文件裏新增一行配置便可.springboot
spring.data.mongodb.uri=mongodb://localhost:27017/test
複製代碼
若是鏈接有用戶驗證的 MongoDB 服務器,則uri 形式爲
mongodb://name:password@ip:port/dbName
服務器
配置後以後,接下來咱們先建立一個實體 Post
, 包含屬性: id
,title
,content
,createTime
markdown
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
如何實現數據增刪改查的呢?
實現了基本的數據操做以後,咱們如今就來看下這一切是怎麼作到的呢? 首先咱們對測試用例 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
方法的地方,調試進去跟以前同樣在 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
的實戰分析就結束了,細看內部源碼,雖然結構層次清晰,但因爲模塊間複雜調用關係,也每每容易迷失於源碼中,這時候耐心和明確的目標就相當重要.這算也是本次源碼分析的收穫吧,但願這篇文章能有更多收穫,咱們下篇再見吧.😁😁😁