Skip to content

Split service discoverer and configurer #212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
Expand Down Expand Up @@ -31,8 +31,8 @@
import io.grpc.ServerServiceDefinition;

/**
* Default {@link GrpcServiceConfigurer} that binds and configures services with
* interceptors.
* Default {@link GrpcServiceConfigurer} implementation that binds and configures services
* with interceptors.
*
* @author Chris Bono
*/
Expand All @@ -52,8 +52,8 @@ public void afterPropertiesSet() {
}

@Override
public ServerServiceDefinition configure(BindableService bindableService, @Nullable GrpcServiceInfo serviceInfo) {
return bindInterceptors(bindableService, serviceInfo);
public ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceDefinitionSpec) {
return bindInterceptors(serviceDefinitionSpec.service(), serviceDefinitionSpec.serviceInfo());
}

private List<ServerInterceptor> findGlobalInterceptors() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
Expand All @@ -24,32 +24,41 @@

import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServiceDescriptor;

/**
* The default {@link GrpcServiceDiscoverer} that finds all {@link BindableService} beans
* and configures and binds them.
* Default {@link GrpcServiceDiscoverer} implementation that finds all
* {@link BindableService} beans in the application context.
*
* @author Chris Bono
*/
public class DefaultGrpcServiceDiscoverer implements GrpcServiceDiscoverer {

private final GrpcServiceConfigurer serviceConfigurer;

private final ApplicationContext applicationContext;

public DefaultGrpcServiceDiscoverer(GrpcServiceConfigurer serviceConfigurer,
ApplicationContext applicationContext) {
this.serviceConfigurer = serviceConfigurer;
public DefaultGrpcServiceDiscoverer(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

@Override
public List<ServerServiceDefinition> findServices() {
public List<ServerServiceDefinitionSpec> findServices() {
return ApplicationContextBeanLookupUtils
.getOrderedBeansWithAnnotation(this.applicationContext, BindableService.class, GrpcService.class)
.entrySet()
.stream()
.map((e) -> this.serviceConfigurer.configure(e.getKey(), this.serviceInfo(e.getValue())))
.map((e) -> new ServerServiceDefinitionSpec(e.getKey(), this.serviceInfo(e.getValue())))
.toList();
}

@Override
public List<String> listServiceNames() {
return ApplicationContextBeanLookupUtils
.getOrderedBeansWithAnnotation(this.applicationContext, BindableService.class, GrpcService.class)
.keySet()
.stream()
.map(BindableService::bindService)
.map(ServerServiceDefinition::getServiceDescriptor)
.map(ServiceDescriptor::getName)
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
Expand All @@ -16,25 +16,25 @@

package org.springframework.grpc.server.service;

import org.springframework.lang.Nullable;

import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;

/**
* Configures and binds a {@link BindableService gRPC Service}.
* Configures and binds a {@link ServerServiceDefinitionSpec service spec} into a
* {@link ServerServiceDefinition service definition} that can then be added to a gRPC
* server.
*
* @author Chris Bono
*/
@FunctionalInterface
public interface GrpcServiceConfigurer {

/**
* Configure and bind a gRPC service.
* @param bindableService service to bind and configure
* @param serviceInfo optional additional service information
* @return configured service definition
* Configure and bind a gRPC service spec resulting in a service definition that can
* then be added to a gRPC server.
* @param serviceSpec the spec containing the info about the service
* @return bound and configured service definition that is ready to be added to the
* server
*/
ServerServiceDefinition configure(BindableService bindableService, @Nullable GrpcServiceInfo serviceInfo);
ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceSpec);

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
Expand All @@ -18,44 +18,26 @@

import java.util.List;

import io.grpc.ServerServiceDefinition;

/**
* Discovers {@link ServerServiceDefinition gRPC services} to be provided by the server.
* Discovers gRPC services to be provided by the server.
*
* @author Michael (yidongnan@gmail.com)
* @author Chris Bono
*/
@FunctionalInterface
public interface GrpcServiceDiscoverer {

/**
* Find gRPC services for the server to provide.
* @return list of services to add to the server - empty when no services available
* Find the specs of the available gRPC services. The spec can then be passed into a
* {@link GrpcServiceConfigurer service configurer} to bind and configure an actual
* service definition.
* @return list of service specs - empty when no services available
*/
List<ServerServiceDefinition> findServices();
List<ServerServiceDefinitionSpec> findServices();

/**
* Find gRPC service names.
* Find the names of the available gRPC services.
* @return list of service names - empty when no services available
*/
default List<String> listServiceNames() {
return findServices().stream()
.map(ServerServiceDefinition::getServiceDescriptor)
.map(descriptor -> descriptor.getName())
.toList();
}

/**
* Find gRPC service.
* @param name the service name
* @return a service - null if no service has this name
*/
default ServerServiceDefinition findService(String name) {
return findServices().stream()
.filter(service -> service.getServiceDescriptor().getName().equals(name))
.findFirst()
.orElse(null);
}
List<String> listServiceNames();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024-2025 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.grpc.server.service;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;

/**
* Encapsulates enough information to construct an actual {@link ServerServiceDefinition}.
*
* @param service the bindable service
* @param serviceInfo optional additional information about the service (e.g.
* interceptors)
* @author Chris Bono
*/
public record ServerServiceDefinitionSpec(BindableService service, @Nullable GrpcServiceInfo serviceInfo) {
public ServerServiceDefinitionSpec {
Assert.notNull(service, "service must not be null");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ private void customizeContextAndRunServiceConfigurerWithServiceInfo(
.run((context) -> {
DefaultGrpcServiceConfigurer configurer = context.getBean(DefaultGrpcServiceConfigurer.class);
if (expectedExceptionType != null) {
assertThatThrownBy(() -> configurer.configure(service, serviceInfo))
assertThatThrownBy(
() -> configurer.configure(new ServerServiceDefinitionSpec(service, serviceInfo)))
.isInstanceOf(expectedExceptionType);
serverInterceptorsMocked.verifyNoInteractions();
}
else {
configurer.configure(service, serviceInfo);
configurer.configure(new ServerServiceDefinitionSpec(service, serviceInfo));
serverInterceptorsMocked
.verify(() -> ServerInterceptors.interceptForward(serviceDef, expectedInterceptors));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
package org.springframework.grpc.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.grpc.server.service.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_A;
import static org.springframework.grpc.server.service.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_B;

import java.util.LinkedHashMap;
import java.util.Map;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.grpc.server.service.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsServiceConfig.SERVICE_A;
import static org.springframework.grpc.server.service.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsServiceConfig.SERVICE_B;

import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
Expand All @@ -35,6 +34,7 @@

import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServiceDescriptor;

/**
* Tests for {@link DefaultGrpcServiceDiscoverer}.
Expand All @@ -43,28 +43,55 @@
*/
class DefaultGrpcServiceDiscovererTests {

@Test
void whenNoServicesRegisteredThenListServiceNamesReturnsEmptyList() {
new ApplicationContextRunner().withUserConfiguration(DefaultGrpcServiceDiscovererTestsBaseConfig.class)
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::listServiceNames, InstanceOfAssertFactories.LIST)
.isEmpty());
}

@Test
void whenServicesRegisteredThenListServiceNamesReturnsNames() {
new ApplicationContextRunner()
.withUserConfiguration(DefaultGrpcServiceDiscovererTestsBaseConfig.class,
DefaultGrpcServiceDiscovererTestsServiceConfig.class)
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::listServiceNames, InstanceOfAssertFactories.LIST)
.containsExactly("serviceB", "serviceA"));

}

@Test
void servicesAreFoundInProperOrderWithExpectedGrpcServiceAnnotations() {
new ApplicationContextRunner().withUserConfiguration(DefaultGrpcServiceDiscovererTestsConfig.class)
.run((context) -> {
assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::findServices, InstanceOfAssertFactories.LIST)
.containsExactly(DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_B,
DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_A);
TestServiceConfigurer configurer = context.getBean(TestServiceConfigurer.class);
assertThat(configurer.invocations).hasSize(2);
assertThat(configurer.invocations.keySet()).containsExactly(SERVICE_B, SERVICE_A);
assertThat(configurer.invocations).containsEntry(SERVICE_B, null);
assertThat(configurer.invocations).hasEntrySatisfying(SERVICE_A, (serviceInfo) -> {
assertThat(serviceInfo.interceptors()).isEmpty();
assertThat(serviceInfo.interceptorNames()).isEmpty();
assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
});
Comment on lines -54 to -62
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the fruits of this simplification (goodbye verifying cofigurer things in discoverer tests)

});
new ApplicationContextRunner()
.withUserConfiguration(DefaultGrpcServiceDiscovererTestsBaseConfig.class,
DefaultGrpcServiceDiscovererTestsServiceConfig.class)
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::findServices,
InstanceOfAssertFactories.list(ServerServiceDefinitionSpec.class))
.satisfies((serviceSpecs) -> {
assertThat(serviceSpecs).hasSize(2);
assertThat(serviceSpecs).element(0).isEqualTo(new ServerServiceDefinitionSpec(SERVICE_B, null));
assertThat(serviceSpecs).element(1).satisfies((spec) -> {
assertThat(spec.service()).isEqualTo(SERVICE_A);
assertThat(spec.serviceInfo()).isNotNull();
});
}));
}

@Configuration(proxyBeanMethods = false)
static class DefaultGrpcServiceDiscovererTestsConfig {
static class DefaultGrpcServiceDiscovererTestsBaseConfig {

@Bean
GrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicationContext) {
return new DefaultGrpcServiceDiscoverer(applicationContext);
}

}

@Configuration(proxyBeanMethods = false)
static class DefaultGrpcServiceDiscovererTestsServiceConfig {

static BindableService SERVICE_A = Mockito.mock();

Expand All @@ -74,44 +101,27 @@ static class DefaultGrpcServiceDiscovererTestsConfig {

static ServerServiceDefinition SERVICE_DEF_B = Mockito.mock();

@Bean
TestServiceConfigurer testServiceConfigurer() {
return new TestServiceConfigurer();
}

@Bean
GrpcServiceDiscoverer grpcServiceDiscoverer(GrpcServiceConfigurer grpcServiceConfigurer,
ApplicationContext applicationContext) {
return new DefaultGrpcServiceDiscoverer(grpcServiceConfigurer, applicationContext);
}

@GrpcService
@Bean
@Order(200)
BindableService serviceA() {
Mockito.when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
ServiceDescriptor descriptor = mock();
when(descriptor.getName()).thenReturn("serviceA");
when(SERVICE_DEF_A.getServiceDescriptor()).thenReturn(descriptor);
when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
return SERVICE_A;
}

@Bean
@Order(100)
BindableService serviceB() {
Mockito.when(SERVICE_B.bindService()).thenReturn(SERVICE_DEF_B);
ServiceDescriptor descriptor = mock();
when(descriptor.getName()).thenReturn("serviceB");
when(SERVICE_DEF_B.getServiceDescriptor()).thenReturn(descriptor);
when(SERVICE_B.bindService()).thenReturn(SERVICE_DEF_B);
return SERVICE_B;
}

}

static class TestServiceConfigurer implements GrpcServiceConfigurer {

Map<BindableService, GrpcServiceInfo> invocations = new LinkedHashMap<>();

@Override
public ServerServiceDefinition configure(BindableService bindableService, GrpcServiceInfo serviceInfo) {
invocations.put(bindableService, serviceInfo);
return bindableService.bindService();
}

}

}
Loading