在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数据源。假设我们有两个数据源:default
和csfa
。
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()
可以参考下面的文章:
有了上面的配置之后,就可以使用下面的方法灵活切换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、自动
可以通过请求头中的参数动态切换数据源可参考下面的文章:
四、集成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 是不可变的,主要是为了保证线程安全和一致性。
六、MongoRepository自定义方法
因为自定义方法是调用
MongoTemplate
去查询,所以有两个解决方法:
不使用自定义的方法,直接使用
MongoTemplate
查询。通过自定义一个接口和实现类,然后使用继承
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
自定义方法的项目,可以在不改动原有逻辑的情况下,实现数据源动态切换。
评论区