版本信息:
JDK:8
SpringBoot:2.1.3.RELEASE
spring-boot-starter-data-mongodb:2.1.3.RELEASE
JAVA操作Mongo的原生代码:
@Test
public void test3(){
try{
// 连接到 mongodb 服务
MongoClient mongoClient = new MongoClient( "localhost" , 27017);
// 连接到数据库
MongoDatabase mongoDatabase = mongoClient.getDatabase("test");
System.out.println("Connect to database successfully");
MongoCollection collection = mongoDatabase.
getCollection("student3");
//查询条件
Bson query = new BsonDocument();
//更新内容
Document update = new Document("$set", new Document("grades.$[elem]", 100));
//arrayFilter的数组过滤条件
UpdateOptions options=new UpdateOptions();
ArrayList bsons = new ArrayList<>();
bsons.add(Filters.gt("elem",100));
options.arrayFilters(bsons);
//执行结果
UpdateResult updateResult = collection.updateMany(query, update, options);
System.out.println("执行结果:"+JSON.toJSONString(updateResult));
}catch(Exception e){
System.err.println( e.getClass().getName() + ": " + e.getMessage() );
}
}
在Spring环境中,只需要在yml使用如下配置:
spring:
data:
mongodb:
host: localhost
port: 27017
database: test # 指定操作的数据库
就可以使用MongoTemplate对Mongo进行配置。
那么Spring是如何自动完成自动装载的。我们能否对源码进行个性化扩展呢?
1. 源码分析
在Spring4.3之前,一般是依赖注解(例如:@Autowired)完成属性的依赖注入。
Spring4.3后依赖注入有两个改动:
Bean单构造函数场景下可以不显式指定注入注解。Spring默认会完成依赖注入。例如MongoProperties配置参数,就是通过隐式方法(构造函数)来完成的依赖注入。
引入ObjectProvider,它是现有ObjectFactory接口的扩展。其作用相当于一个延迟类,会在项目启动通过getIfAvailable或者getIfUnique来检索Bean。
@Configuration
@ConditionalOnClass({ MongoClient.class, com.mongodb.client.MongoClient.class,
MongoTemplate.class })
@Conditional(AnyMongoClientAvailable.class)
@EnableConfigurationProperties(MongoProperties.class)
@Import(MongoDataConfiguration.class)
@AutoConfigureAfter(MongoAutoConfiguration.class)
public class MongoDataAutoConfiguration {
private final MongoProperties properties;
//对MongoProperties完成依赖注入
public MongoDataAutoConfiguration(MongoProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(MongoDbFactory.class)
public MongoDbFactorySupport> mongoDbFactory(ObjectProvider mongo,
ObjectProvider mongoClient) {
//项目启动后,通过getIfAvailable来判断Bean是否存在。
//若不使用ObjectProvider,那么bean不存在的话,会在项目启动时报错。
MongoClient preferredClient = mongo.getIfAvailable();
if (preferredClient != null) {
return new SimpleMongoDbFactory(preferredClient,
this.properties.getMongoClientDatabase());
}
com.mongodb.client.MongoClient fallbackClient = mongoClient.getIfAvailable();
if (fallbackClient != null) {
return new SimpleMongoClientDbFactory(fallbackClient,
this.properties.getMongoClientDatabase());
}
throw new IllegalStateException("Expected to find at least one MongoDB client.");
}
//重点关注!!!MongoDbFactory这个Bean对象。
@Bean
@ConditionalOnMissingBean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,
MongoConverter converter) {
return new MongoTemplate(mongoDbFactory, converter);
}
@Bean
@ConditionalOnMissingBean(MongoConverter.class)
public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory,
MongoMappingContext context, MongoCustomConversions conversions) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver,
context);
mappingConverter.setCustomConversions(conversions);
return mappingConverter;
}
}
上面这段代码,就是将MongoTemplate注入到Spring容器中,使用下面代码就可以操作mongo。
@Service
@Slf4j
public class MongoDbService {
@Autowired
private MongoTemplate mongoTemplate;
public List findAll() {
return mongoTemplate.findAll(Book.class);
}
}
2. MongoDbFactory
我们想操纵mongo,就必须先创建MongoDatabase,我们如何去创建MongoDatabase。原生代码中用到了MongoClient和dbName。
MongoClient mongoClient = new MongoClient( "localhost" , 27017);
//“test”就是数据库名。
MongoDatabase mongoDatabase = mongoClient.getDatabase("test");
Spring创建SimpleMongoDbFactory时,也将MongoClient和dbName参数通过构造方法传入。
2.1 查看顶级接口的行为规范
接口是一种行为规范,需要先了解顶级接口。
MongoDbFactory接口是如下描述的:“创建MongoDatabase实例。”
Interface for factories creating {@link MongoDatabase} instances.
原来Spring依赖MongoDbFactory来生成MongoDatabase实例!
public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvider {
/**
* Creates a default {@link MongoDatabase} instance.
*
* @return
* @throws DataAccessException
*/
MongoDatabase getDb() throws DataAccessException;
/**
* Creates a {@link DB} instance to access the database with the given name.
*
* @param dbName must not be {@literal null} or empty.
* @return
* @throws DataAccessException
*/
MongoDatabase getDb(String dbName) throws DataAccessException;
/**
* Exposes a shared {@link MongoExceptionTranslator}.
*
* @return will never be {@literal null}.
*/
PersistenceExceptionTranslator getExceptionTranslator();
/**
* Get the legacy database entry point. Please consider {@link #getDb()} instead.
*
* @return
* @deprecated since 2.1, use {@link #getDb()}. This method will be removed with a future version as it works only
* with the legacy MongoDB driver.
*/
@Deprecated
DB getLegacyDb();
/**
* Get the underlying {@link CodecRegistry} used by the MongoDB Java driver.
*
* @return never {@literal null}.
*/
@Override
default CodecRegistry getCodecRegistry() {
return getDb().getCodecRegistry();
}
/**
* Obtain a {@link ClientSession} for given ClientSessionOptions.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
ClientSession getSession(ClientSessionOptions options);
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances
* that are aware and bound to a new session with given {@link ClientSessionOptions options}.
*
* @param options must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
default MongoDbFactory withSession(ClientSessionOptions options) {
return withSession(getSession(options));
}
/**
* Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances
* that are aware and bound to the given session.
*
* @param session must not be {@literal null}.
* @return never {@literal null}.
* @since 2.1
*/
MongoDbFactory withSession(ClientSession session);
/**
* Returns if the given {@link MongoDbFactory} is bound to a {@link ClientSession} that has an
* {@link ClientSession#hasActiveTransaction() active transaction}.
*
* @return {@literal true} if there's an active transaction, {@literal false} otherwise.
* @since 2.1.3
*/
default boolean isTransactionActive() {
return false;
}
}
接口中我们需要关注getDb()方法和getDb(String dbName)这两个方法,这两个方法负责输出MongoDatabase对象。而MongoTemplate正是依赖MongoDatabase对象去操纵mongo。
很明显:若想实现动态切库,那么必须要重写getDb方法。其实就是使用装饰器模式加强功能。
2.2 Spring默认使用的实现类
@Bean
@ConditionalOnMissingBean(MongoDbFactory.class)
public MongoDbFactorySupport> mongoDbFactory(ObjectProvider mongo,
ObjectProvider mongoClient) {
//在Spring容器获取MongoClient 对象
MongoClient preferredClient = mongo.getIfAvailable();
if (preferredClient != null) {
// this.properties.getMongoClientDatabase()就是yml配置的dbName。
return new SimpleMongoDbFactory(preferredClient,
this.properties.getMongoClientDatabase());
}
com.mongodb.client.MongoClient fallbackClient = mongoClient.getIfAvailable();
if (fallbackClient != null) {
return new SimpleMongoClientDbFactory(fallbackClient,
this.properties.getMongoClientDatabase());
}
throw new IllegalStateException("Expected to find at least one MongoDB client.");
}
Spring使用SimpleMongoDbFactory来去实现。我们需要关注的是getDb方法!!!
Spring使用模板方法模式,即父类定义算法骨架,子类只需要实现个性化功能。
在SimpleMongoDbFactory父类MongoDbFactorySupport中,定义了两个方法逻辑。
public abstract class MongoDbFactorySupport implements MongoDbFactory {
private final C mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
private final PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
//获取默认的MongoDatabase库的实例
public MongoDatabase getDb() throws DataAccessException {
return getDb(databaseName);
}
//根据传入的dbName来获取MongoDatabase库的实例
@Override
public MongoDatabase getDb(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty!");
//需要子类实现的钩子方法(一般是do开头)
MongoDatabase db = doGetMongoDatabase(dbName);
if (writeConcern == null) {
return db;
}
return db.withWriteConcern(writeConcern);
}
//抽象方法,又子类去实现。看注释:由client生成实际的MongoDatabase,参数dbName不会为空。
/**
* Get the actual {@link MongoDatabase} from the client.
*
* @param dbName must not be {@literal null} or empty.
* @return
*/
protected abstract MongoDatabase doGetMongoDatabase(String dbName);
//可以看做是参数的getter地方,
/**
* @return the Mongo client object.
*/
protected C getMongoClient() {
return mongoClient;
}
}
上面说到,生成MongoDatabase两个要素:(1)dbName(2)MongoClient。MongoDbFactorySupport让子类去生成个性化的MongoDatabase对象。那么我们看一下子类的实现。
public class SimpleMongoDbFactory extends MongoDbFactorySupport implements DisposableBean {
//子类是这样去实现的
protected MongoDatabase doGetMongoDatabase(String dbName) {
return getMongoClient().getDatabase(dbName);
}
}
子类调用MongoDbFactorySupport.getMongoClient()方法来获取MongoClient。然后使用dbName生成MongoDatabase对象!!!
而MongoClient和databaseName就是Spring初始化MongoDbFactorySupport>时通过构造函数传入的。
对于getDb方法,SimpleMongoDbFactory类啥也没干!!!!而我们就需要去重写doGetMongoDatabase方法,来完成动态切切库
3. 二次开发源码
SimpleMongoDbFactory类最后生成的MongoDatabase对象,实际使用的就是yml的配置参数。但是配置参数只能写死,不能根据请求去动态切库。
请求头未携带数据时,使用配置文件默认的配置;
请求头携带指定数据,灵活切库。
使用ThreadLocal来完成这个需求。
注意一点是:每个线程中都有一个ThreadLocalMap集合,执行的threadLocal.set(data)方法,只是将threadLocal引用作为key,data作为value。作为一个元素放入线程的ThreadLocalMap中!!!线程就可以想在哪用就在哪用。
线程进入时,在拦截去中解析请求。获取databaseName和MongoClient放入线程的ThreadLocalMap中。
线程使用MongoDbFactory来生成MongoDatabase对象,在ThreadLocalMap获取到实际的MongoClient和databaseName来完成创建。
3.线程离开时,销毁ThreadLocalMap对应的记录。
3.1 二次改造源码
装饰器模式扩展SimpleMongoDbFactor类。
public class DynamicSimpleMongoDbFactory extends SimpleMongoDbFactory {
private static ThreadLocal dbNameThreadLocal = new ThreadLocal<>();
private static ThreadLocal mongoClientThreadLocal = new ThreadLocal<>();
public static ThreadLocal getDbNameThreadLocal() {
return dbNameThreadLocal;
}
public static ThreadLocal getMongoClientThreadLocal() {
return mongoClientThreadLocal;
}
public DynamicSimpleMongoDbFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
}
//根据请求参数灵活的切换数据库
//可以在一个ip:host下自由切库,也可在多个ip:host下自由切库。
@Override
protected MongoDatabase doGetMongoDatabase(String dbName) {
//是否动态切库?
String dynamicDbName = dbNameThreadLocal.get();
if (dynamicDbName != null) {
dbName = dynamicDbName;
}
//是否修改mongoClient?
MongoClient mongoClient = mongoClientThreadLocal.get();
if (mongoClient == null) {
mongoClient = getMongoClient();
}
return mongoClient.getDatabase(dbName);
}
}
过滤器处理请求
@Component
@WebFilter(urlPatterns = "/", filterName = "mongoFilter")
@Order(Integer.MAX_VALUE - 4)
@Slf4j
public class MongoFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ThreadLocal dbThreadLocal = null;
ThreadLocal mongoClientThreadLocal = null;
try {
//读取请求参数
HttpServletRequest request = (HttpServletRequest) servletRequest;
String dbName = request.getHeader("dbName");
if (StringUtils.isNotBlank(dbName)) {
log.info("[连接的Mongo库为{}]", dbName);
dbThreadLocal = DynamicSimpleMongoDbFactory.getDbNameThreadLocal();
dbThreadLocal.set(dbName);
MongoClient mongoClient = null;
//比如:知道pro库在另一台服务器上,那么切换mongoClient。
//这里可以使用枚举来充当数据字典(此处仅是一个案例)
if ("pro".equals(dbName))
log.info("[连接的Mongo库为{}]", "pro");
//MongoClientDepository是一个仓库,缓存mongoClient对象
mongoClient = MongoClientDepository.getMongoClient("pro");
mongoClientThreadLocal = DynamicSimpleMongoDbFactory.getMongoClientThreadLocal();
mongoClientThreadLocal.set(mongoClient);
}
filterChain.doFilter(servletRequest, servletResponse);
} finally {
if (dbThreadLocal != null)
dbThreadLocal.remove();
if (mongoClientThreadLocal != null)
mongoClientThreadLocal.remove();
}
}
}
MongoClient仓库(伪代码)
public class MongoClientDepository {
private static Map mongoClientMap = new ConcurrentHashMap<>();
//获取MongoClient的值
public static MongoClient getMongoClient(String name) {
return mongoClientMap.computeIfAbsent(name, (k) -> {
//读取配置(简易代码,其实是读取配置文件的数据,创建的MongoClient并缓存)
//代碼參考org.springframework.boot.autoconfigure.mongo.MongoClientFactory.createNetworkMongoClient
String host = "127.0.0.2";
int port = 20010;
List seeds = Collections
.singletonList(new ServerAddress(host, port));
MongoClientOptions options = MongoClientOptions.builder().build();
return new MongoClient(seeds, options);
});
}
}
各种配置(因为手动注入mongoDbFactory后,Spring自动注入的配置会失效。所以自己需要重新注入下。)
简易代码:
@Configuration
@EnableConfigurationProperties(MongoProperties.class)
public class MyMonogoConfig {
@Autowired
private MongoProperties properties;
@Bean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,
MongoConverter converter) {
return new MongoTemplate(mongoDbFactory, converter);
}
@Bean
public MongoDbFactory mongoDbFactory(ObjectProvider mongo,
ObjectProvider mongoClient) {
MongoClient preferredClient = mongo.getIfAvailable();
if (preferredClient != null) {
// 返回自定义的动态扩展的DynamicSimpleMongoDbFactory对象。
return new DynamicSimpleMongoDbFactory(preferredClient,
this.properties.getMongoClientDatabase());
}
com.mongodb.client.MongoClient fallbackClient = mongoClient.getIfAvailable();
if (fallbackClient != null) {
return new SimpleMongoClientDbFactory(fallbackClient,
this.properties.getMongoClientDatabase());
}
throw new IllegalStateException("Expected to find at least one MongoDB client.");
}
}
@Configuration
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
public class MyMongoAutoConfiguration {
private final MongoClientOptions options;
private final MongoClientFactory factory;
private MongoClient mongo;
public MyMongoAutoConfiguration(MongoProperties properties,
ObjectProvider options, Environment environment) {
this.options = options.getIfAvailable();
//读取的是配置文件的属性,用于创建MongoClient
this.factory = new MongoClientFactory(properties, environment);
}
@PreDestroy
public void close() {
if (this.mongo != null) {
this.mongo.close();
}
}
@Bean
public MongoClient mongo() {
this.mongo = this.factory.createMongoClient(this.options);
return this.mongo;
}
}