Spring Boot 3.1 中的 ConnectionDetails

1、概览

本文将带你了解 Spring Boot 3.1 中引入的 ConnectionDetails 接口,用于编程式定义连接属性(可从外部服务中加载)。Spring Boot 提供了开箱即用的抽象,用于与远程服务集成,如关系型数据库、NoSQL 数据库、消息队列服务等

传统上,连接信息都是配置在 application.properties 文件中的。因此,如果需要从 AWS Secret Manager、Hashicorp Vault 等外部服务加载连接配置的话就比较麻烦。

为了解决这个问题,Spring Boot 引入了 ConnectionDetails。这个接口是空的,即标记接口。Spring 提供了该接口的子接口,如 JdbcConnectionDetailsCassandraConnectionDetailsKafkaConnectionDetails 等。它们可以在 Spring 配置类中实现和指定为 Bean。之后,Spring 将依赖这些配置 Bean 来动态检索连接属性,而不是静态的 application.properties 文件。

2、场景

把敏感的配置信息(如数据库)定义在 application.properties 文件中存在一定的安全隐患。最好的方式是使用专业的私密信息存储服务。

HashiCorp Vault 是一款企业级私密信息管理工具,用于安全地存储和访问敏感数据,例如API密钥、数据库凭据、密码等。支持多种认证方法和加密机制,并提供 API 和命令行界面,以便与应用程序和工具集成。还支持动态秘密生成、密钥轮换和审计日志等功能,使数据的管理和安全性更加便捷和可靠。

Spring Boot 从 Vault 中读取敏感信息的流程如下:

Spring Boot 从 Vault 中读取敏感信息的流程

Spring Boot 通过 Secret Key 调用 Vault 服务来获取连接配置信息。然后,使用该配置和远程服务创建连接。

4、ConnectionDetails

通过 ConnectionDetails 接口,Spring Boot 应用可以自行发现连接细节(Connection Detail),而无需任何手动干预。注意,ConnectionDetails 优先于 application.properties 文件。不过,仍有一些非连接属性(如 JDBC 连接池大小)可以通过 application.properties 文件进行配置。

接下来,本文通过 Spring Boot Docker Compose 特性,展示各种 ConnectionDetails 实现类的实际应用。

4.1、JDBC

以 Spring Boot 应用与 Postgres 数据库的整合为例。

从类关系图开始:

JdbcConnectionDetails 类体系图

在上述类体系图中,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

JdbcConnectionDetails 类似,Spring Boot 也提供了用于与 RabbitMQ Server 集成的接口 RabbitConnectionDetails

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

现在,来看看用于 Redis 的 Spring ConnectionDetails

类体系图:

RedisConnectionDetails 类体系

创建 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

类体系图如下:

MongoConnectionDetails 类体系

创建 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

Spring Boot 还通过 R2dbcConnectionDetails 为响应式编程中的关系数据库连接提供了 ConnectionDetails

类体系图如下:

R2dbcConnectionDetails 类体系图

创建 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

Spring Boot 提供了 ElasticsearchConnectionDetails 接口,用于 Elasticsearch 服务的连接配置。

类体系图如下:

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

类体系图如下:

CassandraConnectionDetails 类体系

创建 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

Spring Boot 为流行的图数据库 Neo4j Database 提供了 ConnectionDetails 抽象:

Neo4jConnectionDetails 体系

创建 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

Kafka 是一种流行且功能强大的 Messaging Broker,Spring Boot 也为其提供了 KafkaConnectionDetails 用于整合。类体系图如下:

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

Spring Boot 还提供了 CouchbaseConnectionDetails 接口,用于 Couchbase 数据库的连接信息。类体系图如下:

CouchbaseConnectionDetails 类体系

创建 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

最后是 ZipkinConnectionDetails 接口,该接口用于 Zipkin Server(一种流行的分布式追踪系统)的连接属性。类体系图如下:

ZipkinConnectionDetails 类体系

创建 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 控制台中的追踪信息:

Zipkin 控制台中的追踪信息

5、总结

本文介绍了 Spring Boot 3.1 中的 ConnectionDetails 接口,以及如何通过该接口从远程服务中获取敏感的连接信息。注意,一些与连接无关的信息仍然可以从 application.properties 文件中读取。


参考:https://www.baeldung.com/spring-boot-3-1-connectiondetails-abstraction