Skip to content
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

[FEATURE] Request Validation via GrpcValidator Annotation and Custom Validators #1176

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
Expand Up @@ -62,6 +62,10 @@ public final class InterceptorOrder {
* The order value for security interceptors related to authorization checks.
*/
public static final int ORDER_SECURITY_AUTHORISATION = 5200;
/**
* The order value for request validation interceptors.
*/
public static final int REQUEST_VALIDATION = 6000;
/**
* The order value for interceptors that should be executed last. This is equivalent to
* {@link Ordered#LOWEST_PRECEDENCE}. This is the default for interceptors without specified priority.
Expand Down
2 changes: 2 additions & 0 deletions grpc-server-spring-boot-starter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
api 'io.grpc:grpc-services'
api 'io.grpc:grpc-api'

implementation 'build.buf.protoc-gen-validate:pgv-java-stub:0.9.1'

testImplementation 'io.grpc:grpc-testing'
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2016-2025 The gRPC-Spring 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
*
* 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 net.devh.boot.grpc.server.validator;

import io.envoyproxy.pgv.ReflectiveValidatorIndex;
import io.envoyproxy.pgv.ValidationException;
import io.envoyproxy.pgv.ValidatorIndex;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;

/**
* The {@code BaseValidator} interface provides a default method for validating gRPC request objects
* using the Envoy Proxy's Protoc-Gen-Validate library.
*
* @author Pritesh ([email protected])
* @since 02/10/25
*
* <p>
* Example usage:
* <pre>
* {@code
* public class MyRequestValidator implements BaseValidator {
* public void validateMyRequest(MyRequest request) {
* // Business validation
* }
* }
* }
* </pre>
* </p>
*/
public interface BaseValidator {

/**
* Validates the provided request object using the Envoy Proxy Protoc-Gen-Validate library.
* <p>
* This method uses the {@link ValidatorIndex} (specifically the {@link ReflectiveValidatorIndex}) to look up
* the appropriate validator for the request's class and performs validation. If the request is invalid, a
* {@link StatusRuntimeException} with a {@link Status} status is thrown, including details
* of the validation failure.
* </p>
*
* @param request the request object to validate
* @param <T> the type of the request object
* @throws StatusRuntimeException if the validation fails, this exception is thrown with a description of the failure
* and a cause explaining the validation issue.
*/
default <T> void validate(T request) {
ValidatorIndex validatorIndex = new ReflectiveValidatorIndex();
try {
validatorIndex.validatorFor(request.getClass()).assertValid(request);
} catch (ValidationException e) {
throw new StatusRuntimeException(Status.FAILED_PRECONDITION.withCause(e)
.withDescription(e.getField() + " : " + e.getReason() + ". Received value: " + e.getMessage()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2016-2023 The gRPC-Spring 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
*
* 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 net.devh.boot.grpc.server.validator;

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.stereotype.Component;

/**
* Annotation to mark methods that require gRPC validation. This annotation is typically used for methods in a service
* class where a gRPC request needs to be validated using a specific validator class and method.
*
* @author Pritesh ([email protected])
* @since 02/10/25
*
* <p>
* Usage Example:
* </p>
*
* <pre>
* {@code @GrpcValidator(requestClass = MyRequest.class, validatorClass = MyRequestValidator.class, validatorMethod = "validateRequest")
* public void myGrpcMethod(MyRequest request) {
* // Your gRPC method implementation
* }
* }
* </pre>
*
* <p>
* In this example, the annotation tells the system to use the {@code MyRequestValidator} class and call its
* {@code validateRequest} method to validate the incoming {@code MyRequest}.
* </p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface GrpcValidator {

/**
* The request class that will be validated.
*
* <p>
* This represents the type of the gRPC request that needs validation. The validator will perform checks based on
* this class.
* </p>
*
* @return the class type of the gRPC request
*/
Class<?> requestClass();

/**
* The validator class that contains the validation logic.
*
* <p>
* This class is responsible for validating the request class. It should provide the validation method specified by
* {@link #validatorMethod()}.
* </p>
*
* @return the class type of the validator
*/
Class<?> validatorClass();

/**
* The name of the method in the validator class that performs the validation.
*
* <p>
* This method is invoked at runtime to perform the validation logic on the request class. It should take the
* request class type as an argument and return a validation result, typically a boolean or a validation exception.
* </p>
*
* @return the name of the validation method
*/
String validatorMethod();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2016-2025 The gRPC-Spring 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
*
* 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 net.devh.boot.grpc.server.validator;

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import com.netflix.discovery.shared.Pair;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

/**
* The {@code GrpcValidatorDiscoverer} class is responsible for discovering and registering gRPC validators
* in the Spring application context based on custom annotations.
*
* @author Pritesh ([email protected])
* @since 02/10/25
*
* <p>
* Example usage: If a service contains a method annotated with {@link GrpcValidator} for a specific request class,
* the {@code GrpcValidatorDiscoverer} will discover this method and register it in the map so that it can later be
* used for validation during gRPC request processing.
* </p>
*/
@Slf4j
@Component
public class GrpcValidatorDiscoverer implements ApplicationContextAware {

private ApplicationContext applicationContext;

/**
* A map of request classes to their corresponding validator bean and method.
* The map is populated during the initialization phase using the {@link GrpcValidator} annotation.
* The key is the request class, and the value is a {@link Pair} containing the validator bean and method name.
*/
@Getter
private Map<Class<?>, Pair<Object, String>> requestValidatorMethodMap;

/**
* Sets the application context. This method is called by the Spring container.
* It allows access to the Spring {@link ApplicationContext} to retrieve beans and methods.
*
* @param applicationContext the Spring {@link ApplicationContext} to be set
*/
@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

/**
* Initializes the {@code GrpcValidatorDiscoverer} by scanning all beans in the Spring context that are
* annotated with {@link Service}. For each of these beans, the method scans its declared methods for the
* {@link GrpcValidator} annotation.
* <p>
* It collects the mapping between request classes and their corresponding validator beans and methods,
* storing it in the {@link #requestValidatorMethodMap} field.
* </p>
* <p>
* This method is executed after the bean has been initialized (as indicated by the {@link PostConstruct} annotation),
* and it logs the initialization status and the resulting map of validators.
* </p>
*/
@PostConstruct
public void init() {
log.info("Initializing registration of GrpcValidator annotation");

// Discover beans annotated with @Service and their methods annotated with @GrpcValidator
requestValidatorMethodMap = applicationContext.getBeansWithAnnotation(Service.class).values().stream()
.flatMap(bean -> Arrays.stream(bean.getClass().getDeclaredMethods())) // Flatten the methods in the bean
.filter(method -> method.isAnnotationPresent(GrpcValidator.class)) // Filter methods annotated with @GrpcValidator
.map(method -> {
GrpcValidator validator = method.getAnnotation(GrpcValidator.class);
Object validatorBean = applicationContext.getBean(validator.validatorClass()); // Retrieve the validator bean from the context
return new Pair<>(validator.requestClass(), new Pair<>(validatorBean, validator.validatorMethod())); // Return a pair of the request class and the validator info
})
.collect(Collectors.toMap(Pair::first, Pair::second)); // Collect into a map: request class -> (validator bean, method name)

log.info("Request to validator map instantiated: {}", requestValidatorMethodMap);
}
}
Loading