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. 用例描述
假设有一个跨国银行,叫马尔古迪银行(Bank of Malgudi)。它运营着大量运行在Spring Boot上的应用程序。这些应用连接到各种远程服务。目前,这些远程服务的连接信息存储在application.properties文件中。
在一次审查后,银行的合规部门提出了对这些属性安全性的担忧。他们的一些要求包括:
- ✅ 加密所有机密信息
- ✅ 定期轮换机密信息
- ✅ 禁止通过邮件交换机密信息
3. 解决方案与设计
马尔古迪银行的应用负责人针对上述问题进行了头脑风暴,最终提出了解决方案:将所有机密信息迁移到Hashicorp Vault。
因此,所有Spring Boot应用都需要从Vault读取机密信息。以下是高层设计图:
现在,Spring Boot应用需要使用密钥调用Vault服务来检索机密信息。然后,使用检索到的机密信息调用远程服务以获取连接对象,进行后续操作。
这样,应用将依赖Vault来安全存储机密信息。Vault会根据组织策略定期轮换机密信息。如果应用缓存了机密信息,也需要重新加载。
4. 使用ConnectionDetails实现
通过ConnectionDetails接口,Spring Boot应用可以自动发现连接信息,无需手动干预。需要注意的是,ConnectionDetails的优先级高于application.properties文件。但是,一些非连接属性(如JDBC连接池大小)仍可通过application.properties配置。
在接下来的部分,我们将利用Spring Boot的Docker Compose特性,展示各种ConnectionDetails实现类的使用。
4.1 外部化JDBC连接信息
这里以Spring Boot应用集成PostgreSQL数据库为例。首先看类图:
上图中,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是应用中的配置类:
@Configuration(proxyBeanMethods = false)
public class JdbcConnectionDetailsConfiguration {
@Bean
@Primary
public JdbcConnectionDetails getPostgresConnection() {
return new PostgresConnectionDetails();
}
}
Spring Boot在应用启动过程中会自动发现并获取JdbcConnectionDetails Bean。如前所述,该Bean包含从Vault检索PostgreSQL数据库连接信息的逻辑。
由于我们使用Docker Compose启动PostgreSQL容器,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 外部化RabbitMQ连接信息
与JdbcConnectionDetails类似,**Spring Boot提供了RabbitConnectionDetails接口来集成RabbitMQ服务器**。让我们看看如何使用该接口外部化连接RabbitMQ服务器的Spring Boot属性:
首先,按照约定实现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的队列发送消息,然后读取它。rabbitTemplate对象由Spring Boot通过引用RabbitMQConnectionDetails Bean中的连接信息自动配置。我们将rabbitTemplate对象注入测试类,并在上述测试方法中使用。
4.3 外部化Redis连接信息
现在转向Spring在Redis上的ConnectionDetails抽象。首先看类图:
让我们看看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连接信息
让我们看看上面的MongoDBConnectionDetails类的实现:
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服务器:
@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连接信息
*Spring Boot也为响应式关系型数据库连接编程提供了ConnectionDetails*抽象,借助R2dbcConnectionDetails**。看下面的类图来外部化连接信息:
首先,实现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连接信息
为了外部化Elasticsearch服务的连接信息,Spring Boot提供了ElasticsearchConnectionDetails接口。先看类图:
和之前一样,我们采用相同的模式检索连接信息。现在转向实现,从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 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到测试类。然后用于在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数据库中创建了一个键空间。
4.8 外部化Neo4j连接信息
Spring Boot为流行的图数据库Neo4j数据库提供了ConnectionDetails抽象:
继续,实现CustomNeo4jConnectionDetails:
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());
}
值得注意的是,Neo4jTemplate被自动装配到测试类,并将数据保存到数据库。
4.9 外部化Kafka连接信息
Kafka作为流行且功能强大的消息代理,Spring Boot也为其提供了集成库。**KafkaConnectionDetails是Spring支持外部化连接属性的最新特性**。因此,借助以下类图看看如何使用它:
上述设计与之前讨论的设计基本相似。因此,我们直接跳到实现,从CustomKafkaConnectionDetails类开始:
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");
}
}
然后,在CustomCouchBaseConnectionDetails类中创建上述自定义Bean:
@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连接信息
最后,本节讨论ZipkinConnectionDetails接口,用于外部化连接Zipkin服务器的属性,Zipkin是一个流行的分布式追踪系统。从以下类图开始:
使用类图中的设计,首先实现CustomZipkinConnectionDetails:
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
5. 总结
本文介绍了Spring Boot 3.1中的ConnectionDetails接口。我们看到了它如何帮助将敏感连接信息外部化到应用使用的远程服务。值得注意的是,一些非连接相关信息仍从application.properties文件读取。
最后,本文使用的代码示例可在GitHub上找到。