java mango源码,SpringBoot2.x下的Spring Data Mongo源码二次开发(1-动态切库)

版本信息:

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;

}

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值