趣味编程|手写一个集成多数据源mongodb的 starter

2021-02-01 11:07:51 浏览数 (1)

关注公众号“AI码师”领取2021最新面试资料一份,公众号内回复“源码”,获取本项目源码

【前言】

主演:老王(技术总监),小码(本猿)

老王:小码啊,我们项目中需要使用到mongodb,你集成下吧,完成了和我说下。

小码:好的,一会就给你弄好。

小码三下五除二的给集成好了,然后给老王汇报了。

小码:王哥,我已经把mongodb集成好了。

老王:好的,现在由于我们项目中会用到很多mongo数据库,你现在集成的mongo支持多数据源动态切换么?

小码:这个,这个,啥叫多数据源动态切换啊?

老王:就是在运行过程中,能够根据需要动态去连接哪个数据库,咱们项目需要支持多个特性,如果你对这个不太清楚的话,我给你一个思路,你可以考虑使用切面来实现,具体怎么弄,你自己研究下.

小码:好的,王哥。

小码想了很久,各种百度,终于找到了解决方案,花了一上午的时间,终于弄完了,又去给老王汇报了。

小码:王哥,现在项目中的mongo已经实现了多数据源了(哈哈,心里很自豪)。

老王:小伙子,很快嘛,不过现在又来一个任务,你需要把你集成的这个功能封装成一个starter,另外一个项目也需要使用这个功能,你抽时间封装下吧。

小码:好的,王哥,保证完成任务

小码下去之后,就开始研究怎么去封装成一个starter,下班之前弄好了,不过这次他没去找老王了,准备第二天再去,不然又得加班,哈哈!!!

【正文】

前面水了那么多,主要是给大家设置一种场景,让同志们知道为啥要去做这么一个功能,现在就直接进入正题了:

【springboot集成mongodb】

  • 引入mongodb依赖
代码语言:javascript复制
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
  • 配置mongodb连接信息,在application.yml中配置
代码语言:javascript复制
# 设置了用户名和密码的连接
spring:
  data:
    mongodb:
      uri: mongodb://用户名:密码@IP:PORT/数据库?authSource=${auth_db:用户认证数据库}
# 没有设置用户名和密码的连接配置
spring:
    data:
        mongodb:
            uri: mongodb://IP:PORT/数据库
  • 写测试代码

我们创建一个接口,然后在接口方法中去操作monog库:

接口中,直接引入MongoTemplate,就可以直接操作mongo了,这里对mongo如何使用不做过多介绍。

代码语言:javascript复制
/**
 * Created by AI码师 on 2019/4/19.
 * 关注公众号【AI码师】领取2021最新面试资料一份(很全)
 * @return
 */
@RequestMapping("/home")
@RestController
public class HomeController {
    @Autowired
    private MongoTemplate mongoTemplate;
    @PostMapping
    public String addData(@RequestParam(value = "name") String name,@RequestParam(value = "addr") String addr,@RequestParam(value = "email") String email){
        Student student = new Student();
        student.setAddr(addr);
        student.setName(name);
        student.setEmail(email);
        mongoTemplate.insert(student);
        return "添加成功";
    }
}

请求接口:

在这里插入图片描述

响应数据:

响应添加成功,我们看下数据库,是否添加上去了:

在这里插入图片描述

数据已经添加上去了,说明已经集成成功了,但这还是第一步,我们需要做的是支持多数据源,接下来我们一起来完成逼格更高的多数据源mongo吧。

【实现多数据源】

实现思路

先介绍下实现多数据源动态切换的思路:

首先通过AOP技术,在调用方法前后动态替换mongo数据源,这个主要是替换mongo中mongodbfactory(SimpleMongoClientDatabaseFactory)值,每个factory都维护自己需要连接的库,如果在操作之前,替换该参数为自己需要操作的数据库factory,操作结束又切换成原来的,不就可以实现动态切换数据源了么。

说完了思路,我们直接上代码吧

垒代码
  • 添加aop依赖
代码语言:javascript复制
     <!--引入AOP依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 修改数据库连接配置
