目 录CONTENT

文章目录

SpringBoot多数据源集成——MongoDB

陌念
2024-09-09 / 0 评论 / 7 点赞 / 56 阅读 / 0 字
温馨提示:
本文最后更新于2024-10-26,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

在Spring Boot项目中实现根据条件动态切换到不同的MongoDB数据库进行查询,可以通过配置多数据源来实现。需要为每个数据库配置一个数据源,然后根据业务逻辑动态选择数据源进行操作。基于条件动态切换不同的MongoDB数据库,可以使用AbstractRoutingDataSource来配置多数据源,并结合ThreadLocal来存储当前线程所需的数据源标识。

一、引入依赖

<dependencies>
    <!-- Spring Data MongoDB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <!-- 如果使用注解驱动配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

二、配置多数据源

需要配置两个或多个MongoDB数据源。假设我们有两个数据源:defaultcsfa

1、添加配置文件

spring:
  data:
    mongodb:
      host: 127.0.0.1
      port: 27017
      database: dmp
      csfaDatabase: dmp_csfa
      # 如果设置了用户名密码,加上如下配置
      username: root
      password: 123456

2、读取配置类

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;

@Data
@Configuration
// 根据前缀读取配置文件中对应的配置
@ConfigurationProperties(prefix = "spring.data.mongodb")
public class MongoParamsConfig {

  private String host;

  private int port;

  private String database;

  private String csfaDatabase;

  private String username;

  private String password;
}

3、配置多数据源

import java.util.Collections;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;

@Configuration
public class MongoConfig {

  // 4、扩展类
  @Resource
  private MappingMongoConverter mappingMongoConverter;

  @Resource
  private MongoParamsConfig params;

  @Primary
  @Bean(name = "mongoTemplate")
  public MongoTemplate mongoTemplate() {
    return createMongoTemplate(params.getDatabase());
  }

  @Bean(name = "csfaMongoTemplate")
  public MongoTemplate csfaMongoTemplate() {
    return createMongoTemplate(params.getCsfaDatabase());
  }

  private MongoTemplate createMongoTemplate(String dbName) {
    MongoClient mongoClient;
    if (StringUtils.isBlank(params.getUsername()) || StringUtils.isBlank(params.getPassword())) {
      String uri = "mongodb://" + params.getHost() + ":" + params.getPort();
      mongoClient = MongoClients.create(uri);
    } else {
      MongoClientSettings settings = MongoClientSettings.builder()
          .applyToClusterSettings(builder -> builder.hosts(
              Collections.singletonList(new ServerAddress(params.getHost(), params.getPort()))))
          .credential(MongoCredential.createCredential(params.getUsername(), dbName,
              params.getPassword().toCharArray()))
          .build();
      mongoClient = MongoClients.create(settings);
    }

    MongoDbFactory mongoDbFactory = new SimpleMongoClientDbFactory(mongoClient, dbName);
    return new MongoTemplate(mongoDbFactory, mappingMongoConverter);
  }

}

如上使用密码和不使用密码创建mongoTemplate 是有区别的。

4、扩展类

以下配置类是为了在保存数据库的时候不保存_class 属性。

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

/**
 * When using spring data mongo it by default adds a _class key to your collection to be able to
 * handle inheritance. But if your domain model is simple and flat, you can remove it by overriding
 * the default MappingMongoConverter
 */
@Configuration
public class MongoExtendConfig {

  @Bean("mappingMongoConverter")
  public MongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context,
      BeanFactory beanFactory) {
    DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
    MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
    try {
      mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
    } catch (NoSuchBeanDefinitionException ignore) {
    }

    // Don't save _class to mongo
    mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));

    return mappingConverter;
  }
}

三、使用动态数据源

1、手动

根据标识去选择使用对应的数据源

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import com.rongxin.dmp.mdm.config.datasource.DynamicDataSource;

@Component
public class DynamicMongoTemplate {

  @Resource
  private MongoTemplate defaultMongoTemplate;

