Add support for service specific server interceptors
This commit introduces the `@GrpcService` annotation which can be used to associate one or more `ServerInterceptor` beans with a `BindableService`. See #5 Signed-off-by: Chris Bono <chris.bono@gmail.com>
This commit is contained in:
@@ -108,10 +108,28 @@ ServerInterceptor myGlobalLoggingInterceptor() {
|
||||
----
|
||||
|
||||
=== Per-Service
|
||||
This is a **WIP** and will be available soon.
|
||||
To add a server interceptor to be applied to a single service you can simply register a server interceptor bean and then annotate your `BindableService` bean with `@GrpcService`, specifying the interceptor using either the `interceptors` or `interceptorNames` attribute.
|
||||
|
||||
The interceptors are ordered according to their position in the attribute list.
|
||||
When using both `interceptors` and `interceptorNames`, the former entries precede the latter.
|
||||
|
||||
In the following example, the `myServerInterceptor` will be applied to the `myService` service.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
MyServerInterceptor myServerInterceptor() {
|
||||
return new MyServerInterceptor();
|
||||
}
|
||||
|
||||
@GrpcService(interceptors = MyServerInterceptor.class)
|
||||
BindableService myService() {
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
== Observability
|
||||
|
||||
Spring gRPC provides an autoconfigured interceptor that can be used to provide observability to your gRPC services.
|
||||
All you need to do is add Spring Boot actuators to your project, and optionally a bridge to your observability platform of choice (just like https://docs.spring.io/spring-boot/reference/actuator/observability.html[any other Spring Boot application]).
|
||||
The `grpc-tomcat` sample in the Spring gRPC repository shows how to do it, and you should see trace logging and metrics when you connect to the server.
|
||||
The `grpc-tomcat` sample in the Spring gRPC repository shows how to do it, and you should see trace logging and metrics when you connect to the server.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2023-2024 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.autoconfigure.server;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Convenience methods performing bean lookups that are not currently provided by the
|
||||
* standard Spring facilities.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
final class ApplicationContextBeanLookupUtils {
|
||||
|
||||
private ApplicationContextBeanLookupUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all beans of the given type which may be annotated with the supplied
|
||||
* {@link Annotation} type and returns a map of matching bean instances with the
|
||||
* annotation instance (or null if the bean is not annotated with the specified
|
||||
* annotation type).
|
||||
* <p>
|
||||
* Note that the map is ordered according to the matching beans {@link Order}
|
||||
* annotation.
|
||||
* <p>
|
||||
* Note that this method considers objects created by FactoryBeans, which means that
|
||||
* FactoryBeans will get initialized in order to determine their object type.
|
||||
* @param applicationContext the application context
|
||||
* @param beanType the type of bean
|
||||
* @param annotationType the type of annotation
|
||||
* @param <B> type of bean
|
||||
* @param <A> type of annotation
|
||||
* @return a map of matching bean instances with the annotation instance (or null)
|
||||
* ordered according to their {@link Order} with annotation
|
||||
*/
|
||||
static <B, A extends Annotation> LinkedHashMap<B, A> getOrderedBeansWithAnnotation(
|
||||
ApplicationContext applicationContext, Class<B> beanType, Class<A> annotationType) {
|
||||
Assert.notNull(applicationContext, () -> "applicationContext must not be null");
|
||||
var annotatedBeanNamesToBeans = applicationContext.getBeansWithAnnotation(annotationType);
|
||||
var annotatedBeansToBeanNames = new LinkedHashMap<Object, String>();
|
||||
annotatedBeanNamesToBeans.forEach((name, bean) -> annotatedBeansToBeanNames.put(bean, name));
|
||||
var orderedBeans = new LinkedHashMap<B, A>();
|
||||
applicationContext.getBeanProvider(beanType).orderedStream().forEachOrdered((b) -> {
|
||||
var beanName = annotatedBeansToBeanNames.get(b);
|
||||
A beanAnnotation = null;
|
||||
if (beanName != null) {
|
||||
beanAnnotation = applicationContext.findAnnotationOnBean(beanName, annotationType);
|
||||
}
|
||||
orderedBeans.put(b, beanAnnotation);
|
||||
});
|
||||
return orderedBeans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all beans which are annotated with the supplied {@link Annotation} type.
|
||||
* <p>
|
||||
* Note that this method considers objects created by FactoryBeans, which means that
|
||||
* FactoryBeans will get initialized in order to determine their object type.
|
||||
* @param applicationContext the application context
|
||||
* @param beanType the type of bean
|
||||
* @param annotationType the type of annotation
|
||||
* @param <B> type of bean
|
||||
* @param <A> type of annotation
|
||||
* @return a list of the matching beans ordered according to their {@link Order}
|
||||
* annotation
|
||||
*/
|
||||
static <B, A extends Annotation> List<B> getBeansWithAnnotation(ApplicationContext applicationContext,
|
||||
Class<B> beanType, Class<A> annotationType) {
|
||||
Assert.notNull(applicationContext, () -> "applicationContext must not be null");
|
||||
var nameToBeanMap = applicationContext.getBeansWithAnnotation(annotationType);
|
||||
var beanToNameMap = new LinkedHashMap<Object, String>();
|
||||
nameToBeanMap.forEach((name, bean) -> beanToNameMap.put(bean, name));
|
||||
return applicationContext.getBeanProvider(beanType).orderedStream().filter(beanToNameMap::containsKey).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2023-2024 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.autoconfigure.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* Default {@link GrpcServiceConfigurer} that binds and configures services with
|
||||
* interceptors.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public class DefaultGrpcServiceConfigurer implements GrpcServiceConfigurer {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private List<ServerInterceptor> globalInterceptors;
|
||||
|
||||
public DefaultGrpcServiceConfigurer(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void initializeGlobalInterceptors() {
|
||||
this.globalInterceptors = findGlobalInterceptors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerServiceDefinition configure(BindableService bindableService, @Nullable GrpcServiceInfo serviceInfo) {
|
||||
return bindInterceptors(bindableService, serviceInfo);
|
||||
}
|
||||
|
||||
private List<ServerInterceptor> findGlobalInterceptors() {
|
||||
return ApplicationContextBeanLookupUtils.getBeansWithAnnotation(this.applicationContext,
|
||||
ServerInterceptor.class, GlobalServerInterceptor.class);
|
||||
}
|
||||
|
||||
private ServerServiceDefinition bindInterceptors(BindableService bindableService,
|
||||
@Nullable GrpcServiceInfo serviceInfo) {
|
||||
var serviceDef = bindableService.bindService();
|
||||
if (serviceInfo == null) {
|
||||
return ServerInterceptors.interceptForward(serviceDef, this.globalInterceptors);
|
||||
}
|
||||
// Add global interceptors first
|
||||
List<ServerInterceptor> allInterceptors = new ArrayList<>(this.globalInterceptors);
|
||||
// Add interceptors by type
|
||||
Arrays.stream(serviceInfo.interceptors())
|
||||
.forEachOrdered(
|
||||
(interceptorClass) -> allInterceptors.add(this.applicationContext.getBean(interceptorClass)));
|
||||
// Add interceptors by name
|
||||
Arrays.stream(serviceInfo.interceptorNames())
|
||||
.forEachOrdered((interceptorBeanName) -> allInterceptors
|
||||
.add(this.applicationContext.getBean(interceptorBeanName, ServerInterceptor.class)));
|
||||
// TODO handle blend
|
||||
return ServerInterceptors.interceptForward(serviceDef, allInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2023 The gRPC-Spring Authors
|
||||
* Copyright 2023-2024 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* 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,
|
||||
@@ -16,17 +16,14 @@
|
||||
|
||||
package org.springframework.grpc.autoconfigure.server;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.grpc.server.GrpcServiceDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
||||
/**
|
||||
* The default {@link GrpcServiceDiscoverer} that finds all {@link BindableService} beans
|
||||
@@ -34,40 +31,31 @@ import org.springframework.grpc.server.GrpcServiceDiscoverer;
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class DefaultGrpcServiceDiscoverer implements GrpcServiceDiscoverer {
|
||||
public class DefaultGrpcServiceDiscoverer implements GrpcServiceDiscoverer {
|
||||
|
||||
private final ObjectProvider<BindableService> grpcServicesProvider;
|
||||
|
||||
private final ObjectProvider<ServerInterceptor> serverInterceptorsProvider;
|
||||
private final GrpcServiceConfigurer serviceConfigurer;
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public DefaultGrpcServiceDiscoverer(ObjectProvider<BindableService> grpcServicesProvider,
|
||||
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
|
||||
this.grpcServicesProvider = grpcServicesProvider;
|
||||
this.serverInterceptorsProvider = serverInterceptorsProvider;
|
||||
public DefaultGrpcServiceDiscoverer(GrpcServiceConfigurer serviceConfigurer,
|
||||
ApplicationContext applicationContext) {
|
||||
this.serviceConfigurer = serviceConfigurer;
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServerServiceDefinition> findServices() {
|
||||
List<ServerInterceptor> globalInterceptors = findGlobalInterceptors();
|
||||
return grpcServicesProvider.orderedStream()
|
||||
.map(BindableService::bindService)
|
||||
.map((svc) -> ServerInterceptors.interceptForward(svc, globalInterceptors))
|
||||
return ApplicationContextBeanLookupUtils
|
||||
.getOrderedBeansWithAnnotation(this.applicationContext, BindableService.class, GrpcService.class)
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map((e) -> this.serviceConfigurer.configure(e.getKey(), this.serviceInfo(e.getValue())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// VisibleForTesting
|
||||
List<ServerInterceptor> findGlobalInterceptors() {
|
||||
// We find unordered map of beans (keyed by name) with the annotation and then
|
||||
// reverse the map for easy lookup by bean.
|
||||
// We then get an ordered stream of all server interceptors and filter
|
||||
// out those that are not present in map of annotated interceptor beans.
|
||||
var nameToBeanMap = applicationContext.getBeansWithAnnotation(GlobalServerInterceptor.class);
|
||||
var beanToNameMap = new HashMap<Object, String>();
|
||||
nameToBeanMap.forEach((name, bean) -> beanToNameMap.put(bean, name));
|
||||
return this.serverInterceptorsProvider.orderedStream().filter(beanToNameMap::containsKey).toList();
|
||||
@Nullable
|
||||
private GrpcServiceInfo serviceInfo(@Nullable GrpcService grpcService) {
|
||||
return grpcService != null ? GrpcServiceInfo.from(grpcService) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import io.grpc.BindableService;
|
||||
import io.grpc.CompressorRegistry;
|
||||
import io.grpc.DecompressorRegistry;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.ServerInterceptor;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side components.
|
||||
@@ -77,10 +76,15 @@ public class GrpcServerAutoConfiguration {
|
||||
|
||||
@ConditionalOnMissingBean
|
||||
@Bean
|
||||
GrpcServiceDiscoverer grpcServiceDiscoverer(ObjectProvider<BindableService> bindableServicesProvider,
|
||||
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
|
||||
return new DefaultGrpcServiceDiscoverer(bindableServicesProvider, serverInterceptorsProvider,
|
||||
applicationContext);
|
||||
GrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) {
|
||||
return new DefaultGrpcServiceConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingBean
|
||||
@Bean
|
||||
GrpcServiceDiscoverer grpcServiceDiscoverer(GrpcServiceConfigurer grpcServiceConfigurer,
|
||||
ApplicationContext applicationContext) {
|
||||
return new DefaultGrpcServiceDiscoverer(grpcServiceConfigurer, applicationContext);
|
||||
}
|
||||
|
||||
@ConditionalOnBean(CompressorRegistry.class)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2016-2024 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.
|
||||
*
|
||||
* Copy from net.devh:grpc-spring-boot-starter.
|
||||
*/
|
||||
|
||||
package org.springframework.grpc.autoconfigure.server;
|
||||
|
||||
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.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerInterceptor;
|
||||
|
||||
/**
|
||||
* Annotation that marks gRPC services that should be registered with a gRPC server.
|
||||
* <p>
|
||||
* <b>NOTE:</b> This annotation is optional as all {@link BindableService} beans will be
|
||||
* registered with a gRPC server. However, this annotation allows specifying additional
|
||||
* information about the service (e.g. interceptors).
|
||||
* <p>
|
||||
* <b>NOTE:</b> This annotation should only be added to {@link BindableService} beans.
|
||||
*
|
||||
* @author Michael (yidongnan@gmail.com)
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Service
|
||||
@Bean
|
||||
public @interface GrpcService {
|
||||
|
||||
/**
|
||||
* The {@link ServerInterceptor} bean types to be applied to the service.
|
||||
* @return the interceptor bean types to be applied to the service
|
||||
*/
|
||||
Class<? extends ServerInterceptor>[] interceptors() default {};
|
||||
|
||||
/**
|
||||
* The {@link ServerInterceptor} bean names to be applied to the service.
|
||||
* @return the interceptor bean names to be applied to the service
|
||||
*/
|
||||
String[] interceptorNames() default {};
|
||||
|
||||
/**
|
||||
* Whether the service specific interceptors should be blended with the global
|
||||
* interceptors.
|
||||
* <p>
|
||||
* When false, the global interceptors are applied first, followed by the service
|
||||
* specific interceptors.
|
||||
* <p>
|
||||
* When true, the global interceptors are merged and sorted (blended) with the service
|
||||
* specific interceptors.
|
||||
* @return whether the service specific interceptors should be blended with the global
|
||||
* interceptors
|
||||
*/
|
||||
boolean blendWithGlobalInterceptors() default false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023-2024 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.autoconfigure.server;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
||||
/**
|
||||
* Configures and binds a {@link BindableService gRPC Service}.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
ServerServiceDefinition configure(BindableService bindableService, @Nullable GrpcServiceInfo serviceInfo);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.autoconfigure.server;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import io.grpc.ServerInterceptor;
|
||||
|
||||
/**
|
||||
* Additional information about a gRPC service that can be used when configuring the
|
||||
* service.
|
||||
*
|
||||
* @param interceptors list of {@link ServerInterceptor} bean types to be applied to the
|
||||
* service
|
||||
* @param interceptorNames list of {@link ServerInterceptor} bean names to be applied to
|
||||
* the service
|
||||
* @param blendWithGlobalInterceptors whether the service specific interceptors should be
|
||||
* merged and sorted (blended) with the global interceptors
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public record GrpcServiceInfo(Class<? extends ServerInterceptor>[] interceptors, String[] interceptorNames,
|
||||
boolean blendWithGlobalInterceptors) {
|
||||
|
||||
public GrpcServiceInfo {
|
||||
Assert.notNull(interceptors, "interceptors must not be null");
|
||||
Assert.notNull(interceptorNames, "interceptorNames must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a service info from a {@link GrpcService} annotation.
|
||||
* @param grpcService the service annotation
|
||||
* @return the service info or null if the supplied annotation is null
|
||||
*/
|
||||
@Nullable
|
||||
public static GrpcServiceInfo from(@Nullable GrpcService grpcService) {
|
||||
return grpcService != null ? new GrpcServiceInfo(grpcService.interceptors(), grpcService.interceptorNames(),
|
||||
grpcService.blendWithGlobalInterceptors()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a service info with the specified interceptors.
|
||||
* @param interceptors non-null list of interceptor bean types
|
||||
* @return the service info with the supplied interceptors
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static GrpcServiceInfo withInterceptors(List<Class<? extends ServerInterceptor>> interceptors) {
|
||||
Assert.notNull(interceptors, "interceptors must not be null");
|
||||
return new GrpcServiceInfo(interceptors.toArray(new Class[0]), new String[0], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a service info with the specified interceptors names.
|
||||
* @param interceptorNames non-null list of interceptor bean names
|
||||
* @return the service info with the supplied interceptors names
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static GrpcServiceInfo withInterceptorNames(List<String> interceptorNames) {
|
||||
Assert.notNull(interceptorNames, "interceptorNames must not be null");
|
||||
return new GrpcServiceInfo(new Class[0], interceptorNames.toArray(new String[0]), false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright 2023-2024 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.autoconfigure.server;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
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.server.lifecycle.GrpcServerLifecycle;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultGrpcServiceConfigurer}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class DefaultGrpcServiceConfigurerTests {
|
||||
|
||||
private ApplicationContextRunner contextRunner() {
|
||||
// NOTE: we use noop server lifecycle to avoid startup
|
||||
return new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
|
||||
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalServerInterceptorsAreFoundInProperOrder() {
|
||||
this.contextRunner()
|
||||
.withUserConfiguration(GlobalServerInterceptorsConfig.class)
|
||||
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceConfigurer.class)
|
||||
.extracting("globalInterceptors", InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithNoServiceInfoSpecified {
|
||||
|
||||
@Test
|
||||
void whenNoGlobalInterceptorsRegisteredThenServiceGetsNoInterceptors() {
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), null, List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGlobalInterceptorsRegisteredThenServiceGetsGlobalInterceptors() {
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(GlobalServerInterceptorsConfig.class), null,
|
||||
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoWithSingleInterceptor {
|
||||
|
||||
@Test
|
||||
void whenSingleBeanOfInterceptorTypeRegisteredThenItIsUsed() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(TestServerInterceptorA.class));
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleBeansOfInterceptorTypeRegisteredThenThrowsException() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class));
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, NoUniqueBeanDefinitionException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBeanOfInterceptorTypeRegisteredThenThrowsException() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class));
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo,
|
||||
NoSuchBeanDefinitionException.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoWithMultipleInterceptors {
|
||||
|
||||
@Test
|
||||
void whenSingleBeanOfEachInterceptorTypeRegisteredThenTheyAreUsed() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo
|
||||
.withInterceptors(List.of(TestServerInterceptorB.class, TestServerInterceptorA.class));
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoWithSingleInterceptorName {
|
||||
|
||||
@Test
|
||||
void whenSingleBeanWithInterceptorNameRegisteredThenItIsUsed() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptorB"));
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBeanWithInterceptorNameRegisteredThenThrowsException() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptor1"));
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo,
|
||||
NoSuchBeanDefinitionException.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoWithMultipleInterceptorNames {
|
||||
|
||||
@Test
|
||||
void whenSingleBeanWithEachInterceptorNameRegisteredThenTheyAreUsed() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptorB", "interceptorA"));
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoWithInterceptorAndInterceptorName {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void whenSingleBeanOfEachAvailableThenTheyAreBothUsed() {
|
||||
GrpcServiceInfo serviceInfo = new GrpcServiceInfo(new Class[] { TestServerInterceptorB.class },
|
||||
new String[] { "interceptorA" }, false);
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithServiceInfoCombinedWithGlobalInterceptors {
|
||||
|
||||
@Test
|
||||
void whenBlendInterceptorsFalseThenGlobalInterceptorsAddedFirst() {
|
||||
GrpcServiceInfo serviceInfo = GrpcServiceInfo
|
||||
.withInterceptors(List.of(TestServerInterceptorB.class, TestServerInterceptorA.class));
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner
|
||||
.withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Disabled("Needs 'blend interceptors' to be implemented")
|
||||
@Test
|
||||
void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() {
|
||||
GrpcServiceInfo serviceInfo = new GrpcServiceInfo(
|
||||
new Class[] { TestServerInterceptorB.class, TestServerInterceptorA.class }, new String[0], true);
|
||||
List<ServerInterceptor> expectedInterceptors = List.of(
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO,
|
||||
ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner
|
||||
.withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class),
|
||||
serviceInfo, expectedInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
Function<ApplicationContextRunner, ApplicationContextRunner> contextCustomizer, GrpcServiceInfo serviceInfo,
|
||||
List<ServerInterceptor> expectedInterceptors) {
|
||||
this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo,
|
||||
expectedInterceptors, null);
|
||||
}
|
||||
|
||||
private void customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
Function<ApplicationContextRunner, ApplicationContextRunner> contextCustomizer, GrpcServiceInfo serviceInfo,
|
||||
Class<? extends Throwable> expectedExceptionType) {
|
||||
this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo, null,
|
||||
expectedExceptionType);
|
||||
}
|
||||
|
||||
private void customizeContextAndRunServiceConfigurerWithServiceInfo(
|
||||
Function<ApplicationContextRunner, ApplicationContextRunner> contextCustomizer, GrpcServiceInfo serviceInfo,
|
||||
@Nullable List<ServerInterceptor> expectedInterceptors,
|
||||
@Nullable Class<? extends Throwable> expectedExceptionType) {
|
||||
// It gets difficult to verify interceptors are added properly to mocked services.
|
||||
// To make it easier, we just static mock ServerInterceptors.interceptForward to
|
||||
// echo back the service def. This way we can verify the interceptors were passed
|
||||
// in the proper order as we rely on ServerInterceptors.interceptForward being
|
||||
// well tested in grpc-java.
|
||||
try (MockedStatic<ServerInterceptors> serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
|
||||
serverInterceptorsMocked
|
||||
.when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
|
||||
.thenAnswer((Answer<ServerServiceDefinition>) invocation -> invocation.getArgument(0));
|
||||
BindableService service = mock();
|
||||
ServerServiceDefinition serviceDef = mock();
|
||||
when(service.bindService()).thenReturn(serviceDef);
|
||||
this.contextRunner()
|
||||
.withBean("service", BindableService.class, () -> service)
|
||||
.with(contextCustomizer)
|
||||
.run((context) -> {
|
||||
DefaultGrpcServiceConfigurer configurer = context.getBean(DefaultGrpcServiceConfigurer.class);
|
||||
if (expectedExceptionType != null) {
|
||||
assertThatThrownBy(() -> configurer.configure(service, serviceInfo))
|
||||
.isInstanceOf(expectedExceptionType);
|
||||
serverInterceptorsMocked.verifyNoInteractions();
|
||||
}
|
||||
else {
|
||||
configurer.configure(service, serviceInfo);
|
||||
serverInterceptorsMocked
|
||||
.verify(() -> ServerInterceptors.interceptForward(serviceDef, expectedInterceptors));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface TestServerInterceptorA extends ServerInterceptor {
|
||||
|
||||
}
|
||||
|
||||
interface TestServerInterceptorB extends ServerInterceptor {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GlobalServerInterceptorsConfig {
|
||||
|
||||
static BindableService SERVICE_A = mock();
|
||||
|
||||
static ServerServiceDefinition SERVICE_DEF_A = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
|
||||
|
||||
@Bean
|
||||
BindableService serviceA() {
|
||||
when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
|
||||
return SERVICE_A;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
@GlobalServerInterceptor
|
||||
ServerInterceptor globalInterceptorFoo() {
|
||||
return GLOBAL_INTERCEPTOR_FOO;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(150)
|
||||
ServerInterceptor globalInterceptorIgnored() {
|
||||
return GLOBAL_INTERCEPTOR_IGNORED;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
@GlobalServerInterceptor
|
||||
ServerInterceptor globalInterceptorBar() {
|
||||
return GLOBAL_INTERCEPTOR_BAR;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ServiceSpecificInterceptorsConfig {
|
||||
|
||||
static TestServerInterceptorB SVC_INTERCEPTOR_B = mock();
|
||||
|
||||
static TestServerInterceptorA SVC_INTERCEPTOR_A = mock();
|
||||
|
||||
@Bean
|
||||
@Order(150)
|
||||
TestServerInterceptorB interceptorB() {
|
||||
return SVC_INTERCEPTOR_B;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(225)
|
||||
TestServerInterceptorA interceptorA() {
|
||||
return SVC_INTERCEPTOR_A;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,18 +16,18 @@
|
||||
|
||||
package org.springframework.grpc.autoconfigure.server;
|
||||
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.grpc.autoconfigure.server.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_A;
|
||||
import static org.springframework.grpc.autoconfigure.server.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_B;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
@@ -36,12 +36,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultGrpcServiceDiscoverer}.
|
||||
@@ -58,50 +54,29 @@ class DefaultGrpcServiceDiscovererTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalServerInterceptorsAreFoundInProperOrder() {
|
||||
void servicesAreFoundInProperOrderWithExpectedGrpcServiceAnnotations() {
|
||||
TestServiceConfigurer configurer = new TestServiceConfigurer();
|
||||
this.contextRunner()
|
||||
.withUserConfiguration(GlobalServerInterceptorsConfig.class)
|
||||
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
|
||||
.extracting(DefaultGrpcServiceDiscoverer::findGlobalInterceptors, InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void servicesAreFoundInProperOrderWithGlobalInterceptorsApplied() {
|
||||
// It gets difficult to verify interceptors are added properly to mocked services.
|
||||
// To make it easier, we just static mock ServerInterceptors.interceptForward to
|
||||
// echo back the service def. This way we can verify the interceptors were passed
|
||||
// in the proper order as we rely/trust that ServerInterceptors.interceptForward
|
||||
// is
|
||||
// tested well in grpc-java.
|
||||
try (MockedStatic<ServerInterceptors> serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
|
||||
serverInterceptorsMocked
|
||||
.when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
|
||||
.thenAnswer((Answer<ServerServiceDefinition>) invocation -> invocation.getArgument(0));
|
||||
this.contextRunner().withUserConfiguration(GlobalServerInterceptorsConfig.class).run((context) -> {
|
||||
.withUserConfiguration(DefaultGrpcServiceDiscovererTestsConfig.class)
|
||||
.withBean("customServiceConfigurer", GrpcServiceConfigurer.class, () -> configurer)
|
||||
.run((context) -> {
|
||||
assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
|
||||
.extracting(DefaultGrpcServiceDiscoverer::findServices, InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
|
||||
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
|
||||
ArgumentCaptor<ServerServiceDefinition> serviceDefArg = ArgumentCaptor.captor();
|
||||
ArgumentCaptor<List<ServerInterceptor>> interceptorsArg = ArgumentCaptor.captor();
|
||||
serverInterceptorsMocked.verify(
|
||||
() -> ServerInterceptors.interceptForward(serviceDefArg.capture(), interceptorsArg.capture()),
|
||||
times(2));
|
||||
assertThat(serviceDefArg.getAllValues()).containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
|
||||
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
|
||||
assertThat(interceptorsArg.getAllValues()).containsExactly(
|
||||
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO),
|
||||
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
.containsExactly(DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_B,
|
||||
DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_A);
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GlobalServerInterceptorsConfig {
|
||||
static class DefaultGrpcServiceDiscovererTestsConfig {
|
||||
|
||||
static BindableService SERVICE_A = mock();
|
||||
|
||||
@@ -111,12 +86,7 @@ class DefaultGrpcServiceDiscovererTests {
|
||||
|
||||
static ServerServiceDefinition SERVICE_DEF_B = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
|
||||
|
||||
static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
|
||||
|
||||
@GrpcService
|
||||
@Bean
|
||||
@Order(200)
|
||||
BindableService serviceA() {
|
||||
@@ -131,24 +101,16 @@ class DefaultGrpcServiceDiscovererTests {
|
||||
return SERVICE_B;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
@GlobalServerInterceptor
|
||||
ServerInterceptor globalInterceptorFoo() {
|
||||
return GLOBAL_INTERCEPTOR_FOO;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(150)
|
||||
ServerInterceptor globalInterceptorIgnored() {
|
||||
return GLOBAL_INTERCEPTOR_IGNORED;
|
||||
}
|
||||
static class TestServiceConfigurer implements GrpcServiceConfigurer {
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
@GlobalServerInterceptor
|
||||
ServerInterceptor globalInterceptorBar() {
|
||||
return GLOBAL_INTERCEPTOR_BAR;
|
||||
Map<BindableService, GrpcServiceInfo> invocations = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
public ServerServiceDefinition configure(BindableService bindableService, GrpcServiceInfo serviceInfo) {
|
||||
invocations.put(bindableService, serviceInfo);
|
||||
return bindableService.bindService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.autoconfigure.server;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import io.grpc.ServerInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcServiceInfo}.
|
||||
*/
|
||||
class GrpcServiceInfoTests {
|
||||
|
||||
@Nested
|
||||
class WithInterceptorsApiInvokedWith {
|
||||
|
||||
@Test
|
||||
void nullInterceptors() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> GrpcServiceInfo.withInterceptors(null))
|
||||
.withMessage("interceptors must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void interceptorTypes() {
|
||||
assertThat(GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class))).satisfies((serviceInfo) -> {
|
||||
assertThat(serviceInfo.interceptors()).containsExactly(ServerInterceptor.class);
|
||||
assertThat(serviceInfo.interceptorNames()).isEmpty();
|
||||
assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithInterceptorNamesApiInvokedWith {
|
||||
|
||||
@Test
|
||||
void nullInterceptorNames() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> GrpcServiceInfo.withInterceptorNames(null))
|
||||
.withMessage("interceptorNames must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void interceptorNames() {
|
||||
assertThat(GrpcServiceInfo.withInterceptorNames(List.of("myInterceptor"))).satisfies((serviceInfo) -> {
|
||||
assertThat(serviceInfo.interceptors()).isEmpty();
|
||||
assertThat(serviceInfo.interceptorNames()).containsExactly("myInterceptor");
|
||||
assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FromApiInvokedWith {
|
||||
|
||||
@Test
|
||||
void nullGrpcService() {
|
||||
assertThat(GrpcServiceInfo.from(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcServiceAnnotationWithDefaults() {
|
||||
var grpcServiceAnnotation = AnnotationUtils.findAnnotation(TestServiceMarkedWithDefaults.class,
|
||||
GrpcService.class);
|
||||
assertThat(GrpcServiceInfo.from(grpcServiceAnnotation)).satisfies((serviceInfo) -> {
|
||||
assertThat(serviceInfo.interceptors()).isEmpty();
|
||||
assertThat(serviceInfo.interceptorNames()).isEmpty();
|
||||
assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcServiceAnnotationWithAttributes() {
|
||||
var grpcServiceAnnotation = AnnotationUtils.findAnnotation(TestServiceMarkedWithAttributes.class,
|
||||
GrpcService.class);
|
||||
assertThat(GrpcServiceInfo.from(grpcServiceAnnotation)).satisfies((serviceInfo) -> {
|
||||
assertThat(serviceInfo.interceptors()).containsExactly(ServerInterceptor.class);
|
||||
assertThat(serviceInfo.interceptorNames()).containsExactly("myInterceptor");
|
||||
assertThat(serviceInfo.blendWithGlobalInterceptors()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@GrpcService
|
||||
static class TestServiceMarkedWithDefaults {
|
||||
|
||||
}
|
||||
|
||||
@GrpcService(interceptors = ServerInterceptor.class, interceptorNames = "myInterceptor",
|
||||
blendWithGlobalInterceptors = true)
|
||||
static class TestServiceMarkedWithAttributes {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user