代码语言:javascript复制
# 设置了用户名和密码的连接
spring:
  data:
    mongodb:
      uri: mongodb://用户名:密码@IP:PORT/#?authSource=${auth_db:用户认证数据库}
# 没有设置用户名和密码的连接配置
spring:
    data:
        mongodb:
            uri: mongodb://IP:PORT/#
            
与上述配置,做了小小的改动,将操作的数据库名称替换成了#,用来做后续备用
  • 创建切面
代码语言:javascript复制
package com.aimashi.dynamicmongo.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoDatabaseFactorySupport;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
 * Created by AI码师 on 2019/4/19.
 * 关注公众号【AI码师】领取2021最新面试资料一份(很全)
 * @return
 */
@Component
@Aspect
public class MongoSwitch {
  private final Logger logger = LoggerFactory.getLogger(MongoSwitch.class);

  @Autowired private MongoDatabaseFactorySupport mongoDbFactory;
  private final Map<String, MongoDatabaseFactorySupport> templateMuliteMap = new HashMap<>();
  // 获取配置文件的副本集连接
  @Value("${spring.data.mongodb.uri}")
  private String uri;

  // @Pointcut("@annotation(com.pig4cloud.pig.common.log.annotation.MongoLog)")
  @Pointcut("execution(public * com.aimashi.dynamicmongo.config.MongotemplteService.*(..))")
  public void routeMongoDB() {}

  @Around("routeMongoDB()")
  public Object routeMongoDB(ProceedingJoinPoint joinPoint) {
    Object result = null;
    // 获取需要访问的项目数据库
    String dbName = (String) joinPoint.getArgs()[0];
    Object o = joinPoint.getTarget();
    Field[] fields = o.getClass().getDeclaredFields();
    MultiMongoTemplate mongoTemplate = null;

    try {
      for (Field field : fields) {
        field.setAccessible(true);

        Class fieldclass = field.getType();
        // 找到Template的变量
        if (fieldclass == MongoTemplate.class || fieldclass == MultiMongoTemplate.class) {
          // 查找项目对应的MongFactory
          SimpleMongoClientDatabaseFactory simpleMongoClientDbFactory = null;
          // 实例化
          if (templateMuliteMap.get(dbName) == null) { // 替换数据源
            simpleMongoClientDbFactory =
                new SimpleMongoClientDatabaseFactory(this.uri.replace("#", dbName));
            templateMuliteMap.put(dbName, simpleMongoClientDbFactory);
          } else {
            simpleMongoClientDbFactory =
                (SimpleMongoClientDatabaseFactory) templateMuliteMap.get(dbName);
          }
          // 如果第一次,赋值成自定义的MongoTemplate子类
          if (fieldclass == MongoTemplate.class) {
            mongoTemplate = new MultiMongoTemplate(simpleMongoClientDbFactory);
          } else if (fieldclass == MultiMongoTemplate.class) {
            Object fieldObject = field.get(o);
            mongoTemplate = (MultiMongoTemplate) fieldObject;
          }
          // 设置MongoFactory
          mongoTemplate.setMongoDbFactory(simpleMongoClientDbFactory);
          // 重新赋值
          field.set(o, mongoTemplate);
          break;
        }
      }
      try {
        result = joinPoint.proceed();
        // 清理ThreadLocal的变量
        mongoTemplate.removeMongoDbFactory();
      } catch (Throwable t) {
        logger.error("", t);
        mongoTemplate.removeMongoDbFactory();
      }
    } catch (Exception e) {
      logger.error("", e);
    }

    return result;
  }
}
  • 创建相关配置类
代码语言:javascript复制
package com.aimashi.dynamicmongo.config;

import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
/**
 * Created by AI码师 on 2019/4/19.
 * 关注公众号【AI码师】领取2021最新面试资料一份(很全)
 * @return
 */
@Service
public class MongotemplteService {
  private MongoTemplate mongoTemplate;
  public <T> T save(String dbName, T var1) {
    return mongoTemplate.save(var1);
  }
}
代码语言:javascript复制
package com.aimashi.dynamicmongo.config;

