MongoDB
作为一个基于分布式文件存储的数据库,在微服务领域中广泛使用.本篇文章将学习 Spring Boot
程序如何执行 MongoDB
操作以及底层实现方式的源码分析,来更好地帮助我们理解Spring程序操作 MongoDB
数据库的行为.以下两点是源码分析的收获,让我们一起来看下这些是怎么发现的吧.
AOP 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
服务器,由于 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
如何实现数据增删改查的呢?
实现了基本的数据操作之后,我们现在就来看下这一切是怎么做到的呢? 首先我们对测试用例 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
的实战分析就结束了,细看内部源码,虽然结构层次清晰,但由于模块间复杂调用关系,也往往容易迷失于源码中,这时候耐心和明确的目标就至关重要.这算也是本次源码分析的收获吧,希望这篇文章能有更多收获,我们下篇再见吧.:grin::grin::grin: