Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a491d43
Add Spring gRPC support
onobc Sep 20, 2025
854cd34
Update gRPC and protobuf versions
onobc Sep 22, 2025
fb11831
Update gRPC Kotlin version to 1.5.0
onobc Sep 22, 2025
a9b6527
Update gRPC and protobuf versions (take 2)
onobc Sep 22, 2025
549d7ad
Use the non-deprecated env post processor key
onobc Sep 23, 2025
ed0e6a1
Fix config prop name in gRPC client and server
onobc Sep 23, 2025
42af221
Fix checkstyle
onobc Sep 24, 2025
a1c2b04
Move grpc test out of spring-boot-test-autoconfigure
onobc Sep 25, 2025
5c81286
Incorporate feedback from server code review
onobc Sep 26, 2025
c232213
Incorporate feedback from server code review
onobc Sep 26, 2025
bbc77a3
Consistent order for annotations in server auto config
onobc Sep 30, 2025
e46aa6d
Make ChannelBuilderCustomizers package-protected
onobc Sep 30, 2025
4d6d524
Make GrpcCodecConfiguration package protected
onobc Sep 30, 2025
dffd4ed
Remove ClientInterceptorsConfiguration
onobc Sep 30, 2025
148c6d5
Cleanup ClientScanConfiguration.java
onobc Oct 1, 2025
0206340
Add javadoc and remove var usages
onobc Oct 1, 2025
a60daf6
Fix ordering of members / getters / setters in client properties
onobc Oct 1, 2025
c5346c6
Make `isEnabled` anemic (move logic to determineEnabled)
onobc Oct 1, 2025
850990b
Make NamedChannelCredentialsProvider package-protected
onobc Oct 1, 2025
b3ee513
Fix code configuration (client-side)
onobc Oct 1, 2025
a653f22
Fix codec configuration (server side)
onobc Oct 1, 2025
cb27249
Incorporate fix from Spring gRPC (serviceConfig map)
onobc Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ void documentConfigurationProperties() throws IOException {
Snippets snippets = new Snippets(this.configurationPropertyMetadata);
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);
Expand Down Expand Up @@ -159,6 +160,10 @@ private void dataMigrationPrefixes(Config prefix) {
prefix.accept("spring.sql.init");
}

private void grpcPrefixes(Config prefix) {
prefix.accept("spring.grpc");
}

private void integrationPrefixes(Config prefix) {
prefix.accept("spring.activemq");
prefix.accept("spring.artemis");
Expand Down
1 change: 1 addition & 0 deletions config/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<suppress files="OutputCaptureRuleTests" checks="SpringJUnit5" />
<suppress files="[\\/]smoke-test[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="[\\/]smoke-test[\\/]spring-boot-smoke-test-testng[\\/]" checks="SpringJUnit5" />
<suppress files="[\\/]smoke-test[\\/]spring-boot-smoke-test-grpc[\\/]build[\\/]generated[\\/]sources[\\/]proto" checks=".*" />
<suppress files="[\\/]test-support[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="[\\/]src[\\/]dockerTest[\\/]java[\\/]" checks="JavadocPackage" />
<suppress files="[\\/]src[\\/]dockerTest[\\/]java[\\/]" checks="SpringJavadoc" message="\@since" />
Expand Down
3 changes: 3 additions & 0 deletions documentation/spring-boot-docs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ 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-health"))
implementation(project(path: ":module:spring-boot-hibernate"))
implementation(project(path: ":module:spring-boot-http-converter"))
Expand Down Expand Up @@ -195,6 +197,7 @@ dependencies {
implementation("org.springframework.data:spring-data-r2dbc")
implementation("org.springframework.graphql:spring-graphql")
implementation("org.springframework.graphql:spring-graphql-test")
implementation("org.springframework.grpc:spring-grpc-core")
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.kafka:spring-kafka-test")
implementation("org.springframework.pulsar:spring-pulsar")
Expand Down
67 changes: 67 additions & 0 deletions module/spring-boot-grpc-client/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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")

compileOnly("com.fasterxml.jackson.core:jackson-annotations")

optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-actuator"))
optional(project(":module:spring-boot-actuator-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-client"))
optional(project(":module:spring-boot-security-oauth2-resource-server"))
optional("io.grpc:grpc-servlet-jakarta")
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.micrometer:micrometer-core")
optional("io.netty:netty-transport-native-epoll")
optional("io.projectreactor:reactor-core")
optional("jakarta.servlet:jakarta.servlet-api")
optional("org.springframework:spring-web")
optional("org.springframework.security:spring-security-config")
optional("org.springframework.security:spring-security-oauth2-client")
optional("org.springframework.security:spring-security-oauth2-resource-server")
optional("org.springframework.security:spring-security-oauth2-jose")
optional("org.springframework.security:spring-security-web")

testCompileOnly("com.fasterxml.jackson.core:jackson-annotations")

testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))

testRuntimeOnly("ch.qos.logback:logback-classic")
}
Original file line number Diff line number Diff line change
@@ -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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import io.grpc.ManagedChannelBuilder;

import org.springframework.boot.util.LambdaSafe;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;

/**
* Invokes the available {@link GrpcChannelBuilderCustomizer} instances for a given
* {@link ManagedChannelBuilder}.
*
* @author Chris Bono
*/
class ChannelBuilderCustomizers {

private final List<GrpcChannelBuilderCustomizer<?>> customizers;

ChannelBuilderCustomizers(List<? extends GrpcChannelBuilderCustomizer<?>> customizers) {
this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList();
}

/**
* Customize the specified {@link ManagedChannelBuilder}. Locates all
* {@link GrpcChannelBuilderCustomizer} beans able to handle the specified instance
* and invoke {@link GrpcChannelBuilderCustomizer#customize} on them.
* @param <T> the type of channel builder
* @param authority the target authority of the channel
* @param channelBuilder the builder to customize
* @return the customized builder
*/
@SuppressWarnings("unchecked")
<T extends ManagedChannelBuilder<?>> T customize(String authority, T channelBuilder) {
LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, channelBuilder)
.withLogger(ChannelBuilderCustomizers.class)
.invoke((customizer) -> customizer.customize(authority, channelBuilder));
return channelBuilder;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.Map;
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.ChannelConfig;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
import org.springframework.util.unit.DataSize;

/**
* A {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties client
* properties} to a channel builder.
*
* @param <T> the type of the builder
* @author David Syer
* @author Chris Bono
*/
class ClientPropertiesChannelBuilderCustomizer<T extends ManagedChannelBuilder<T>>
implements GrpcChannelBuilderCustomizer<T> {

private final GrpcClientProperties properties;

ClientPropertiesChannelBuilderCustomizer(GrpcClientProperties properties) {
this.properties = properties;
}

@Override
public void customize(String authority, T builder) {
ChannelConfig channel = this.properties.getChannel(authority);
PropertyMapper mapper = PropertyMapper.get();
mapper.from(channel.getUserAgent()).to(builder::userAgent);
if (!authority.startsWith("unix:") && !authority.startsWith("in-process:")) {
mapper.from(channel.getDefaultLoadBalancingPolicy()).to(builder::defaultLoadBalancingPolicy);
}
mapper.from(channel.getMaxInboundMessageSize()).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize);
mapper.from(channel.getMaxInboundMetadataSize()).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize);
mapper.from(channel.getKeepAliveTime()).to(durationProperty(builder::keepAliveTime));
mapper.from(channel.getKeepAliveTimeout()).to(durationProperty(builder::keepAliveTimeout));
mapper.from(channel.getIdleTimeout()).to(durationProperty(builder::idleTimeout));
mapper.from(channel.isKeepAliveWithoutCalls()).to(builder::keepAliveWithoutCalls);
Map<String, Object> defaultServiceConfig = channel.extractServiceConfig();
if (channel.getHealth().isEnabled()) {
String serviceNameToCheck = (channel.getHealth().getServiceName() != null)
? channel.getHealth().getServiceName() : "";
defaultServiceConfig.put("healthCheckConfig", Map.of("serviceName", serviceNameToCheck));
}
if (!defaultServiceConfig.isEmpty()) {
builder.defaultServiceConfig(defaultServiceConfig);
}
if (channel.getDefaultDeadline() != null && channel.getDefaultDeadline().toMillis() > 0L) {
builder.intercept(new DefaultDeadlineSetupClientInterceptor(channel.getDefaultDeadline()));
}
}

Consumer<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.GrpcChannelFactory;

/**
* {@link EnableAutoConfiguration Auto-configuration} for a
* {@link CompositeGrpcChannelFactory}.
*
* @author Chris Bono
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnGrpcClientEnabled
@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 {

}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.grpc.stub.AbstractStub;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;

/**
* {@link Conditional @Conditional} that only matches when the {@code io.grpc:grpc-stub}
* module is in the classpath and the {@code spring.grpc.client.enabled} property is not
* explicitly set to {@code false}.
*
* @author Freeman Freeman
* @author Chris Bono
* @since 4.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@ConditionalOnClass(AbstractStub.class)
@ConditionalOnProperty(prefix = "spring.grpc.client", name = "enabled", matchIfMissing = true)
public @interface ConditionalOnGrpcClientEnabled {

}
Loading