import com.mongodb.client.MongoDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoDatabaseFactorySupport;
import org.springframework.data.mongodb.core.MongoTemplate;

public class MultiMongoTemplate extends MongoTemplate {
    private Logger logger= LoggerFactory.getLogger(MultiMongoTemplate.class);
//用来缓存当前MongoDbFactory
    private static ThreadLocal<MongoDatabaseFactorySupport> mongoDbFactoryThreadLocal;
    public MultiMongoTemplate(MongoDatabaseFactorySupport mongoDbFactory){
        super(mongoDbFactory);
        if(mongoDbFactoryThreadLocal==null) {
            mongoDbFactoryThreadLocal = new ThreadLocal<>();
        }
    }

    public void setMongoDbFactory(MongoDatabaseFactorySupport factory){
        mongoDbFactoryThreadLocal.set(factory);
    }

    public void removeMongoDbFactory(){
        mongoDbFactoryThreadLocal.remove();
    }

    @Override
    public MongoDatabase getDb() {
        return mongoDbFactoryThreadLocal.get().getMongoDatabase();
    }
}
  • 添加测试接口
代码语言:javascript复制
/**
 * Created by AI码师 on 2019/4/19.
 * 关注公众号【AI码师】领取2021最新面试资料一份(很全)
 * @return
 */
// dbName 为数据库名称
   @PutMapping
    public String addDataByDynamic(@RequestParam(value = "dbName") String dbName,@RequestParam(value = "name") String name,@RequestParam(value = "addr") String addr,@RequestParam(value = "email") String email){
        Student student = new Student();
        student.setAddr(addr);
        student.setName(name);
        student.setEmail(email);
        mongotemplteService.insert(dbName,student);
        return "添加成功";
    }

请求接口:数据库名参数传了ams1

在这里插入图片描述

请求响应:响应成功

我们看下数据库,发现在数据库ams1下面已经有了此数据:

在这里插入图片描述

我们将数据库名参数修改为:ams2,进行请求

发现数据源已经切换成功了。

到这里,大家有没有发现自己很牛逼了啊,不过本篇文章还没算完,现在虽然已经实现了动态切换数据源的功能,但是还只能在自己项目上用,别的项目需要使用,只能直接复制过去,我们接下来需要做一个更牛逼的事情:手写一个starter来封装这个功能,别人只需要引入依赖,即可开箱即用:

【整合到starter里面】

  • 创建一个maven项目:dynamicmongo-starter

将如下文件拷贝到新项目中

  • 创建自动装配文件
代码语言:javascript复制
package com.aimashi.dynamicmongo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created by AI码师 on 2019/4/19.
 * 关注公众号【AI码师】领取2021最新面试资料一份(很全)
 * @return
 */
@Configuration(proxyBeanMethods = false)
public class MongodbAutoConfiguration {

 @Bean
 public MongoSwitch mongoSwitch() {
  return new MongoSwitch();
 }
 @Bean
 public MongotemplteService mongotemplteService() {
  return new MongotemplteService();
 }

}
  • 新建 resources/META_INF/spring.factories 文件
代码语言:javascript复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
    com.aimashi.dynamicmongo.config.MongodbAutoConfiguration

到这里starter已经编写完成,是不是很简单。。

【使用starter】

starter已经编写好,我们只需要在项目中引入该依赖

代码语言:javascript复制
        <dependency>
            <groupId>com.aimashi</groupId>
            <artifactId>dynamicmongo-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

然后在需要操作mongod方法的地方,引入:MongotemplteService即可;

注意 MongotemplteService 里面的方法大家按需扩充,目前只写了一个,大家使用的时候,只需要把mongoTemplate里面的方法名写到MongotemplteService中,然后再去调用mongoTemplate里面对应方法即可。

【总结】

很少写这么长的实践类文章,现在已经十一点半了,该休息了,后面会有更多文章和大家一起分享,希望大家能有所收获,晚安!

0 人点赞