  @Resource
  private MongoTemplate csfaMongoTemplate;

  private final Map<String, MongoTemplate> cache = new ConcurrentHashMap<>();

  public MongoTemplate getMongoTemplate() {
    String dataSource = DynamicDataSource.getDataSourceKey();
    return cache.computeIfAbsent(dataSource, this::getMongoTemplate);
  }

  private MongoTemplate getMongoTemplate(String dataSource) {
    switch (dataSource) {
      case "csfa":
        return csfaMongoTemplate;
      case "cac":
      default:
        return defaultMongoTemplate;
    }
  }

}

上面的DynamicDataSource.getDataSourceKey() 可以参考下面的文章:

https://halo.wyong.fun/archives/1725088423801#%E5%9B%9B%E3%80%81%E9%85%8D%E7%BD%AE%E6%95%B0%E6%8D%AE%E6%BA%90%E5%88%87%E6%8D%A2

有了上面的配置之后,就可以使用下面的方法灵活切换MongoTemplate数据源。

public class CollectServiceImpl implements ICollectervice {
  @Resource
  private DynamicMongoTemplate dynamicMongoTemplate;

  public Boolean isEnable(String id) {
    DynamicDataSource.setDataSourceKey('csfa');
    Collect collect = dynamicMongoTemplate.getMongoTemplate().findById(id, Collect.class);
  }
}

2、自动

可以通过请求头中的参数动态切换数据源可参考下面的文章:

https://halo.wyong.fun/archives/1725088423801#3.-%E4%BD%BF%E7%94%A8%E6%8B%A6%E6%88%AA%E5%99%A8%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2

四、集成MongoRepository

1、创建Mongo文档对应实体类

@Document(collection = "collect.data")
@Data
public class CollectData {
  @Id
  private String id;

  private String name;

  private Integer age;
}

2、创建仓库实现类

继承接口MongoRepository 则可以使用一些内置方法,如findById、save、updateById

import com.rongxin.dmp.mdm.collect.mongodb.custom.CollectCustomRepo;
import com.rongxin.dmp.mdm.collect.mongodb.domain.CollectData;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface CollectDataRepo extends MongoRepository<CollectData, String> {}

3、创建AOP切面

使用AOP切面可以实现在代码中无侵入的动态切换MongoRepository 数据源。

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
import javax.annotation.Resource;
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.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.framework.ReflectiveMethodInvocation;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.rongxin.dmp.mdm.config.datasource.DynamicDataSource;
import com.rongxin.dmp.mdm.config.mongo.DynamicMongoTemplate;
import lombok.extern.slf4j.Slf4j;

/**
 * 通过AOP操作,动态更改更改mongo的repository层mongoTemplate 以实现mongo分库
 * 
 * @date 2024年08月29日
 * @author wyong
 */
@Slf4j
@Aspect
@Component
public class MongoRepoAspect {

  @Resource
  private DynamicMongoTemplate dynamicMongoTemplate;

  // 该切入点匹配所有执行org.springframework.data.mongodb.repository.MongoRepository接口及其子接口中的任意方法的连接点,换句话说,它匹配所有Spring Data MongoDB库中的MongoRepository接口及其子接口中的方法调用。
  @Pointcut("execution(* org.springframework.data.mongodb.repository.MongoRepository+.*(..))")
  public void repositoryMethods2() {}

  @Around("repositoryMethods2()")
  public Object setMongoOperations(ProceedingJoinPoint joinPoint) throws Throwable {
    setMongoTemplate4Repository(joinPoint, dynamicMongoTemplate.getMongoTemplate());
    return joinPoint.proceed();
  }

