背景

在开发中可能遇到这样的场景:比如使用MongoDB存储文件,但是同时又想支持MinIO 方式。在代码层面做了多种方式的接入。但就在做接入的时候遇到这样的问题:
在application.properties 中配置了MongoDB的连接:

file.storage.mode=MONGODB
spring.data.mongodb.host=MONGO_DB_HOST
spring.data.mongodb.database=MONGO_DB_DATABASE

熟悉SpringBoot 的小伙伴应该都知道,这样写是对应了SpingBoot 中的一个配置类的属性文件:org.springframework.boot.autoconfigure.mongo.MongoProperties

由于此处是用 MongoDB 来存储文件,另又认为MongoDB默认连接超时的时间过长。因此重新进行相关的类进行配置:

@Configuration
@ConditionalOnProperty(name = "file.storage.mode",havingValue = "MONGODB")
@EnableConfigurationProperties(MongoProperties.class)
public class MongoDBConfiguration {

    @Autowired
    private MongoProperties properties;

    @Bean
    public MongoClient mongoClient() {
        MongoClientOptions.Builder builder = MongoClientOptions.builder().
        connectTimeout(8000).serverSelectionTimeout(8000);
        return new MongoClient(properties.getHost(), builder.build());
    }

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient) {
        MongoDatabase database = mongoClient.getDatabase(properties.getDatabase());
        GridFSBucket bucket = GridFSBuckets.create(database);
        return bucket;
    }

}

现象

将MongoDB的配置写好后,启动系统测试上传文件,并没有问题,可以正常存进数据库中。但是将如果将application.properties 中的属性:file.storage.mode 改为:MinIO (ps: 此时系统不再需要配置MONGO_DB_HOST的值,直接空字符串就好。)但在系统启动时,会提示MongoDB连接失败, 并且连接的主机默认为:127.0.0.1。虽然并不影响系统后续的运行,但是总是看着这个报错不太舒服,再想想以后这个代码还要上到生产环境的就更不舒畅了。具体报错:

INFO 21364 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[127.0.0.1:27017], mode=SINGLE, requiredCluste
rType=UNKNOWN, serverSelectionTimeout='8000 ms', maxWaitQueueSize=500}
INFO 21364 --- [127.0.0.1:27017] org.mongodb.driver.cluster               : Exception in monitor thread while connecting to server 127.0.0.1:27017

com.mongodb.MongoSocketOpenException: Exception opening socket
        at com.mongodb.internal.connection.SocketStream.open(SocketStream.java:70)
        at com.mongodb.internal.connection.InternalStreamConnection.open(InternalStreamConnection.java:128)
        at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.run(DefaultServerMonitor.java:117)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.net.ConnectException: Connection refused: connect
        at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
        at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
        at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:606)
        at com.mongodb.internal.connection.SocketStreamHelper.initialize(SocketStreamHelper.java:64)
        at com.mongodb.internal.connection.SocketStream.initializeSocket(SocketStream.java:79)
        at com.mongodb.internal.connection.SocketStream.open(SocketStream.java:65)
        ... 3 common frames omitted

于是开始找解决的办法,也有同样的网友给出的答案是:将下面两个类的自动配置排除在Spring 容器外:

org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration

org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration

// 具体写法:在SpringBoot 启动类上加上注解 @SpringBootApplication
@SpringBootApplication(exclude = { MongoAutoConfiguration.class,MongoDataAutoConfiguration.class })

解决方式——暂且一试

在启动类上加上注解:@SpringBootApplication(exclude = { MongoAutoConfiguration.class,MongoDataAutoConfiguration.class }) 此时是没有启用MongoDB的情况,启动后也没有报错信息了,故事是按想象的发展。
打开MongoDB的配置,将 application.properties 中的属性:file.storage.mode 改为:MONGODB ,重新启动后,在启动过程中报错:找不到Bean:GridFsTemplate 。具体信息如下:

dependency expressed through field 'fileService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException:
  Error creating bean with name 'gridFSService': Unsatisfied dependency expressed through field 'gridFsTemplate';
   nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No qualifying bean of type 'org.springframework.data.mongodb.gridfs.GridFsTemplate' available: expected at least 1 bean which qualifies as autowire candidate.
    Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

