Reputation: 149
How to throw a custom exception in the proper way when using @javax.validation.Valid
?
I'm using @Valid
in controller, and @AssertTrue
to validate request body fields.
public ResponseEntity<Foo> createFoo(
@Valid @RequestBody Foo FooRequest ...
@AssertTrue()
public boolean isFooValid() {
if (invalid)
return false;
...
}
However, I want to throw customized Exception class in some condition.
@AssertTrue()
public boolean isFooValid() {
if (invalid)
return false;
...
// note below
if (invalidInAnotherCondition)
throw new CustomizedException(...);
}
I know this is not desirable way to utilize @Valid
in controller, and @AssertTrue
. Nevertheless, as I can make my own Exception class which contains customized error info, with the convenience of @Valid
.
However the error happens.
javax.validation.ValidationException: HV000090: Unable to access isFooValid
at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:245)
at org.hibernate.validator.internal.metadata.location.GetterConstraintLocation.getValue(GetterConstraintLocation.java:89)
at org.hibernate.validator.internal.engine.ValueContext.getValue(ValueContext.java:235)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:549)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:515)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:485)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:447)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:397)
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:173)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:117)
at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:70)
at org.springframework.validation.DataBinder.validate(DataBinder.java:889)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.validateIfApplicable(AbstractMessageConverterMethodArgumentResolver.java:266)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:137)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)
at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)
at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:376)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.reflect.InvocationTargetException: null
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:242)
... 70 common frames omitted
Caused by: com.finda.services.finda.common.exception.CustomizedException: 'df282e0d-1205-4574-adaa-0af819af66c0'
at ...
... 75 common frames omitted
I think this happens because originally, @AssertTrue
throws its own Exception itself and it is to be processed through the internal logic; However, customized thrown Exception is not acceptable which can be seen in Caused by: java.lang.reflect.InvocationTargetException: null
and javax.validation.ValidationException: HV000090: Unable to access isFooValid
So my final question is below,
Can I bypass this error, still throwing customized Exception?
I really appreciate that you read this long posting in advance.
Upvotes: 12
Views: 35839
Reputation: 123
step 1.add the custom exception method in your annotation:
@Documented
@Constraint(validatedBy = {})
@Target({ FIELD, TYPE_USE,RECORD_COMPONENT })
@Retention(RUNTIME)
public @interface ValidIdNumber {
String message() default "The '${validatedValue}' not a valid identification number!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Throwable> exception() default InvalidIdNumberException.class;
}
step 2.add AbstractValidator
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Consumer;
import javax.validation.Validation;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.metadata.ConstraintDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractValidator<T> implements Consumer<ConstraintViolation<T>> {
public AbstractValidator(){
}
private static final Logger LOGGER= LoggerFactory.getLogger(AbstractValidator.class);
private static class ExceptionFactory {
@SuppressWarnings("unchecked")
private static <A extends Annotation, E extends Throwable> Class<E> getExceptionTypeFromAnnotation(
ConstraintDescriptor<A> constraintDescriptor) throws E {
Annotation annotation = constraintDescriptor.getAnnotation();
Class<A> type = (Class<A>) annotation.annotationType();
Class<E> e = Arrays.stream(type.getDeclaredMethods()).filter(method -> {
Type genericReturnType = method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType parameterizedType) {
if (parameterizedType.getRawType().equals(Class.class)) {
Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
if (actualTypeArgument instanceof WildcardType wildcardType) {
var upperBounds = wildcardType.getUpperBounds();
if (upperBounds.length > 0) {
Class<?> clazz = (Class<?>) upperBounds[0];
return Throwable.class.isAssignableFrom(clazz);
}
}
}
}
return false;
}).map(method -> {
try {
return (Class<E>) method.invoke(annotation);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
LOGGER.error(e1.getMessage(), e1);
return null;
}
}).findFirst().orElseThrow();
return e;
}
public static <A extends Annotation, E extends Throwable> E create(ConstraintDescriptor<A> constraintDescriptor,
String message) throws NoSuchElementException, E {
Class<E> e = getExceptionTypeFromAnnotation(constraintDescriptor);
try {
return e.getConstructor(String.class).newInstance(message);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e1) {
LOGGER.error(e1.getMessage(), e1);
e1.printStackTrace();
return null;
}
}
}
@Override
public void accept(ConstraintViolation<T> constraintViolation) throws IllegalArgumentException {
switch (constraintViolation.getPropertyPath().toString()) {
default -> throw new IllegalArgumentException(
constraintViolation.getPropertyPath().toString() + " " + constraintViolation.getMessage());
}
}
public <E extends Throwable> void validate(T obj, Class<?>... groups) throws E {
Set<ConstraintViolation<T>> set = Validation.buildDefaultValidatorFactory().getValidator().validate(obj, groups);
if (!set.isEmpty()) {
ConstraintViolation<T> constraintViolation = set.iterator().next();
String message = constraintViolation.getMessage();
try {
throw ExceptionFactory.create(constraintViolation.getConstraintDescriptor(), message);
} catch (NoSuchElementException e) {
accept(constraintViolation);
}
}
}
}
step 3.add a impl of AbstractValidator:
public class ValidatorImpl extends AbstractValidator<Object>{
}
step 4.test:
record DTO(@ValidIdNumber String idNumber){}
public static void main(String[] args) throws InvalidIdNumberException{
DTO dto=new DTO("1234");
new ValidatorImpl().validate();
}
Upvotes: 0
Reputation: 168
Here is a solution I used (that might not answer this question directly but possibly help others who get to this page that came with similar intentions as I had):
My aim was foremost to respond with a custom error message to the client who sent a request with an invalid object.
@Valid
annotation from javax.validation.Valid before the parameter@NotNull(message = "Field XYZ has to be provided")
from javax.validation.constraints.NotNull@ControllerAdvice
public class MyControllerAdvice extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException exception,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return new ResponseEntity<>(
new JSONObject().put("message", extractValidationMessage(exception)).toString(),
HttpStatus.BAD_REQUEST);
}
private String extractValidationMessage(MethodArgumentNotValidException exception) {
String exceptionMessage = exception.getMessage();
String[] messageParts = exceptionMessage.split(";");
String finalPart = messageParts[messageParts.length -1];
return finalPart.trim().replaceAll("default message \\[|]]","");
}
}
{"message":"Field XYZ has to be provided"}
Upvotes: 3
Reputation: 78579
Consider the example below where I implemented something like what you asking for:
@RestController
@RequestMapping("/accounts")
public class SavingsAccountController {
private final BankAccountService accountService;
@Autowired
public SavingsAccountController(SavingsAccountService accountService) {
this.accountService = accountService;
}
@PutMapping("withdraw")
public ResponseEntity<AccountBalance> onMoneyWithdrawal(@RequestBody @Validated WithdrawMoney withdrawal, BindingResult errors) {
//this is the validation barrier
if (errors.hasErrors()) {
throw new ValidationException(errors);
}
double balance = accountService.withdrawMoney(withdrawal);
return ResponseEntity.ok(new AccountBalance(
withdrawal.getAccountNumber(), balance));
}
@PutMapping("save")
public ResponseEntity<AccountBalance> onMoneySaving(@RequestBody @Validated SaveMoney savings, BindingResult errors) {
//this is the validation barrier
if (errors.hasErrors()) {
throw new ValidationException(errors);
}
double balance = accountService.saveMoney(savings);
return ResponseEntity.ok(new AccountBalance(
savings.getAccountNumber(), balance));
}
}
In the code above, we're using Bean Validation to check that the user's DTO contains valid information. Any errors found in the DTO are provided through the BindingResult
errors variable, from where the developer can extract all the details of what went wrong during the validation phase.
To make it easier for the developers to deal with this pattern, in the code above, I simply wrap the BindingResult
into a custom ValidationException
which knows how to extract the validation error details.
public class ValidationException extends RuntimeException {
private final BindingResult errors;
public ValidationException(BindingResult errors) {
this.errors = errors;
}
public List<String> getMessages() {
return getValidationMessage(this.errors);
}
@Override
public String getMessage() {
return this.getMessages().toString();
}
//demonstrate how to extract a message from the binging result
private static List<String> getValidationMessage(BindingResult bindingResult) {
return bindingResult.getAllErrors()
.stream()
.map(ValidationException::getValidationMessage)
.collect(Collectors.toList());
}
private static String getValidationMessage(ObjectError error) {
if (error instanceof FieldError) {
FieldError fieldError = (FieldError) error;
String className = fieldError.getObjectName();
String property = fieldError.getField();
Object invalidValue = fieldError.getRejectedValue();
String message = fieldError.getDefaultMessage();
return String.format("%s.%s %s, but it was %s", className, property, message, invalidValue);
}
return String.format("%s: %s", error.getObjectName(), error.getDefaultMessage());
}
}
Notice that in my controller definition I do not use Bean Validation's @Valid annotation, but the Spring counterpart @Validated, but under the hood Spring will use Bean Validation.
How to Serialize the Custom Exception?
In the code above the ValidationException
will be thrown when the payload is invalid. How should the controller create a response for the client out of this?
There are multiple ways to deal with this, but perhaps the simplest solution is to define a class annotated as @ControllerAdvice
. In this annotated class we will place our exception handlers for any specific exception that we want to handle and turn them into a valid response object to travel back to our clients:
@ControllerAdvice
public class ExceptionHandlers {
@ExceptionHandler
public ResponseEntity<ErrorModel> handle(ValidationException ex) {
return ResponseEntity.badRequest()
.body(new ErrorModel(ex.getMessages()));
}
//...
}
I wrote a few other examples of this and other validation techniques with Spring in case you may be interested in reading more about it.
Upvotes: 14