  private void setMongoTemplate4Repository(ProceedingJoinPoint joinPoint, MongoTemplate template)
      throws NoSuchFieldException, IllegalAccessException {
    String name = joinPoint.getSignature().getName();
    if ("toString".equals(name)) {
      return;
    }
    // 通过反射获取到target
    Field methodInvocationField = joinPoint.getClass().getDeclaredField("methodInvocation");
    methodInvocationField.setAccessible(true);
    ReflectiveMethodInvocation invocation =
        (ReflectiveMethodInvocation) methodInvocationField.get(joinPoint);

    Method method = invocation.getMethod();
    Object[] arguments = invocation.getArguments();

    // 获取目标对象
    Object target = invocation.getThis();

    // 获得SimpleMongoRepository,并往里面填入指定mongoTemplate
    Object singletonTarget = AopProxyUtils.getSingletonTarget(target);
    if (Objects.isNull(singletonTarget)) {
      singletonTarget = target;
    }

    Field mongoOperationsField = singletonTarget.getClass().getDeclaredField("mongoOperations");
    mongoOperationsField.setAccessible(true);
    mongoOperationsField.set(singletonTarget, template);
  }

}

4、使用动态MongoRepository

在要使用的地方,直接引入CollectDataRepo 即可。

public class CollectServiceImpl implements ICollectervice {
  @Resource
  private CollectDataRepo dataRepo;

  public CollectData find(String id) {
    return dataRepo.findById(id).get()
  }
}

注意:

这里需要注意一个问题,上面的AOP切面只会对内置的方法生效,如findById、save、updateById 等方法;

根据规则创建的自定义方法,如findByName、findByAge 等方法是不会生效的,因为自定义的方法,Spring Data Mongo 框架会去解析方法名,然后去组装成查询条件调用MongoTemplate去查询,使用的是默认数据源查询,所以无论怎么设置,自定义方法都不会生效。

想要使自定义方法生效,也有解决方法,在第六点介绍。

五、MongoTemplate和MongoRepository的机制

1、问题

那就又引出一个问题,为什么MongoRepository可以使用AOP切面 动态切换数据源,那MongoTemplate是不是也可以这么做呢? ??

2、原因

Spring Data MongoDB中,MongoTemplate 一旦创建,通常是不变的。如果你需要在运行时更改 MongoTemplate 的配置,最直接的方式确实是创建一个新的 MongoTemplate 实例。MongoTemplate 是不可变的,主要是为了保证线程安全和一致性。

MongoTemplate和MongoRepository

六、MongoRepository自定义方法

因为自定义方法是调用MongoTemplate去查询,所以有两个解决方法:

  1. 不使用自定义的方法,直接使用MongoTemplate查询。

  2. 通过自定义一个接口和实现类,然后使用继承Repository的接口再继承新创建的接口,则可以在不改动原有代码逻辑的基础上,实现动态数据源切换。

1、创建自定义接口

public interface CollectCustomRepo {
  CollectData findByName(String name);
  CollectData findByAge(String age);
}

2、创建实现类

public class CollectCustomRepoImpl implements CollectCustomRepo {

  @Resource
  DynamicMongoTemplate dynamicMongoTemplate;

  @Override
  public List<CollectData> findByName(String name) {
    return dynamicMongoTemplate.getMongoTemplate().find(Query.query(Criteria.where("name").is("zhangsan")), CollectData.class);
  }
  
  @Override
  public List<CollectData> findByAge(String age) {
    return dynamicMongoTemplate.getMongoTemplate().find(Query.query(Criteria.where("age").is(23)), CollectData.class);
  }
}

3、CollectDataRepo 继承接口 CollectCustomRepo

/**
 * 因使用多数据源,自定义方法不在该处定义,转而在 CollectCustomRepo 中定义
 */
public interface CollectDataRepo extends MongoRepository<CollectData, String>, CollectCustomRepo {}

4、上手使用

@Service
public class CollectServiceImpl implements CollectService {
  @Resource
  private CollectDataRepo dataRepo;

  public List<CollectData> findByName(String name) {
    return dataRepo.findByName(name);
  }
}

第二种方法适用于改造已经大量使用MongoRepository自定义方法的项目,可以在不改动原有逻辑的情况下,实现数据源动态切换。

7
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
  3. QQ打赏

    qrcode qq

评论区