51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot 3.1 中的 ConnectionDetails

1、概览 {#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、场景 {#2场景}

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

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

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

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 类体系图

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

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

类体系图:

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 {#44mongodb}

类体系图如下:

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 {#45r2dbc}

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 {#46elasticsearch}

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 {#47cassandra}

类体系图如下:

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 {#48neo4j}

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 {#49kafka}

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 {#410couchbase}

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 {#411zipkin}

最后是 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、总结 {#5总结}

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


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

赞(2)
未经允许不得转载:工具盒子 » Spring Boot 3.1 中的 ConnectionDetails