SpringBoot实战分析-MongoDB操作

前言

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-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 数据库中,并且能够查询出来了.

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

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

实战分析

postRepository的执行底层

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

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

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

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

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

首先我们看下 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调用参数为postRepositoryMongoRepositoryFactoryBean实例,也就是在创建postRepository实例的时候完成的.

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

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

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

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

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

结语

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

参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值