转载

SpringBoot实战分析-MongoDB操作

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.

SpringBoot实战分析-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-mongodbspring-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 数据库中,并且能够查询出来了.

SpringBoot实战分析-MongoDB操作

我们也可以在 MongoDB 服务器里查到这条记录:

SpringBoot实战分析-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 字段的值去查询到匹配到的记录呢 ? 这样也在下面实战分析里看个明白吧.

SpringBoot实战分析-MongoDB操作

到这里我们对数据的增,改,查都已经试过了,删除其实也很简单,只要调用 postRepositorydelete 方法即可,现在最主要还是探究 PostRepository 仅通过继承 MongoRepository 如何实现数据增删改查的呢?

实战分析

postRepository的执行底层

实现了基本的数据操作之后,我们现在就来看下这一切是怎么做到的呢? 首先我们对测试用例 testUpdate 中的 postRepository#save 进行断点调试,观察程序的执行路径.在单步进入 save 方法内部,代码执行到了 JdkDynamicAopProxy 类型下, 此时代码调用链如下图所示

SpringBoot实战分析-MongoDB操作

很显然这里是用到 SpringJDK 动态代理,而 invoke 方法内这个 proxy 对象十分引人注意, 方法执行时实际调用的 proxysave 方法,而这个 proxy 则是 org.springframework.data.mongodb.repository.support.SimpleMongoRepository@8deb645

, 是 SimpleMongoRepository 类的实例.那么最后调用就会落到 SimpleMongoRepository# save 方法中,我们在这个方法里再次进行断点然后继续运行.

SpringBoot实战分析-MongoDB操作

从这里可以看出,save 方法内部有两个操作: 如果是传入的实体是新纪录则执行 insert ,否则执行 save 更新操作.显然现在要执行的是后者.

而要完成操作跟两个对象 entityInformationmongoOperations 有着密切关系,他们又是干什么的呢,什么时候初始化的呢.

SpringBoot实战分析-MongoDB操作

首先我们看下 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 对象时的调用链.发现整个链路如下,从运行测试用例到这里很长的执行链路,这里只标识出了我们所需要关注的那些类和方法.

SpringBoot实战分析-MongoDB操作

从一层层源码可以跟踪到 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 类里构造的.

SpringBoot实战分析-MongoDB操作

在调用链的下半截里,我们再看下发生着一切的来源在哪, 找到 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean 方法,内部创建 Bean 实例的 doCreateBean 调用参数为 postRepositoryMongoRepositoryFactoryBean 实例,也就是在创建 postRepository 实例的时候完成的.

SpringBoot实战分析-MongoDB操作

而创建 postRepository 对应实体对象实际为 MongoRepositoryFactoryBean 这个工厂 Bean

SpringBoot实战分析-MongoDB操作

当需要使用 postRepository 对象时,实际就是使用工厂对象的方法 MongoRepositoryFactoryBean#getObject 返回的 SimpleMongoRepository 对象,详见当类 AbstractBeanFactorydoGetBean 方法,当参数 namepostRepository 时代码调用链.

SpringBoot实战分析-MongoDB操作

好了,到这里基本说完 postRepository 是如何完成 MongoDB 数据库操作的,还有个问题就是仅定义了接口方法 findByTitle ,如何实现根据 title 字段查找的.

findByTitle的查找实现

断点到执行 findByTitle 方法的地方,调试进去跟之前一样在 JdkDynamicAopProxy 类中执行,而在获取调用链时

,这个代理对象的所拥有的拦截器中一个拦截器类 org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor 引起了我的注意.从命名上看是专门处理查询方法的拦截器.我尝试在这个拦截的 invoke 方法进行断点,果然执行 findByTitle 时,程序执行到了这里.

SpringBoot实战分析-MongoDB操作

然后在拦截器方法中判断该方法是否为查询方法,如果是就会携带参数调用 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 属性,这个对象就是构建条件查询的关系.

SpringBoot实战分析-MongoDB操作

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 符合框架默认的正则要求,所以能自动提取到 Posttitle 字段作为查询字段. 除此之外,使用类似 queryBy , getBy 等等也可以达到同样效果, 这里体现的就是 Spring Framework 约定由于配置的思想, 如果我们随意定义方法名,那框架就无法直接识别出查询字段了.

好了到这里, 我们再次总结一下源码分析成果:

  • 定义 postRepository 实现 MongoRepository 接口,操作MongoDB 数据的底层使用的 MongoDBTemplate, 而实际使用时通过JDK 动态代理和 AOP 拦截器方式层层调用.
  • postRepository 中自定义查询方法是要符合 spring-boot-data-mongodb 框架的方法命名规则,才能达到完全自动处理的效果.

结语

到这里,我们的 Spring BootMongoDB 的实战分析就结束了,细看内部源码,虽然结构层次清晰,但由于模块间复杂调用关系,也往往容易迷失于源码中,这时候耐心和明确的目标就至关重要.这算也是本次源码分析的收获吧,希望这篇文章能有更多收获,我们下篇再见吧.:grin::grin::grin:

原文  https://juejin.im/post/5bcbe6f7f265da0aa41ea413
正文到此结束
Loading...