1、概览 {#1概览}
本文将带你了解 Spring Boot 3.1 中引入的 ConnectionDetails 接口,用于编程式定义连接属性(可从外部服务中加载)。Spring Boot 提供了开箱即用的抽象,用于与远程服务集成,如关系型数据库、NoSQL 数据库、消息队列服务等
传统上,连接信息都是配置在 application.properties
文件中的。因此,如果需要从 AWS Secret Manager、Hashicorp Vault 等外部服务加载连接配置的话就比较麻烦。
为了解决这个问题,Spring Boot 引入了 ConnectionDetails
。这个接口是空的,即标记接口。Spring 提供了该接口的子接口,如 JdbcConnectionDetails
、CassandraConnectionDetails
、KafkaConnectionDetails
等。它们可以在 Spring 配置类中实现和指定为 Bean。之后,Spring 将依赖这些配置 Bean 来动态检索连接属性,而不是静态的 application.properties
文件。
2、场景 {#2场景}
把敏感的配置信息(如数据库)定义在 application.properties
文件中存在一定的安全隐患。最好的方式是使用专业的私密信息存储服务。
HashiCorp Vault 是一款企业级私密信息管理工具,用于安全地存储和访问敏感数据,例如API密钥、数据库凭据、密码等。支持多种认证方法和加密机制,并提供 API 和命令行界面,以便与应用程序和工具集成。还支持动态秘密生成、密钥轮换和审计日志等功能,使数据的管理和安全性更加便捷和可靠。
Spring Boot 从 Vault 中读取敏感信息的流程如下:
Spring Boot 通过 Secret Key 调用 Vault 服务来获取连接配置信息。然后,使用该配置和远程服务创建连接。
4、ConnectionDetails
{#4connectiondetails}
通过 ConnectionDetails
接口,Spring Boot 应用可以自行发现连接细节(Connection Detail),而无需任何手动干预。注意,ConnectionDetails
优先于 application.properties
文件。不过,仍有一些非连接属性(如 JDBC 连接池大小)可以通过 application.properties
文件进行配置。
接下来,本文通过 Spring Boot Docker Compose 特性,展示各种 ConnectionDetails
实现类的实际应用。
4.1、JDBC {#41jdbc}
以 Spring Boot 应用与 Postgres 数据库的整合为例。
从类关系图开始:
在上述类体系图中,JdbcConnectionDetails
接口来自 Spring Boot 框架。
自定义 PostgresConnectionDetails
类继承它,并且实现了从 Vault 获取配置的接口方法:
public class PostgresConnectionDetails implements JdbcConnectionDetails {
@Override
public String getUsername() {
return VaultAdapter.getSecret("postgres_user_key");
}
@Override
public String getPassword() {
return VaultAdapter.getSecret("postgres_secret_key");
}
@Override
public String getJdbcUrl() {
return VaultAdapter.getSecret("postgres_jdbc_url");
}
}
定义 JdbcConnectionDetailsConfiguration
配置类,用于配置 PostgresConnectionDetails
Bean:
@Configuration(proxyBeanMethods = false)
public class JdbcConnectionDetailsConfiguration {
@Bean
@Primary
public JdbcConnectionDetails getPostgresConnection() {
return new PostgresConnectionDetails();
}
}
使用 Docker Compose 启动 Postgres 数据库容器,Spring Boot 会自动创建一个 ConnectionDetails
Bean,其中包含必要的连接信息。因此,使用 @Primary
注解让 JdbcConectionDetails
Bean 优先于它。
测试:
@Test
public void givenSecretVault_whenIntegrateWithPostgres_thenConnectionSuccessful() {
String sql = "select current_date;";
Date date = jdbcTemplate.queryForObject(sql, Date.class);
assertEquals(LocalDate.now().toString(), date.toString());
}
不出所料,应用连接到数据库并成功获取结果。
4.2、Rabbit MQ {#42rabbit-mq}
与 JdbcConnectionDetails
类似,Spring Boot 也提供了用于与 RabbitMQ Server 集成的接口 RabbitConnectionDetails。
创建 RabbitMQConnectionDetails
类,实现 RabbitConnectionDetails
接口,从 Vault 获取连接属性:
public class RabbitMQConnectionDetails implements RabbitConnectionDetails {
@Override
public String getUsername() {
return VaultAdapter.getSecret("rabbitmq_username");
}
@Override
public String getPassword() {
return VaultAdapter.getSecret("rabbitmq_password");
}
@Override
public String getVirtualHost() {
return "/";
}
@Override
public List<Address> getAddresses() {
return List.of(this.getFirstAddress());
}
@Override
public Address getFirstAddress() {
return new Address(VaultAdapter.getSecret("rabbitmq_host"),
Integer.valueOf(VaultAdapter.getSecret("rabbitmq_port")));
}
}
定义配置类 RabbitMQConnectionDetailsConfiguration
,用于配置 RabbitMQConnectionDetails
Bean:
@Configuration(proxyBeanMethods = false)
public class RabbitMQConnectionDetailsConfiguration {
@Primary
@Bean
public RabbitConnectionDetails getRabbitmqConnection() {
return new RabbitMQConnectionDetails();
}
}
最后,进行测试:
@Test
public void givenSecretVault_whenPublishMessageToRabbitmq_thenSuccess() {
final String MSG = "this is a test message";
this.rabbitTemplate.convertAndSend(queueName, MSG);
assertEquals(MSG, this.rabbitTemplate.receiveAndConvert(queueName));
}
上述方法向 RabbitMQ 中的队列发送消息,然后读取消息。Spring Boot 通过 RabbitMQConnectionDetails
Bean 中的连接信息自动配置了 rabbitTemplate
对象。我们将 rabbitTemplate
对象注入测试类,然后在上述测试方法中使用它。
4.3、Redis {#43redis}
现在,来看看用于 Redis 的 Spring ConnectionDetails
。
类体系图:
创建 RedisCacheConnectionDetails
类,实现 RedisConnectionDetails
,从远程 Vault 中读取 Redis 的连接信息:
public class RedisCacheConnectionDetails implements RedisConnectionDetails {
@Override
public String getPassword() {
return VaultAdapter.getSecret("redis_password");
}
@Override
public Standalone getStandalone() {
return new Standalone() {
@Override
public String getHost() {
return VaultAdapter.getSecret("redis_host");
}
@Override
public int getPort() {
return Integer.valueOf(VaultAdapter.getSecret("redis_port"));
}
};
}
}
创建配置类 RedisConnectionDetailsConfiguration
:
@Configuration(proxyBeanMethods = false)
@Profile("redis")
public class RedisConnectionDetailsConfiguration {
@Bean
@Primary
public RedisConnectionDetails getRedisCacheConnection() {
return new RedisCacheConnectionDetails();
}
}
最后,测试是否能成功整合 Redis:
@Test
public void giveSecretVault_whenStoreInRedisCache_thenSuccess() {
redisTemplate.opsForValue().set("City", "New York");
assertEquals("New York", redisTemplate.opsForValue().get("City"));
}
在测试类中注入 redisTemplate
,然后把 key / value 对添加到缓存中,最后根据 key 读取到了 value。
4.4、MongoDB {#44mongodb}
类体系图如下:
创建 MongoDBConnectionDetails
类,实现 MongoConnectionDetails
接口:
public class MongoDBConnectionDetails implements MongoConnectionDetails {
@Override
public ConnectionString getConnectionString() {
return new ConnectionString(VaultAdapter.getSecret("mongo_connection_string"));
}
}
getConnectionString()
方法从 Vault 中获取连接字符串。
定义 MongoDBConnectionDetailsConfiguration
配置类:
@Configuration(proxyBeanMethods = false)
public class MongoDBConnectionDetailsConfiguration {
@Bean
@Primary
public MongoConnectionDetails getMongoConnectionDetails() {
return new MongoDBConnectionDetails();
}
}
测试,是否能成功整合 MongoDB Server:
@Test
public void givenSecretVault_whenExecuteQueryOnMongoDB_ReturnResult() {
mongoTemplate.insert("{\"msg\":\"My First Entry in MongoDB\"}", "myDemoCollection");
String result = mongoTemplate.find(new Query(), String.class, "myDemoCollection").get(0);
JSONObject jsonObject = new JSONObject(result);
result = jsonObject.get("msg").toString();
assertEquals("My First Entry in MongoDB", result);
}
如上,该方法将数据插入 MongoDB,然后成功检索数据。Spring Boot 通过 MongoDBConnectionDetailsConfiguration
中定义的 MongoConnectionDetails
Bean 创建 mongoTemplate
Bean。
4.5、R2dbc {#45r2dbc}
Spring Boot 还通过 R2dbcConnectionDetails
为响应式编程中的关系数据库连接提供了 ConnectionDetails
。
类体系图如下:
创建 R2dbcPostgresConnectionDetails
类,实现 R2dbcConnectionDetails
:
public class R2dbcPostgresConnectionDetails implements R2dbcConnectionDetails {
@Override
public ConnectionFactoryOptions getConnectionFactoryOptions() {
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
.option(ConnectionFactoryOptions.DRIVER, "postgresql")
.option(ConnectionFactoryOptions.HOST, VaultAdapter.getSecret("r2dbc_postgres_host"))
.option(ConnectionFactoryOptions.PORT, Integer.valueOf(VaultAdapter.getSecret("r2dbc_postgres_port")))
.option(ConnectionFactoryOptions.USER, VaultAdapter.getSecret("r2dbc_postgres_user"))
.option(ConnectionFactoryOptions.PASSWORD, VaultAdapter.getSecret("r2dbc_postgres_secret"))
.option(ConnectionFactoryOptions.DATABASE, VaultAdapter.getSecret("r2dbc_postgres_database"))
.build();
return options;
}
}
与前面一样,这里也使用 VaultAdapter
来检索连接详情。
创建 R2dbcPostgresConnectionDetailsConfiguration
配置类,配置 R2dbcPostgresConnectionDetails
Bean:
@Configuration(proxyBeanMethods = false)
public class R2dbcPostgresConnectionDetailsConfiguration {
@Bean
@Primary
public R2dbcConnectionDetails getR2dbcPostgresConnectionDetails() {
return new R2dbcPostgresConnectionDetails();
}
}
由于上述 Bean 的存在,Spring Boot 会自动配置 R2dbcEntityTemplate
。可以在需要的地方注入,用于响应式查询。
@Test
public void givenSecretVault_whenQueryPostgresReactive_thenSuccess() {
String sql = "select * from information_schema.tables";
List<String> result = r2dbcEntityTemplate.getDatabaseClient().sql(sql).fetch().all()
.map(r -> {
return "hello " + r.get("table_name").toString();
}).collectList().block();
logger.info("count ------" + result.size());
}
4.6、Elasticsearch {#46elasticsearch}
Spring Boot 提供了 ElasticsearchConnectionDetails
接口,用于 Elasticsearch 服务的连接配置。
类体系图如下:
创建 CustomElasticsearchConnectionDetails
类,实现 ElasticsearchConnectionDetails
接口。与之前一样,采用相同的模式来检索连接信息。
public class CustomElasticsearchConnectionDetails implements ElasticsearchConnectionDetails {
@Override
public List<Node> getNodes() {
Node node1 = new Node(
VaultAdapter.getSecret("elastic_host"),
Integer.valueOf(VaultAdapter.getSecret("elastic_port1")),
Node.Protocol.HTTP
);
Node node2 = new Node(
VaultAdapter.getSecret("elastic_host"),
Integer.valueOf(VaultAdapter.getSecret("elastic_port2")),
Node.Protocol.HTTP
);
return List.of(node1, node2);
}
@Override
public String getUsername() {
return VaultAdapter.getSecret("elastic_user");
}
@Override
public String getPassword() {
return VaultAdapter.getSecret("elastic_secret");
}
}
通过 VaultAdapter
获取连接详情。
接下来是 ElasticSearchConnectionDetails
Bean 的配置类:
@Configuration(proxyBeanMethods = false)
@Profile("elastic")
public class CustomElasticsearchConnectionDetailsConfiguration {
@Bean
@Primary
public ElasticsearchConnectionDetails getCustomElasticConnectionDetails() {
return new CustomElasticsearchConnectionDetails();
}
}
最后,测试:
@Test
public void givenSecretVault_whenCreateIndexInElastic_thenSuccess() {
Boolean result = elasticsearchTemplate.indexOps(Person.class).create();
logger.info("index created:" + result);
assertTrue(result);
}
Spring Boot 会自动将正确的连接信息配置到测试类中的 elasticsearchTemplate
中 然后,使用 elasticsearchTemplate
在 Elasticsearch 中创建索引。
4.7、Cassandra {#47cassandra}
类体系图如下:
创建 CustomCassandraConnectionDetails
,实现 CassandraConnectionDetails
接口:
public class CustomCassandraConnectionDetails implements CassandraConnectionDetails {
@Override
public List<Node> getContactPoints() {
Node node = new Node(
VaultAdapter.getSecret("cassandra_host"),
Integer.parseInt(VaultAdapter.getSecret("cassandra_port"))
);
return List.of(node);
}
@Override
public String getUsername() {
return VaultAdapter.getSecret("cassandra_user");
}
@Override
public String getPassword() {
return VaultAdapter.getSecret("cassandra_secret");
}
@Override
public String getLocalDatacenter() {
return "datacenter-1";
}
}
如上,从 Vault 中检索大部分敏感的连接信息。
创建 CustomCassandraConnectionDetails
Bean 的配置类,CustomCassandraConnectionDetailsConfiguration
:
@Configuration(proxyBeanMethods = false)
public class CustomCassandraConnectionDetailsConfiguration {
@Bean
@Primary
public CassandraConnectionDetails getCustomCassandraConnectionDetails() {
return new CustomCassandraConnectionDetails();
}
}
最后,测试 Spring Boot 能否自动配置 CassandraTemplate
:
@Test
public void givenSecretVaultVault_whenRunQuery_thenSuccess() {
Boolean result = cassandraTemplate.getCqlOperations()
.execute("CREATE KEYSPACE IF NOT EXISTS spring_cassandra"
+ " WITH replication = {'class':'SimpleStrategy', 'replication_factor':3}");
logger.info("the result -" + result);
assertTrue(result);
}
上述方法成功地通过 cassandraTemplate
在 Cassandra 数据库中创建了一个 keyspace。
4.8、Neo4j {#48neo4j}
Spring Boot 为流行的图数据库 Neo4j Database 提供了 ConnectionDetails
抽象:
创建 CustomNeo4jConnectionDetails
实现 Neo4jConnectionDetails
接口:
public class CustomNeo4jConnectionDetails implements Neo4jConnectionDetails {
@Override
public URI getUri() {
try {
return new URI(VaultAdapter.getSecret("neo4j_uri"));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public AuthToken getAuthToken() {
return AuthTokens.basic("neo4j", VaultAdapter.getSecret("neo4j_secret"));
}
}
同样,在这里使用 VaultAdapter
从 Vault 获取连接信息。
实现 CustomNeo4jConnectionDetailsConfiguration
配置类,以定义 Neo4jConnectionDetails
Bean:
@Configuration(proxyBeanMethods = false)
public class CustomNeo4jConnectionDetailsConfiguration {
@Bean
@Primary
public Neo4jConnectionDetails getNeo4jConnectionDetails() {
return new CustomNeo4jConnectionDetails();
}
}
最后,测试是否能成功连接到 Neo4j 数据库:
@Test
public void giveSecretVault_whenRunQuery_thenSuccess() {
Person person = new Person();
person.setName("James");
person.setZipcode("751003");
Person data = neo4jTemplate.save(person);
assertEquals("James", data.getName());
}
测试类中自动注入了 Neo4jTemplate
,并用它把数据保存到了数据库。
4.9、Kafka {#49kafka}
Kafka 是一种流行且功能强大的 Messaging Broker,Spring Boot 也为其提供了 KafkaConnectionDetails
用于整合。类体系图如下:
创建 CustomKafkaConnectionDetails
类,实现 KafkaConnectionDetails
:
public class CustomKafkaConnectionDetails implements KafkaConnectionDetails {
@Override
public List<String> getBootstrapServers() {
return List.of(VaultAdapter.getSecret("kafka_servers"));
}
}
对于非常基本的 Kafka 单节点服务器设置,上述类只是重写了 getBootstrapServers()
方法,以从 Vault 中读取属性。对于更复杂的多节点设置,还可以重写其他方法。
定义 CustomKafkaConnectionDetailsConfiguration
配置类:
@Configuration(proxyBeanMethods = false)
public class CustomKafkaConnectionDetailsConfiguration {
@Bean
public KafkaConnectionDetails getKafkaConnectionDetails() {
return new CustomKafkaConnectionDetails();
}
}
上述配置类会创建 KafkaConnectionDetails
Bean,以及 KafkaTemplate
。
测试:
@Test
public void givenSecretVault_whenPublishMsgToKafkaQueue_thenSuccess() {
assertDoesNotThrow(kafkaTemplate::getDefaultTopic);
}
4.10、Couchbase {#410couchbase}
Spring Boot 还提供了 CouchbaseConnectionDetails
接口,用于 Couchbase 数据库的连接信息。类体系图如下:
创建 CustomCouchBaseConnectionDetails
类,实现 CouchbaseConnectionDetails
接口。覆写其获取用户名,密码和连接字符串的方法。
public class CustomCouchBaseConnectionDetails implements CouchbaseConnectionDetails {
@Override
public String getConnectionString() {
return VaultAdapter.getSecret("couch_connection_string");
}
@Override
public String getUsername() {
return VaultAdapter.getSecret("couch_user");
}
@Override
public String getPassword() {
return VaultAdapter.getSecret("couch_secret");
}
}
然后,在 CustomCouchBaseConnectionDetails
配置类中创建上述自定义 Bean:
@Configuration(proxyBeanMethods = false)
@Profile("couch")
public class CustomCouchBaseConnectionDetailsConfiguration {
@Bean
public CouchbaseConnectionDetails getCouchBaseConnectionDetails() {
return new CustomCouchBaseConnectionDetails();
}
}
测试,是否能成功连接到 Couchbase 服务器:
@Test
public void givenSecretVault_whenConnectWithCouch_thenSuccess() {
assertDoesNotThrow(cluster.ping()::version);
}
在测试方法中注入 Cluster
类,用于和数据库集成。
4.11、Zipkin {#411zipkin}
最后是 ZipkinConnectionDetails
接口,该接口用于 Zipkin Server(一种流行的分布式追踪系统)的连接属性。类体系图如下:
创建 CustomZipkinConnectionDetails
类,实现 ZipkinConnectionDetails
接口:
public class CustomZipkinConnectionDetails implements ZipkinConnectionDetails {
@Override
public String getSpanEndpoint() {
return VaultAdapter.getSecret("zipkin_span_endpoint");
}
}
在 getSpanEndpoint()
方法中,使用 VaultAdapter
从 Vault 获取 Zipkin API 端点。
接着,创建 CustomZipkinConnectionDetailsConfiguration
配置类:
@Configuration(proxyBeanMethods = false)
@Profile("zipkin")
public class CustomZipkinConnectionDetailsConfiguration {
@Bean
@Primary
public ZipkinConnectionDetails getZipkinConnectionDetails() {
return new CustomZipkinConnectionDetails();
}
}
如上,配置类中定义了 ZipkinConnectionDetails
Bean。Spring Boot 应用启动会发现该 Bean,这样 Zipkin 库 就能将追踪信息推送到 Zipkin 中。
首先,运行应用。
mvn spring-boot:run -P connection-details
-Dspring-boot.run.arguments="--spring.config.location=./target/classes/connectiondetails/application-zipkin.properties"
在运行应用之前,必须在本地运行 Zipkin。
然后,运行以下命令访问 ZipkinDemoController
中定义的控制器端点:
curl http://localhost:8080/zipkin/test
最后,查看 Zipkin 控制台中的追踪信息:
5、总结 {#5总结}
本文介绍了 Spring Boot 3.1 中的 ConnectionDetails
接口,以及如何通过该接口从远程服务中获取敏感的连接信息。注意,一些与连接无关的信息仍然可以从 application.properties
文件中读取。
参考:https://www.baeldung.com/spring-boot-3-1-connectiondetails-abstraction