1. 概述
在本教程中,我們將學習 Spring Boot 3.1 中引入的 接口,用於外部化連接屬性。 Spring Boot 提供了一套開箱即用的抽象,用於與遠程服務集成,例如關係型數據庫、NoSQL 數據庫、消息隊列服務等。
傳統上,application.properties 文件用於存儲遠程服務的連接詳細信息。因此,將這些屬性外部化到外部服務(如 AWS Secret Manager、Hashicorp Vault 等)變得困難。
為了解決這個問題,Spring Boot 引入了 ConnectionDetails 接口。該接口為空,充當一個標記。 Spring 提供了該接口的子接口,例如 JdbcConnectionDetails、CassandraConnectionDetails、KafkaConnectionDetails,以及更多。 它們可以實現並指定在 Spring 配置類中作為 Bean。之後,Spring 將依賴這些配置 Bean 來動態檢索連接屬性,而不是靜態的 application.properties 文件。
我們首先將介紹一個用例,然後討論其實現。
2. 用例描述
設想一家跨國銀行,名為“馬古地銀行”。它運行着大量的應用程序,這些應用程序在 Spring Boot 上運行,並連接到各種遠程服務。目前,這些遠程服務的連接詳細信息存儲在 application.properties 文件中。
由於最近的審查,馬古地銀行的合規部門對這些屬性的安全性表示了擔憂。他們提出的幾個要求如下:
- 加密所有密鑰
- 定期輪換密鑰
- 禁止在電子郵件中交換密鑰
3. 方案與設計
在銀行的馬古迪,應用所有者針對上述問題進行了頭腦風暴,最終提出了一個解決方案。他們建議將所有密鑰遷移到 Hashicorp Vault。
因此,所有 Spring Boot 應用都需要從 Vault 讀取密鑰。以下是方案的高層設計:
現在,Spring Boot 應用需要通過提供密鑰來調用 Vault 服務以檢索密鑰。然後,使用檢索到的密鑰,它就可以調用遠程服務以獲取進一步操作的連接對象。
因此,應用程序將依賴 Vault 安全地存儲密鑰。Vault 將根據組織的政策定期輪換密鑰。如果應用程序緩存密鑰,則必須重新加載它們。
4. 使用 ConnectionDetails 接口
藉助 ConnectionDetails 接口,Spring Boot 應用程序可以無需任何人工干預,自行發現連接詳情。
儘管如此,需要注意的是,ConnectionDetails 優先於 application.properties 文件。但是,仍然可以通過 application.properties 文件配置一些非連接屬性,例如 JDBC 連接池大小。
在下一部分,我們將通過利用 Spring Boot docker compose 功能,觀察各種 ConnectionDetails 實現類的實際應用。
4.1. 外部化 JDBC 連接信息
這裏我們將以 Spring Boot 應用與 Postgres 數據庫集成的示例為例。首先來看一下類圖:
在上面的類圖中,<em >JdbcConnectionDetails</em> 接口來自 Spring Boot 框架。<em >PostgresConnectionDetails</em> 類實現了接口的方法,用於從 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 是應用程序中的配置類:
@Configuration(proxyBeanMethods = false)
public class JdbcConnectionDetailsConfiguration {
@Bean
@Primary
public JdbcConnectionDetails getPostgresConnection() {
return new PostgresConnectionDetails();
}
}有趣的是,Spring Boot會在應用程序啓動過程中自動發現它,並獲取 JdbcConnectionDetails Bean。正如前面所解釋的,該 Bean 包含從 Vault 中檢索 Postgres 數據庫連接詳細信息的邏輯。
由於我們使用 Docker Compose 啓動 Postgres 數據庫容器,Spring Boot 會自動創建一個 ConnectionDetails Bean,該 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. 外部化 RabbitMQ 連接信息
類似於 <em >JdbcConnectionDetails</em >,`Spring Boot 提供了接口 RabbitConnectionDetails> 用於與 RabbitMQ Server 集成。 讓我們看看如何使用此接口來外部化 Spring Boot 屬性,以便連接到 RabbitMQ Server:
首先,根據約定,我們使用 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")));
}
}接下來,我們將定義上述 Bean <em >RabbitMQConnectionDetails</em> 在 <em >RabbitMQConnectionDetailsConfiguration</em> 類中:
@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 隊列發送一條消息,然後讀取該消息。對象 rabbitTemplate 由 Spring Boot 通過引用 RabbitMQConnectionDetails bean 中的連接信息自動配置。我們已將 rabbitTemplate 對象注入到測試類中,然後在上述測試方法中使用它。
4.3. 外部化 Redis 連接信息
現在我們繼續討論 Spring 中的 ConnectionDetails 抽象以及它與 Redis 的關係。首先,讓我們查看一下類圖:
接下來,我們來看 RedisCacheConnectionDetails ,它通過實現 RedisConnectionDetails ,將 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 返回 RedisConnectionDetails Bean:
@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"));
}
首先,Spring 框架成功地將 redisTemplate 注入到測試類中。然後,它被用於將鍵值對添加到緩存中。最後,我們檢索了該值。
4.4. 外部化 MongoDB 連接信息
與之前一樣,我們首先來看一下標準的類圖:
接下來,讓我們看看 `MongoConnectionDetails 類的實現:
public class MongoDBConnectionDetails implements MongoConnectionDetails {
@Override
public ConnectionString getConnectionString() {
return new ConnectionString(VaultAdapter.getSecret("mongo_connection_string"));
}
}類似於類圖,我們已實現 MongoConnectionDetails 接口中的 getConnectionString() 方法。該方法從 Vault 中檢索連接字符串。
現在,我們來看 MongoDBConnectionDetailsConfiguration 類如何創建 MongoConnectionDetails Bean:
@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 創建了 mongoTemplate 託管 Bean,藉助 MongoConnectionDetails Bean,該 Bean 定義在 MongoDBConnectionDetailsConfiguration 中。
4.5. 外部化 R2dbc 連接詳情
Spring Boot 還提供了 ConnectionDetails 抽象,用於通過 R2dbcConnectionDetails 編程與 Reactive Relational Database Connection。 讓我們通過以下類圖來外部化連接詳情:
首先,讓我們實現 R2dbcPostgresConnectionDetails:
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 返回為一個 Spring 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 連接信息
Spring Boot 提供接口 `ElasticsearchConnectionDetails>,用於外部化 Elasticsearch 服務連接信息。下面是相關的類圖:
正如之前一樣,我們採用相同的模式來檢索連接信息。現在,我們可以繼續到實現部分,從 `CustomElasticsearchConnectionDetails 類開始:
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 設置連接詳情。
下面我們來看 Spring Boot 使用的用於發現 ElasticSearchConnectionDetails 豆的配置類:
@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,並將其正確的連接信息注入到測試類中。然後,它被用於在 Elasticsearch 中創建索引。
4.7. 外部化 Cassandra 連接詳情
如往常一樣,以下是所提議實現的類圖:
根據 Spring Boot,我們必須實現 CassandraConnectionDetails 接口中的方法,如上所示。 讓我們看看 CustomCassandraConnectionDetails 類的實現。
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 的配置類:
@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 連接信息
Spring Boot 提供 <em >ConnectionDetails</em> 抽象,用於 Neo4j 數據庫,這是一種流行的圖數據庫:
接下來,讓我們實現 <a href="https://docs.enterprise.spring.io/spring-boot/docs/3.2.15.1/api/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.html"><em >CustomNeo4jConnectionDetails</em></a>。
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。
@Configuration(proxyBeanMethods = false)
public class CustomNeo4jConnectionDetailsConfiguration {
@Bean
@Primary
public Neo4jConnectionDetails getNeo4jConnectionDetails() {
return new CustomNeo4jConnectionDetails();
}
}Spring Boot 框架使用上述配置類加載 Neo4jConncetionDetails Bean。
最後,我們來查看以下方法是否成功連接到 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());
}<p>值得注意的是,<a href="https://docs.spring.io/spring-data/neo4j/docs/current/api/org/springframework/data/neo4j/core/Neo4jTemplate.html">Neo4jTemplate</a> 被自動注入到測試類中,並將數據保存到數據庫。</p>
4.9. 外部化 Kafka 連接信息
Kafka 作為一種流行的且極其強大的消息中間件,Spring Boot 也提供了對其的支持庫。<em >KafkaConnectionDetails</em> 是 Spring Boot 最近推出的最新特性,用於支持外部化連接屬性。因此,讓我們通過以下類圖來了解如何使用它:
上述設計與之前討論的設計類似。因此,我們將直接跳轉到其實現,從 <em >CustomKafkaConnectionDetails</em> 類開始:
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。 並且最終,Spring 會將其用於注入到以下方法中 kafkaTemplate:
@Test
public void givenSecretVault_whenPublishMsgToKafkaQueue_thenSuccess() {
assertDoesNotThrow(kafkaTemplate::getDefaultTopic);
}4.10. 外部化 Couchbase 連接信息
Spring Boot 還提供了 CouchbaseConnectionDetails 接口,用於外部化 Couchbase 數據庫的連接屬性。 請看下面的類圖:
我們首先通過重寫該接口的方法來實現 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");
}
}然後,我們將在上文中創建的自定義 Bean 在 CustomCouchBaseConnectionDetails 類中:
@Configuration(proxyBeanMethods = false)
@Profile("couch")
public class CustomCouchBaseConnectionDetailsConfiguration {
@Bean
public CouchbaseConnectionDetails getCouchBaseConnectionDetails() {
return new CustomCouchBaseConnectionDetails();
}
}Spring Boot 在應用程序啓動時加載上述配置類。
現在,我們可以檢查以下方法,該方法能夠成功連接到 Couchbase 服務器:
@Test
public void givenSecretVault_whenConnectWithCouch_thenSuccess() {
assertDoesNotThrow(cluster.ping()::version);
}Cluster 類在方法中通過自動注入,然後用於與數據庫集成。
4.11. 外部化 Zipkin 連接信息
最後,在本節中,我們將討論 <em>ZipkinConnectionDetails</em> 接口,用於外部化連接到 Zipkin Server 的屬性,Zipkin Server 是一個流行的分佈式跟蹤系統。下面是相應的類圖:
根據上述類圖設計,我們首先將實現 <em>CustomZipkinConnectionDetails</em>。
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. 結論
在本文中,我們學習了 Spring Boot 3.1 中的 ConnectionDetails 接口。我們瞭解到它如何幫助將敏感的連接詳細信息外部化到應用程序使用的遠程服務中。值得注意的是,一些與連接無關的信息仍然從 application.properties 文件中讀取。