这个类是用来存储文件或者查询文件的,总之是不可或缺的。猜测应该是去掉了SpringBoot 与MongoDB 整合的配置类导致的原因(因为如果不排除自动配置类,系统就可以正常启动)。

尝试中

总结上面的问题和现象,如果用MongoDB 少不了配置类,不用MongoDB 则不需要配置类,但是如何让 @SpringBootApplication 能够动态根据自己的配置进行是否排除自动配置类呢? 嗯,这东西是SpringBoot的东西,改它是不可能的了。此种方式行不通。换种方式: 既然提示找不到Bean:GridFsTemplate ,那是不是可以注入一个? 然后打开注解类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" })
	public MongoClient mongo(MongoProperties properties, ObjectProvider<MongoClientOptions> options,
			Environment environment) {
		return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable());
	}

}

这个类和我自己的配置类差别不大,就是配置了一个 MongoClient ,再看下一个:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ MongoClient.class, com.mongodb.client.MongoClient.class, MongoTemplate.class })
@EnableConfigurationProperties(MongoProperties.class)
@Import({ MongoDataConfiguration.class, MongoDbFactoryConfiguration.class, MongoDbFactoryDependentConfiguration.class })
@AutoConfigureAfter(MongoAutoConfiguration.class)
public class MongoDataAutoConfiguration {

}

这个类导入了三个配置类:MongoDataConfiguration,MongoDbFactoryConfiguration,MongoDbFactoryDependentConfiguration.
在最后一个类中,找到了我们需要的GridFsTemplate :

@Bean
@ConditionalOnMissingBean(GridFsOperations.class)
GridFsTemplate gridFsTemplate(MongoDbFactory mongoDbFactory, MongoTemplate mongoTemplate) {
	return new GridFsTemplate(new GridFsMongoDbFactory(mongoDbFactory, this.properties),
			mongoTemplate.getConverter());
}

尝试了一番,在这里 获取不到 MongoDbFactory 类。好吧,找同事讨论一下。

柳暗花明又一村

得同事启发,在spring-boot-autoconfigure 中的 spring.factories 文件中找到:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration

我们要找的就是 MongoDataAutoConfiguration 类了。查看源码:

package org.springframework.boot.autoconfigure.mongo;

import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" })
	public MongoClient mongo(MongoProperties properties, ObjectProvider<MongoClientOptions> options,
			Environment environment) {
		return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable());
	}

}

其它都没有问题,就是类上的注解 @ConditionalOnClass(MongoClient.class) 条件不符合这里的场景需求。
那既然SpringBoot 也可以这样写,不妨在我们自己的项目中新建一个重名的类: MongoAutoConfiguration 。类名和包名都和原有的保持一致,只是将其存放在自己的项目中。于是将类改造为如下:

package org.springframework.boot.autoconfigure.mongo;

import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 覆盖SpringBoot 自动配置类
 */
@Configuration
@ConditionalOnExpression(value = "'${spring.data.mongodb.host}'!=''")
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {

    @Autowired
    private MongoProperties properties;

    @Bean
    @ConditionalOnMissingBean(type = {"com.mongodb.MongoClient", "com.mongodb.client.MongoClient"})
    public MongoClient mongoClient() {
        MongoClientOptions.Builder builder = MongoClientOptions.builder().connectTimeout(8000).serverSelectionTimeout(8000);
        return new MongoClient(properties.getHost(), builder.build());
    }

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient) {
        MongoDatabase database = mongoClient.getDatabase(properties.getDatabase());
        GridFSBucket bucket = GridFSBuckets.create(database);
        return bucket;
    }

}

这样在系统自动配置MongoDB 时,如果没有配置MongoDB的host 就不注入MongoDB 相关的类,这也比较符合实际的使用。

总结

此种方式轻松瞒过类加载器,找最近的类,这也是不到万不得已的解决方式。暂未发现任何缺点。如果后续遇到再补充。