mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-01 14:59:50 +00:00
Add Spring gRPC support
Closes gh-49540
This commit is contained in:
@@ -49,6 +49,7 @@ dependencies {
|
||||
implementation("org.apache.commons:commons-compress")
|
||||
implementation("org.springframework:spring-core")
|
||||
|
||||
optional("com.google.protobuf:protobuf-gradle-plugin")
|
||||
optional("org.graalvm.buildtools:native-gradle-plugin")
|
||||
optional("org.cyclonedx:cyclonedx-gradle-plugin") {
|
||||
exclude(group: "javax.annotation", module: "javax.annotation-api")
|
||||
|
||||
+11
@@ -104,3 +104,14 @@ When the {url-cyclonedx-docs-gradle-plugin}[CycloneDX plugin] is applied to a pr
|
||||
.. Disable full license texts.
|
||||
. Adds the SBOM under `META-INF/sbom` in the generated jar or war file.
|
||||
. Adds the `Sbom-Format` and `Sbom-Location` to the manifest of the jar or war file.
|
||||
|
||||
|
||||
|
||||
[[reacting-to-other-plugins.protobuf]]
|
||||
== Reacting to the Protobuf Plugin
|
||||
|
||||
When the {url-protobuf-docs-gradle-plugin}[Protobuf plugin] is applied to a project, the Spring Boot plugin:
|
||||
|
||||
. Configures `protoc` to use the artifact `com.google.protobuf:protoc`, aligning its version with that of Protobuf dependencies on the runtime classpath.
|
||||
. Configures the `grpc` plugin to use the artifact `io.grpc:protoc-gen-grpc-java`, aligning its version with that of gRPC dependencies on the runtime classpath.
|
||||
. Configures the `grpc` plugin of all generate proto tasks with the option `@generated=omit`.
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.gradle.plugin;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.protobuf.gradle.ExecutableLocator;
|
||||
import com.google.protobuf.gradle.GenerateProtoTask;
|
||||
import com.google.protobuf.gradle.GenerateProtoTask.PluginOptions;
|
||||
import com.google.protobuf.gradle.ProtobufExtension;
|
||||
import com.google.protobuf.gradle.ProtobufExtension.GenerateProtoTaskCollection;
|
||||
import com.google.protobuf.gradle.ProtobufPlugin;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.NamedDomainObjectContainer;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.DependencyResolveDetails;
|
||||
import org.gradle.api.artifacts.ModuleVersionSelector;
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
|
||||
import org.gradle.api.artifacts.result.DependencyResult;
|
||||
import org.gradle.api.artifacts.result.ResolvedComponentResult;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* {@link Action} that is executed in response to the {@link ProtobufPlugin} being
|
||||
* applied.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class ProtobufPluginAction implements PluginApplicationAction {
|
||||
|
||||
private static final Dependency protocDependency = new Dependency("com.google.protobuf", "protoc");
|
||||
|
||||
private static final Dependency grpcDependency = new Dependency("io.grpc", "protoc-gen-grpc-java");
|
||||
|
||||
private static final List<VersionAlignment> versionAlignment = List.of(
|
||||
protocDependency.alignVersionWith("com.google.protobuf", "protobuf-java-util"),
|
||||
grpcDependency.alignVersionWith("io.grpc", "grpc-util"));
|
||||
|
||||
@Override
|
||||
public Class<? extends Plugin<? extends Project>> getPluginClass() {
|
||||
return ProtobufPlugin.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Project project) {
|
||||
ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class);
|
||||
protobuf.protoc(this::configureProtoc);
|
||||
protobuf.plugins(this::configurePlugins);
|
||||
protobuf.generateProtoTasks(this::configureGenerateProtoTasks);
|
||||
project.getConfigurations()
|
||||
.named(this::isProtobufToolsLocator)
|
||||
.configureEach((configuration) -> configureProtobufToolsLocator(project, configuration));
|
||||
}
|
||||
|
||||
private void configureProtoc(ExecutableLocator protoc) {
|
||||
protoc.setArtifact(protocDependency.asDependencySpec());
|
||||
}
|
||||
|
||||
private ExecutableLocator configurePlugins(NamedDomainObjectContainer<ExecutableLocator> plugins) {
|
||||
return plugins.create("grpc", (grpc) -> grpc.setArtifact(grpcDependency.asDependencySpec()));
|
||||
}
|
||||
|
||||
private void configureGenerateProtoTasks(GenerateProtoTaskCollection tasks) {
|
||||
tasks.all().configureEach(this::configureGenerateProtoTask);
|
||||
}
|
||||
|
||||
private void configureGenerateProtoTask(GenerateProtoTask task) {
|
||||
task.plugins((plugins) -> plugins.create("grpc", this::configureGrpcOptions));
|
||||
}
|
||||
|
||||
private void configureGrpcOptions(PluginOptions grpc) {
|
||||
grpc.option("@generated=omit");
|
||||
}
|
||||
|
||||
private boolean isProtobufToolsLocator(String name) {
|
||||
return name.startsWith("protobufToolsLocator_");
|
||||
}
|
||||
|
||||
private void configureProtobufToolsLocator(Project project, Configuration configuration) {
|
||||
configuration.getResolutionStrategy().eachDependency((details) -> {
|
||||
VersionAlignment versionAlignment = versionAlignmentFor(details);
|
||||
if (versionAlignment != null) {
|
||||
versionAlignment.applyIfPossible(project, details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @Nullable VersionAlignment versionAlignmentFor(DependencyResolveDetails details) {
|
||||
if ("null".equals(details.getRequested().getVersion())) {
|
||||
for (VersionAlignment alignment : versionAlignment) {
|
||||
if (alignment.accepts(details)) {
|
||||
return alignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record Dependency(String group, String module) {
|
||||
|
||||
private VersionAlignment alignVersionWith(String group, String module) {
|
||||
return new VersionAlignment(this, new Dependency(group, module));
|
||||
}
|
||||
|
||||
private String asDependencySpec() {
|
||||
return this.group + ":" + this.module;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record VersionAlignment(Dependency target, Dependency source) {
|
||||
|
||||
void applyIfPossible(Project project, DependencyResolveDetails details) {
|
||||
versionFromRuntimeClasspath(project, source()).ifPresent(details::useVersion);
|
||||
}
|
||||
|
||||
boolean accepts(DependencyResolveDetails details) {
|
||||
ModuleVersionSelector requested = details.getRequested();
|
||||
return target().group().equals(requested.getGroup()) && target().module().equals(requested.getName());
|
||||
}
|
||||
|
||||
private Optional<String> versionFromRuntimeClasspath(Project project, Dependency source) {
|
||||
return project.getConfigurations()
|
||||
.getByName("runtimeClasspath")
|
||||
.getIncoming()
|
||||
.getResolutionResult()
|
||||
.getAllDependencies()
|
||||
.stream()
|
||||
.map(DependencyResult::getFrom)
|
||||
.map(ResolvedComponentResult::getId)
|
||||
.filter(ModuleComponentIdentifier.class::isInstance)
|
||||
.map(ModuleComponentIdentifier.class::cast)
|
||||
.filter((id) -> id.getGroup().equals(source.group()) && id.getModule().equals(source.module()))
|
||||
.map(ModuleComponentIdentifier::getVersion)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -148,7 +148,7 @@ public class SpringBootPlugin implements Plugin<Project> {
|
||||
List<PluginApplicationAction> actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact),
|
||||
new WarPluginAction(singlePublishedArtifact), new DependencyManagementPluginAction(),
|
||||
new ApplicationPluginAction(), new KotlinPluginAction(), new NativeImagePluginAction(),
|
||||
new CyclonedxPluginAction());
|
||||
new CyclonedxPluginAction(), new ProtobufPluginAction());
|
||||
for (PluginApplicationAction action : actions) {
|
||||
withPluginClassOfAction(action,
|
||||
(pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project)));
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.gradle.plugin;
|
||||
|
||||
import org.junit.jupiter.api.TestTemplate;
|
||||
|
||||
import org.springframework.boot.gradle.junit.GradleCompatibility;
|
||||
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ProtobufPluginAction}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@GradleCompatibility
|
||||
class ProtobufPluginActionIntegrationTests {
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
GradleBuild gradleBuild;
|
||||
|
||||
@TestTemplate
|
||||
void configuresProtocArtifact() {
|
||||
assertThat(this.gradleBuild.build("protocArtifact").getOutput())
|
||||
.contains("protoc artifact: 'com.google.protobuf:protoc'");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void configuresGrpcPlugin() {
|
||||
assertThat(this.gradleBuild.build("grpcPlugin").getOutput())
|
||||
.contains("grpc plugin artifact: 'io.grpc:protoc-gen-grpc-java'");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void configuresGenerateProtoTasksToOmitGenerated() {
|
||||
assertThat(this.gradleBuild.build("generateProtoTasksGrpcPluginOptions").getOutput())
|
||||
.contains("generateProto: [[@generated=omit]]")
|
||||
.contains("generateTestProto: [[@generated=omit]]");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void alignsVersionOfProtocDependency() {
|
||||
assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput())
|
||||
.contains("com.google.protobuf:protoc:null -> 4.34.0");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void alignsVersionOfGrpcDependency() {
|
||||
assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput())
|
||||
.contains("io.grpc:protoc-gen-grpc-java:null -> 1.79.0");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void usesVersionOfProtocDependencyWhenSpecified() {
|
||||
assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput())
|
||||
.contains("com.google.protobuf:protoc:4.33.5");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void usesVersionOfGrpcPluginDependencyWhenSpecified() {
|
||||
assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput())
|
||||
.contains("io.grpc:protoc-gen-grpc-java:1.78.0");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.google.protobuf.gradle.ProtobufPlugin;
|
||||
import com.sun.jna.Platform;
|
||||
import io.spring.gradle.dependencymanagement.DependencyManagementPlugin;
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
@@ -124,6 +125,10 @@ public class PluginClasspathGradleBuild extends GradleBuild {
|
||||
classpath.add(new File(pathOfJarContaining("org.codehaus.plexus.util.xml.pull.XmlPullParserException")));
|
||||
classpath.add(new File(pathOfJarContaining("org.codehaus.stax2.ri.Stax2WriterAdapter")));
|
||||
classpath.add(new File(pathOfJarContaining("org.cyclonedx.model.ExternalReference")));
|
||||
// Protobuf dependencies
|
||||
classpath.add(new File(pathOfJarContaining(ProtobufPlugin.class)));
|
||||
classpath.add(new File(pathOfJarContaining("com.google.gradle.osdetector.OsDetectorPlugin")));
|
||||
classpath.add(new File(pathOfJarContaining("kr.motd.maven.os.FileOperationProvider")));
|
||||
return classpath;
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the License);
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
protobuf {
|
||||
plugins {
|
||||
grpc {
|
||||
artifact = "io.grpc:protoc-gen-grpc-java:1.78.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the License);
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.33.5"
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the License);
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.protobuf:protobuf-java-util:4.34.0")
|
||||
implementation("io.grpc:grpc-util:1.79.0")
|
||||
}
|
||||
|
||||
tasks.register("protocArtifact") {
|
||||
doFirst {
|
||||
protobuf {
|
||||
protoc {
|
||||
println "protoc artifact: '$artifact'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("grpcPlugin") {
|
||||
doFirst {
|
||||
protobuf {
|
||||
plugins {
|
||||
grpc {
|
||||
println "grpc plugin artifact: '$artifact'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("generateProtoTasksGrpcPluginOptions") {
|
||||
doFirst {
|
||||
tasks.withType(com.google.protobuf.gradle.GenerateProtoTask).each { println "${it.name}: ${it.pluginsForCaching.collect { it.options }}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,13 @@ public class BomExtension {
|
||||
return this.libraries;
|
||||
}
|
||||
|
||||
public Library getLibrary(String name) {
|
||||
return getLibraries().stream()
|
||||
.filter((library) -> library.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No library found named '%s'".formatted(name)));
|
||||
}
|
||||
|
||||
public void upgrade(Action<UpgradeHandler> action) {
|
||||
action.execute(this.upgradeHandler);
|
||||
}
|
||||
|
||||
+5
@@ -63,6 +63,7 @@ public abstract class DocumentConfigurationProperties extends DefaultTask {
|
||||
Snippets snippets = new Snippets(this.configurationPropertyMetadata, getDeprecated().getOrElse(false));
|
||||
snippets.add("application-properties.core", "Core Properties", this::corePrefixes);
|
||||
snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes);
|
||||
snippets.add("application-properties.grpc", "gRPC Properties", this::grpcPrefixes);
|
||||
snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes);
|
||||
snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes);
|
||||
snippets.add("application-properties.data", "Data Properties", this::dataPrefixes);
|
||||
@@ -115,6 +116,10 @@ public abstract class DocumentConfigurationProperties extends DefaultTask {
|
||||
config.accept("spring.cache");
|
||||
}
|
||||
|
||||
private void grpcPrefixes(Config prefix) {
|
||||
prefix.accept("spring.grpc");
|
||||
}
|
||||
|
||||
private void mailPrefixes(Config config) {
|
||||
config.accept("spring.mail");
|
||||
config.accept("spring.sendgrid");
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ url-native-build-tools-docs-gradle-plugin={url-native-build-tools-docs}/gradle-p
|
||||
url-native-build-tools-docs-maven-plugin={url-native-build-tools-docs}/maven-plugin.html
|
||||
url-paketo-docs=https://paketo.io/docs
|
||||
url-paketo-docs-java-buildpack={url-paketo-docs}/buildpacks/language-family-buildpacks/java
|
||||
url-protobuf-docs-gradle-plugin=https://github.com/google/protobuf-gradle-plugin
|
||||
url-pulsar-client-api-javadoc=https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{version-pulsar-client-api}
|
||||
url-spring-boot-for-apache-geode-docs=https://docs.spring.io/spring-boot-data-geode-build/2.0.x/reference/html5
|
||||
url-spring-boot-for-apache-geode-site=https://github.com/spring-projects/spring-boot-data-geode
|
||||
|
||||
+2
-4
@@ -34,8 +34,6 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpoint;
|
||||
import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup;
|
||||
import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups;
|
||||
import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper;
|
||||
import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper;
|
||||
import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator;
|
||||
import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
|
||||
import org.springframework.boot.health.application.DiskSpaceHealthIndicator;
|
||||
import org.springframework.boot.health.autoconfigure.registry.HealthContributorNameGenerator;
|
||||
@@ -136,9 +134,9 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
|
||||
|
||||
private static final class TestHealthEndpointGroup implements HealthEndpointGroup {
|
||||
|
||||
private final StatusAggregator statusAggregator = new SimpleStatusAggregator();
|
||||
private final StatusAggregator statusAggregator = StatusAggregator.getDefault();
|
||||
|
||||
private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper();
|
||||
private final HttpCodeStatusMapper httpCodeStatusMapper = HttpCodeStatusMapper.getDefault();
|
||||
|
||||
@Override
|
||||
public boolean isMember(String name) {
|
||||
|
||||
@@ -110,6 +110,9 @@ dependencies {
|
||||
implementation(project(path: ":module:spring-boot-data-redis-test"))
|
||||
implementation(project(path: ":module:spring-boot-devtools"))
|
||||
implementation(project(path: ":module:spring-boot-graphql-test"))
|
||||
implementation(project(path: ":module:spring-boot-grpc-client"))
|
||||
implementation(project(path: ":module:spring-boot-grpc-server"))
|
||||
implementation(project(path: ":module:spring-boot-grpc-test"))
|
||||
implementation(project(path: ":module:spring-boot-health"))
|
||||
implementation(project(path: ":module:spring-boot-hibernate"))
|
||||
implementation(project(path: ":module:spring-boot-http-converter"))
|
||||
|
||||
+3
@@ -43,6 +43,9 @@ Spring Boot offers several focused, feature-specific `-test` modules:
|
||||
|`spring-boot-graphql-test`
|
||||
|Testing applications that use Spring GraphQL. Provides the `@GraphQlTest` test slice.
|
||||
|
||||
|`spring-boot-grpc-test`
|
||||
|Testing applications that use Spring gRPC.
|
||||
|
||||
|`spring-boot-jdbc-test`
|
||||
|Testing applications that using Spring JDBC. Provides the `@JdbcTest` test slice.
|
||||
|
||||
|
||||
@@ -24,5 +24,6 @@ snakeYamlVersion=2.6
|
||||
springFrameworkVersion=7.0.6
|
||||
springFramework60xVersion=6.0.23
|
||||
tomcatVersion=11.0.18
|
||||
protobufGradlePluginVersion=0.9.6
|
||||
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+2
-4
@@ -23,8 +23,6 @@ import org.springframework.boot.health.actuate.endpoint.AdditionalHealthEndpoint
|
||||
import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup;
|
||||
import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups;
|
||||
import org.springframework.boot.health.actuate.endpoint.HttpCodeStatusMapper;
|
||||
import org.springframework.boot.health.actuate.endpoint.SimpleHttpCodeStatusMapper;
|
||||
import org.springframework.boot.health.actuate.endpoint.SimpleStatusAggregator;
|
||||
import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
|
||||
|
||||
/**
|
||||
@@ -34,9 +32,9 @@ import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
|
||||
*/
|
||||
class TestHealthEndpointGroup implements HealthEndpointGroup {
|
||||
|
||||
private final StatusAggregator statusAggregator = new SimpleStatusAggregator();
|
||||
private final StatusAggregator statusAggregator = StatusAggregator.getDefault();
|
||||
|
||||
private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper();
|
||||
private final HttpCodeStatusMapper httpCodeStatusMapper = HttpCodeStatusMapper.getDefault();
|
||||
|
||||
private final Predicate<String> memberPredicate;
|
||||
|
||||
|
||||
@@ -97,6 +97,12 @@ dependencies {
|
||||
api(project(":module:spring-boot-graphql")) {
|
||||
transitive = false
|
||||
}
|
||||
api(project(":module:spring-boot-grpc-client")) {
|
||||
transitive = false
|
||||
}
|
||||
api(project(":module:spring-boot-grpc-server")) {
|
||||
transitive = false
|
||||
}
|
||||
api(project(":module:spring-boot-groovy-templates")) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the License);
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.auto-configuration"
|
||||
id "org.springframework.boot.configuration-properties"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
}
|
||||
|
||||
description = "Spring Boot gRPC Client"
|
||||
|
||||
dependencies {
|
||||
api(project(":core:spring-boot"))
|
||||
api("org.springframework.grpc:spring-grpc-core")
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
optional(project(":module:spring-boot-micrometer-observation"))
|
||||
optional("io.grpc:grpc-grpclb")
|
||||
optional("io.grpc:grpc-stub")
|
||||
optional("io.grpc:grpc-netty")
|
||||
optional("io.grpc:grpc-netty-shaded")
|
||||
optional("io.grpc:grpc-inprocess")
|
||||
optional("io.grpc:grpc-kotlin-stub") {
|
||||
exclude group: "javax.annotation", module: "javax.annotation-api"
|
||||
}
|
||||
optional("io.grpc:grpc-xds") {
|
||||
exclude group: "javax.annotation", module: "javax.annotation-api"
|
||||
}
|
||||
optional("io.micrometer:micrometer-core")
|
||||
|
||||
testImplementation(project(":core:spring-boot-test"))
|
||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||
testImplementation("org.yaml:snakeyaml")
|
||||
|
||||
testRuntimeOnly("ch.qos.logback:logback-classic")
|
||||
}
|
||||
|
||||
tasks.named("compileTestJava") {
|
||||
options.nullability.checking = "tests"
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.stub.AbstractStub;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.grpc.client.CompositeGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for a
|
||||
* {@link CompositeGrpcChannelFactory}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class })
|
||||
@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true)
|
||||
@Conditional(CompositeChannelFactoryAutoConfiguration.MultipleNonPrimaryChannelFactoriesCondition.class)
|
||||
public final class CompositeChannelFactoryAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider<GrpcChannelFactory> channelFactoriesProvider) {
|
||||
return new CompositeGrpcChannelFactory(channelFactoriesProvider.orderedStream().toList());
|
||||
}
|
||||
|
||||
static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions {
|
||||
|
||||
MultipleNonPrimaryChannelFactoriesCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean(GrpcChannelFactory.class)
|
||||
static class NoChannelFactoryCondition {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnSingleCandidate(GrpcChannelFactory.class)
|
||||
static class SingleInjectableChannelFactoryCondition {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
|
||||
/**
|
||||
* {@link Conditional @Conditional} that matches when
|
||||
* {@code spring.grpc.client.channelfactory.enabled} is {@code true} or missing.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Documented
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.client.channelfactory.enabled", matchIfMissing = true)
|
||||
@interface ConditionalOnGrpcClientChannelFactoryEnabled {
|
||||
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* Invokes the customizations to a {@link ManagedChannelBuilder} based on the provided
|
||||
* beans.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
class GrpcChannelBuilderCustomizers {
|
||||
|
||||
private final List<GrpcChannelBuilderCustomizer<?>> customizers;
|
||||
|
||||
GrpcChannelBuilderCustomizers(GrpcClientProperties grpcClientProperties,
|
||||
ObjectProvider<CompressorRegistry> compressorRegistry,
|
||||
ObjectProvider<DecompressorRegistry> decompressorRegistry,
|
||||
ObjectProvider<GrpcChannelBuilderCustomizer<?>> customizers,
|
||||
ObjectProvider<GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
|
||||
this(grpcClientProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(),
|
||||
customizers.orderedStream().toList(), defaultServiceConfigCustomizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
GrpcChannelBuilderCustomizers(List<? extends GrpcChannelBuilderCustomizer<?>> customizers) {
|
||||
this(null, null, null, customizers, Collections.emptyList());
|
||||
}
|
||||
|
||||
GrpcChannelBuilderCustomizers(@Nullable GrpcClientProperties grpcClientProperties,
|
||||
@Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry,
|
||||
List<? extends GrpcChannelBuilderCustomizer<?>> customizers,
|
||||
List<? extends GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
|
||||
List<GrpcChannelBuilderCustomizer<?>> all = new ArrayList<>();
|
||||
addCustomizer(all, compressorRegistry, ManagedChannelBuilder::compressorRegistry);
|
||||
addCustomizer(all, decompressorRegistry, ManagedChannelBuilder::decompressorRegistry);
|
||||
if (grpcClientProperties != null) {
|
||||
all.add(new PropertiesGrpcChannelBuilderCustomizer<>(grpcClientProperties));
|
||||
}
|
||||
all.addAll(customizers);
|
||||
all.add(customizeDefaultServiceConfig(grpcClientProperties, defaultServiceConfigCustomizers));
|
||||
this.customizers = List.copyOf(all);
|
||||
}
|
||||
|
||||
private static <B extends ManagedChannelBuilder<B>, T> void addCustomizer(
|
||||
List<GrpcChannelBuilderCustomizer<?>> customizers, @Nullable T bean, BiConsumer<B, T> action) {
|
||||
if (bean != null) {
|
||||
GrpcChannelBuilderCustomizer<B> customizer = (target, builder) -> action.accept(builder, bean);
|
||||
customizers.add(customizer);
|
||||
}
|
||||
}
|
||||
|
||||
private <B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> customizeDefaultServiceConfig(
|
||||
@Nullable GrpcClientProperties properties,
|
||||
List<? extends GrpcClientDefaultServiceConfigCustomizer> customizers) {
|
||||
PropertiesGrpcClientDefaultServiceConfigCustomizer propertiesCustomizer = (properties != null)
|
||||
? new PropertiesGrpcClientDefaultServiceConfigCustomizer(properties) : null;
|
||||
return (target, builder) -> {
|
||||
Map<String, Object> defaultServiceConfig = new LinkedHashMap<>();
|
||||
if (propertiesCustomizer != null) {
|
||||
propertiesCustomizer.customize(target, defaultServiceConfig);
|
||||
}
|
||||
customizers.forEach((customizer) -> customizer.customize(target, defaultServiceConfig));
|
||||
if (!defaultServiceConfig.isEmpty()) {
|
||||
builder.defaultServiceConfig(defaultServiceConfig);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
<T extends ManagedChannelBuilder<T>> List<GrpcChannelBuilderCustomizer<T>> forFactory() {
|
||||
return List.of(this::apply);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T extends ManagedChannelBuilder<?>> void apply(String target, T builder) {
|
||||
LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, builder)
|
||||
.withLogger(GrpcChannelBuilderCustomizers.class)
|
||||
.invoke((customizer) -> customizer.customize(target, builder));
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link GrpcChannelFactory} before it is fully initialized, in particular to tune its
|
||||
* configuration.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface GrpcChannelFactoryCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the given {@link GrpcChannelFactory}.
|
||||
* @param factory the factory to customize
|
||||
*/
|
||||
void customize(GrpcChannelFactory factory);
|
||||
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import io.grpc.stub.AbstractStub;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.CoroutineStubFactory;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC clients.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class)
|
||||
@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class })
|
||||
@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(GrpcClientProperties.class)
|
||||
@Import({ GrpcClientCodecConfiguration.class, ShadedNettyGrpcClientConfiguration.class,
|
||||
NettyGrpcClientConfiguration.class, InProcessGrpcClientConfiguration.class })
|
||||
public final class GrpcClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientInterceptorsConfigurer grpcClientInterceptorsConfigurer(ApplicationContext applicationContext) {
|
||||
return new ClientInterceptorsConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ChannelCredentialsProvider.class)
|
||||
PropertiesChannelCredentialsProvider grpcChannelCredentialsProvider(SslBundles bundles,
|
||||
GrpcClientProperties properties) {
|
||||
return new PropertiesChannelCredentialsProvider(properties, bundles);
|
||||
}
|
||||
|
||||
@Bean
|
||||
<T extends ManagedChannelBuilder<T>> PropertiesGrpcChannelBuilderCustomizer<T> grpcClientPropertiesChannelCustomizer(
|
||||
GrpcClientProperties properties) {
|
||||
return new PropertiesGrpcChannelBuilderCustomizer<>(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
GrpcChannelBuilderCustomizers grpcDefaultServicesChannelBuilderCustomizer(GrpcClientProperties grpcClientProperties,
|
||||
ObjectProvider<CompressorRegistry> compressorRegistry,
|
||||
ObjectProvider<DecompressorRegistry> decompressorRegistry,
|
||||
ObjectProvider<GrpcChannelBuilderCustomizer<?>> customizers,
|
||||
ObjectProvider<GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
|
||||
return new GrpcChannelBuilderCustomizers(grpcClientProperties, compressorRegistry, decompressorRegistry,
|
||||
customizers, defaultServiceConfigCustomizers);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub")
|
||||
static class GrpcClientCoroutineStubConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
CoroutineStubFactory coroutineStubFactory() {
|
||||
return new CoroutineStubFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.grpc.Codec;
|
||||
import io.grpc.Compressor;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.Decompressor;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ServerBuilder;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* The configuration that contains all codec related beans for clients.
|
||||
*
|
||||
* @author Andrei Lisa
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(Codec.class)
|
||||
class GrpcClientCodecConfiguration {
|
||||
|
||||
/**
|
||||
* The compressor registry that is set on the
|
||||
* {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} .
|
||||
* @param compressors the compressors to use on the registry
|
||||
* @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the
|
||||
* specified compressors or the {@link CompressorRegistry#getDefaultInstance() default
|
||||
* registry} if no custom compressors are available in the application context.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
CompressorRegistry grpcCompressorRegistry(List<Compressor> compressors) {
|
||||
if (compressors.isEmpty()) {
|
||||
return CompressorRegistry.getDefaultInstance();
|
||||
}
|
||||
CompressorRegistry registry = CompressorRegistry.newEmptyInstance();
|
||||
compressors.forEach(registry::register);
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompressor registry that is set on the
|
||||
* {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}.
|
||||
* @param decompressors the decompressors to use on the registry
|
||||
* @return a new {@link DecompressorRegistry#emptyInstance() registry} with the
|
||||
* specified decompressors or the {@link DecompressorRegistry#getDefaultInstance()
|
||||
* default registry} if no custom decompressors are available in the application
|
||||
* context.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
DecompressorRegistry grpcDecompressorRegistry(List<Decompressor> decompressors) {
|
||||
if (decompressors.isEmpty()) {
|
||||
return DecompressorRegistry.getDefaultInstance();
|
||||
}
|
||||
DecompressorRegistry registry = DecompressorRegistry.emptyInstance();
|
||||
for (Decompressor decompressor : decompressors) {
|
||||
registry = registry.with(decompressor, false);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.VirtualTargets;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize the default service config of the
|
||||
* {@link GrpcChannelBuilderCustomizer}.
|
||||
* <p>
|
||||
* This customizer should be used instead of calling
|
||||
* {@link ManagedChannelBuilder#defaultServiceConfig(Map)} from a
|
||||
* {@link GrpcChannelBuilderCustomizer} since it allows multiple customizers to update the
|
||||
* same default service config, rather than having a "last wins" outcome.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
* @see GrpcChannelBuilderCustomizer
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface GrpcClientDefaultServiceConfigCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the given default service config.
|
||||
* @param target the target (which may be a {@link VirtualTargets virtual target}).
|
||||
* @param defaultServiceConfig the default service config to customize
|
||||
*/
|
||||
void customize(String target, Map<String, Object> defaultServiceConfig);
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.stub.AbstractStub;
|
||||
import io.micrometer.core.instrument.binder.grpc.GrpcClientObservationConvention;
|
||||
import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.grpc.client.GlobalClientInterceptor;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC observation support.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
|
||||
@AutoConfiguration(
|
||||
afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration")
|
||||
@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class, ObservationRegistry.class,
|
||||
ObservationGrpcClientInterceptor.class })
|
||||
@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true)
|
||||
@ConditionalOnProperty(name = "spring.grpc.client.observation.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnBean(ObservationRegistry.class)
|
||||
public final class GrpcClientObservationAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@GlobalClientInterceptor
|
||||
@ConditionalOnMissingBean
|
||||
ObservationGrpcClientInterceptor grpcClientObservationInterceptor(ObservationRegistry observationRegistry,
|
||||
ObjectProvider<GrpcClientObservationConvention> GrpcClientObservationConvention) {
|
||||
ObservationGrpcClientInterceptor interceptor = new ObservationGrpcClientInterceptor(observationRegistry);
|
||||
GrpcClientObservationConvention.ifAvailable(interceptor::setCustomConvention);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* Configuration properties for gRPC clients.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.grpc.client")
|
||||
public class GrpcClientProperties {
|
||||
|
||||
/**
|
||||
* Map of channel configured by name.
|
||||
*/
|
||||
private final Map<String, Channel> channel = new LinkedHashMap<>();
|
||||
|
||||
public Map<String, Channel> getChannel() {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel Properties.
|
||||
*/
|
||||
public static class Channel {
|
||||
|
||||
static final String DEFAULT_TARGET = "static://localhost:9090";
|
||||
|
||||
/**
|
||||
* The channel target address.
|
||||
*/
|
||||
private String target = DEFAULT_TARGET;
|
||||
|
||||
/**
|
||||
* The custom User-Agent for the channel.
|
||||
*/
|
||||
private @Nullable String userAgent;
|
||||
|
||||
/**
|
||||
* Bypass certificate validation for easier testing (so the remote certificate
|
||||
* could be anonymous). Should not be set in production.
|
||||
*/
|
||||
private boolean bypassCertificateValidation;
|
||||
|
||||
/**
|
||||
* Service config for the channel.
|
||||
*/
|
||||
private @Nullable ServiceConfig serviceConfig;
|
||||
|
||||
private final Inbound inbound = new Inbound();
|
||||
|
||||
@Name("default")
|
||||
private final Default defaultProperties = new Default();
|
||||
|
||||
private final Idle idle = new Idle();
|
||||
|
||||
private final Keepalive keepalive = new Keepalive();
|
||||
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
private final Health health = new Health();
|
||||
|
||||
public String getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public @Nullable String getUserAgent() {
|
||||
return this.userAgent;
|
||||
}
|
||||
|
||||
public void setUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public boolean isBypassCertificateValidation() {
|
||||
return this.bypassCertificateValidation;
|
||||
}
|
||||
|
||||
public void setBypassCertificateValidation(boolean bypassCertificateValidation) {
|
||||
this.bypassCertificateValidation = bypassCertificateValidation;
|
||||
}
|
||||
|
||||
public @Nullable ServiceConfig getServiceConfig() {
|
||||
return this.serviceConfig;
|
||||
}
|
||||
|
||||
public void setServiceConfig(@Nullable ServiceConfig serviceConfig) {
|
||||
this.serviceConfig = serviceConfig;
|
||||
}
|
||||
|
||||
public Inbound getInbound() {
|
||||
return this.inbound;
|
||||
}
|
||||
|
||||
public Default getDefault() {
|
||||
return this.defaultProperties;
|
||||
}
|
||||
|
||||
public Idle getIdle() {
|
||||
return this.idle;
|
||||
}
|
||||
|
||||
public Keepalive getKeepalive() {
|
||||
return this.keepalive;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public Health getHealth() {
|
||||
return this.health;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound properties.
|
||||
*/
|
||||
public static class Inbound {
|
||||
|
||||
private final Message message = new Message();
|
||||
|
||||
private final Metadata metadata = new Metadata();
|
||||
|
||||
public Message getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public Metadata getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound message properties.
|
||||
*/
|
||||
public static class Message {
|
||||
|
||||
/**
|
||||
* Maximum message size allowed to be received by the channel. Set to '-1'
|
||||
* to use the highest possible limit (not recommended).
|
||||
*/
|
||||
private DataSize maxSize = DataSize.ofBytes(4194304);
|
||||
|
||||
public DataSize getMaxSize() {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(DataSize maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound metadata properties.
|
||||
*/
|
||||
public static class Metadata {
|
||||
|
||||
/**
|
||||
* Maximum metadata size allowed to be received by the channel. Set to
|
||||
* '-1' to use the highest possible limit (not recommended).
|
||||
*/
|
||||
private DataSize maxSize = DataSize.ofBytes(8192);
|
||||
|
||||
public DataSize getMaxSize() {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(DataSize maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties for client defaults.
|
||||
*/
|
||||
public static class Default {
|
||||
|
||||
/**
|
||||
* The default deadline for RPCs performed on this channel.
|
||||
*/
|
||||
private @Nullable Duration deadline;
|
||||
|
||||
/**
|
||||
* The load balancing policy the channel should use.
|
||||
*/
|
||||
private String loadBalancingPolicy = "round_robin";
|
||||
|
||||
public @Nullable Duration getDeadline() {
|
||||
return this.deadline;
|
||||
}
|
||||
|
||||
public void setDeadline(@Nullable Duration deadline) {
|
||||
this.deadline = deadline;
|
||||
}
|
||||
|
||||
public String getLoadBalancingPolicy() {
|
||||
return this.loadBalancingPolicy;
|
||||
}
|
||||
|
||||
public void setLoadBalancingPolicy(String loadBalancingPolicy) {
|
||||
this.loadBalancingPolicy = loadBalancingPolicy;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Idle properties.
|
||||
*/
|
||||
public static class Idle {
|
||||
|
||||
/**
|
||||
* The duration without ongoing RPCs before going to idle mode.
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration timeout = Duration.ofSeconds(20);
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep-alive properties.
|
||||
*/
|
||||
public static class Keepalive {
|
||||
|
||||
/**
|
||||
* The delay before sending a keepAlive. Note that shorter intervals increase
|
||||
* the network burden for the server and this value can not be lower than
|
||||
* 'permitKeepAliveTime' on the server.
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration time = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* The default timeout for a keepAlives ping request.
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration timeout = Duration.ofSeconds(20);
|
||||
|
||||
/**
|
||||
* Whether a keepAlive will be performed when there are no outstanding RPC on
|
||||
* a connection.
|
||||
*/
|
||||
private boolean withoutCalls;
|
||||
|
||||
public Duration getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
public void setTime(Duration time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public boolean isWithoutCalls() {
|
||||
return this.withoutCalls;
|
||||
}
|
||||
|
||||
public void setWithoutCalls(boolean withoutCalls) {
|
||||
this.withoutCalls = withoutCalls;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Health properties.
|
||||
*/
|
||||
public static class Health {
|
||||
|
||||
/**
|
||||
* Whether to enable client-side health check for the channel.
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* Name of the service to check health on.
|
||||
*/
|
||||
private @Nullable String serviceName;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public @Nullable String getServiceName() {
|
||||
return this.serviceName;
|
||||
}
|
||||
|
||||
public void setServiceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL properties.
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* Whether to enable SSL support. Enabled automatically if "bundle" is
|
||||
* provided unless specified otherwise.
|
||||
*/
|
||||
private @Nullable Boolean enabled;
|
||||
|
||||
/**
|
||||
* SSL bundle name.
|
||||
*/
|
||||
private @Nullable String bundle;
|
||||
|
||||
public @Nullable Boolean getEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(@Nullable Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public @Nullable String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(@Nullable String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.client.ClientInterceptorFilter;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for an in-process gRPC client.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(InProcessChannelBuilder.class)
|
||||
@ConditionalOnMissingBean(InProcessGrpcChannelFactory.class)
|
||||
@ConditionalOnGrpcClientChannelFactoryEnabled
|
||||
@ConditionalOnProperty(name = "spring.grpc.client.inprocess.enabled", havingValue = "true", matchIfMissing = true)
|
||||
class InProcessGrpcClientConfiguration {
|
||||
|
||||
@Bean
|
||||
InProcessGrpcChannelFactory inProcessGrpcChannelFactory(GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
|
||||
ClientInterceptorsConfigurer interceptorsConfigurer,
|
||||
ObjectProvider<ClientInterceptorFilter> interceptorFilter,
|
||||
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers) {
|
||||
InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(
|
||||
grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer);
|
||||
interceptorFilter.ifAvailable(factory::setInterceptorFilter);
|
||||
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.NettyGrpcChannelFactory;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for a Netty gRPC client.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ Channel.class, NettyChannelBuilder.class })
|
||||
@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class)
|
||||
@ConditionalOnGrpcClientChannelFactoryEnabled
|
||||
class NettyGrpcClientConfiguration {
|
||||
|
||||
@Bean
|
||||
NettyGrpcChannelFactory nettyGrpcChannelFactory(Environment environment, GrpcClientProperties properties,
|
||||
GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
|
||||
ClientInterceptorsConfigurer interceptorsConfigurer,
|
||||
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers,
|
||||
ChannelCredentialsProvider credentials) {
|
||||
NettyGrpcChannelFactory factory = new NettyGrpcChannelFactory(grpcChannelBuilderCustomizers.forFactory(),
|
||||
interceptorsConfigurer);
|
||||
factory.setCredentialsProvider(credentials);
|
||||
factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties));
|
||||
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.InsecureChannelCredentials;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import io.grpc.TlsChannelCredentials.Builder;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Ssl;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.internal.InsecureTrustManagerFactory;
|
||||
|
||||
/**
|
||||
* {@link ChannelCredentialsProvider} backed by {@link GrpcClientProperties}.
|
||||
*
|
||||
* @param properties the client properties
|
||||
* @param bundles the SSL bundles
|
||||
* @author David Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record PropertiesChannelCredentialsProvider(GrpcClientProperties properties,
|
||||
SslBundles bundles) implements ChannelCredentialsProvider {
|
||||
|
||||
@Override
|
||||
public ChannelCredentials getChannelCredentials(String target) {
|
||||
Channel channel = this.properties.getChannel().get(target);
|
||||
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
|
||||
if (channel == null || isInsecure(channel.getSsl())) {
|
||||
return InsecureChannelCredentials.create();
|
||||
}
|
||||
Builder builder = TlsChannelCredentials.newBuilder();
|
||||
if (channel.getSsl().getBundle() != null) {
|
||||
SslBundle bundle = this.bundles.getBundle(channel.getSsl().getBundle());
|
||||
builder.trustManager(bundle.getManagers().getTrustManagerFactory().getTrustManagers());
|
||||
builder.keyManager(bundle.getManagers().getKeyManagerFactory().getKeyManagers());
|
||||
}
|
||||
if (channel.isBypassCertificateValidation()) {
|
||||
builder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private boolean isInsecure(Ssl ssl) {
|
||||
return Boolean.FALSE.equals(ssl.getEnabled()) || (ssl.getBundle() == null && ssl.getEnabled() == null);
|
||||
}
|
||||
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties} to a
|
||||
* {@link ManagedChannelBuilder}.
|
||||
*
|
||||
* @param <T> the type of the builder
|
||||
* @param properties the properties to map
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record PropertiesGrpcChannelBuilderCustomizer<T extends ManagedChannelBuilder<T>>(
|
||||
GrpcClientProperties properties) implements GrpcChannelBuilderCustomizer<T> {
|
||||
|
||||
private static final Channel STOCK_DEFAULT_CHANNEL = new Channel();
|
||||
|
||||
@Override
|
||||
public void customize(String target, T builder) {
|
||||
Channel channel = getChannel(target);
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(channel::getUserAgent).to(builder::userAgent);
|
||||
map.from(channel.getInbound().getMessage()::getMaxSize).asInt(this::maxSize).to(builder::maxInboundMessageSize);
|
||||
map.from(channel.getInbound().getMetadata()::getMaxSize)
|
||||
.asInt(this::maxSize)
|
||||
.to(builder::maxInboundMetadataSize);
|
||||
map.from(channel.getDefault()::getDeadline)
|
||||
.when((deadline) -> deadline.toMillis() > 0L)
|
||||
.as(DefaultDeadlineSetupClientInterceptor::new)
|
||||
.to(builder::intercept);
|
||||
map.from(channel.getDefault()::getLoadBalancingPolicy)
|
||||
.when((policy) -> supportsLoadBalancing(target, channel))
|
||||
.to(builder::defaultLoadBalancingPolicy);
|
||||
map.from(channel.getIdle()::getTimeout).to(durationProperty(builder::idleTimeout));
|
||||
map.from(channel.getKeepalive()::getTime).to(durationProperty(builder::keepAliveTime));
|
||||
map.from(channel.getKeepalive()::getTimeout).to(durationProperty(builder::keepAliveTimeout));
|
||||
map.from(channel.getKeepalive()::isWithoutCalls).to(builder::keepAliveWithoutCalls);
|
||||
}
|
||||
|
||||
private Channel getChannel(String target) {
|
||||
Channel channel = this.properties.getChannel().get(target);
|
||||
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
|
||||
return (channel != null) ? channel : STOCK_DEFAULT_CHANNEL;
|
||||
}
|
||||
|
||||
private boolean supportsLoadBalancing(String target, Channel channel) {
|
||||
return !(isUnixOrInProcessTarget(target) || isUnixOrInProcessTarget(channel.getTarget()));
|
||||
}
|
||||
|
||||
private boolean isUnixOrInProcessTarget(String target) {
|
||||
return target.startsWith("unix:") || target.startsWith("in-process:");
|
||||
}
|
||||
|
||||
Consumer<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
|
||||
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private int maxSize(DataSize maxSize) {
|
||||
long bytes = maxSize.toBytes();
|
||||
Assert.state(bytes >= 0 || bytes == -1, () -> "Unsupported max size value " + maxSize);
|
||||
return (bytes >= 0 && bytes <= Integer.MAX_VALUE) ? (int) bytes : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Health;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link GrpcClientDefaultServiceConfigCustomizer} to apply {@link GrpcClientProperties}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @param properties the client properties
|
||||
*/
|
||||
record PropertiesGrpcClientDefaultServiceConfigCustomizer(
|
||||
GrpcClientProperties properties) implements GrpcClientDefaultServiceConfigCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(String target, Map<String, Object> defaultServiceConfig) {
|
||||
Channel channel = this.properties.getChannel().get(target);
|
||||
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
applyServiceConfig(channel.getServiceConfig(), defaultServiceConfig);
|
||||
applyHealth(channel.getHealth(), defaultServiceConfig);
|
||||
}
|
||||
|
||||
private void applyServiceConfig(@Nullable ServiceConfig serviceConfig, Map<String, Object> defaultServiceConfig) {
|
||||
if (serviceConfig != null) {
|
||||
serviceConfig.applyTo(defaultServiceConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyHealth(Health health, Map<String, Object> defaultServiceConfig) {
|
||||
if (!health.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
String serviceName = (health.getServiceName() != null) ? health.getServiceName() : "";
|
||||
Map<String, Object> healthCheckConfig = cloneOrCreateHealthCheckConfig(defaultServiceConfig);
|
||||
String existingServiceName = (String) healthCheckConfig.get(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY);
|
||||
Assert.state(existingServiceName == null || serviceName.equals(existingServiceName),
|
||||
() -> "Unable to change health check config service name from '%s' to '%s'"
|
||||
.formatted(existingServiceName, serviceName));
|
||||
healthCheckConfig.put(ServiceConfig.HEALTH_CHECK_SERVICE_NAME_KEY, serviceName);
|
||||
defaultServiceConfig.put(ServiceConfig.HEALTH_CHECK_CONFIG_KEY, healthCheckConfig);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> cloneOrCreateHealthCheckConfig(Map<String, Object> defaultServiceConfig) {
|
||||
Map<String, Object> healthCheckConfig = (Map<String, Object>) defaultServiceConfig
|
||||
.get(ServiceConfig.HEALTH_CHECK_CONFIG_KEY);
|
||||
return new LinkedHashMap<>((healthCheckConfig != null) ? healthCheckConfig : Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.core.env.PropertyResolver;
|
||||
import org.springframework.grpc.client.VirtualTargets;
|
||||
|
||||
/**
|
||||
* {@link VirtualTargets} supporting named channels from {@link GrpcClientProperties} and
|
||||
* directly specified targets (which may include property placeholders).
|
||||
*
|
||||
* @param propertyResolver the property resolver
|
||||
* @param properties the client properties
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record PropertiesVirtualTargets(PropertyResolver propertyResolver,
|
||||
GrpcClientProperties properties) implements VirtualTargets {
|
||||
|
||||
@Override
|
||||
public String getTarget(String target) {
|
||||
Channel channel = this.properties.getChannel().get(target);
|
||||
if (channel != null) {
|
||||
return clean(this.propertyResolver.resolvePlaceholders(channel.getTarget()));
|
||||
}
|
||||
if ("default".equals(target)) {
|
||||
return clean(Channel.DEFAULT_TARGET);
|
||||
}
|
||||
target = this.propertyResolver.resolvePlaceholders(target);
|
||||
if (target.contains(":/") || target.startsWith("unix:")) {
|
||||
return clean(target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private String clean(String target) {
|
||||
if (target.startsWith("static:") || target.startsWith("tcp:")) {
|
||||
String withoutScheme = target.substring(target.indexOf(":") + 1);
|
||||
return withoutScheme.replaceFirst("/*", "");
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.grpc.Status;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.context.properties.PropertyMapper.Source.Adapter;
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* Bindable service configuration for gRPC channel. Allows type safe binding of common
|
||||
* service configuration options which can ultimately be applied to the {@link Map}
|
||||
* provided by a {@link GrpcClientDefaultServiceConfigCustomizer}.
|
||||
* <p>
|
||||
* The configuration provided here is a subset of the canonical <a href=
|
||||
* "https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto">service_config.proto</a>
|
||||
* protocol definition. For advanced or experimental service configurations, use the
|
||||
* {@link GrpcClientDefaultServiceConfigCustomizer} to directly add any entries supported
|
||||
* by {@code grpc-java}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param loadbalancing load balancing configurations in the order that they should be
|
||||
* applied
|
||||
* @param method method configuration
|
||||
* @param retrythrottling retry throttling policy
|
||||
* @param healthcheck health check configuration
|
||||
* @since 4.1.0
|
||||
* @see GrpcClientDefaultServiceConfigCustomizer
|
||||
* @see io.grpc.internal.ServiceConfigUtil
|
||||
*/
|
||||
public record ServiceConfig(@Nullable List<LoadBalancingConfig> loadbalancing, @Nullable List<MethodConfig> method,
|
||||
@Nullable RetryThrottlingPolicy retrythrottling, @Nullable HealthCheckConfig healthcheck) {
|
||||
|
||||
static final String HEALTH_CHECK_CONFIG_KEY = "healthCheckConfig";
|
||||
|
||||
static final String HEALTH_CHECK_SERVICE_NAME_KEY = "serviceName";
|
||||
|
||||
/**
|
||||
* Apply this service config to the given gRPC Java config Map.
|
||||
* @param grpcJavaConfig the gRPC Java config map
|
||||
*/
|
||||
public void applyTo(Map<String, Object> grpcJavaConfig) {
|
||||
applyTo(new GrpcJavaConfig(grpcJavaConfig));
|
||||
}
|
||||
|
||||
private void applyTo(GrpcJavaConfig config) {
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::loadbalancing)
|
||||
.as(listOf(LoadBalancingConfig::grpcJavaConfig))
|
||||
.to(config.in("loadBalancingConfig"));
|
||||
map.from(this::method).as(listOf(MethodConfig::grpcJavaConfig)).to(config.in("methodConfig"));
|
||||
map.from(this::retrythrottling).as(RetryThrottlingPolicy::grpcJavaConfig).to(config.in("retryThrottling"));
|
||||
map.from(this::healthcheck).as(HealthCheckConfig::grpcJavaConfig).to(config.in(HEALTH_CHECK_CONFIG_KEY));
|
||||
}
|
||||
|
||||
static <T> Adapter<List<T>, @Nullable List<Map<String, Object>>> listOf(Function<T, Map<String, Object>> adapter) {
|
||||
return (list) -> (!CollectionUtils.isEmpty(list)) ? list.stream().map(adapter).toList() : null;
|
||||
}
|
||||
|
||||
static String durationString(Duration duration) {
|
||||
return duration.getSeconds() + "." + duration.getNano() + "s";
|
||||
}
|
||||
|
||||
static String bytesString(DataSize dataSize) {
|
||||
return Long.toString(dataSize.toBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load balancing config.
|
||||
*
|
||||
* @param pickfirst 'pick first' load balancing
|
||||
* @param roundrobin 'round robin' load balancing
|
||||
* @param weightedroundrobin 'weighted round robin' load balancing
|
||||
* @param grpc 'grpc' load balancing
|
||||
*/
|
||||
public record LoadBalancingConfig(@Nullable PickFirstLoadBalancingConfig pickfirst,
|
||||
@Nullable RoundRobinLoadBalancingConfig roundrobin,
|
||||
@Nullable WeightedRoundRobinLoadBalancingConfig weightedroundrobin,
|
||||
@Nullable GrpcLoadBalancingConfig grpc) {
|
||||
|
||||
public LoadBalancingConfig {
|
||||
if (pickfirst == null && roundrobin == null && weightedroundrobin == null && grpc == null) {
|
||||
throw new InvalidConfigurationPropertyValueException("loadbalancing", null,
|
||||
"Missing load balancing strategy");
|
||||
}
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("loadbalancing.pickfirst", pickfirst);
|
||||
entries.put("loadbalancing.roundrobin", roundrobin);
|
||||
entries.put("loadbalancing.weightedroundrobin", weightedroundrobin);
|
||||
entries.put("loadbalancing.grpc", grpc);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
LinkedHashMap<String, Object> grpcJavaConfig = new LinkedHashMap<>();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::pickfirst)
|
||||
.as(PickFirstLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("pick_first", loadBalancingConfig));
|
||||
map.from(this::roundrobin)
|
||||
.as(RoundRobinLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("round_robin", loadBalancingConfig));
|
||||
map.from(this::weightedroundrobin)
|
||||
.as(WeightedRoundRobinLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("weighted_round_robin", loadBalancingConfig));
|
||||
map.from(this::grpc)
|
||||
.as(GrpcLoadBalancingConfig::grpcJavaConfig)
|
||||
.to((loadBalancingConfig) -> grpcJavaConfig.put("grpclb", loadBalancingConfig));
|
||||
return grpcJavaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'pick first' load balancing.
|
||||
*
|
||||
* @param shuffleAddressList randomly shuffle the list of addresses received from
|
||||
* the name resolver before attempting to connect to them.
|
||||
*/
|
||||
public record PickFirstLoadBalancingConfig(Boolean shuffleAddressList) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with PickFirstLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::shuffleAddressList).to(grpcJavaConfig.in("shuffleAddressList"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'round robin' load balancing.
|
||||
*/
|
||||
public record RoundRobinLoadBalancingConfig() {
|
||||
|
||||
/**
|
||||
* Return the gRPC java config as supported by the
|
||||
* {@code SecretRoundRobinLoadBalancerProvider}.
|
||||
* @return the config
|
||||
*/
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'weighted round robin' load balancing.
|
||||
*
|
||||
* @param blackoutPeriod must report load metrics continuously for at least this
|
||||
* long before the endpoint weight will be used
|
||||
* @param weightExpirationPeriod if has not reported load metrics in this long,
|
||||
* then we stop using the reported weight
|
||||
* @param outOfBandReportingPeriod load reporting interval to request from the
|
||||
* server
|
||||
* @param enableOutOfBandLoadReport whether to enable out-of-band utilization
|
||||
* reporting collection from the endpoints
|
||||
* @param weightUpdatePeriod how often endpoint weights are recalculated
|
||||
* @param errorUtilizationPenalty multiplier used to adjust endpoint weights with
|
||||
* the error rate calculated as eps/qps
|
||||
*/
|
||||
public record WeightedRoundRobinLoadBalancingConfig(Duration blackoutPeriod, Duration weightExpirationPeriod,
|
||||
Duration outOfBandReportingPeriod, Boolean enableOutOfBandLoadReport, Duration weightUpdatePeriod,
|
||||
Float errorUtilizationPenalty) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with WeightedRoundRobinLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::blackoutPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("blackoutPeriod"));
|
||||
map.from(this::weightExpirationPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("weightExpirationPeriod"));
|
||||
map.from(this::outOfBandReportingPeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("oobReportingPeriod"));
|
||||
map.from(this::enableOutOfBandLoadReport).to(grpcJavaConfig.in("enableOobLoadReport"));
|
||||
map.from(this::weightUpdatePeriod)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("weightUpdatePeriod"));
|
||||
map.from(this::errorUtilizationPenalty).to(grpcJavaConfig.in("errorUtilizationPenalty"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'grpc' load balancing.
|
||||
*
|
||||
* @param child what load balancer policies to use for routing between the backend
|
||||
* addresses
|
||||
* @param serviceName override of the service name to be sent to the balancer
|
||||
* @param initialFallbackTimeout timeout in seconds for receiving the server list
|
||||
*/
|
||||
public record GrpcLoadBalancingConfig(List<LoadBalancingConfig> child, String serviceName,
|
||||
Duration initialFallbackTimeout) {
|
||||
|
||||
public GrpcLoadBalancingConfig {
|
||||
child.forEach(this::assertChild);
|
||||
}
|
||||
|
||||
private void assertChild(LoadBalancingConfig child) {
|
||||
if (child.pickfirst() == null && child.roundrobin() == null) {
|
||||
throw new InvalidConfigurationPropertyValueException("loadbalancing.grpc.child", null,
|
||||
"Only 'pickfirst' or 'roundrobin' child load balancer strategies can be used");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
// Aligned with GrpclbLoadBalancerProvider
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::child)
|
||||
.as(listOf(LoadBalancingConfig::grpcJavaConfig))
|
||||
.to(grpcJavaConfig.in("childPolicy"));
|
||||
map.from(this::serviceName).to(grpcJavaConfig.in("serviceName"));
|
||||
map.from(this::initialFallbackTimeout)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("initialFallbackTimeout"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method configuration.
|
||||
*
|
||||
* @param name Names of the methods to which this configuration applies
|
||||
* @param waitForReady Whether RPCs sent to this method should wait until the
|
||||
* connection is ready by default
|
||||
* @param maxRequestMessage maximum allowed payload size for an individual request or
|
||||
* object in a stream
|
||||
* @param maxResponseMessage maximum allowed payload size for an individual response
|
||||
* or object in a stream
|
||||
* @param timeout default timeout for RPCs sent to this method
|
||||
* @param retry retry policy for outgoing RPCs
|
||||
* @param hedging hedging policy for outgoing RPCs
|
||||
*/
|
||||
public record MethodConfig(List<Name> name, Boolean waitForReady, DataSize maxRequestMessage,
|
||||
DataSize maxResponseMessage, Duration timeout, RetryPolicy retry, HedgingPolicy hedging) {
|
||||
|
||||
public MethodConfig {
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("method.retry", retry);
|
||||
entries.put("method.hedging", hedging);
|
||||
});
|
||||
}
|
||||
|
||||
static @Nullable List<Map<String, Object>> grpcJavaConfigs(List<MethodConfig> methodConfigs) {
|
||||
return (!CollectionUtils.isEmpty(methodConfigs))
|
||||
? methodConfigs.stream().map(MethodConfig::grpcJavaConfig).toList() : null;
|
||||
}
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::name).as(listOf(Name::grpcJavaConfig)).to(grpcJavaConfig.in("name"));
|
||||
map.from(this::waitForReady).to(grpcJavaConfig.in("waitForReady"));
|
||||
map.from(this::maxRequestMessage)
|
||||
.as(ServiceConfig::bytesString)
|
||||
.to(grpcJavaConfig.in("maxRequestMessageBytes"));
|
||||
map.from(this::maxResponseMessage)
|
||||
.as(ServiceConfig::bytesString)
|
||||
.to(grpcJavaConfig.in("maxResponseMessageBytes"));
|
||||
map.from(this::timeout).as(ServiceConfig::durationString).to(grpcJavaConfig.in("timeout"));
|
||||
map.from(this::retry).as(RetryPolicy::grpcJavaConfig).to(grpcJavaConfig.in("retryPolicy"));
|
||||
map.from(this::hedging).as(HedgingPolicy::grpcJavaConfig).to(grpcJavaConfig.in("hedgingPolicy"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of a gRPC method.
|
||||
*
|
||||
* @param service service name
|
||||
* @param method method name
|
||||
*/
|
||||
public record Name(String service, String method) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::service).to(grpcJavaConfig.in("service"));
|
||||
map.from(this::method).to(grpcJavaConfig.in("method"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry policy for outgoing RPCs.
|
||||
*
|
||||
* @param maxAttempts maximum number of RPC attempts, including the original
|
||||
* attempt
|
||||
* @param initialBackoff initial exponential backoff
|
||||
* @param maxBackoff maximum exponential backoff
|
||||
* @param backoffMultiplier exponential backoff multiplier
|
||||
* @param perAttemptReceiveTimeout per-attempt receive timeout
|
||||
* @param retryableStatusCodes status codes which may be retried
|
||||
*/
|
||||
public record RetryPolicy(Integer maxAttempts, Duration initialBackoff, Duration maxBackoff,
|
||||
Double backoffMultiplier, Duration perAttemptReceiveTimeout, Set<Status.Code> retryableStatusCodes) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts"));
|
||||
map.from(this::initialBackoff)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("initialBackoff"));
|
||||
map.from(this::maxBackoff).as(ServiceConfig::durationString).to(grpcJavaConfig.in("maxBackoff"));
|
||||
map.from(this::backoffMultiplier).to(grpcJavaConfig.in("backoffMultiplier"));
|
||||
map.from(this::perAttemptReceiveTimeout)
|
||||
.as(ServiceConfig::durationString)
|
||||
.to(grpcJavaConfig.in("perAttemptRecvTimeout"));
|
||||
map.from(this::retryableStatusCodes)
|
||||
.as((codes) -> codes.stream().map(Objects::toString).toList())
|
||||
.to(grpcJavaConfig.in("retryableStatusCodes"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hedging policy for outgoing RPCs.
|
||||
*
|
||||
* @param maxAttempts maximum number of send attempts
|
||||
* @param delay delay for subsequent RPCs
|
||||
* @param nonFatalStatusCodes status codes which indicate other hedged RPCs may
|
||||
* still succeed
|
||||
*/
|
||||
public record HedgingPolicy(Integer maxAttempts, Duration delay, Set<Status.Code> nonFatalStatusCodes) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxAttempts).as(Objects::toString).to(grpcJavaConfig.in("maxAttempts"));
|
||||
map.from(this::delay).as(ServiceConfig::durationString).to(grpcJavaConfig.in("hedgingDelay"));
|
||||
map.from(this::nonFatalStatusCodes)
|
||||
.as((codes) -> codes.stream().map(Objects::toString).toList())
|
||||
.to(grpcJavaConfig.in("nonFatalStatusCodes"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry throttling policy.
|
||||
*
|
||||
* @param maxTokens maximum number of tokens
|
||||
* @param tokenRatio the token ratio
|
||||
*/
|
||||
public record RetryThrottlingPolicy(Float maxTokens, Float tokenRatio) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::maxTokens).as(Objects::toString).to(grpcJavaConfig.in("maxTokens"));
|
||||
map.from(this::tokenRatio).as(Objects::toString).to(grpcJavaConfig.in("tokenRatio"));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check configuration.
|
||||
*
|
||||
* @param serviceName service name to use in the health-checking request.
|
||||
*/
|
||||
public record HealthCheckConfig(String serviceName) {
|
||||
|
||||
Map<String, Object> grpcJavaConfig() {
|
||||
GrpcJavaConfig grpcJavaConfig = new GrpcJavaConfig();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(this::serviceName).to(grpcJavaConfig.in(HEALTH_CHECK_SERVICE_NAME_KEY));
|
||||
return grpcJavaConfig.asMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to collection gRPC java config.
|
||||
*
|
||||
* @param asMap the underling data as a map
|
||||
*/
|
||||
record GrpcJavaConfig(Map<String, Object> asMap) {
|
||||
|
||||
GrpcJavaConfig() {
|
||||
this(new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
<T> Consumer<T> in(String key) {
|
||||
return (value) -> this.asMap.put(key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for a Shaded Netty gRPC client.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ io.grpc.netty.shaded.io.netty.channel.Channel.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class })
|
||||
@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class)
|
||||
@ConditionalOnGrpcClientChannelFactoryEnabled
|
||||
class ShadedNettyGrpcClientConfiguration {
|
||||
|
||||
@Bean
|
||||
ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(Environment environment,
|
||||
GrpcClientProperties properties, GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
|
||||
ClientInterceptorsConfigurer interceptorsConfigurer,
|
||||
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers,
|
||||
ChannelCredentialsProvider credentials) {
|
||||
ShadedNettyGrpcChannelFactory factory = new ShadedNettyGrpcChannelFactory(
|
||||
grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer);
|
||||
factory.setCredentialsProvider(credentials);
|
||||
factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties));
|
||||
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC clients.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.grpc.client.channelfactory.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable gRPC channel factory bean auto-configuration.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.client.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable gRPC client auto-configuration.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.client.inprocess.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to configure the in-process channel factory.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.client.observation.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable Observations on the client.",
|
||||
"defaultValue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration
|
||||
org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration
|
||||
org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.grpc.client.CompositeGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link CompositeChannelFactoryAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class CompositeChannelFactoryAutoConfigurationTests {
|
||||
|
||||
private ApplicationContextRunner contextRunnerWithoutChannelFactories() {
|
||||
return new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class,
|
||||
CompositeChannelFactoryAutoConfiguration.class))
|
||||
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class,
|
||||
NettyChannelBuilder.class, InProcessChannelBuilder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoChannelFactoriesDoesNotAutoconfigureComposite() {
|
||||
this.contextRunnerWithoutChannelFactories()
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSingleChannelFactoryDoesNotAutoconfigureComposite() {
|
||||
GrpcChannelFactory channelFactory1 = mock();
|
||||
this.contextRunnerWithoutChannelFactories()
|
||||
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcChannelFactory.class)
|
||||
.getBean(GrpcChannelFactory.class)
|
||||
.isNotInstanceOf(CompositeGrpcChannelFactory.class)
|
||||
.isSameAs(channelFactory1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleChannelFactoriesWithPrimaryDoesNotAutoconfigureComposite() {
|
||||
GrpcChannelFactory channelFactory1 = mock();
|
||||
GrpcChannelFactory channelFactory2 = mock();
|
||||
this.contextRunnerWithoutChannelFactories()
|
||||
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
|
||||
.withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2, (bd) -> bd.setPrimary(true))
|
||||
.run((context) -> {
|
||||
assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsOnlyKeys("channelFactory1", "channelFactory2");
|
||||
assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isNotInstanceOf(CompositeGrpcChannelFactory.class)
|
||||
.isSameAs(channelFactory2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleChannelFactoriesDoesAutoconfigureComposite() {
|
||||
GrpcChannelFactory channelFactory1 = mock();
|
||||
GrpcChannelFactory channelFactory2 = mock();
|
||||
this.contextRunnerWithoutChannelFactories()
|
||||
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
|
||||
.withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2)
|
||||
.run((context) -> {
|
||||
assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsOnlyKeys("channelFactory1", "channelFactory2", "compositeChannelFactory");
|
||||
assertThat(context).getBean(GrpcChannelFactory.class).isInstanceOf(CompositeGrpcChannelFactory.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositeAutoconfiguredAsExpected() {
|
||||
this.contextRunnerWithoutChannelFactories()
|
||||
.withUserConfiguration(MultipleFactoriesTestConfig.class)
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(CompositeGrpcChannelFactory.class)
|
||||
.extracting("channelFactories")
|
||||
.asInstanceOf(InstanceOfAssertFactories.list(GrpcChannelFactory.class))
|
||||
.containsExactly(MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAR,
|
||||
MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAZ,
|
||||
MultipleFactoriesTestConfig.CHANNEL_FACTORY_FOO));
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MultipleFactoriesTestConfig {
|
||||
|
||||
static GrpcChannelFactory CHANNEL_FACTORY_FOO = mock();
|
||||
static GrpcChannelFactory CHANNEL_FACTORY_BAR = mock();
|
||||
static GrpcChannelFactory CHANNEL_FACTORY_BAZ = mock();
|
||||
|
||||
@Bean
|
||||
@Order(3)
|
||||
GrpcChannelFactory channelFactoryFoo() {
|
||||
return CHANNEL_FACTORY_FOO;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
GrpcChannelFactory channelFactoryBar() {
|
||||
return CHANNEL_FACTORY_BAR;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
GrpcChannelFactory channelFactoryBaz() {
|
||||
return CHANNEL_FACTORY_BAZ;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.ServiceConfig.HealthCheckConfig;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcChannelBuilderCustomizers}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GrpcChannelBuilderCustomizersTests {
|
||||
|
||||
@Test
|
||||
void applyWhenHasProperties() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.setUserAgent("spring-boot");
|
||||
properties.getChannel().put("target", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
then(builder).should().userAgent("spring-boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasCompressorRegistry() {
|
||||
CompressorRegistry compressorRegistry = mock();
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, compressorRegistry, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
then(builder).should().compressorRegistry(compressorRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasDecompressorRegistry() {
|
||||
DecompressorRegistry decompressorRegistry = mock();
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, decompressorRegistry,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
then(builder).should().decompressorRegistry(decompressorRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenEmptyCustomizersDoesNothing() {
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
new GrpcChannelBuilderCustomizers(Collections.emptyList()).apply("target", builder);
|
||||
then(builder).shouldHaveNoInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenSimpleChannelBuilder() {
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(
|
||||
List.of(new SimpleChannelBuilderCustomizer()));
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
then(builder).should().flowControlWindow(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void applyWhenGenericCustomizersRespectsGeneric() {
|
||||
List<TestCustomizer<?>> list = new ArrayList<>();
|
||||
list.add(new TestCustomizer<>());
|
||||
list.add(new TestNettyChannelBuilderCustomizer());
|
||||
list.add(new TestShadedNettyChannelBuilderCustomizer());
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(list);
|
||||
customizers.apply("target", mock(ManagedChannelBuilder.class));
|
||||
assertThat(list.get(0).getCount()).isOne();
|
||||
assertThat(list.get(1).getCount()).isZero();
|
||||
assertThat(list.get(2).getCount()).isZero();
|
||||
customizers.apply("target", mock(NettyChannelBuilder.class));
|
||||
assertThat(list.get(0).getCount()).isEqualTo(2);
|
||||
assertThat(list.get(1).getCount()).isOne();
|
||||
assertThat(list.get(2).getCount()).isZero();
|
||||
customizers.apply("target", mock(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class));
|
||||
assertThat(list.get(0).getCount()).isEqualTo(3);
|
||||
assertThat(list.get(1).getCount()).isOne();
|
||||
assertThat(list.get(2).getCount()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasGrpcClientDefaultServiceConfigCustomizers() {
|
||||
GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer1 = (target, defaultServiceConfig) -> {
|
||||
defaultServiceConfig.put("c", "v1");
|
||||
defaultServiceConfig.put("c1", "v1");
|
||||
};
|
||||
GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer2 = (target, defaultServiceConfig) -> {
|
||||
defaultServiceConfig.put("c", "v2");
|
||||
defaultServiceConfig.put("c2", "v2");
|
||||
};
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, null,
|
||||
Collections.emptyList(), List.of(defaultConfigCustomizer1, defaultConfigCustomizer2));
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("c", "v2");
|
||||
expected.put("c1", "v1");
|
||||
expected.put("c2", "v2");
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasChannelHealthAddsHealthServiceConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.getHealth().setEnabled(true);
|
||||
channel.getHealth().setServiceName("testservice");
|
||||
properties.getChannel().put("target", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("healthCheckConfig", Map.of("serviceName", "testservice"));
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasDefaultHealthAddsHealthServiceConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.getHealth().setEnabled(true);
|
||||
channel.getHealth().setServiceName("testdefaultservice");
|
||||
properties.getChannel().put("default", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("healthCheckConfig", Map.of("serviceName", "testdefaultservice"));
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasServiceConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("test"));
|
||||
channel.setServiceConfig(serviceConfig);
|
||||
properties.getChannel().put("default", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("healthCheckConfig", Map.of("serviceName", "test"));
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHasClashingServiceConfigAndHealth() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.getHealth().setEnabled(true);
|
||||
channel.getHealth().setServiceName("fromhealth");
|
||||
ServiceConfig serviceConfig = new ServiceConfig(null, null, null, new HealthCheckConfig("fromservice"));
|
||||
channel.setServiceConfig(serviceConfig);
|
||||
properties.getChannel().put("default", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
assertThatIllegalStateException().isThrownBy(() -> customizers.apply("target", builder))
|
||||
.withMessage("Unable to change health check config service name from 'fromservice' to 'fromhealth'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.getHealth().setEnabled(true);
|
||||
properties.getChannel().put("target", channel);
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
expected.put("healthCheckConfig", Map.of("serviceName", ""));
|
||||
then(builder).should().defaultServiceConfig(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenNoCustomizersOrHealthDoesSetDefaultServiceConfig() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
|
||||
customizers.apply("target", builder);
|
||||
then(builder).should(never()).defaultServiceConfig(any());
|
||||
}
|
||||
|
||||
static class SimpleChannelBuilderCustomizer implements GrpcChannelBuilderCustomizer<NettyChannelBuilder> {
|
||||
|
||||
@Override
|
||||
public void customize(String target, NettyChannelBuilder channelBuilder) {
|
||||
channelBuilder.flowControlWindow(100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test customizer that will match any {@link GrpcChannelBuilderCustomizer}.
|
||||
*
|
||||
* @param <T> the builder type
|
||||
*/
|
||||
static class TestCustomizer<T extends ManagedChannelBuilder<T>> implements GrpcChannelBuilderCustomizer<T> {
|
||||
|
||||
private int count;
|
||||
|
||||
@Override
|
||||
public void customize(String targetOrChannelName, T channelBuilder) {
|
||||
this.count++;
|
||||
}
|
||||
|
||||
int getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test customizer that will match only {@link NettyChannelBuilder}.
|
||||
*/
|
||||
static class TestNettyChannelBuilderCustomizer extends TestCustomizer<NettyChannelBuilder> {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test customizer that will match only
|
||||
* {@link io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder}.
|
||||
*/
|
||||
static class TestShadedNettyChannelBuilderCustomizer
|
||||
extends TestCustomizer<io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.grpc.Codec;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.kotlin.AbstractCoroutineStub;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.stub.AbstractStub;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.test.scan.DummyBlockingGrpc;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.NettyGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcClientAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
class GrpcClientAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
private final ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory = this.contextRunner
|
||||
.withPropertyValues("spring.grpc.client.inprocess.enabled=false");
|
||||
|
||||
@Test
|
||||
void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCoroutineStubConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() {
|
||||
ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class);
|
||||
this.contextRunner
|
||||
.withBean("customCredentialsProvider", ChannelCredentialsProvider.class, () -> customCredentialsProvider)
|
||||
.run((context) -> assertThat(context).getBean(ChannelCredentialsProvider.class)
|
||||
.isSameAs(customCredentialsProvider));
|
||||
}
|
||||
|
||||
@Test
|
||||
void credentialsProviderAutoConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(PropertiesChannelCredentialsProvider.class)
|
||||
.hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class))
|
||||
.extracting("bundles")
|
||||
.isInstanceOf(SslBundles.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientPropertiesAutoConfiguredResolvesPlaceholders() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.client.channel.c1.target=my-server-${channelName}:8888", "channelName=foo")
|
||||
.run((context) -> assertThat(context).getBean(GrpcClientProperties.class).satisfies((properties) -> {
|
||||
Channel channel = properties.getChannel().get("c1");
|
||||
assertThat(channel).isNotNull();
|
||||
assertThat(channel.getTarget()).isEqualTo("my-server-foo:8888");
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.client.channel.test.health.enabled=true",
|
||||
"spring.grpc.client.channel.test.health.service-name=my-service")
|
||||
.run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("test", builder);
|
||||
Map<String, ?> healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", "my-service"));
|
||||
then(builder).should().defaultServiceConfig(healthCheckConfig);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() {
|
||||
this.contextRunner.run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("test", builder);
|
||||
then(builder).should(never()).defaultServiceConfig(anyMap());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void compressionCustomizerAutoConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
CompressorRegistry compressorRegistry = context.getBean(CompressorRegistry.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("testChannel", builder);
|
||||
then(builder).should().compressorRegistry(compressorRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void compressionCustomizerWhenNoRegistrry() {
|
||||
// Codec class guards the imported GrpcCodecConfiguration to hide registry
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("testChannel", builder);
|
||||
then(builder).should(never()).compressorRegistry(any());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void decompressionCustomizerAutoConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
DecompressorRegistry decompressorRegistry = context.getBean(DecompressorRegistry.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("testChannel", builder);
|
||||
then(builder).should().decompressorRegistry(decompressorRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() {
|
||||
// Codec class guards the imported GrpcCodecConfiguration to hide registry
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> {
|
||||
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
customizers.apply("testChannel", builder);
|
||||
then(builder).should(never()).compressorRegistry(any());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenInProcessEnabledPropNotSetDoesAutoconfigureInProcess() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsKey("inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenInProcessEnabledPropSetToTrueDoesAutoconfigureInProcess() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=true")
|
||||
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsKey("inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenInProcessEnabledPropSetToFalseDoesNotAutoconfigureInProcess() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=false")
|
||||
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.doesNotContainKey("inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenInProcessIsNotOnClasspathDoesNotAutoconfigureInProcess() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(InProcessChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.doesNotContainKey("inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedInProcessChannelFactoryDoesNotAutoConfigureBean() {
|
||||
InProcessGrpcChannelFactory customChannelFactory = mock();
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.withBean("customChannelFactory", InProcessGrpcChannelFactory.class, () -> customChannelFactory)
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedChannelFactoryDoesNotAutoConfigureNettyOrShadedNetty() {
|
||||
GrpcChannelFactory customChannelFactory = mock();
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory)
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void userDefinedChannelFactoryWithInProcessChannelFactory() {
|
||||
GrpcChannelFactory customChannelFactory = mock();
|
||||
this.contextRunner.withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory)
|
||||
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsOnlyKeys("customChannelFactory", "inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() {
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(ShadedNettyGrpcChannelFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shadedNettyWithInProcessChannelFactory() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsOnlyKeys("shadedNettyGrpcChannelFactory", "inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() {
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(NettyGrpcChannelFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonShadedNettyWithInProcessChannelFactory() {
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
|
||||
.containsOnlyKeys("nettyGrpcChannelFactory", "inProcessGrpcChannelFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenShadedNettyAndNettyNotOnClasspathNoChannelFactoryIsAutoConfigured() {
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noChannelFactoryWithInProcessChannelFactory() {
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(InProcessGrpcChannelFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shadedNettyChannelFactoryAutoConfiguredAsExpected() {
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(ShadedNettyGrpcChannelFactory.class)
|
||||
.hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class))
|
||||
.extracting("targets")
|
||||
.isInstanceOf(PropertiesVirtualTargets.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nettyChannelFactoryAutoConfiguredAsExpected() {
|
||||
this.contextRunnerWithoutInProcessChannelFactory
|
||||
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(NettyGrpcChannelFactory.class)
|
||||
.hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class))
|
||||
.extracting("targets")
|
||||
.isInstanceOf(PropertiesVirtualTargets.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inProcessChannelFactoryAutoConfiguredAsExpected() {
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(InProcessGrpcChannelFactory.class)
|
||||
.extracting("credentials")
|
||||
.isSameAs(ChannelCredentialsProvider.INSECURE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shadedNettyChannelFactoryAutoConfiguredWithCustomizers() {
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder builder = mock();
|
||||
channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory, builder,
|
||||
ShadedNettyGrpcChannelFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nettyChannelFactoryAutoConfiguredWithCustomizers() {
|
||||
NettyChannelBuilder builder = mock();
|
||||
channelFactoryAutoConfiguredWithCustomizers(
|
||||
this.contextRunnerWithoutInProcessChannelFactory.withClassLoader(
|
||||
new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)),
|
||||
builder, NettyGrpcChannelFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inProcessChannelFactoryAutoConfiguredWithCustomizers() {
|
||||
InProcessChannelBuilder builder = mock();
|
||||
channelFactoryAutoConfiguredWithCustomizers(
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
|
||||
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)),
|
||||
builder, InProcessGrpcChannelFactory.class);
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> void channelFactoryAutoConfiguredWithCustomizers(
|
||||
ApplicationContextRunner contextRunner, ManagedChannelBuilder<T> mockChannelBuilder,
|
||||
Class<?> expectedChannelFactoryType) {
|
||||
contextRunner.withUserConfiguration(ChannelBuilderCustomizersConfig.class)
|
||||
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
|
||||
.isInstanceOf(expectedChannelFactoryType)
|
||||
.extracting("globalCustomizers", InstanceOfAssertFactories.list(GrpcChannelBuilderCustomizer.class))
|
||||
.satisfies((allCustomizers) -> {
|
||||
allCustomizers.forEach((c) -> c.customize("channel1", mockChannelBuilder));
|
||||
InOrder ordered = inOrder(mockChannelBuilder);
|
||||
ordered.verify(mockChannelBuilder).keepAliveTime(40L, TimeUnit.SECONDS);
|
||||
ordered.verify(mockChannelBuilder).keepAliveTime(50L, TimeUnit.SECONDS);
|
||||
}));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigurationPackage(basePackageClasses = DummyBlockingGrpc.class)
|
||||
static class AutoConfigurePackagesConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ChannelBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
<T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> customizerOne() {
|
||||
return (target, builder) -> builder.keepAliveTime(40L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
<T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> customizerTwo() {
|
||||
return (target, builder) -> builder.keepAliveTime(50L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.stub.AbstractStub;
|
||||
import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.grpc.client.GlobalClientInterceptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for the {@link GrpcClientObservationAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class GrpcClientObservationAutoConfigurationTests {
|
||||
|
||||
private static final AutoConfigurations autoConfigurations = AutoConfigurations
|
||||
.of(GrpcClientObservationAutoConfiguration.class);
|
||||
|
||||
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(autoConfigurations)
|
||||
.withBean("observationRegistry", ObservationRegistry.class, Mockito::mock);
|
||||
|
||||
@Test
|
||||
void whenObservationRegistryNotOnClasspathAutoConfigSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationRegistry.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationGrpcClientInterceptorNotOnClasspathAutoConfigSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(ObservationGrpcClientInterceptor.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationRegistryNotProvidedThenAutoConfigSkipped() {
|
||||
new ApplicationContextRunner().withConfiguration(autoConfigurations)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationPropertyEnabledThenAutoConfigNotSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.observation.enabled=true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationPropertyDisabledThenAutoConfigIsSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.observation.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertyNotSetThenAutoConfigNotSkipped() {
|
||||
this.contextRunner
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertySetTrueThenAutoConfigIsNotSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenClientEnabledPropertySetFalseThenAutoConfigIsSkipped() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGrpcStubNotOnClasspathThenAutoConfigIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ObservationGrpcClientInterceptor.class);
|
||||
Map<String, Object> annotated = context.getBeansWithAnnotation(GlobalClientInterceptor.class);
|
||||
assertThat(annotated).hasEntrySatisfying("grpcClientObservationInterceptor",
|
||||
ObservationGrpcClientInterceptor.class::isInstance);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcClientProperties}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GrpcClientPropertiesTests {
|
||||
|
||||
@Test
|
||||
@WithResource(name = "client.yaml", content = """
|
||||
channel:
|
||||
a:
|
||||
target: static://my-server:8888
|
||||
b:
|
||||
user-agent: me""")
|
||||
void defaultValues() throws Exception {
|
||||
GrpcClientProperties properties = bind();
|
||||
Channel channelA = properties.getChannel().get("a");
|
||||
assertThat(channelA).isNotNull();
|
||||
assertThat(channelA.getUserAgent()).isNull();
|
||||
assertThat(channelA.isBypassCertificateValidation()).isFalse();
|
||||
assertThat(channelA.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(4194304));
|
||||
assertThat(channelA.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(8192));
|
||||
assertThat(channelA.getDefault().getDeadline()).isNull();
|
||||
assertThat(channelA.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(channelA.getKeepalive().getTime()).isEqualTo(Duration.ofMinutes(5));
|
||||
assertThat(channelA.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(channelA.getKeepalive().isWithoutCalls()).isFalse();
|
||||
assertThat(channelA.getSsl().getEnabled()).isNull();
|
||||
assertThat(channelA.getSsl().getBundle()).isNull();
|
||||
Channel channelB = properties.getChannel().get("b");
|
||||
assertThat(channelB).isNotNull();
|
||||
assertThat(channelB.getTarget()).isEqualTo("static://localhost:9090");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "client.yaml", content = """
|
||||
channel:
|
||||
test:
|
||||
target: static://my-server:8888
|
||||
user-agent: me
|
||||
bypass-certificate-validation: true
|
||||
inbound:
|
||||
message:
|
||||
max-size: 200MB
|
||||
metadata:
|
||||
max-size: 1GB
|
||||
default:
|
||||
deadline: 1s
|
||||
load-balancing-policy: pick_first
|
||||
idle:
|
||||
timeout: 1m
|
||||
keepalive:
|
||||
time: 200s
|
||||
timeout: 60000ms
|
||||
without-calls: true
|
||||
ssl:
|
||||
enabled: true
|
||||
bundle: my-bundle
|
||||
health:
|
||||
enabled: true
|
||||
service-name: my-service""")
|
||||
void specificProperties() throws Exception {
|
||||
GrpcClientProperties properties = bind();
|
||||
Channel channel = properties.getChannel().get("test");
|
||||
assertThat(channel).isNotNull();
|
||||
assertThat(channel.getTarget()).isEqualTo("static://my-server:8888");
|
||||
assertThat(channel.getUserAgent()).isEqualTo("me");
|
||||
assertThat(channel.isBypassCertificateValidation()).isTrue();
|
||||
assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(200));
|
||||
assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofGigabytes(1));
|
||||
assertThat(channel.getDefault().getDeadline()).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofMinutes(1));
|
||||
assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(200));
|
||||
assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofMillis(60000));
|
||||
assertThat(channel.getKeepalive().isWithoutCalls()).isTrue();
|
||||
assertThat(channel.getSsl().getEnabled()).isTrue();
|
||||
assertThat(channel.getSsl().getBundle()).isEqualTo("my-bundle");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "client.yaml", content = """
|
||||
channel:
|
||||
test:
|
||||
idle:
|
||||
timeout: 1
|
||||
keepalive:
|
||||
time: 60
|
||||
timeout: 5""")
|
||||
void withoutKeepAliveUnitsSpecified() throws Exception {
|
||||
GrpcClientProperties properties = bind();
|
||||
Channel channel = properties.getChannel().get("test");
|
||||
assertThat(channel).isNotNull();
|
||||
assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(60));
|
||||
assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "client.yaml", content = """
|
||||
channel:
|
||||
test:
|
||||
inbound:
|
||||
message:
|
||||
max-size: 1000
|
||||
metadata:
|
||||
max-size: 256""")
|
||||
void withoutInboundSizeUnitsSpecified() throws Exception {
|
||||
GrpcClientProperties properties = bind();
|
||||
Channel channel = properties.getChannel().get("test");
|
||||
assertThat(channel).isNotNull();
|
||||
assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(1000));
|
||||
assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(256));
|
||||
}
|
||||
|
||||
private GrpcClientProperties bind() throws Exception {
|
||||
StandardEnvironment environment = new StandardEnvironment();
|
||||
new YamlPropertySourceLoader().load("client.yaml", new ClassPathResource("client.yaml"))
|
||||
.forEach(environment.getPropertySources()::addLast);
|
||||
return Binder.get(environment)
|
||||
.bind("", Bindable.of(GrpcClientProperties.class))
|
||||
.orElseGet(GrpcClientProperties::new);
|
||||
}
|
||||
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import io.grpc.Codec;
|
||||
import io.grpc.Compressor;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.Decompressor;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcClientCodecConfiguration}.
|
||||
*
|
||||
* @author Andrei Lisa
|
||||
*/
|
||||
class GrpcCodecConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcClientCodecConfiguration.class));
|
||||
|
||||
@Test
|
||||
void whenCodecNotOnClasspathThenAutoconfigurationSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCodecConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomCompressorRegistryDoesNotAutoConfigureBean() {
|
||||
CompressorRegistry customRegistry = mock();
|
||||
this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry)
|
||||
.run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compressorRegistryAutoConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class)
|
||||
.isSameAs(CompressorRegistry.getDefaultInstance()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomCompressorsThenCompressorRegistryIsNewInstance() {
|
||||
Compressor compressor = mock();
|
||||
given(compressor.getMessageEncoding()).willReturn("foo");
|
||||
this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> {
|
||||
assertThat(context).hasSingleBean(CompressorRegistry.class);
|
||||
CompressorRegistry registry = context.getBean(CompressorRegistry.class);
|
||||
assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance());
|
||||
assertThat(registry.lookupCompressor("foo")).isSameAs(compressor);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomDecompressorRegistryDoesNotAutoConfigureBean() {
|
||||
DecompressorRegistry customRegistry = mock();
|
||||
this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry)
|
||||
.run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void decompressorRegistryAutoConfiguredAsExpected() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class)
|
||||
.isSameAs(DecompressorRegistry.getDefaultInstance()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomDecompressorsThenDecompressorRegistryIsNewInstance() {
|
||||
Decompressor decompressor = mock();
|
||||
given(decompressor.getMessageEncoding()).willReturn("foo");
|
||||
this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> {
|
||||
assertThat(context).hasSingleBean(DecompressorRegistry.class);
|
||||
DecompressorRegistry registry = context.getBean(DecompressorRegistry.class);
|
||||
assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance());
|
||||
assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.InsecureChannelCredentials;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesChannelCredentialsProvider}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesChannelCredentialsProviderTests {
|
||||
|
||||
private final TrustManager[] trustManagers = { mock() };
|
||||
|
||||
private final KeyManager[] keyManagers = { mock() };
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenTargetMatchesChannel() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setEnabled(true);
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
assertThat(credentials.getTrustManagers()).isNull();
|
||||
assertThat(credentials.getKeyManagers()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasDefault() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setEnabled(true);
|
||||
properties.getChannel().put("default", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
assertThat(credentials.getTrustManagers()).isNull();
|
||||
assertThat(credentials.getKeyManagers()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasNoDefaultUsesInsecure() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
ChannelCredentials credentials = provider.getChannelCredentials("test");
|
||||
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenSslExplictlyDisabled() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setEnabled(false);
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
ChannelCredentials credentials = provider.getChannelCredentials("test");
|
||||
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenSslExplictlyEnabledAndNoBundle() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setEnabled(true);
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
assertThat(credentials.getTrustManagers()).isNull();
|
||||
assertThat(credentials.getKeyManagers()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenNoSslEnabledSetButHasBundle() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setBundle("test");
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers);
|
||||
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenNoSslEnabledSetAndNoBundle() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
ChannelCredentials credentials = provider.getChannelCredentials("test");
|
||||
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenSslEnabledAndHasBundle() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.getSsl().setEnabled(true);
|
||||
channelProperties.getSsl().setBundle("test");
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers);
|
||||
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChannelCredentialsWhenBypassCertificateValidation() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
channelProperties.setBypassCertificateValidation(true);
|
||||
channelProperties.getSsl().setBundle("test");
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
|
||||
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
|
||||
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
|
||||
TrustManager trustManager = credentials.getTrustManagers().get(0);
|
||||
assertThat(trustManager.getClass().getName()).contains("InsecureTrustManager");
|
||||
assertThat(((X509ExtendedTrustManager) trustManager).getAcceptedIssuers()).isEmpty();
|
||||
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
|
||||
}
|
||||
|
||||
private SslBundle mockBundle() {
|
||||
SslBundle bundle = mock();
|
||||
SslManagerBundle managerBundle = mock();
|
||||
TrustManagerFactory trustManagerFactory = mock();
|
||||
KeyManagerFactory keyManagerFactory = mock();
|
||||
given(bundle.getManagers()).willReturn(managerBundle);
|
||||
given(managerBundle.getTrustManagerFactory()).willReturn(trustManagerFactory);
|
||||
given(managerBundle.getKeyManagerFactory()).willReturn(keyManagerFactory);
|
||||
given(trustManagerFactory.getTrustManagers()).willReturn(this.trustManagers);
|
||||
given(keyManagerFactory.getKeyManagers()).willReturn(this.keyManagers);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesGrpcChannelBuilderCustomizer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesGrpcChannelBuilderCustomizerTests {
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenHasMatchingChannel() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
properties.getChannel().put("test", createTestChannelProperties());
|
||||
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
|
||||
T builder = mock();
|
||||
customizer.customize("test", builder);
|
||||
assertMapped(builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenHasDefaultChannel() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
properties.getChannel().put("default", createTestChannelProperties());
|
||||
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
|
||||
T builder = mock();
|
||||
customizer.customize("test", builder);
|
||||
assertMapped(builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenHasNoMatchAndNoDefault() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
properties.getChannel().put("other", createTestChannelProperties());
|
||||
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
|
||||
T builder = mock();
|
||||
customizer.customize("test", builder);
|
||||
assertMappedStockDefaults(builder);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenChannelDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() {
|
||||
assertNoLoadBalancerMappedBasedOnChannel("unix:test");
|
||||
assertNoLoadBalancerMappedBasedOnChannel("in-process:test");
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> void assertNoLoadBalancerMappedBasedOnChannel(String target) {
|
||||
T builder = getBuilder((channelProperties) -> {
|
||||
channelProperties.setTarget(target);
|
||||
channelProperties.getDefault().setLoadBalancingPolicy("testlbp");
|
||||
});
|
||||
then(builder).should(never()).defaultLoadBalancingPolicy(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenTargetDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() {
|
||||
GrpcChannelBuilderCustomizer<?> customizer = getCustomizer(
|
||||
(channelProperties) -> channelProperties.setTarget("static://localhost:1234"));
|
||||
assertNoLoadBalancerMappedBasedOnTarget(customizer, "unix:test");
|
||||
assertNoLoadBalancerMappedBasedOnTarget(customizer, "in-process:test");
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> void assertNoLoadBalancerMappedBasedOnTarget(
|
||||
GrpcChannelBuilderCustomizer<T> customizer, String target) {
|
||||
T builder = mock();
|
||||
customizer.customize(target, builder);
|
||||
then(builder).should(never()).defaultLoadBalancingPolicy(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsMinusOneUsesMaxValue() {
|
||||
T builder = getBuilder(
|
||||
(channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-1)));
|
||||
then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsTooBigUsesMaxValue() {
|
||||
T builder = getBuilder((channelProperties) -> channelProperties.getInbound()
|
||||
.getMessage()
|
||||
.setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100)));
|
||||
then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsNegativeAndNotMinusOneThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> getBuilder(
|
||||
(channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-2))))
|
||||
.withMessage("Unsupported max size value -2B");
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsMinusOneUsesMaxValue() {
|
||||
T builder = getBuilder(
|
||||
(channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-1)));
|
||||
then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsTooBigUsesMaxValue() {
|
||||
T builder = getBuilder((channelProperties) -> channelProperties.getInbound()
|
||||
.getMetadata()
|
||||
.setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100)));
|
||||
then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsNegativeAndNotMinusOneThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> getBuilder(
|
||||
(channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-2))))
|
||||
.withMessage("Unsupported max size value -2B");
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> T getBuilder(Consumer<Channel> setup) {
|
||||
GrpcChannelBuilderCustomizer<T> customizer = getCustomizer(setup);
|
||||
T builder = mock();
|
||||
customizer.customize("test", builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> getCustomizer(
|
||||
Consumer<Channel> setup) {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channelProperties = new Channel();
|
||||
setup.accept(channelProperties);
|
||||
properties.getChannel().put("test", channelProperties);
|
||||
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
|
||||
return customizer;
|
||||
}
|
||||
|
||||
private Channel createTestChannelProperties() {
|
||||
Channel properties = new Channel();
|
||||
properties.setUserAgent("testua");
|
||||
properties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(10));
|
||||
properties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(20));
|
||||
properties.getDefault().setDeadline(Duration.ofMinutes(5));
|
||||
properties.getDefault().setLoadBalancingPolicy("testlbp");
|
||||
properties.getIdle().setTimeout(Duration.ofMinutes(6));
|
||||
properties.getKeepalive().setTime(Duration.ofMinutes(7));
|
||||
properties.getKeepalive().setTimeout(Duration.ofMinutes(8));
|
||||
properties.getKeepalive().setWithoutCalls(true);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> void assertMapped(T builder) {
|
||||
then(builder).should().userAgent("testua");
|
||||
then(builder).should().maxInboundMessageSize(10);
|
||||
then(builder).should().maxInboundMetadataSize(20);
|
||||
ArgumentCaptor<ClientInterceptor[]> interceptors = ArgumentCaptor.captor();
|
||||
then(builder).should().intercept(interceptors.capture());
|
||||
ClientInterceptor interceptor = interceptors.getValue()[0];
|
||||
assertThat(interceptor).isInstanceOf(DefaultDeadlineSetupClientInterceptor.class)
|
||||
.extracting("defaultDeadline")
|
||||
.isEqualTo(Duration.ofMinutes(5));
|
||||
then(builder).should().defaultLoadBalancingPolicy("testlbp");
|
||||
then(builder).should().idleTimeout(360000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveTime(420000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveTimeout(480000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveWithoutCalls(true);
|
||||
}
|
||||
|
||||
private <T extends ManagedChannelBuilder<T>> void assertMappedStockDefaults(T builder) {
|
||||
then(builder).should().maxInboundMessageSize(4194304);
|
||||
then(builder).should().maxInboundMetadataSize(8192);
|
||||
then(builder).should().defaultLoadBalancingPolicy("round_robin");
|
||||
then(builder).should().idleTimeout(20000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveTime(300000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveTimeout(20000000000L, TimeUnit.NANOSECONDS);
|
||||
then(builder).should().keepAliveWithoutCalls(false);
|
||||
then(builder).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
|
||||
import org.springframework.grpc.client.VirtualTargets;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesVirtualTargets}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesVirtualTargetsTests {
|
||||
|
||||
@Test
|
||||
void getTargetWhenHasMatchingChannel() {
|
||||
GrpcClientProperties properties = createProperties("test", "my-server:8888");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenDefaultAndDefaultChannelDefined() {
|
||||
GrpcClientProperties properties = createProperties("default", "my-server:8888");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("default")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenDefaultAndNoDefaultChannelDefined() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("default")).isEqualTo("localhost:9090");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenChannelHasStaticTargetReturnsStrippedTarget() {
|
||||
GrpcClientProperties properties = createProperties("test", "static://my-server:8888");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenChannelHasTcpTargetReturnsStrippedTarget() {
|
||||
GrpcClientProperties properties = createProperties("test", "tcp://my-server:8888");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenChannelHasOtherUrlTarget() {
|
||||
GrpcClientProperties properties = createProperties("test", "foo://my-server:8888");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("test")).isEqualTo("foo://my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenStaticReturnsStripped() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("static://my-server:8888")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenTcpReturnsStripped() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("tcp://my-server:8888")).isEqualTo("my-server:8888");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenUnixUrlDoesNotPrependStatic() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("foo://bar")).isEqualTo("foo://bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenUrlReturnsAsIs() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("unix:foo")).isEqualTo("unix:foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetUrlWhenHasColonWithoutSlashReturnsAsIs() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
|
||||
assertThat(targets.getTarget("localhost:123/bar")).isEqualTo("localhost:123/bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetWhenNotChannelNameResolvesPlaceholders() {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty("channelName", "foo");
|
||||
VirtualTargets targets = new PropertiesVirtualTargets(environment, properties);
|
||||
assertThat(targets.getTarget("my-server-${channelName}:8888")).isEqualTo("my-server-foo:8888");
|
||||
}
|
||||
|
||||
private GrpcClientProperties createProperties(String name, String target) {
|
||||
GrpcClientProperties properties = new GrpcClientProperties();
|
||||
Channel channel = new Channel();
|
||||
channel.setTarget(target);
|
||||
properties.getChannel().put(name, channel);
|
||||
return properties;
|
||||
}
|
||||
|
||||
}
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.LoadBalancerRegistry;
|
||||
import io.grpc.NameResolver.ConfigOrError;
|
||||
import io.grpc.Status.Code;
|
||||
import io.grpc.internal.AutoConfiguredLoadBalancerFactory;
|
||||
import io.grpc.internal.ScParser;
|
||||
import io.grpc.internal.ServiceConfigUtil;
|
||||
import io.grpc.internal.ServiceConfigUtil.LbConfig;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.BindException;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServiceConfig}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ServiceConfigTests {
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
""")
|
||||
void pickFirstLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst:
|
||||
shuffle-address-list: true
|
||||
""")
|
||||
void pickFirstLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("PickFirstLoadBalancer");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("shuffleAddressList").isEqualTo(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- roundrobin: {}
|
||||
""")
|
||||
void roundRobinLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("policy=round_robin")
|
||||
.contains("no service config");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- weightedroundrobin: {}
|
||||
""")
|
||||
void weightedRoundRobinLoadBalancing() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- weightedroundrobin:
|
||||
blackout-period: 1m
|
||||
weight-expiration-period: 500ms
|
||||
out-of-band-reporting-period: 1s
|
||||
enable-out-of-band-load-report: true
|
||||
weight-update-period: 2s
|
||||
error-utilization-penalty: 0.5
|
||||
""")
|
||||
void weightedRoundRobinLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("weighted_round_robin");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("WeightedRoundRobinLoadBalancerProvider");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("blackoutPeriodNanos")
|
||||
.isEqualTo(Duration.ofMinutes(1).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightExpirationPeriodNanos")
|
||||
.isEqualTo(Duration.ofMillis(500).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("enableOobLoadReport").isEqualTo(true);
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("oobReportingPeriodNanos")
|
||||
.isEqualTo(Duration.ofSeconds(1).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("weightUpdatePeriodNanos")
|
||||
.isEqualTo(Duration.ofSeconds(2).toNanos());
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("errorUtilizationPenalty").isEqualTo(0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- grpc:
|
||||
child:
|
||||
- roundrobin: {}
|
||||
- pickfirst: {}
|
||||
service-name: test
|
||||
initial-fallback-timeout: 10s
|
||||
""")
|
||||
void grpcLoadBalancingWithProperties() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(1);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("grpclb");
|
||||
PolicySelection loadBalancingPolicySelection = getLoadBalancingPolicySelection(loadBalancingConfigs);
|
||||
assertThat(loadBalancingPolicySelection.toString()).contains("GrpclbLoadBalancerProvider");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("mode").hasToString("ROUND_ROBIN");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("serviceName").isEqualTo("test");
|
||||
assertThat(loadBalancingPolicySelection.getConfig()).extracting("fallbackTimeoutMs")
|
||||
.isEqualTo(Duration.ofSeconds(10).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
- weightedroundrobin: {}
|
||||
""")
|
||||
void multipleLoadBalancerPolicies() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("loadBalancingConfig");
|
||||
List<Map<String, ?>> loadBalancingConfigs = ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(map);
|
||||
assertThat(loadBalancingConfigs).hasSize(2);
|
||||
assertThat(loadBalancingConfigs.get(0)).containsKey("pick_first");
|
||||
assertThat(loadBalancingConfigs.get(1)).containsKey("weighted_round_robin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- pickfirst: {}
|
||||
weightedroundrobin: {}
|
||||
""")
|
||||
void whenMultileLoadBalancingPoliciesInListItemThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
load-balancing:
|
||||
- {}
|
||||
""")
|
||||
void whenNoLoadBalancingPoliciesInListItemThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(InvalidConfigurationPropertyValueException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
- service: s-two
|
||||
method: m-two
|
||||
wait-for-ready: true
|
||||
max-request-message: 10KB
|
||||
max-response-message: 20KB
|
||||
timeout: 30s
|
||||
""")
|
||||
@SuppressWarnings("unchecked")
|
||||
void methodConfig() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("methodConfig");
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, false);
|
||||
assertThat(serviceMethodMap).containsOnlyKeys("s-one/m-one", "s-two/m-two");
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("timeoutNanos").isEqualTo(Duration.ofSeconds(30).toNanos());
|
||||
assertThat(methodInfo).extracting("waitForReady").isEqualTo(Boolean.TRUE);
|
||||
assertThat(methodInfo).extracting("maxOutboundMessageSize").isEqualTo(10240);
|
||||
assertThat(methodInfo).extracting("maxInboundMessageSize").isEqualTo(20480);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
retry:
|
||||
max-attempts: 2
|
||||
initial-backoff: 1m
|
||||
max-backoff: 1h
|
||||
backoff-multiplier: 2.5
|
||||
per-attempt-receive-timeout: 2s
|
||||
retryable-status-codes:
|
||||
- cancelled
|
||||
- already-exists
|
||||
""")
|
||||
void methodConfigRetryPolicy() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, true);
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("retryPolicy.maxAttempts").isEqualTo(2);
|
||||
assertThat(methodInfo).extracting("retryPolicy.initialBackoffNanos").isEqualTo(Duration.ofMinutes(1).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.maxBackoffNanos").isEqualTo(Duration.ofHours(1).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.backoffMultiplier").isEqualTo(2.5);
|
||||
assertThat(methodInfo).extracting("retryPolicy.perAttemptRecvTimeoutNanos")
|
||||
.isEqualTo(Duration.ofSeconds(2).toNanos());
|
||||
assertThat(methodInfo).extracting("retryPolicy.retryableStatusCodes")
|
||||
.asInstanceOf(InstanceOfAssertFactories.SET)
|
||||
.containsExactlyInAnyOrder(Code.CANCELLED, Code.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
hedging:
|
||||
max-attempts: 4
|
||||
delay: 6s
|
||||
non-fatal-status-codes:
|
||||
- invalid-argument
|
||||
- deadline-exceeded
|
||||
""")
|
||||
void methodConfigHedgingPolicy() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
Map<String, ?> serviceMethodMap = getServiceMethodMap(map, true);
|
||||
Object methodInfo = serviceMethodMap.get("s-one/m-one");
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.maxAttempts").isEqualTo(4);
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.hedgingDelayNanos").isEqualTo(Duration.ofSeconds(6).toNanos());
|
||||
assertThat(methodInfo).extracting("hedgingPolicy.nonFatalStatusCodes")
|
||||
.asInstanceOf(InstanceOfAssertFactories.SET)
|
||||
.containsExactlyInAnyOrder(Code.INVALID_ARGUMENT, Code.DEADLINE_EXCEEDED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
method:
|
||||
- name:
|
||||
- service: s-one
|
||||
method: m-one
|
||||
retry: {}
|
||||
hedging: {}
|
||||
""")
|
||||
void whenMultiplePoliciesInMethodConfigThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindAndGetAsMap())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
retrythrottling:
|
||||
max-tokens: 2.5
|
||||
token-ratio: 1.5
|
||||
""")
|
||||
void retryThrottling() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("retryThrottling");
|
||||
Object throttle = ReflectionTestUtils.invokeMethod(ServiceConfigUtil.class, "getThrottlePolicy", map);
|
||||
assertThat(throttle).extracting("maxTokens").isEqualTo(2500);
|
||||
assertThat(throttle).extracting("tokenRatio").isEqualTo(1500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "config.yaml", content = """
|
||||
config:
|
||||
healthcheck:
|
||||
service-name: test
|
||||
""")
|
||||
@SuppressWarnings("unchecked")
|
||||
void healthCheck() throws Exception {
|
||||
Map<String, Object> map = bindAndGetAsMap();
|
||||
assertThat(map).containsKey("healthCheckConfig");
|
||||
Map<String, Object> healthCheckedService = (Map<String, Object>) ServiceConfigUtil.getHealthCheckedService(map);
|
||||
assertThat(healthCheckedService).hasSize(1).containsEntry("serviceName", "test");
|
||||
}
|
||||
|
||||
private PolicySelection getLoadBalancingPolicySelection(List<Map<String, ?>> rawConfigs) {
|
||||
List<LbConfig> unwrappedConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList(rawConfigs);
|
||||
LoadBalancerRegistry registry = LoadBalancerRegistry.getDefaultRegistry();
|
||||
ConfigOrError selected = ServiceConfigUtil.selectLbPolicyFromList(unwrappedConfigs, registry);
|
||||
assertThat(selected).isNotNull();
|
||||
PolicySelection policySelection = (PolicySelection) selected.getConfig();
|
||||
if (policySelection == null) {
|
||||
System.err.println(selected);
|
||||
System.err.println(selected.getError());
|
||||
if (selected.getError() != null && selected.getError().asException() != null) {
|
||||
selected.getError().asException().printStackTrace();
|
||||
}
|
||||
}
|
||||
assertThat(policySelection).isNotNull();
|
||||
return policySelection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, ?> getServiceMethodMap(Map<String, Object> map, boolean retryEnabled) {
|
||||
ScParser scParser = new ScParser(retryEnabled, 100, 100, new AutoConfiguredLoadBalancerFactory("pick_first"));
|
||||
Object config = scParser.parseServiceConfig(map).getConfig();
|
||||
assertThat(config).isNotNull();
|
||||
Object serviceMethodMap = ReflectionTestUtils.getField(config, "serviceMethodMap");
|
||||
assertThat(serviceMethodMap).isNotNull();
|
||||
return (Map<String, ?>) serviceMethodMap;
|
||||
}
|
||||
|
||||
private Map<String, Object> bindAndGetAsMap() throws Exception {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
bind().applyTo(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
private ServiceConfig bind() throws Exception {
|
||||
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
|
||||
PropertySource<?> propertySource = loader.load("config.yaml", new ClassPathResource("config.yaml")).get(0);
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.getPropertySources().addLast(propertySource);
|
||||
Binder binder = Binder.get(environment);
|
||||
return binder.bind("config", ServiceConfig.class).get();
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure.test.scan;
|
||||
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.stub.AbstractBlockingStub;
|
||||
import io.grpc.stub.AbstractStub.StubFactory;
|
||||
|
||||
public final class DummyBlockingGrpc {
|
||||
|
||||
private DummyBlockingGrpc() {
|
||||
}
|
||||
|
||||
public static DummyBlockingStub newBlockingStub(io.grpc.Channel channel) {
|
||||
return AbstractBlockingStub.newStub((StubFactory<DummyBlockingStub>) DummyBlockingStub::new, channel);
|
||||
}
|
||||
|
||||
public static class DummyBlockingStub extends AbstractBlockingStub<DummyBlockingStub> {
|
||||
|
||||
protected DummyBlockingStub(Channel channel, CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DummyBlockingStub build(Channel channel, CallOptions callOptions) {
|
||||
return new DummyBlockingStub(channel, callOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.client.autoconfigure.test.scan;
|
||||
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.stub.AbstractBlockingStub;
|
||||
import io.grpc.stub.AbstractStub.StubFactory;
|
||||
|
||||
public final class DummyBlockingV2Grpc {
|
||||
|
||||
private DummyBlockingV2Grpc() {
|
||||
}
|
||||
|
||||
public static DummyBlockingV2Stub newBlockingV2Stub(io.grpc.Channel channel) {
|
||||
return AbstractBlockingStub.newStub((StubFactory<DummyBlockingV2Stub>) DummyBlockingV2Stub::new, channel);
|
||||
}
|
||||
|
||||
public static class DummyBlockingV2Stub extends AbstractBlockingStub<DummyBlockingV2Stub> {
|
||||
|
||||
protected DummyBlockingV2Stub(Channel channel, CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DummyBlockingV2Stub build(Channel channel, CallOptions callOptions) {
|
||||
return new DummyBlockingV2Stub(channel, callOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.client.autoconfigure.test.scan;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the License);
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.auto-configuration"
|
||||
id "org.springframework.boot.configuration-properties"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
}
|
||||
|
||||
description = "Spring Boot gRPC Server"
|
||||
|
||||
dependencies {
|
||||
api(project(":core:spring-boot"))
|
||||
api("org.springframework.grpc:spring-grpc-core")
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
optional(project(":module:spring-boot-health"))
|
||||
optional(project(":module:spring-boot-micrometer-observation"))
|
||||
optional(project(":module:spring-boot-security"))
|
||||
optional(project(":module:spring-boot-security-oauth2-resource-server"))
|
||||
optional("com.fasterxml.jackson.core:jackson-annotations")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
optional("io.grpc:grpc-servlet-jakarta")
|
||||
optional("io.grpc:grpc-services")
|
||||
optional("io.grpc:grpc-netty")
|
||||
optional("io.grpc:grpc-netty-shaded")
|
||||
optional("io.grpc:grpc-inprocess")
|
||||
optional("io.grpc:grpc-kotlin-stub") {
|
||||
exclude group: "javax.annotation", module: "javax.annotation-api"
|
||||
}
|
||||
optional("io.micrometer:micrometer-core")
|
||||
optional("io.micrometer:context-propagation")
|
||||
optional("jakarta.servlet:jakarta.servlet-api")
|
||||
|
||||
testImplementation(project(":core:spring-boot-test"))
|
||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||
testImplementation(testFixtures(project(":core:spring-boot-autoconfigure")))
|
||||
testImplementation("org.springframework:spring-web")
|
||||
|
||||
testRuntimeOnly("ch.qos.logback:logback-classic")
|
||||
}
|
||||
|
||||
tasks.named("compileTestJava") {
|
||||
options.nullability.checking = "tests"
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
import io.grpc.ServiceDescriptor;
|
||||
import io.grpc.servlet.jakarta.GrpcServlet;
|
||||
import io.grpc.servlet.jakarta.ServletServerBuilder;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletRegistration.Dynamic;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.web.servlet.DynamicRegistrationBean;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceSpec;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link DynamicRegistrationBean} that can be used to register a {@link GrpcServlet}.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Toshiaki Maki
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class GrpcServletRegistration extends DynamicRegistrationBean<Dynamic> {
|
||||
|
||||
private static Log logger = LogFactory.getLog(GrpcServletRegistration.class);
|
||||
|
||||
private final GrpcServlet servlet;
|
||||
|
||||
private final String[] urlMappings;
|
||||
|
||||
/**
|
||||
* Create a new {@link GrpcServletRegistration} instance.
|
||||
* @param serviceDiscoverer the gRPC service discoverer
|
||||
* @param serviceConfigurer the gRPC service configurer
|
||||
*/
|
||||
public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer) {
|
||||
this(serviceDiscoverer, serviceConfigurer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link GrpcServletRegistration} instance.
|
||||
* @param serviceDiscoverer the gRPC service discoverer
|
||||
* @param serviceConfigurer the gRPC service configurer
|
||||
* @param serverBuilderCustomizer an optional customizer to configure the
|
||||
* {@link ServletServerBuilder}
|
||||
*/
|
||||
public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
|
||||
@Nullable Consumer<ServletServerBuilder> serverBuilderCustomizer) {
|
||||
Assert.notNull(serviceDiscoverer, "'serviceDiscoverer' must not be null");
|
||||
Assert.notNull(serviceConfigurer, "'serviceConfigurer' must not be null");
|
||||
ServletServerBuilder builder = new ServletServerBuilder();
|
||||
List<String> urlMappings = new ArrayList<>();
|
||||
for (GrpcServiceSpec spec : serviceDiscoverer.findServices()) {
|
||||
ServiceDescriptor descriptor = spec.service().bindService().getServiceDescriptor();
|
||||
logger.info(LogMessage.format("Registering servlet gRPC service: %s", descriptor.getName()));
|
||||
urlMappings.add("/" + descriptor.getName() + "/*");
|
||||
ServerServiceDefinition definition = serviceConfigurer.configure(spec, null);
|
||||
builder.addService(definition);
|
||||
}
|
||||
if (serverBuilderCustomizer != null) {
|
||||
serverBuilderCustomizer.accept(builder);
|
||||
}
|
||||
this.servlet = builder.buildServlet();
|
||||
this.urlMappings = urlMappings.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dynamic addRegistration(String description, ServletContext servletContext) {
|
||||
return servletContext.addServlet(getName(), this.servlet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(Dynamic registration) {
|
||||
super.configure(registration);
|
||||
if (!ObjectUtils.isEmpty(this.urlMappings)) {
|
||||
registration.addMapping(this.urlMappings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDescription() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
private String getName() {
|
||||
return getOrDeduceName(this.servlet);
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
|
||||
/**
|
||||
* {@link Conditional @Conditional} that matches when
|
||||
* {@code spring.grpc.server.factory.enabled} is {@code true} or missing.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Documented
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.factory.enabled", matchIfMissing = true)
|
||||
@interface ConditionalOnGrpcServerFactoryEnabled {
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.grpc.server.InProcessGrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link Conditional @Conditional} that matches when no network gRPC server is found.
|
||||
* Concretely:
|
||||
* <ul>
|
||||
* <li>There are no {@link GrpcServletRegistration} beans.</li>
|
||||
* <li>There are no {@link GrpcServerFactory} beans (ignoring
|
||||
* {@link InProcessGrpcServerFactory} beans)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Documented
|
||||
@Conditional(MissingNetworkGrpcServerCondition.class)
|
||||
@interface ConditionalOnMissingNetworkGrpcServer {
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.Grpc;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.server.GlobalServerInterceptor;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.grpc.server.ServerBuilderCustomizer;
|
||||
import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler;
|
||||
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
|
||||
import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor;
|
||||
import org.springframework.grpc.server.exception.ReactiveStubBeanDefinitionRegistrar;
|
||||
import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring gRPC server-side
|
||||
* components.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class })
|
||||
@ConditionalOnBean(BindableService.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(GrpcServerProperties.class)
|
||||
@Import({ GrpcServerCodecConfiguration.class, ServletGrpcServerConfiguration.class,
|
||||
ShadedNettyGrpcServerConfiguration.class, NettyGrpcServerConfiguration.class,
|
||||
InProcessGrpcServerConfiguration.class })
|
||||
public final class GrpcServerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties,
|
||||
ObjectProvider<CompressorRegistry> compressorRegistry,
|
||||
ObjectProvider<DecompressorRegistry> decompressorRegistry,
|
||||
ObjectProvider<GrpcServerExecutorProvider> executorProvider,
|
||||
ObjectProvider<ServerBuilderCustomizer<?>> customizers) {
|
||||
return new GrpcServerBuilderCustomizers(grpcServerProperties, compressorRegistry, decompressorRegistry,
|
||||
executorProvider, customizers);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(GrpcServiceConfigurer.class)
|
||||
DefaultGrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) {
|
||||
return new DefaultGrpcServiceConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(GrpcServiceDiscoverer.class)
|
||||
DefaultGrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicationContext) {
|
||||
return new DefaultGrpcServiceDiscoverer(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@GlobalServerInterceptor
|
||||
@ConditionalOnMissingBean
|
||||
GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor(List<GrpcExceptionHandler> exceptionHandlers) {
|
||||
CompositeGrpcExceptionHandler compositeHandler = new CompositeGrpcExceptionHandler(
|
||||
exceptionHandlers.toArray(GrpcExceptionHandler[]::new));
|
||||
return new GrpcExceptionHandlerInterceptor(compositeHandler);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "com.salesforce.reactivegrpc.common.Function")
|
||||
@Import(ReactiveStubBeanDefinitionRegistrar.class)
|
||||
static class ReactiveStubConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ServerBuilder;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.grpc.server.ServerBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* Invokes the customizations to a {@link ServerBuilder} based on the provided beans.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GrpcServerBuilderCustomizers {
|
||||
|
||||
private final List<ServerBuilderCustomizer<?>> customizers;
|
||||
|
||||
GrpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties,
|
||||
ObjectProvider<CompressorRegistry> compressorRegistry,
|
||||
ObjectProvider<DecompressorRegistry> decompressorRegistry,
|
||||
ObjectProvider<GrpcServerExecutorProvider> executorProvider,
|
||||
ObjectProvider<ServerBuilderCustomizer<?>> customizers) {
|
||||
this(grpcServerProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(),
|
||||
executorProvider.getIfAvailable(), customizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
GrpcServerBuilderCustomizers(List<? extends ServerBuilderCustomizer<?>> customizers) {
|
||||
this(null, null, null, null, customizers);
|
||||
}
|
||||
|
||||
GrpcServerBuilderCustomizers(@Nullable GrpcServerProperties grpcServerProperties,
|
||||
@Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry,
|
||||
@Nullable GrpcServerExecutorProvider executorProvider,
|
||||
List<? extends ServerBuilderCustomizer<?>> customizers) {
|
||||
List<ServerBuilderCustomizer<?>> all = new ArrayList<>();
|
||||
addCustomizer(all, compressorRegistry, ServerBuilder::compressorRegistry);
|
||||
addCustomizer(all, decompressorRegistry, ServerBuilder::decompressorRegistry);
|
||||
addCustomizer(all, executorProvider, (builder, bean) -> builder.executor(bean.getExecutor()));
|
||||
if (grpcServerProperties != null) {
|
||||
all.add(new PropertiesServerBuilderCustomizer<>(grpcServerProperties));
|
||||
}
|
||||
all.addAll(customizers);
|
||||
this.customizers = List.copyOf(all);
|
||||
}
|
||||
|
||||
private static <B extends ServerBuilder<B>, T> void addCustomizer(List<ServerBuilderCustomizer<?>> customizers,
|
||||
@Nullable T bean, BiConsumer<B, T> action) {
|
||||
if (bean != null) {
|
||||
ServerBuilderCustomizer<B> customizer = (builder) -> action.accept(builder, bean);
|
||||
customizers.add(customizer);
|
||||
}
|
||||
}
|
||||
|
||||
<T extends ServerBuilder<T>> List<ServerBuilderCustomizer<T>> forFactory() {
|
||||
return List.of(this::apply);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T extends ServerBuilder<?>> void apply(T builder) {
|
||||
LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, builder)
|
||||
.withLogger(GrpcServerBuilderCustomizers.class)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.grpc.Compressor;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.Decompressor;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ServerBuilder;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* The configuration that contains all gRPC codec related beans.
|
||||
*
|
||||
* @author Andrei Lisa
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class GrpcServerCodecConfiguration {
|
||||
|
||||
/**
|
||||
* The compressor registry that is set on the
|
||||
* {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} .
|
||||
* @param compressors the compressors to use on the registry
|
||||
* @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the
|
||||
* specified compressors or the {@link CompressorRegistry#getDefaultInstance() default
|
||||
* registry} if no custom compressors are available in the application context.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
CompressorRegistry grpcCompressorRegistry(List<Compressor> compressors) {
|
||||
if (compressors.isEmpty()) {
|
||||
return CompressorRegistry.getDefaultInstance();
|
||||
}
|
||||
CompressorRegistry registry = CompressorRegistry.newEmptyInstance();
|
||||
compressors.forEach(registry::register);
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompressor registry that is set on the
|
||||
* {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}.
|
||||
* @param decompressors the decompressors to use on the registry
|
||||
* @return a new {@link DecompressorRegistry#emptyInstance() registry} with the
|
||||
* specified decompressors or the {@link DecompressorRegistry#getDefaultInstance()
|
||||
* default registry} if no custom decompressors are available in the application
|
||||
* context.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
DecompressorRegistry grpcDecompressorRegistry(List<Decompressor> decompressors) {
|
||||
if (decompressors.isEmpty()) {
|
||||
return DecompressorRegistry.getDefaultInstance();
|
||||
}
|
||||
DecompressorRegistry registry = DecompressorRegistry.emptyInstance();
|
||||
for (Decompressor decompressor : decompressors) {
|
||||
registry = registry.with(decompressor, false);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Strategy interface to determine the {@link Executor} to use for the gRPC server.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface GrpcServerExecutorProvider {
|
||||
|
||||
/**
|
||||
* Returns a {@link Executor} for the gRPC server, if it needs to be customized.
|
||||
* @return the executor to use for the gRPC server
|
||||
*/
|
||||
Executor getExecutor();
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link GrpcServerFactory server factory} before it is fully initialized.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface GrpcServerFactoryCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the given {@link GrpcServerFactory}.
|
||||
* @param serverFactory the server factory to customize
|
||||
*/
|
||||
void customize(GrpcServerFactory serverFactory);
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor;
|
||||
import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.grpc.server.GlobalServerInterceptor;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side observations.
|
||||
*
|
||||
* @author Sunny Tang
|
||||
* @author Chris Bono
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@AutoConfiguration(
|
||||
afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration")
|
||||
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObservationRegistry.class,
|
||||
ObservationGrpcServerInterceptor.class })
|
||||
@ConditionalOnBean(ObservationRegistry.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.observation.enabled", matchIfMissing = true)
|
||||
public final class GrpcServerObservationAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
@GlobalServerInterceptor
|
||||
ObservationGrpcServerInterceptor grpcServerObservationInterceptor(ObservationRegistry observationRegistry) {
|
||||
return new ObservationGrpcServerInterceptor(observationRegistry);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = { "io.grpc.kotlin.AbstractCoroutineStub", "io.micrometer.context.ContextRegistry" })
|
||||
static class GrpcServerCoroutineStubConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(10)
|
||||
@GlobalServerInterceptor
|
||||
ObservationCoroutineContextServerInterceptor grpcServerObservationCoroutineContextServerInterceptor(
|
||||
ObservationRegistry observationRegistry) {
|
||||
return new ObservationCoroutineContextServerInterceptor(observationRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import io.grpc.TlsServerCredentials.ClientAuth;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.convert.DataSizeUnit;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.util.unit.DataUnit;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties Properties} for Spring gRPC servers.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.grpc.server")
|
||||
public class GrpcServerProperties {
|
||||
|
||||
/**
|
||||
* Port on which the gRPC server should listen. Use '0' to bind to a dynamic port.
|
||||
*/
|
||||
private @Nullable Integer port;
|
||||
|
||||
/**
|
||||
* Network address to which the gRPC server should bind.
|
||||
*/
|
||||
private @Nullable InetAddress address;
|
||||
|
||||
private final Shutdown shutdown = new Shutdown();
|
||||
|
||||
private final Inbound inbound = new Inbound();
|
||||
|
||||
private final Inprocess inprocess = new Inprocess();
|
||||
|
||||
private final Keepalive keepalive = new Keepalive();
|
||||
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
private final Netty netty = new Netty();
|
||||
|
||||
private final Servlet servlet = new Servlet();
|
||||
|
||||
public @Nullable Integer getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(@Nullable Integer port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public @Nullable InetAddress getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public void setAddress(@Nullable InetAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public Shutdown getShutdown() {
|
||||
return this.shutdown;
|
||||
}
|
||||
|
||||
public Inbound getInbound() {
|
||||
return this.inbound;
|
||||
}
|
||||
|
||||
public Inprocess getInprocess() {
|
||||
return this.inprocess;
|
||||
}
|
||||
|
||||
public Keepalive getKeepalive() {
|
||||
return this.keepalive;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public Netty getNetty() {
|
||||
return this.netty;
|
||||
}
|
||||
|
||||
public Servlet getServlet() {
|
||||
return this.servlet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server shutdown properties.
|
||||
*/
|
||||
public static class Shutdown {
|
||||
|
||||
/**
|
||||
* Maximum time to wait for the server to gracefully shutdown. When the value is
|
||||
* negative, the server waits forever. When the value is 0, the server will force
|
||||
* shutdown immediately. The default is 30 seconds.
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private Duration gracePeriod = Duration.ofSeconds(30);
|
||||
|
||||
public Duration getGracePeriod() {
|
||||
return this.gracePeriod;
|
||||
}
|
||||
|
||||
public void setGracePeriod(Duration gracePeriod) {
|
||||
this.gracePeriod = gracePeriod;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound properties.
|
||||
*/
|
||||
public static class Inbound {
|
||||
|
||||
private final Message message = new Message();
|
||||
|
||||
private final Metadata metadata = new Metadata();
|
||||
|
||||
public Message getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public Metadata getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound message properties.
|
||||
*/
|
||||
public static class Message {
|
||||
|
||||
/**
|
||||
* Maximum message size allowed to be received by the server (default 4MiB).
|
||||
*/
|
||||
@DataSizeUnit(DataUnit.BYTES)
|
||||
private DataSize maxSize = DataSize.ofBytes(4194304);
|
||||
|
||||
public DataSize getMaxSize() {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(DataSize maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* In-bound metadata properties.
|
||||
*/
|
||||
public static class Metadata {
|
||||
|
||||
/**
|
||||
* Maximum metadata size allowed to be received by the server (default 8KiB).
|
||||
*/
|
||||
@DataSizeUnit(DataUnit.BYTES)
|
||||
private DataSize maxSize = DataSize.ofBytes(8192);
|
||||
|
||||
public DataSize getMaxSize() {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(DataSize maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* In-process gRPC properties.
|
||||
*/
|
||||
public static class Inprocess {
|
||||
|
||||
/**
|
||||
* The name of the in-process server or null to not start the in-process server.
|
||||
*/
|
||||
private @Nullable String name;
|
||||
|
||||
public @Nullable String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(@Nullable String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep-alive properties.
|
||||
*/
|
||||
public static class Keepalive {
|
||||
|
||||
/**
|
||||
* Duration without read activity before sending a keep alive ping (default 2h).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration time = Duration.ofHours(2);
|
||||
|
||||
/**
|
||||
* Maximum time to wait for read activity after sending a keep alive ping. If
|
||||
* sender does not receive an acknowledgment within this time, it will close the
|
||||
* connection (default 20s).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration timeout = Duration.ofSeconds(20);
|
||||
|
||||
private final Permit permit = new Permit();
|
||||
|
||||
private final Connection connection = new Connection();
|
||||
|
||||
public @Nullable Duration getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
public void setTime(@Nullable Duration time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public @Nullable Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(@Nullable Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Permit getPermit() {
|
||||
return this.permit;
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep-alive permit properties.
|
||||
*/
|
||||
public static class Permit {
|
||||
|
||||
/**
|
||||
* Maximum keep-alive time clients are permitted to configure (default 5m).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration time = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* Whether clients are permitted to send keep alive pings when there are no
|
||||
* outstanding RPCs on the connection (default false).
|
||||
*/
|
||||
private boolean withoutCalls;
|
||||
|
||||
public @Nullable Duration getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
public void setTime(@Nullable Duration time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public boolean isWithoutCalls() {
|
||||
return this.withoutCalls;
|
||||
}
|
||||
|
||||
public void setWithoutCalls(boolean withoutCalls) {
|
||||
this.withoutCalls = withoutCalls;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep-alive connection properties.
|
||||
*/
|
||||
public static class Connection {
|
||||
|
||||
/**
|
||||
* Maximum time a connection can remain idle before being gracefully
|
||||
* terminated (default infinite).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration maxIdleTime;
|
||||
|
||||
/**
|
||||
* Maximum time a connection may exist before being gracefully terminated
|
||||
* (default infinite).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration maxAge;
|
||||
|
||||
/**
|
||||
* Maximum time for graceful connection termination (default infinite).
|
||||
*/
|
||||
@DurationUnit(ChronoUnit.SECONDS)
|
||||
private @Nullable Duration gracePeriod = Duration.ofSeconds(30);
|
||||
|
||||
public @Nullable Duration getMaxIdleTime() {
|
||||
return this.maxIdleTime;
|
||||
}
|
||||
|
||||
public void setMaxIdleTime(@Nullable Duration maxIdleTime) {
|
||||
this.maxIdleTime = maxIdleTime;
|
||||
}
|
||||
|
||||
public @Nullable Duration getMaxAge() {
|
||||
return this.maxAge;
|
||||
}
|
||||
|
||||
public void setMaxAge(@Nullable Duration maxAge) {
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
|
||||
public @Nullable Duration getGracePeriod() {
|
||||
return this.gracePeriod;
|
||||
}
|
||||
|
||||
public void setGracePeriod(@Nullable Duration gracePeriod) {
|
||||
this.gracePeriod = gracePeriod;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL properties.
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* Whether to enable SSL support.
|
||||
*/
|
||||
private @Nullable Boolean enabled;
|
||||
|
||||
/**
|
||||
* Client authentication mode.
|
||||
*/
|
||||
private ClientAuth clientAuth = ClientAuth.NONE;
|
||||
|
||||
/**
|
||||
* SSL bundle name. Should match a bundle configured in spring.ssl.bundle.
|
||||
*/
|
||||
private @Nullable String bundle;
|
||||
|
||||
/**
|
||||
* Flag to indicate that client authentication is secure (i.e. certificates are
|
||||
* checked). Do not set this to false in production.
|
||||
*/
|
||||
private boolean secure = true;
|
||||
|
||||
public @Nullable Boolean getEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(@Nullable Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public @Nullable String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(@Nullable String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public void setClientAuth(ClientAuth clientAuth) {
|
||||
this.clientAuth = clientAuth;
|
||||
}
|
||||
|
||||
public ClientAuth getClientAuth() {
|
||||
return this.clientAuth;
|
||||
}
|
||||
|
||||
public void setSecure(boolean secure) {
|
||||
this.secure = secure;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return this.secure;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Netty server properties.
|
||||
*/
|
||||
public static class Netty {
|
||||
|
||||
/**
|
||||
* Transport mechanism used for Netty and Netty Shaded servers. If not specified
|
||||
* will the appropriate transport will be picked based on the
|
||||
* 'deomain-socket-path' or 'address/port'.
|
||||
*/
|
||||
private @Nullable Transport transport;
|
||||
|
||||
/**
|
||||
* Path of the domain socket that should be used.
|
||||
*/
|
||||
private @Nullable String domainSocketPath;
|
||||
|
||||
public @Nullable Transport getTransport() {
|
||||
return this.transport;
|
||||
}
|
||||
|
||||
public void setTransport(@Nullable Transport transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public @Nullable String getDomainSocketPath() {
|
||||
return this.domainSocketPath;
|
||||
}
|
||||
|
||||
public void setDomainSocketPath(@Nullable String domainSocketPath) {
|
||||
this.domainSocketPath = domainSocketPath;
|
||||
}
|
||||
|
||||
public enum Transport {
|
||||
|
||||
/**
|
||||
* TCP transport.
|
||||
*/
|
||||
TCP,
|
||||
|
||||
/**
|
||||
* Domain socket transport.
|
||||
*/
|
||||
DOMAIN_SOCKET
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Servlet properties.
|
||||
*/
|
||||
public static class Servlet {
|
||||
|
||||
/**
|
||||
* Whether to use a servlet server in a servlet-based web application. When the
|
||||
* value is false, a native gRPC server will be created as long as one is
|
||||
* available, and it will listen on its own port. Should only be needed if the
|
||||
* GrpcServlet is on the classpath.
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* Whether to validate that HTTP/2 is enabled. Validation may need to be skipped
|
||||
* if your servlet container is not configured using properties.
|
||||
*/
|
||||
private boolean validateHttp2 = true;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isValidateHttp2() {
|
||||
return this.validateHttp2;
|
||||
}
|
||||
|
||||
public void setValidateHttp2(boolean validateHttp2) {
|
||||
this.validateHttp2 = validateHttp2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.protobuf.services.ProtoReflectionServiceV1;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server services.
|
||||
*
|
||||
* @author Haris Zujo
|
||||
* @author Dave Syer
|
||||
* @author Chris Bono
|
||||
* @author Andrey Litvitski
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(before = GrpcServerAutoConfiguration.class)
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, io.grpc.Grpc.class })
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
public final class GrpcServerServicesAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ProtoReflectionServiceV1.class)
|
||||
@ConditionalOnBean(BindableService.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.reflection.enabled", matchIfMissing = true)
|
||||
static class GrpcServerReflectionServiceConfiguration {
|
||||
|
||||
@Bean
|
||||
BindableService grpcServerReflectionService() {
|
||||
return ProtoReflectionServiceV1.newInstance();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.InProcessGrpcServerFactory;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for an in-process gRPC server.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Toshiaki Maki
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnMissingBean(InProcessGrpcServerFactory.class)
|
||||
@ConditionalOnProperty("spring.grpc.server.inprocess.name")
|
||||
@ConditionalOnGrpcServerFactoryEnabled
|
||||
class InProcessGrpcServerConfiguration {
|
||||
|
||||
@Bean
|
||||
InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties,
|
||||
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
|
||||
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers,
|
||||
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
|
||||
String inProcessName = properties.getInprocess().getName();
|
||||
Assert.state(inProcessName != null, "No inprocess name provided");
|
||||
InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(inProcessName,
|
||||
grpcServerBuilderCustomizers.forFactory());
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
serviceDiscoverer.findServices()
|
||||
.stream()
|
||||
.map((spec) -> serviceConfigurer.configure(spec, factory))
|
||||
.forEach(factory::addService);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(InProcessGrpcServerFactory.class)
|
||||
@ConditionalOnMissingBean(name = "inProcessGrpcServerLifecycle")
|
||||
GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory,
|
||||
GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) {
|
||||
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
|
||||
}
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration;
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.grpc.server.InProcessGrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link Condition} that matches when no network gRPC server is found.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class MissingNetworkGrpcServerCondition extends AllNestedConditions {
|
||||
|
||||
MissingNetworkGrpcServerCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean(GrpcServletRegistration.class)
|
||||
static class MissingGrpcServletRegistrationBean {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class)
|
||||
static class MissingGrpcFactoryBean {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Netty.Transport;
|
||||
import org.springframework.grpc.internal.GrpcUtils;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Address {@link GrpcServerFactory} address.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param transport the transport to use
|
||||
* @param address the bind address
|
||||
* @param port the listen port
|
||||
* @param domainSocketPath the domain socket path
|
||||
*/
|
||||
record NettyAddress(@Nullable Transport transport, @Nullable InetAddress address, @Nullable Integer port,
|
||||
@Nullable String domainSocketPath) {
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
Transport transport = (this.transport != null) ? this.transport : deduceTransport();
|
||||
return switch (transport) {
|
||||
case TCP -> tcpAddress();
|
||||
case DOMAIN_SOCKET -> domainSocketAddress();
|
||||
};
|
||||
}
|
||||
|
||||
private Transport deduceTransport() {
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("spring.grpc.server.address", this.address);
|
||||
entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath);
|
||||
});
|
||||
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
|
||||
entries.put("spring.grpc.server.port", this.port);
|
||||
entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath);
|
||||
});
|
||||
if (this.address != null || this.port != null) {
|
||||
return Transport.TCP;
|
||||
}
|
||||
if (this.domainSocketPath != null) {
|
||||
return Transport.DOMAIN_SOCKET;
|
||||
}
|
||||
return Transport.TCP;
|
||||
}
|
||||
|
||||
private String tcpAddress() {
|
||||
String address = (this.address != null) ? toString(this.address) : GrpcUtils.ANY_IP_ADDRESS;
|
||||
int port = (this.port != null) ? this.port : GrpcUtils.DEFAULT_PORT;
|
||||
return address + ":" + port;
|
||||
}
|
||||
|
||||
private String domainSocketAddress() {
|
||||
if (!StringUtils.hasText(this.domainSocketPath)) {
|
||||
throw new InvalidConfigurationPropertyValueException("spring.grpc.server.netty.domain-socket-path",
|
||||
this.domainSocketPath,
|
||||
"A path is required when spring.grpc.server.netty.transport is set to 'domain-socket'");
|
||||
}
|
||||
return "unix:" + this.domainSocketPath;
|
||||
}
|
||||
|
||||
private static String toString(InetAddress address) {
|
||||
String hostName = address.getHostName();
|
||||
return (hostName != null) ? hostName : address.getHostAddress();
|
||||
}
|
||||
|
||||
static NettyAddress fromProperties(GrpcServerProperties properties) {
|
||||
return new NettyAddress(properties.getNetty().getTransport(), properties.getAddress(), properties.getPort(),
|
||||
properties.getNetty().getDomainSocketPath());
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import io.grpc.netty.NettyServerBuilder;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.NettyGrpcServerFactory;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for a Netty gRPC server.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Toshiaki Maki
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(NettyServerBuilder.class)
|
||||
@ConditionalOnMissingNetworkGrpcServer
|
||||
@ConditionalOnGrpcServerFactoryEnabled
|
||||
class NettyGrpcServerConfiguration {
|
||||
|
||||
@Bean
|
||||
NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties,
|
||||
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
|
||||
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles,
|
||||
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
|
||||
NettyAddress address = NettyAddress.fromProperties(properties);
|
||||
ServerCredentials credentials = ServerCredentials.get(properties.getSsl(), bundles,
|
||||
InsecureTrustManagerFactory.INSTANCE);
|
||||
NettyGrpcServerFactory factory = new NettyGrpcServerFactory(address.toString(),
|
||||
grpcServerBuilderCustomizers.forFactory(), credentials.keyManagerFactory(),
|
||||
credentials.trustManagerFactory(), credentials.clientAuth());
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
serviceDiscoverer.findServices()
|
||||
.stream()
|
||||
.map((spec) -> serviceConfigurer.configure(spec, factory))
|
||||
.forEach(factory::addService);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "nettyGrpcServerLifecycle")
|
||||
GrpcServerLifecycle nettyGrpcServerLifecycle(NettyGrpcServerFactory factory, GrpcServerProperties properties,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import io.grpc.ServerBuilder;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Inbound;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Keepalive;
|
||||
import org.springframework.grpc.server.ServerBuilderCustomizer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link ServerBuilderCustomizer} that maps {@link GrpcServerProperties} to a
|
||||
* {@link ManagedChannelBuilder}.
|
||||
*
|
||||
* @param <T> the type of server builder
|
||||
* @param properties the properties to map
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record PropertiesServerBuilderCustomizer<T extends ServerBuilder<T>>(
|
||||
GrpcServerProperties properties) implements ServerBuilderCustomizer<T> {
|
||||
|
||||
@Override
|
||||
public void customize(T builder) {
|
||||
mapInboundProperties(this.properties.getInbound(), builder);
|
||||
if (supportsKeepAliveProperties(builder)) {
|
||||
mapKeepaliveProperties(this.properties.getKeepalive(), builder);
|
||||
}
|
||||
}
|
||||
|
||||
private void mapInboundProperties(Inbound properties, T builder) {
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(properties.getMessage()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize);
|
||||
map.from(properties.getMetadata()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize);
|
||||
}
|
||||
|
||||
private void mapKeepaliveProperties(Keepalive properties, T builder) {
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(properties::getTime).to(durationProperty(builder::keepAliveTime));
|
||||
map.from(properties::getTimeout).to(durationProperty(builder::keepAliveTimeout));
|
||||
map.from(properties.getConnection()::getMaxIdleTime).to(durationProperty(builder::maxConnectionIdle));
|
||||
map.from(properties.getConnection()::getMaxAge).to(durationProperty(builder::maxConnectionAge));
|
||||
map.from(properties.getConnection()::getGracePeriod).to(durationProperty(builder::maxConnectionAgeGrace));
|
||||
map.from(properties.getPermit()::getTime).to(durationProperty(builder::permitKeepAliveTime));
|
||||
map.from(properties.getPermit()::isWithoutCalls).to(builder::permitKeepAliveWithoutCalls);
|
||||
}
|
||||
|
||||
private Consumer<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
|
||||
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private boolean supportsKeepAliveProperties(T builder) {
|
||||
return !isInstance("io.grpc.inprocess.InProcessServerBuilder", builder)
|
||||
&& !isInstance("io.grpc.servlet.jakarta.ServletServerBuilder", builder);
|
||||
}
|
||||
|
||||
private boolean isInstance(String className, T builder) {
|
||||
try {
|
||||
return ClassUtils.forName(className, builder.getClass().getClassLoader()).isInstance(builder);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import io.grpc.TlsServerCredentials.ClientAuth;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Server credential details to use with gRPC servers.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @param keyManagerFactory the key manager factory to use or {@code null}
|
||||
* @param trustManagerFactory the trust manager factory to use or {@code null}
|
||||
* @param clientAuth the client auth to use
|
||||
*/
|
||||
record ServerCredentials(@Nullable KeyManagerFactory keyManagerFactory,
|
||||
@Nullable TrustManagerFactory trustManagerFactory, ClientAuth clientAuth) {
|
||||
|
||||
/**
|
||||
* Return the credentials to use based on the given properties.
|
||||
* @param properties the SSL properties
|
||||
* @param bundles the SSL bundles
|
||||
* @param insecureTrustManagerFactory the trust manager factory to use for insecure
|
||||
* connections
|
||||
* @return the server credentials to use
|
||||
*/
|
||||
static ServerCredentials get(GrpcServerProperties.Ssl properties, SslBundles bundles,
|
||||
TrustManagerFactory insecureTrustManagerFactory) {
|
||||
Boolean enabled = properties.getEnabled();
|
||||
String bundle = properties.getBundle();
|
||||
ClientAuth clientAuth = properties.getClientAuth();
|
||||
if (Boolean.FALSE.equals(enabled) || (enabled == null && bundle == null)) {
|
||||
return new ServerCredentials(null, null, clientAuth);
|
||||
}
|
||||
Assert.state(bundle != null,
|
||||
() -> "SSL bundle-name is requested when 'spring.grpc.server.ssl.enabled' is true");
|
||||
SslManagerBundle managers = bundles.getBundle(bundle).getManagers();
|
||||
KeyManagerFactory keyManagerFactory = managers.getKeyManagerFactory();
|
||||
TrustManagerFactory trustManagerFactory = (!properties.isSecure()) ? insecureTrustManagerFactory
|
||||
: managers.getTrustManagerFactory();
|
||||
return new ServerCredentials(keyManagerFactory, trustManagerFactory, clientAuth);
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import io.grpc.servlet.jakarta.GrpcServlet;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.diagnostics.FailureAnalyzedException;
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for a Servlet gRPC server.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Toshiaki Maki
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@ConditionalOnClass(GrpcServlet.class)
|
||||
@ConditionalOnMissingNetworkGrpcServer
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.servlet.enabled", matchIfMissing = true)
|
||||
class ServletGrpcServerConfiguration {
|
||||
|
||||
@Bean
|
||||
GrpcServletRegistration grpcServletRegistration(Environment environment, GrpcServerProperties properties,
|
||||
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
|
||||
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers) {
|
||||
if (properties.getServlet().isValidateHttp2()
|
||||
&& !Boolean.TRUE.equals(environment.getProperty("server.http2.enabled", Boolean.class))) {
|
||||
throw new FailureAnalyzedException(
|
||||
"Configuration property 'server.http2.enabled' should be set to true for gRPC support",
|
||||
"Update your application to correct the invalid configuration.\n"
|
||||
+ "You can also set 'spring.grpc.server.servlet.validate-http2' to false to disable the validation.");
|
||||
}
|
||||
return new GrpcServletRegistration(serviceDiscoverer, serviceConfigurer, grpcServerBuilderCustomizers::apply);
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
|
||||
import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
|
||||
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for a Shaded Netty gRPC server.
|
||||
*
|
||||
* @author David Syer
|
||||
* @author Chris Bono
|
||||
* @author Toshiaki Maki
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(NettyServerBuilder.class)
|
||||
@ConditionalOnMissingNetworkGrpcServer
|
||||
@ConditionalOnGrpcServerFactoryEnabled
|
||||
class ShadedNettyGrpcServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties,
|
||||
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
|
||||
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles,
|
||||
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
|
||||
NettyAddress address = NettyAddress.fromProperties(properties);
|
||||
ServerCredentials serverCredentials = ServerCredentials.get(properties.getSsl(), bundles,
|
||||
InsecureTrustManagerFactory.INSTANCE);
|
||||
ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(address.toString(),
|
||||
grpcServerBuilderCustomizers.forFactory(), serverCredentials.keyManagerFactory(),
|
||||
serverCredentials.trustManagerFactory(), serverCredentials.clientAuth());
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
|
||||
serviceDiscoverer.findServices()
|
||||
.stream()
|
||||
.map((spec) -> serviceConfigurer.configure(spec, factory))
|
||||
.forEach(factory::addService);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "shadedNettyGrpcServerLifecycle")
|
||||
GrpcServerLifecycle shadedNettyGrpcServerLifecycle(ShadedNettyGrpcServerFactory factory,
|
||||
GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) {
|
||||
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
|
||||
|
||||
/**
|
||||
* Auto-configured {@link HealthCheckedGrpcComponent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent {
|
||||
|
||||
private final HealthContributorMembership membership;
|
||||
|
||||
private final StatusAggregator statusAggregator;
|
||||
|
||||
private final StatusMapper statusMapper;
|
||||
|
||||
AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership membership, StatusAggregator statusAggregator,
|
||||
StatusMapper statusMapper) {
|
||||
this.membership = membership;
|
||||
this.statusAggregator = statusAggregator;
|
||||
this.statusMapper = statusMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMember(String healthContributorName) {
|
||||
return this.membership.isMember(healthContributorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAggregator getStatusAggregator() {
|
||||
return this.statusAggregator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusMapper getStatusMapper() {
|
||||
return this.statusMapper;
|
||||
}
|
||||
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Service;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Status;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Auto-configured {@link HealthCheckedGrpcComponents}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents {
|
||||
|
||||
private final HealthCheckedGrpcComponent server;
|
||||
|
||||
private final Map<String, HealthCheckedGrpcComponent> services;
|
||||
|
||||
/**
|
||||
* Create a new {@link AutoConfiguredHealthCheckedGrpcComponents} instance.
|
||||
* @param applicationContext the application context used to check for override beans
|
||||
* @param properties the grpc server health properties
|
||||
*/
|
||||
AutoConfiguredHealthCheckedGrpcComponents(ApplicationContext applicationContext,
|
||||
GrpcServerHealthProperties properties) {
|
||||
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext configurableContext)
|
||||
? configurableContext.getBeanFactory() : applicationContext;
|
||||
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class,
|
||||
() -> StatusAggregator.of(properties.getStatus().getOrder()));
|
||||
StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class,
|
||||
() -> StatusMapper.of(properties.getStatus().getMapping()));
|
||||
this.server = new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(),
|
||||
statusAggregator, statusMapper);
|
||||
this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper);
|
||||
}
|
||||
|
||||
private Map<String, HealthCheckedGrpcComponent> createServices(Map<String, Service> serviceProperties,
|
||||
BeanFactory beanFactory, StatusAggregator defaultStatusAggregator, StatusMapper defaultStatusMapper) {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new TreeMap<>();
|
||||
serviceProperties.forEach((serviceName, service) -> {
|
||||
Status status = service.getStatus();
|
||||
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, serviceName,
|
||||
() -> createStatusAggregator(status.getOrder(), defaultStatusAggregator));
|
||||
StatusMapper statusMapper = getQualifiedBean(beanFactory, StatusMapper.class, serviceName,
|
||||
() -> createStatusMapper(status.getMapping(), defaultStatusMapper));
|
||||
HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(service.getInclude(),
|
||||
service.getExclude());
|
||||
services.put(serviceName,
|
||||
new AutoConfiguredHealthCheckedGrpcComponent(membership, statusAggregator, statusMapper));
|
||||
});
|
||||
return Collections.unmodifiableMap(services);
|
||||
}
|
||||
|
||||
private StatusAggregator createStatusAggregator(List<String> order, StatusAggregator defaultStatusAggregator) {
|
||||
return (!CollectionUtils.isEmpty(order)) ? StatusAggregator.of(order) : defaultStatusAggregator;
|
||||
}
|
||||
|
||||
private StatusMapper createStatusMapper(Map<String, ServingStatus> mapping, StatusMapper defaultStatusMapper) {
|
||||
return (!CollectionUtils.isEmpty(mapping)) ? StatusMapper.of(mapping) : defaultStatusMapper;
|
||||
}
|
||||
|
||||
private <T> T getNonQualifiedBean(ListableBeanFactory beanFactory, Class<T> type, Supplier<T> fallback) {
|
||||
List<String> candidates = new ArrayList<>();
|
||||
for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
|
||||
String[] aliases = beanFactory.getAliases(beanName);
|
||||
if (!BeanFactoryAnnotationUtils.isQualifierMatch(
|
||||
(qualifier) -> !qualifier.equals(beanName) && !ObjectUtils.containsElement(aliases, qualifier),
|
||||
beanName, beanFactory)) {
|
||||
candidates.add(beanName);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return fallback.get();
|
||||
}
|
||||
if (candidates.size() == 1) {
|
||||
return beanFactory.getBean(candidates.get(0), type);
|
||||
}
|
||||
return beanFactory.getBean(type);
|
||||
}
|
||||
|
||||
private <T> T getQualifiedBean(BeanFactory beanFactory, Class<T> type, String qualifier, Supplier<T> fallback) {
|
||||
try {
|
||||
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
return fallback.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getServer() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getServiceNames() {
|
||||
return this.services.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getService(String serviceName) {
|
||||
return this.services.get(serviceName);
|
||||
}
|
||||
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition;
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator;
|
||||
import org.springframework.boot.health.registry.HealthContributorRegistry;
|
||||
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side health service.
|
||||
*
|
||||
* @author Daniel Theuke
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(
|
||||
afterName = "org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration")
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@Conditional(NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition.class)
|
||||
@EnableConfigurationProperties(GrpcServerHealthProperties.class)
|
||||
public final class GrpcServerHealthAutoConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "enterTerminalState")
|
||||
@ConditionalOnMissingBean
|
||||
HealthStatusManager grpcServerHealthStatusManager() {
|
||||
return new HealthStatusManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
BindableService grpcServerHealthService(HealthStatusManager healthStatusManager) {
|
||||
return healthStatusManager.getHealthService();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBean(type = "org.springframework.boot.health.registry.HealthContributorRegistry")
|
||||
static class GrpcServerHealthContributorConfiguration {
|
||||
|
||||
static final String VALIDATE_MEMBERSHIP_PROPERTY = "spring.grpc.server.health.services.validate-membership";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
StatusAggregator grpcServerHealthStatusAggregator(GrpcServerHealthProperties properties) {
|
||||
return StatusAggregator.of(properties.getStatus().getOrder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
StatusMapper grpcServerHealthHttpCodeStatusMapper(GrpcServerHealthProperties properties) {
|
||||
return StatusMapper.of(properties.getStatus().getMapping());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(HealthCheckedGrpcComponents.class)
|
||||
AutoConfiguredHealthCheckedGrpcComponents grpcServerHealthCheckedGrpcComponents(
|
||||
ApplicationContext applicationContext, GrpcServerHealthProperties properties) {
|
||||
return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
GrpcServerHealth grpcServerHealth(HealthContributorRegistry healthContributorRegistry,
|
||||
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry,
|
||||
HealthCheckedGrpcComponents healthCheckedGrpcComponents) {
|
||||
return new GrpcServerHealth(healthContributorRegistry, reactiveHealthContributorRegistry.getIfAvailable(),
|
||||
healthCheckedGrpcComponents);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBooleanProperty(name = VALIDATE_MEMBERSHIP_PROPERTY, matchIfMissing = true)
|
||||
HealthContributorMembershipValidator grpcServerHealthServiceMembershipValidator(
|
||||
GrpcServerHealthProperties properties, HealthContributorRegistry healthContributorRegistry,
|
||||
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry) {
|
||||
return new HealthContributorMembershipValidator(healthContributorRegistry,
|
||||
reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY,
|
||||
(members) -> properties.getService().forEach((serviceName, service) -> {
|
||||
String property = "spring.grpc.server.health.service." + serviceName;
|
||||
members.member(property + ".include".formatted(serviceName), service.getInclude());
|
||||
members.member(property + ".exclude".formatted(serviceName), service.getExclude());
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition extends AnyNestedCondition {
|
||||
|
||||
NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnBean(BindableService.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled", matchIfMissing = true)
|
||||
static class NotDisabledAndHasBindableService {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled")
|
||||
static class ExplicitlyEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties Properties} for Spring gRPC server health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.grpc.server.health")
|
||||
public class GrpcServerHealthProperties {
|
||||
|
||||
/**
|
||||
* Whether to auto-configure Health feature on the gRPC server.
|
||||
*/
|
||||
private @Nullable Boolean enabled;
|
||||
|
||||
/**
|
||||
* Whether to include the overall server health.
|
||||
*/
|
||||
private boolean includeOverallHealth = true;
|
||||
|
||||
/**
|
||||
* Properties that apply to all services.
|
||||
*/
|
||||
private final Services services = new Services();
|
||||
|
||||
/**
|
||||
* Service specific health reporting.
|
||||
*/
|
||||
private final Map<String, Service> service = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Status configuration.
|
||||
*/
|
||||
private final Status status = new Status();
|
||||
|
||||
/**
|
||||
* Schedule configuration.
|
||||
*/
|
||||
private final Schedule schedule = new Schedule();
|
||||
|
||||
public @Nullable Boolean getEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(@Nullable Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isIncludeOverallHealth() {
|
||||
return this.includeOverallHealth;
|
||||
}
|
||||
|
||||
public void setIncludeOverallHealth(boolean includeOverallHealth) {
|
||||
this.includeOverallHealth = includeOverallHealth;
|
||||
}
|
||||
|
||||
public Services getServices() {
|
||||
return this.services;
|
||||
}
|
||||
|
||||
public Map<String, Service> getService() {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public Schedule getSchedule() {
|
||||
return this.schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties applying to all services.
|
||||
*/
|
||||
public static class Services {
|
||||
|
||||
/**
|
||||
* Whether to validate health membership for services on startup. Validation fails
|
||||
* if a service includes or excludes a health contributor that does not exist.
|
||||
*/
|
||||
private boolean validateMembership = true;
|
||||
|
||||
public boolean isValidateMembership() {
|
||||
return this.validateMembership;
|
||||
}
|
||||
|
||||
public void setValidateMembership(boolean validateMembership) {
|
||||
this.validateMembership = validateMembership;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A health for a specific service.
|
||||
*/
|
||||
public static class Service {
|
||||
|
||||
/**
|
||||
* Health indicator IDs that should be included or '*' for all.
|
||||
*/
|
||||
private @Nullable Set<String> include;
|
||||
|
||||
/**
|
||||
* Health indicator IDs that should be excluded or '*' for all.
|
||||
*/
|
||||
private @Nullable Set<String> exclude;
|
||||
|
||||
/**
|
||||
* Status configuration.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final Status status = new Status();
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public @Nullable Set<String> getInclude() {
|
||||
return this.include;
|
||||
}
|
||||
|
||||
public void setInclude(@Nullable Set<String> include) {
|
||||
this.include = include;
|
||||
}
|
||||
|
||||
public @Nullable Set<String> getExclude() {
|
||||
return this.exclude;
|
||||
}
|
||||
|
||||
public void setExclude(@Nullable Set<String> exclude) {
|
||||
this.exclude = exclude;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Status properties for the group.
|
||||
*/
|
||||
public static class Status {
|
||||
|
||||
/**
|
||||
* List of health statuses in order of severity.
|
||||
*/
|
||||
private List<String> order = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Mapping of health statuses to gRPC service status. By default, registered
|
||||
* health statuses map to sensible defaults (for example, UP maps to SERVING).
|
||||
*/
|
||||
private final Map<String, ServingStatus> mapping = new HashMap<>();
|
||||
|
||||
public List<String> getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
public void setOrder(List<String> statusOrder) {
|
||||
if (!CollectionUtils.isEmpty(statusOrder)) {
|
||||
this.order = statusOrder;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, ServingStatus> getMapping() {
|
||||
return this.mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Health task scheduling.
|
||||
*/
|
||||
public static class Schedule {
|
||||
|
||||
/**
|
||||
* Whether to schedule updates to gRPC server health based on application health.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* How often to update the health status.
|
||||
*/
|
||||
private Duration period = Duration.ofSeconds(5);
|
||||
|
||||
/**
|
||||
* The initial delay before updating the health status the very first time.
|
||||
*/
|
||||
private Duration delay = Duration.ofSeconds(5);
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Duration getPeriod() {
|
||||
return this.period;
|
||||
}
|
||||
|
||||
public void setPeriod(Duration period) {
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public Duration getDelay() {
|
||||
return this.delay;
|
||||
}
|
||||
|
||||
public void setDelay(Duration delay) {
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Schedules gRPC health updates one the gRPC server has been started.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class GrpcServerHealthScheduler implements ApplicationListener<GrpcServerStartedEvent> {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class);
|
||||
|
||||
private final SingletonSupplier<ScheduledFuture<?>> scheduleHealth;
|
||||
|
||||
GrpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager,
|
||||
TaskScheduler taskScheduler, Duration period, Duration delay) {
|
||||
this(Clock.systemDefaultZone(), grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay);
|
||||
}
|
||||
|
||||
GrpcServerHealthScheduler(Clock clock, GrpcServerHealth grpcServerHealth,
|
||||
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period,
|
||||
Duration delay) {
|
||||
this.scheduleHealth = SingletonSupplier.of(() -> {
|
||||
logger.debug(LogMessage
|
||||
.of(() -> "Scheduling gRPC server health updates every %s seconds (after a delay of %s seconds)"
|
||||
.formatted((period.toMillis() / 1000.0), delay.toMillis() / 1000.0)));
|
||||
Runnable task = () -> grpcServerHealth.update(grpcServerHealthStatusManager);
|
||||
return taskScheduler.scheduleAtFixedRate(task, Instant.now(clock).plus(delay), period);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(GrpcServerStartedEvent event) {
|
||||
this.scheduleHealth.get();
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Schedule;
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} to invoke {@link GrpcServerHealth}
|
||||
* updates using a {@link TaskScheduler}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(after = GrpcServerHealthAutoConfiguration.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.schedule.enabled", matchIfMissing = true)
|
||||
@Import(DefaultTaskSchedulerConfiguration.class)
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
|
||||
public final class GrpcServerHealthSchedulerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class })
|
||||
GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth,
|
||||
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler,
|
||||
GrpcServerHealthProperties properties) {
|
||||
Schedule schedule = properties.getSchedule();
|
||||
return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler,
|
||||
schedule.getPeriod(), schedule.getDelay());
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC server health checks.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC server.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC
|
||||
* requests.
|
||||
* <p>
|
||||
* This configurer checks the application context to determine if CSRF protection should
|
||||
* be disabled for gRPC requests based on the property
|
||||
* {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is
|
||||
* disabled unless explicitly enabled in the application properties.
|
||||
* </p>
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @see AbstractHttpConfigurer
|
||||
* @see HttpSecurity
|
||||
*/
|
||||
class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer<GrpcDisableCsrfHttpConfigurer, HttpSecurity> {
|
||||
|
||||
@Override
|
||||
public void init(HttpSecurity http) {
|
||||
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
|
||||
if (context != null && isCsrfConfigurerPresent(http) && hasBean(context, GrpcServiceDiscoverer.class)
|
||||
&& hasBean(context, GrpcServletRegistration.class) && isCsrfEnabled(context)) {
|
||||
http.csrf(this::disable);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isCsrfConfigurerPresent(HttpSecurity http) {
|
||||
return http.getConfigurer(CsrfConfigurer.class) != null;
|
||||
}
|
||||
|
||||
private boolean hasBean(ApplicationContext context, Class<?> type) {
|
||||
return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type));
|
||||
}
|
||||
|
||||
private boolean isCsrfEnabled(ApplicationContext context) {
|
||||
return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true);
|
||||
}
|
||||
|
||||
private void disable(CsrfConfigurer<HttpSecurity> csrf) {
|
||||
csrf.requireCsrfProtectionMatcher(GrpcCsrfRequestMatcher.INSTANCE);
|
||||
}
|
||||
|
||||
static class GrpcCsrfRequestMatcher implements RequestMatcher {
|
||||
|
||||
static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher();
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request) && !GrpcRequest.toAnyService().matches(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.grpc.server.GlobalServerInterceptor;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
|
||||
import org.springframework.grpc.server.security.GrpcSecurity;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Andrey Litvitski
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration",
|
||||
afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration",
|
||||
"org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration" },
|
||||
after = { GrpcServerSecurityAutoConfiguration.class, GrpcServerAutoConfiguration.class })
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
|
||||
@ConditionalOnMissingBean(AuthenticationProcessInterceptor.class)
|
||||
@ConditionalOnBean(GrpcSecurity.class)
|
||||
public final class GrpcServerOAuth2ResourceServerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(OpaqueTokenIntrospector.class)
|
||||
@GlobalServerInterceptor
|
||||
AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity)
|
||||
throws Exception {
|
||||
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
|
||||
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
|
||||
return grpcSecurity.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(JwtDecoder.class)
|
||||
@GlobalServerInterceptor
|
||||
AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) throws Exception {
|
||||
grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
|
||||
grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
|
||||
return grpcSecurity.build();
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.internal.GrpcUtil;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.grpc.server.GrpcServletRegistration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.ExceptionHandlerConfiguration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.server.GlobalServerInterceptor;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.grpc.server.security.CoroutineSecurityContextInterceptor;
|
||||
import org.springframework.grpc.server.security.GrpcSecurity;
|
||||
import org.springframework.grpc.server.security.SecurityContextServerInterceptor;
|
||||
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler;
|
||||
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Chris Bono
|
||||
* @author Andrey Litvitski
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(after = GrpcServerAutoConfiguration.class,
|
||||
afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration")
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
|
||||
@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class,
|
||||
GrpcServletSecurityConfigurerConfiguration.class })
|
||||
public final class GrpcServerSecurityAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(AuthenticationConfiguration.class)
|
||||
static class ExceptionHandlerConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityGrpcExceptionHandler accessExceptionHandler() {
|
||||
return new SecurityGrpcExceptionHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBean(GrpcServerFactory.class)
|
||||
@EnableGlobalAuthentication
|
||||
static class GrpcNativeSecurityConfigurerConfiguration {
|
||||
|
||||
@Bean
|
||||
GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor<Object> objectPostProcessor,
|
||||
AuthenticationConfiguration authenticationConfiguration) {
|
||||
AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration
|
||||
.authenticationManagerBuilder(objectPostProcessor, context);
|
||||
authenticationManagerBuilder
|
||||
.parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
|
||||
return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnBean({ GrpcServletRegistration.class, SecurityFilterChain.class })
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GrpcServletSecurityConfigurerConfiguration {
|
||||
|
||||
@Bean
|
||||
@GlobalServerInterceptor
|
||||
SecurityContextServerInterceptor securityContextInterceptor() {
|
||||
return new SecurityContextServerInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
GrpcServerExecutorProvider grpcServerExecutorProvider() {
|
||||
return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor")
|
||||
static class GrpcClientCoroutineStubConfiguration {
|
||||
|
||||
@Bean
|
||||
@GlobalServerInterceptor
|
||||
@ConditionalOnMissingBean
|
||||
CoroutineSecurityContextInterceptor coroutineSecurityContextInterceptor() {
|
||||
return new CoroutineSecurityContextInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC server security.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher;
|
||||
import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Factory that can be used to create a {@link ServerWebExchangeMatcher} to match against
|
||||
* gRPC service locations.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public final class GrpcRequest {
|
||||
|
||||
private GrpcRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes all gRPC services. The
|
||||
* {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be
|
||||
* used to remove specific services by name if required. For example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* GrpcReactiveRequest.toAnyService().excluding("my-service")
|
||||
* </pre>
|
||||
* @return the configured {@link ServerWebExchangeMatcher}
|
||||
*/
|
||||
public static GrpcReactiveRequestMatcher toAnyService() {
|
||||
return new GrpcReactiveRequestMatcher(Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* The matcher used to match against service locations.
|
||||
*/
|
||||
public static final class GrpcReactiveRequestMatcher
|
||||
extends ApplicationContextServerWebExchangeMatcher<GrpcServiceDiscoverer> {
|
||||
|
||||
private static final ServerWebExchangeMatcher EMPTY_MATCHER = (exchange) -> MatchResult.notMatch();
|
||||
|
||||
private final Set<String> excludes;
|
||||
|
||||
private volatile @Nullable ServerWebExchangeMatcher delegate;
|
||||
|
||||
private GrpcReactiveRequestMatcher(Set<String> excludes) {
|
||||
super(GrpcServiceDiscoverer.class);
|
||||
this.excludes = excludes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
|
||||
* the specified services.
|
||||
* @param services additional services to exclude
|
||||
* @return a new {@link GrpcServletRequestMatcher}
|
||||
*/
|
||||
public GrpcReactiveRequestMatcher excluding(String... services) {
|
||||
return excluding(Set.of(services));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
|
||||
* the specified services.
|
||||
* @param services additional service names to exclude
|
||||
* @return a new {@link GrpcServletRequestMatcher}
|
||||
*/
|
||||
public GrpcReactiveRequestMatcher excluding(Collection<String> services) {
|
||||
Assert.notNull(services, "'services' must not be null");
|
||||
Set<String> excludes = new LinkedHashSet<>(this.excludes);
|
||||
excludes.addAll(services);
|
||||
return new GrpcReactiveRequestMatcher(excludes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<GrpcServiceDiscoverer> context) {
|
||||
this.delegate = createDelegate(context.get());
|
||||
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createDelegate(GrpcServiceDiscoverer serviceDiscoverer) {
|
||||
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(serviceDiscoverer);
|
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers)
|
||||
: EMPTY_MATCHER;
|
||||
}
|
||||
|
||||
private List<ServerWebExchangeMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
|
||||
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
|
||||
}
|
||||
|
||||
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
|
||||
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
|
||||
}
|
||||
|
||||
private boolean isExcluded(String service) {
|
||||
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
|
||||
}
|
||||
|
||||
private String getPath(String service) {
|
||||
return "/" + service + "/**";
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher getDelegateMatcher(String path) {
|
||||
Assert.hasText(path, "'path' must not be empty");
|
||||
return new PathPatternParserServerWebExchangeMatcher(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<MatchResult> matches(ServerWebExchange exchange, Supplier<GrpcServiceDiscoverer> context) {
|
||||
Assert.state(this.delegate != null, "'delegate' must not be null");
|
||||
return this.delegate.matches(exchange);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC web security when using a servlet stack.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Factory that can be used to create a {@link RequestMatcher} to match against gRPC
|
||||
* service locations.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public final class GrpcRequest {
|
||||
|
||||
private static final RequestMatcher EMPTY_MATCHER = (request) -> false;
|
||||
|
||||
private static final GrpcServletRequestMatcher TO_ANY_SERVICE = new GrpcServletRequestMatcher(
|
||||
Collections.emptySet());
|
||||
|
||||
private GrpcRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matcher that includes all gRPC services. The
|
||||
* {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used
|
||||
* to remove specific services by name if required. For example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* GrpcServletRequest.toAnyService().excluding("my-service")
|
||||
* </pre>
|
||||
* @return the configured {@link RequestMatcher}
|
||||
*/
|
||||
public static GrpcServletRequestMatcher toAnyService() {
|
||||
return TO_ANY_SERVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The matcher used to match against service locations.
|
||||
*/
|
||||
public static final class GrpcServletRequestMatcher
|
||||
extends ApplicationContextRequestMatcher<GrpcServiceDiscoverer> {
|
||||
|
||||
private final Set<String> excludes;
|
||||
|
||||
private volatile @Nullable RequestMatcher delegate;
|
||||
|
||||
private GrpcServletRequestMatcher(Set<String> exclusions) {
|
||||
super(GrpcServiceDiscoverer.class);
|
||||
this.excludes = exclusions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
|
||||
* the specified services.
|
||||
* @param services additional services to exclude
|
||||
* @return a new {@link GrpcServletRequestMatcher}
|
||||
*/
|
||||
public GrpcServletRequestMatcher excluding(String... services) {
|
||||
return excluding(Set.of(services));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
|
||||
* the specified services.
|
||||
* @param services additional service names to exclude
|
||||
* @return a new {@link GrpcServletRequestMatcher}
|
||||
*/
|
||||
public GrpcServletRequestMatcher excluding(Collection<String> services) {
|
||||
Assert.notNull(services, "'services' must not be null");
|
||||
Set<String> excludes = new LinkedHashSet<>(this.excludes);
|
||||
excludes.addAll(services);
|
||||
return new GrpcServletRequestMatcher(excludes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialized(Supplier<GrpcServiceDiscoverer> context) {
|
||||
this.delegate = createDelegate(context.get());
|
||||
}
|
||||
|
||||
private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) {
|
||||
List<RequestMatcher> delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer);
|
||||
return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers)
|
||||
: EMPTY_MATCHER;
|
||||
}
|
||||
|
||||
private List<RequestMatcher> getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
|
||||
return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
|
||||
}
|
||||
|
||||
private Stream<String> getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
|
||||
return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
|
||||
}
|
||||
|
||||
private boolean isExcluded(String service) {
|
||||
return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
|
||||
}
|
||||
|
||||
private String getPath(String service) {
|
||||
return "/" + service + "/**";
|
||||
}
|
||||
|
||||
private RequestMatcher getDelegateMatcher(String path) {
|
||||
Assert.hasText(path, "'path' must not be empty");
|
||||
return PathPatternRequestMatcher.withDefaults().matcher(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matches(HttpServletRequest request, Supplier<GrpcServiceDiscoverer> context) {
|
||||
Assert.state(this.delegate != null, "'delegate' must not be null");
|
||||
return this.delegate.matches(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC web security when using a reactive stack.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.CompositeHealthContributor;
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.HealthContributors;
|
||||
import org.springframework.boot.health.contributor.HealthIndicator;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.health.registry.HealthContributorRegistry;
|
||||
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Provides health information for a gRPC server that can be used to update a
|
||||
* {@link HealthStatusManager}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public class GrpcServerHealth {
|
||||
|
||||
private HealthContributorRegistry registry;
|
||||
|
||||
private @Nullable ReactiveHealthContributorRegistry fallbackRegistry;
|
||||
|
||||
private HealthCheckedGrpcComponents components;
|
||||
|
||||
/**
|
||||
* Create a new {@link GrpcServerHealth} instance.
|
||||
* @param registry the health contributor registry
|
||||
* @param fallbackRegistry the fallback registry or {@code null}
|
||||
* @param components the components used to provide the server health
|
||||
*/
|
||||
public GrpcServerHealth(HealthContributorRegistry registry,
|
||||
@Nullable ReactiveHealthContributorRegistry fallbackRegistry, HealthCheckedGrpcComponents components) {
|
||||
Assert.notNull(registry, "'registry' must not be null");
|
||||
Assert.notNull(components, "'components' must not be null");
|
||||
this.registry = registry;
|
||||
this.fallbackRegistry = fallbackRegistry;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public void update(HealthStatusManager manager) {
|
||||
update(manager::setStatus);
|
||||
}
|
||||
|
||||
public void update(BiConsumer<String, ServingStatus> updator) {
|
||||
Cache cache = new Cache();
|
||||
HealthCheckedGrpcComponent serverComponent = this.components.getServer();
|
||||
if (serverComponent != null) {
|
||||
updator.accept("", getServingStatus(cache, serverComponent));
|
||||
}
|
||||
for (String serviceName : this.components.getServiceNames()) {
|
||||
HealthCheckedGrpcComponent serviceComponent = this.components.getService(serviceName);
|
||||
if (!serviceName.isEmpty() && serviceComponent != null) {
|
||||
updator.accept(serviceName, getServingStatus(cache, serviceComponent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ServingStatus getServingStatus(Cache cache, HealthCheckedGrpcComponent component) {
|
||||
Set<Status> statuses = new LinkedHashSet<>();
|
||||
collectStatuses(cache, component, statuses, this.registry, "");
|
||||
if (this.fallbackRegistry != null) {
|
||||
collectStatuses(cache, component, statuses, this.fallbackRegistry.asHealthContributors(), "");
|
||||
}
|
||||
Status status = component.getStatusAggregator().getAggregateStatus(statuses);
|
||||
return component.getStatusMapper().getServingStatus(status);
|
||||
}
|
||||
|
||||
private void collectStatuses(Cache cache, HealthCheckedGrpcComponent component, Set<Status> statuses,
|
||||
HealthContributors contributors, String prefix) {
|
||||
for (HealthContributors.Entry entry : contributors) {
|
||||
String name = (prefix.isEmpty()) ? entry.name() : prefix + "/" + entry.name();
|
||||
if (entry.contributor() instanceof CompositeHealthContributor composite) {
|
||||
collectStatuses(cache, component, statuses, composite, name);
|
||||
}
|
||||
else if (component.isMember(name)) {
|
||||
Health health = cache.getHealth(name, (HealthIndicator) entry.contributor());
|
||||
if (health != null) {
|
||||
statuses.add(health.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Cache {
|
||||
|
||||
private final Map<String, Health> health = new HashMap<>();
|
||||
|
||||
Health getHealth(String name, HealthIndicator indicator) {
|
||||
return this.health.computeIfAbsent(name, (key) -> indicator.health(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
/**
|
||||
* A single gRPC component that can be checked for health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public interface HealthCheckedGrpcComponent {
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given health contributor is a member of this component.
|
||||
* @param healthContributorName the contributor name
|
||||
* @return {@code true} if the contributor is a member
|
||||
*/
|
||||
boolean isMember(String healthContributorName);
|
||||
|
||||
/**
|
||||
* Returns the status aggregator that should be used for this component.
|
||||
* @return the status aggregator
|
||||
*/
|
||||
StatusAggregator getStatusAggregator();
|
||||
|
||||
/**
|
||||
* Returns the {@link StatusMapper} that should be used for this component.
|
||||
* @return the status mapper
|
||||
*/
|
||||
StatusMapper getStatusMapper();
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A collection of {@link HealthCheckedGrpcComponent components} used to check the health
|
||||
* of a gRPC server.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
* @see GrpcServerHealth
|
||||
*/
|
||||
public interface HealthCheckedGrpcComponents {
|
||||
|
||||
/**
|
||||
* Return the component that represents the overall server health or {@code null} if
|
||||
* no overall health should be reported.
|
||||
* @return the server component or {@code null}
|
||||
*/
|
||||
@Nullable HealthCheckedGrpcComponent getServer();
|
||||
|
||||
/**
|
||||
* Return the names of the services that contribute health checks.
|
||||
* @return the service names
|
||||
*/
|
||||
Set<String> getServiceNames();
|
||||
|
||||
/**
|
||||
* Return the component for the service with the specified name or {@code null} if the
|
||||
* name is not known.
|
||||
* @param serviceName the name of the service
|
||||
* @return the service component or {@code null}
|
||||
*/
|
||||
@Nullable HealthCheckedGrpcComponent getService(String serviceName);
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.lang.Contract;
|
||||
|
||||
/**
|
||||
* Simple {@link StatusMapper} backed by map of {@link Status#getCode() status code} to
|
||||
* {@link ServingStatus}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SimpleServingStatusMapper implements StatusMapper {
|
||||
|
||||
static final SimpleServingStatusMapper DEFAULT_MAPPINGS;
|
||||
static {
|
||||
Map<String, ServingStatus> mappings = new HashMap<>();
|
||||
mappings.put(Status.DOWN.getCode(), ServingStatus.NOT_SERVING);
|
||||
mappings.put(Status.OUT_OF_SERVICE.getCode(), ServingStatus.NOT_SERVING);
|
||||
mappings.put(Status.UNKNOWN.getCode(), ServingStatus.UNKNOWN);
|
||||
DEFAULT_MAPPINGS = new SimpleServingStatusMapper(mappings);
|
||||
}
|
||||
|
||||
private final Map<String, ServingStatus> mappings;
|
||||
|
||||
SimpleServingStatusMapper(Map<String, ServingStatus> mappings) {
|
||||
this.mappings = getUniformMappings(mappings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServingStatus getServingStatus(Status status) {
|
||||
String code = getUniformCode(status.getCode());
|
||||
return this.mappings.getOrDefault(code, ServingStatus.SERVING);
|
||||
}
|
||||
|
||||
private static Map<String, ServingStatus> getUniformMappings(Map<String, ServingStatus> mappings) {
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ServingStatus> entry : mappings.entrySet()) {
|
||||
String code = getUniformCode(entry.getKey());
|
||||
if (code != null) {
|
||||
result.putIfAbsent(code, entry.getValue());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
@Contract("!null -> !null")
|
||||
private static @Nullable String getUniformCode(@Nullable String code) {
|
||||
return (code != null) ? code.codePoints()
|
||||
.filter(Character::isLetterOrDigit)
|
||||
.map(Character::toLowerCase)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.lang.Contract;
|
||||
|
||||
/**
|
||||
* {@link StatusAggregator} backed by an ordered status list.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SimpleStatusAggregator implements StatusAggregator {
|
||||
|
||||
static final SimpleStatusAggregator DEFAULT_ORDER = new SimpleStatusAggregator(
|
||||
Status.DEFAULT_ORDER.stream().map(Status::getCode));
|
||||
|
||||
private final List<String> order;
|
||||
|
||||
private final Comparator<Status> comparator = Comparator.comparingInt(this::orderIndex)
|
||||
.thenComparing(Status::getCode);
|
||||
|
||||
SimpleStatusAggregator(Stream<String> order) {
|
||||
this.order = order.map(SimpleStatusAggregator::getUniformCode).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status getAggregateStatus(Set<Status> statuses) {
|
||||
return statuses.stream().filter(this::contains).min(this.comparator).orElse(Status.UNKNOWN);
|
||||
}
|
||||
|
||||
private boolean contains(Status status) {
|
||||
return this.order.contains(getUniformCode(status));
|
||||
}
|
||||
|
||||
private int orderIndex(Status status) {
|
||||
return this.order.indexOf(getUniformCode(status));
|
||||
}
|
||||
|
||||
private static @Nullable String getUniformCode(Status status) {
|
||||
return getUniformCode(status.getCode());
|
||||
}
|
||||
|
||||
@Contract("!null -> !null")
|
||||
private static @Nullable String getUniformCode(@Nullable String code) {
|
||||
return (code != null) ? code.codePoints()
|
||||
.filter(Character::isLetterOrDigit)
|
||||
.map(Character::toLowerCase)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Strategy used to aggregate {@link Status} instances.
|
||||
* <p>
|
||||
* This is required in order to combine subsystem states expressed through
|
||||
* {@link Health#getStatus()} into one state for the gRPC health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StatusAggregator {
|
||||
|
||||
/**
|
||||
* Return the aggregate status for the given set of statuses.
|
||||
* @param statuses the statuses to aggregate
|
||||
* @return the aggregate status
|
||||
*/
|
||||
default Status getAggregateStatus(Status... statuses) {
|
||||
return getAggregateStatus(new LinkedHashSet<>(Arrays.asList(statuses)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the aggregate status for the given set of statuses.
|
||||
* @param statuses the statuses to aggregate
|
||||
* @return the aggregate status
|
||||
*/
|
||||
Status getAggregateStatus(Set<Status> statuses);
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(Status... order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order).map(Status::getCode))
|
||||
: getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(String... order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order)) : getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(List<String> order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(order.stream()) : getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link StatusAggregator} instance using default ordering rules.
|
||||
* @return a {@code StatusAggregator} with default ordering rules.
|
||||
*/
|
||||
static StatusAggregator getDefault() {
|
||||
return SimpleStatusAggregator.DEFAULT_ORDER;
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Strategy used to map a {@link Status health status} to a gRPC {@link ServingStatus}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StatusMapper {
|
||||
|
||||
/**
|
||||
* Return the HTTP status code that corresponds to the given {@link Status health
|
||||
* status}.
|
||||
* @param status the health status to map
|
||||
* @return the corresponding HTTP status code
|
||||
*/
|
||||
ServingStatus getServingStatus(Status status);
|
||||
|
||||
/**
|
||||
* Create a new {@link StatusMapper} with the specified mappings.
|
||||
* @param mappings the mappings to use or {@code null} to use the default mappings
|
||||
* @return a {@link StatusMapper} or {@link #getDefault()}
|
||||
*/
|
||||
static StatusMapper of(@Nullable Map<String, ServingStatus> mappings) {
|
||||
return CollectionUtils.isEmpty(mappings) ? SimpleServingStatusMapper.DEFAULT_MAPPINGS
|
||||
: new SimpleServingStatusMapper(mappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link StatusMapper} instance using default mappings.
|
||||
* @return a mapper using default mappings
|
||||
*/
|
||||
static StatusMapper getDefault() {
|
||||
return SimpleServingStatusMapper.DEFAULT_MAPPINGS;
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for gRPC server health checks.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spring gRPC server support classes.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.grpc.server.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable gRPC server auto-configuration.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.server.factory.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable gRPC server factory bean auto-configuration.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.server.observation.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable Observations on the server.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.grpc.server.reflection.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable Reflection on the gRPC server.",
|
||||
"defaultValue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer=\
|
||||
org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerOAuth2ResourceServerAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user