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<Document> collection = mongoDatabase.
                    getCollection("student3");
            //查询条件
            Bson query = new BsonDocument();
            //更新内容
            Document update = new Document("$set", new Document("grades.$[elem]", 100));
            //arrayFilter的数组过滤条件
            UpdateOptions options=new UpdateOptions();
            ArrayList<Bson> 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. 源码分析

spring ObjectProvider 源码分析

在Spring4.3之前,一般是依赖注解(例如:@Autowired)完成属性的依赖注入。

Spring4.3后依赖注入有两个改动:

  1. Bean单构造函数场景下可以不显式指定注入注解。Spring默认会完成依赖注入。例如MongoProperties配置参数,就是通过隐式方法(构造函数)来完成的依赖注入。

  2. 引入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<MongoClient> mongo,
            ObjectProvider<com.mongodb.client.MongoClient> 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<Book> findAll() {
        return mongoTemplate.findAll(Book.class);
    }
}

2. MongoDbFactory

我们想操纵mongo,就必须先创建MongoDatabase,我们如何去创建MongoDatabase。原生代码中用到了MongoClientdbName

MongoClient mongoClient = new MongoClient( "localhost" , 27017);
//“test”就是数据库名。
MongoDatabase mongoDatabase = mongoClient.getDatabase("test");

Spring创建SimpleMongoDbFactory时,也将MongoClientdbName参数通过构造方法传入。

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<MongoClient> mongo,
            ObjectProvider<com.mongodb.client.MongoClient> 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<C> 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<MongoClient> implements DisposableBean {
    //子类是这样去实现的
    protected MongoDatabase doGetMongoDatabase(String dbName) {
        return getMongoClient().getDatabase(dbName);
    }
}

子类调用MongoDbFactorySupport.getMongoClient()方法来获取MongoClient。然后使用dbName生成MongoDatabase对象!!!

MongoClientdatabaseName就是Spring初始化MongoDbFactorySupport<?>时通过构造函数传入的。

对于getDb方法,SimpleMongoDbFactory类啥也没干!!!!而我们就需要去重写doGetMongoDatabase方法,来完成动态切切库

3. 二次开发源码

SimpleMongoDbFactory类最后生成的MongoDatabase对象,实际使用的就是yml的配置参数。但是配置参数只能写死,不能根据请求去动态切库。

  1. 请求头未携带数据时,使用配置文件默认的配置;
  2. 请求头携带指定数据,灵活切库。

使用ThreadLocal来完成这个需求。

注意一点是:每个线程中都有一个ThreadLocalMap集合,执行的threadLocal.set(data)方法,只是将threadLocal引用作为keydata作为value。作为一个元素放入线程的ThreadLocalMap中!!!线程就可以想在哪用就在哪用。

  1. 线程进入时,在拦截去中解析请求。获取databaseNameMongoClient放入线程的ThreadLocalMap中。

  2. 线程使用MongoDbFactory来生成MongoDatabase对象,在ThreadLocalMap获取到实际的MongoClientdatabaseName来完成创建。

3.线程离开时,销毁ThreadLocalMap对应的记录。

3.1 二次改造源码

  1. 装饰器模式扩展SimpleMongoDbFactor类。
public class DynamicSimpleMongoDbFactory extends SimpleMongoDbFactory {

    private static ThreadLocal<String> dbNameThreadLocal = new ThreadLocal<>();
    private static ThreadLocal<MongoClient> mongoClientThreadLocal = new ThreadLocal<>();

    public static ThreadLocal<String> getDbNameThreadLocal() {
        return dbNameThreadLocal;
    }

    public static ThreadLocal<MongoClient> 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);
    }

}
  1. 过滤器处理请求
@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<String> dbThreadLocal = null;
        ThreadLocal<MongoClient> 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();
        }
    }
}
  1. MongoClient仓库(伪代码)
public class MongoClientDepository {

    private static Map<String, MongoClient> 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<ServerAddress> seeds = Collections
                    .singletonList(new ServerAddress(host, port));
            MongoClientOptions options = MongoClientOptions.builder().build();
            return new MongoClient(seeds, options);
        });
    }
}
  1. 各种配置(因为手动注入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<MongoClient> mongo,
                                         ObjectProvider<com.mongodb.client.MongoClient> 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<MongoClientOptions> 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;
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容