I have a Spring Boot 2 app and I want to be able to validate controller arguments with Hibernate validator - which I'm using successfully. I have all my controller annotated as #Validated and I'm using the validation for request parameters like so #PathVariable #AssertUuid final String customerId - so far so good, everything works.
But, I want to also be able to validate #ModelAttribute from forms.
#Controller
#PreAuthorize("hasRole('ADMIN')")
#RequestMapping(path = "/customers")
#Validated
public class CustomerController
{
private final CustomerFacade customerFacade;
public CustomerController(
final CustomerFacade customerFacade
)
{
this.customerFacade = customerFacade;
}
#GetMapping("/create")
public ModelAndView create(
final AccessToken accessToken
)
{
return new ModelAndView("customer/create")
.addObject("customer", new CreateCustomerRequest());
}
#PostMapping("/create")
public ModelAndView handleCreate(
final AccessToken accessToken,
#Validated #ModelAttribute("customer") final CreateCustomerRequest customerValues,
final BindingResult validation
) throws
UserDoesNotHaveAdminAccessException
{
if (validation.hasErrors()) {
return new ModelAndView("customer/create")
.addObject("customer", customerValues);
}
CustomerResult newCustomer = customerFacade.createCustomer(
accessToken,
customerValues.getName()
);
return new ModelAndView(new RedirectView("..."));
}
public static final class CreateCustomerRequest
{
#NotNull
#NotBlank
private String name;
public CreateCustomerRequest(final String name)
{
this.name = name;
}
public CreateCustomerRequest()
{
}
public String getName()
{
return name;
}
}
}
But this causes the MethodValidationInterceptor to throw ConstraintViolationException when I send invalid data. This would normally make sense and I want this behaviour in every other case, but in this case, as you can see, I want to use the BindingResult to handle the validation errors - which is neccesary when working with forms.
Is there a way I could tell Spring to not validate this particular parameter with MethodValidationInterceptor, because it's already validated by the binder and I want to handle it differently?
I've been digging around in the spring code and it looks like is not designed to work together. I have the some ideas how to fix this:
remove the #Validated from the argument and
call validator.validate() explicitly in the controller method - ugly and dangerous (you might forget to call it)
create another AOP interceptor, that would find "pairs" of #ModelAttribute and BindingResult and call the validator there, forcing the validation globally
Am I going about this completely wrong? Am I missing something? Is there a better way?
I've come up with a solution that allows me to continue working, but I don't consider this problem solved.
As I've hinted in the original question, this Aspect forces validation of the #ModelAttribute when it isn't annotated with #Validated or #Valid.
This means that the ConstraintViolationException is not thrown for invalid #ModelAttribute and you can handle the errors in method body.
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.validation.Valid;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
#SuppressWarnings({"checkstyle:IllegalThrows"})
#Aspect
public class ControllerModelAttributeAutoValidatingAspect
{
private final Validator validator;
public ControllerModelAttributeAutoValidatingAspect(
final Validator validator
)
{
this.validator = validator;
}
#Around("execution(public * ((#org.springframework.web.bind.annotation.RequestMapping *)+).*(..)))")
public Object proceed(final ProceedingJoinPoint pjp) throws Throwable
{
MethodSignature methodSignature = MethodSignature.class.cast(pjp.getSignature());
List<MethodParameter> methodParameters = getMethodParameters(methodSignature);
PeekingIterator<MethodParameter> parametersIterator = Iterators.peekingIterator(methodParameters.iterator());
while (parametersIterator.hasNext()) {
MethodParameter parameter = parametersIterator.next();
if (!parameter.hasParameterAnnotation(ModelAttribute.class)) {
// process only ModelAttribute arguments
continue;
}
if (parameter.hasParameterAnnotation(Validated.class) || parameter.hasParameterAnnotation(Valid.class)) {
// if the argument is annotated as validated, the binder already validated it
continue;
}
MethodParameter nextParameter = parametersIterator.peek();
if (!Errors.class.isAssignableFrom(nextParameter.getParameterType())) {
// the Errors argument has to be right after the ModelAttribute argument to form a pair
continue;
}
Object target = pjp.getArgs()[methodParameters.indexOf(parameter)];
Errors errors = Errors.class.cast(pjp.getArgs()[methodParameters.indexOf(nextParameter)]);
validator.validate(target, errors);
}
return pjp.proceed();
}
private List<MethodParameter> getMethodParameters(final MethodSignature methodSignature)
{
return IntStream.range(0, methodSignature.getParameterNames().length)
.mapToObj(i -> new MethodParameter(methodSignature.getMethod(), i))
.collect(Collectors.toList());
}
}
Now you can just keep using validation annotations in your controller methods as you're used to, and at the same time, the final BindingResult validation works as expected.
#PostMapping("/create")
public ModelAndView handleCreate(
final AccessToken accessToken,
#ModelAttribute("customer") final CreateCustomerRequest customerValues,
final BindingResult validation
)
Thank you for sharing this solution.
I used it as inspiration and as a base for creating a more general method arguments validator that I intend to use on selected methods.
Validation is triggered for methods annotated by #Validate:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
Example:
#Validate
public void testMe(BindingModel bindingModel, Errors errors) {
if (!errors.hasErrors()) {
// bindingModel is valid
}
}
And here's the modified aspect class:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
#Aspect
#Component
public class ValidateAspect {
private final Validator validator;
public ValidateAspect(Validator validator) {
this.validator = validator;
}
#Around("#annotation(Validate)")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
List<MethodParameter> methodParameters = getMethodParameters(methodSignature);
for (int i = 0; i < methodParameters.size() - 1; i++) {
MethodParameter parameter = methodParameters.get(i);
MethodParameter nextParameter = methodParameters.get(i + 1);
if (!Errors.class.isAssignableFrom(nextParameter.getParameterType())) {
// the Errors argument has to be right after the validated argument to form a pair
continue;
}
Object target = pjp.getArgs()[methodParameters.indexOf(parameter)];
Errors errors = (Errors) pjp.getArgs()[methodParameters.indexOf(nextParameter)];
validator.validate(target, errors);
}
return pjp.proceed();
}
private static List<MethodParameter> getMethodParameters(MethodSignature methodSignature) {
return IntStream
.range(0, methodSignature.getParameterNames().length)
.mapToObj(i -> new MethodParameter(methodSignature.getMethod(), i))
.collect(Collectors.toList());
}
}
The above code is tested and (so far) seems to work properly with Spring Boot 2.1.4.RELEASE
Related
I created the following service interface:
import javax.validation.constraints.NotBlank;
import org.springframework.lang.NonNull;
import org.springframework.validation.annotation.Validated;
#Validated
public interface UserService {
User create(#NonNull Long telegramId, #NotBlank String name, #NonNull Boolean isBot);
}
but the following invocation:
userService.create(telegramId, "Mike", null);
passes the #NotNull validation for isBot parameter. How to correctly configure Spring Boot and my service in order to take into account #NonNull annotation and prevent method execution in case of null parameter?
I played around with this problem for a bit.
Your code looks fine to me: Make sure that the implementation of UserService also has the validation annotations present.
Ensure that you allow Spring to create the Bean; it should work as you expect.
Example
Service Definition
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
#Validated
public interface GreetingService {
String greet(#NotNull #NotBlank String greeting);
}
Service Implementation
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
#Service
public class HelloGreetingService implements GreetingService {
public String greet(#NotNull #NotBlank String greeting) {
return "hello " + greeting;
}
}
Testcase
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import javax.validation.ConstraintViolationException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
#SpringBootTest
class HelloGreetingServiceTest {
#Autowired
private GreetingService helloGreetingService;
#Test
void whenGreetWithStringInput_shouldDisplayGreeting() {
String input = "john doe";
assertEquals("hello john doe", helloGreetingService.greet(input));
}
#Test
void whenGreetWithNullInput_shouldThrowException() {
assertThrows(ConstraintViolationException.class, () -> helloGreetingService.greet(null));
}
#Test
void whenGreetWithBlankInput_shouldThrowException() {
assertThrows(ConstraintViolationException.class, () -> helloGreetingService.greet(""));
}
}
Testcases are green for me.
Github: https://github.com/almac777/spring-validation-playground
Source: https://www.baeldung.com/javax-validation-method-constraints
HTH!
Use the same thing in Implementation class instead interface.
Also can write one global exception like:
#Order(Ordered.HIGHEST_PRECEDENCE)
#RestControllerAdvice
public class GlobalRestException extends ResponseEntityExceptionHandler {
...
...
/**
* Handle MethodArgumentNotValidException. Triggered when an object fails #Valid
* validation.
*
* #param ex the MethodArgumentNotValidException that is thrown when #Valid
* validation fails
* #param headers HttpHeaders
* #param status HttpStatus
* #param request WebRequest
* #return the ApiException object
*/
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
Error apiError = new Error(BAD_REQUEST);
apiError.setMessage("Validation error");
apiError.addValidationErrors(ex.getBindingResult().getFieldErrors());
apiError.addValidationError(ex.getBindingResult().getGlobalErrors());
return buildResponseEntity(apiError);
}
}
There are more method that can be override to handle different kind of exception like :
/**
* Handles javax.validation.ConstraintViolationException. Thrown when #Validated
* fails.
*
* #param ex the ConstraintViolationException
* #return the ApiException object
*/
#ExceptionHandler(javax.validation.ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolation(javax.validation.ConstraintViolationException ex) {
Error apiError = new Error(BAD_REQUEST);
apiError.setMessage("Validation error");
apiError.addValidationErrors(ex.getConstraintViolations());
return buildResponseEntity(apiError);
}
You need to make sure that #Validated annotation is used on 'class' which method arguments will need to be validated and Spring configuration need to be added
#Configuration
public class MethodValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
In my Spring (4.3.2) project I'm using Swagger (2.7.0) to automatically generate docs and swagger-ui for my project. This worked great so far.
But now I determined that I need to be able to declare Path Variables at the Controller level (not method level). And I need to teach swagger to discover these path variables and add them to docs and swagger-ui.
I've created custom annotation
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface HasCommonPathVariable {
/**
* The URI template variable to bind to.
*/
String name();
Class<?> type();
String defaultValue() default "";
}
And I'm using it like this:
#RestController
#Secured(SecurityConstants.ROLE_USER)
#RequestMapping(path = "/rest/api/v1/env/{envId}/asset-type")
#HasCommonPathVariable(name = "envId", type = Long.class)
public class AssetTypeRestController extends CustomRestControllerBase<Long, AssetTypeRow, AssetTypeService> {
// ... contorller code
}
I do not have controller methods that mentions parameters with Spring's PathVariable annotation, and the point is I'm not allowed to do so (it's due to the fact that I'm building micro-framework).
So question is: how to teach swagger to discover path variables described using custom annotation HasCommonPathVariable applied at the controller level?
Ok, I've figured it out. Here is the solution. This bean needs to be registered in the context. Swagger will discover this bean and use it as one of the plugins to enrich operations
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.springframework.core.annotation.Order;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;
#Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1000)
public class CommonPathVariableOperationBuilderPlugin implements OperationBuilderPlugin {
protected Logger log = Logger.getLogger(getClass());
private TypeResolver typeResolver;
public CommonPathVariableOperationBuilderPlugin(TypeResolver typeResolver) {
this.typeResolver = typeResolver;
}
#Override
public boolean supports(DocumentationType delimiter) {
return true;
}
#Override
public void apply(OperationContext opCtx) {
List<Parameter> ret = new ArrayList<Parameter>();
Optional<HasCommonPathVariable> annSingle = opCtx.findControllerAnnotation(HasCommonPathVariable.class);
if (annSingle.isPresent()) {
ret.add(addParameter(annSingle.get()));
}
Optional<HasCommonPathVariables> annPlural = opCtx.findControllerAnnotation(HasCommonPathVariables.class);
if (annPlural.isPresent()) {
for (HasCommonPathVariable ann : annPlural.get().value()) {
ret.add(addParameter(ann));
}
}
opCtx.operationBuilder().parameters(ret);
}
private Parameter addParameter(HasCommonPathVariable ann) {
ParameterBuilder pb = new ParameterBuilder();
pb.parameterType("path").name(ann.name()).type(typeResolver.resolve(ann.type()));
pb.modelRef(new ModelRef("string"));
pb.required(true);
if (!"".equals(ann.defaultValue())) {
pb.defaultValue(ann.defaultValue());
}
return pb.build();
}
}
I want to create an aspectJ component within springboot project, that prints log messages wherever #Loggable annotation is present, methods or class, or maybe both (method will be considered).
Loggable annotation:
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE, ElementType.METHOD })
public #interface Loggable {
boolean duration() default false;
}
Aspectj class:
#Aspect
#Component
public class LogInterceptorAspect {
#Pointcut("execution(public * ((#Loggable *)+).*(..)) && within(#Loggable *)")
public boolean loggableDefinition(Loggable loggable) {
return loggable.duration();
}
#Around("loggableDefinition(withDuration)")
public void log(ProceedingJoinPoint joinPoint, boolean withDuration) throws Throwable {
getLogger(joinPoint).info("start {}", joinPoint.getSignature().getName());
StopWatch sw = new StopWatch();
Object returnVal = null;
try {
sw.start();
returnVal = joinPoint.proceed();
} finally {
sw.stop();
}
getLogger(joinPoint).info("return value: {}, duration: {}", returnVal, sw.getTotalTimeMillis()));
}
private Logger getLogger(JoinPoint joinPoint) {
return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
}
}
With the above code I get
java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
What's wrong?
Basically the formal parameter is unbound on the PointCut.
Here is an alternate working example based on the approach detailed in this post: #AspectJ Class level Annotation Advice with Annotation as method argument
I modified your approach slightly to avoid the problem for a couple of reasons :
simplified the initial PointCut and gave it a single responsibility
gave it a descriptive name indicating its purpose
made it more reusable by removing the dependency on Loggable
kept it close in implementation to most of the sample documentation available
broke the Advice into two simpler methods each with a single responsibility that is easy to comprehend
simplified the expressions by removing fancy operators
injected the annotation directly into the Advice where its used rather than attempting to pass from the PointCut which feels like an unnecessary complexity
kept it close in implementation to most of the sample documentation available
added the start of a unit test to verify the expected behavior so that changes can be made responsibly to the PointCut and Advice expressions (you should complete it)
When working with PointCut/Advice Expressions, I generally try to go for the simplest, clearest solutions possible and unit test them thoroughly to ensure the behavior I expect is what I get. The next person to look at your code will appreciate it.
Hope this helps.
package com.spring.aspects;
import static org.junit.Assert.assertEquals;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StopWatch;
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;
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = AspectInjectAnnotationTest.TestContext.class)
public class AspectInjectAnnotationTest {
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE, ElementType.METHOD })
public #interface Loggable {
boolean duration() default false;
}
#Aspect
public static class LogInterceptorAspect {
#Pointcut("execution(public * *(..))")
public void anyPublicMethod() {
}
#Around("anyPublicMethod() && #annotation(loggable)")
public Object aroundLoggableMethods(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
return log(joinPoint, loggable);
}
#Around("(anyPublicMethod() && !#annotation(AspectInjectAnnotationTest.Loggable)) && #within(loggable)")
public Object aroundPublicMethodsOnLoggableClasses(ProceedingJoinPoint joinPoint, Loggable loggable)
throws Throwable {
return log(joinPoint, loggable);
}
public Object log(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
getLogger(joinPoint).info("start [{}], duration [{}]", joinPoint.getSignature().getName(),
loggable.duration());
StopWatch sw = new StopWatch();
Object returnVal = null;
try {
sw.start();
returnVal = joinPoint.proceed();
} finally {
sw.stop();
}
getLogger(joinPoint).info("return value: [{}], duration: [{}]", returnVal, sw.getTotalTimeMillis());
return returnVal;
}
private Logger getLogger(JoinPoint joinPoint) {
return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
}
}
// class level annotation - should only proxy public methods
#Loggable(duration = true)
public static class Service1 {
// public - should be proxied
public String testS1M1(String test) {
return testProtectedM(test);
}
// public - should be proxied
public String testS1M2(String test) {
return testProtectedM(test);
}
// protected - should not be proxied
protected String testProtectedM(String test) {
return testPrivateM(test);
}
// private - should not be proxied
private String testPrivateM(String test) {
return test;
}
}
// no annotation - class uses method level
public static class Service2 {
#Loggable
public String testS2M1(String test) {
return protectedMethod(test);
}
// no annotation - should not be proxied
public String testS2M2(String test) {
return protectedMethod(test);
}
// protected - should not be proxied
protected String protectedMethod(String test) {
return testPrivate(test);
}
// private - should not be proxied
private String testPrivate(String test) {
return test;
}
}
// annotation - class and method level - make sure only call once
#Loggable
public static class Service3 {
#Loggable
public String testS3M1(String test) {
return test;
}
}
// context configuration for the test class
#Configuration
#EnableAspectJAutoProxy
public static class TestContext {
// configure the aspect
#Bean
public LogInterceptorAspect loggingAspect() {
return new LogInterceptorAspect();
}
// configure a proxied beans
#Bean
public Service1 service1() {
return new Service1();
}
// configure a proxied bean
#Bean
public Service2 service2() {
return new Service2();
}
// configure a proxied bean
#Bean
public Service3 service3() {
return new Service3();
}
}
#Autowired
private Service1 service1;
#Autowired
private Service2 service2;
#Autowired
private Service3 service3;
#Test
public void aspectShouldLogAsExpected() {
// observe the output in the log, but craft this into specific
// unit tests to assert the behavior you are expecting.
assertEquals("service-1-method-1", service1.testS1M1("service-1-method-1")); // expect logging
assertEquals("service-1-method-2", service1.testS1M2("service-1-method-2")); // expect logging
assertEquals("service-2-method-1", service2.testS2M1("service-2-method-1")); // expect logging
assertEquals("service-2-method-2", service2.testS2M2("service-2-method-2")); // expect no logging
assertEquals("service-3-method-1", service3.testS3M1("service-3-method-1")); // expect logging once
}
}
I have a java bean being used to send JSON messages to a spring #RestController and I have bean validation setup and running just fine using #Valid. But I want to move to Protobuf/Thrift and move away from REST. It is an internal API and a lot of big companies have done away with REST internally. What this really means is that I no longer have control of the message objects - they are generated externally. I can't put annotations on them anymore.
So now my validation has to be programmatic. How do I do this? I have coded up a Validator and it works just great. But it doesn't use the nice #Valid annotation. I have to do the following:
#Service
public StuffEndpoint implements StuffThriftDef.Iface {
#Autowired
private MyValidator myValidator;
public void things(MyMessage msg) throws BindException {
BindingResult errors = new BeanPropertyBindingResult(msg, msg.getClass().getName());
errors = myValidator.validate(msg);
if (errors.hasErrors()) {
throw new BindException(errors);
} else {
doRealWork();
}
}
}
This stinks. I have to do this in every single method. Now, I can put a lot of that into one method that throws BindException and that makes it one line of code to add to every method. But that's still not great.
What I want is to see it look like this:
#Service
#Validated
public StuffEndpoint implements StuffThriftDef.Iface {
public void things(#Valid MyMessage msg) {
doRealWork();
}
}
And still get the same result. Remember, my bean has no annotations. And yes, I know I can use the #InitBinder annotation on a method. But that only works for web requests.
I don't mind injecting the correct Validator into this class, but I would prefer if my ValidatorFactory could pull the correct one based on the supports() method.
Is this possible? Is there a way to configure bean validation to actually use Spring validation instead? Do I have to hijack a Aspect somewhere? Hack into the LocalValidatorFactory or the MethodValidationPostProcessor?
Thanks.
Its pretty complicated thing to combine Spring validation and JSR-303 constrains. And there is no 'ready to use' way. The main inconvenience is that Spring validation uses BindingResult, and JSR-303 uses ConstraintValidatorContext as result of validation.
You can try to make your own validation engine, using Spring AOP. Let's consider, what we need to do for it. First of all, declare AOP dependencies (if you didn't yet):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.8</version>
</dependency>
I'm using Spring of version 4.2.4.RELEASE, but of cause you can use your own. AspectJ needed for use aspect annotation. Next step, we have to create simple validator registry:
public class CustomValidatorRegistry {
private List<Validator> validatorList = new ArrayList<>();
public void addValidator(Validator validator){
validatorList.add(validator);
}
public List<Validator> getValidatorsForObject(Object o) {
List<Validator> result = new ArrayList<>();
for(Validator validator : validatorList){
if(validator.supports(o.getClass())){
result.add(validator);
}
}
return result;
}
}
As you see it is very simple class, which allow us to find validator for object. Now lets create annotation, that will be mark methods, that need to be validated:
package com.mydomain.validation;
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
}
Because of standard BindingException class is not RuntimeException, we can't use it in overriden methods. This means we need define our own exception:
public class CustomValidatorException extends RuntimeException {
private BindingResult bindingResult;
public CustomValidatorException(BindingResult bindingResult){
this.bindingResult = bindingResult;
}
public BindingResult getBindingResult() {
return bindingResult;
}
}
Now we are ready to create an aspect that will do most of the work. Aspect will execute before methods, which marked with CustomValidation annotation:
#Aspect
#Component
public class CustomValidatingAspect {
#Autowired
private CustomValidatorRegistry registry; //aspect will use our validator registry
#Before(value = "execution(public * *(..)) && annotation(com.mydomain.validation.CustomValidation)")
public void doBefore(JoinPoint point){
Annotation[][] paramAnnotations =
((MethodSignature)point.getSignature()).getMethod().getParameterAnnotations();
for(int i=0; i<paramAnnotations.length; i++){
for(Annotation annotation : paramAnnotations[i]){
//checking for standard org.springframework.validation.annotation.Validated
if(annotation.annotationType() == Validated.class){
Object arg = point.getArgs()[i];
if(arg==null) continue;
validate(arg);
}
}
}
}
private void validate(Object arg) {
List<Validator> validatorList = registry.getValidatorsForObject(arg);
for(Validator validator : validatorList){
BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
validator.validate(arg, errors);
if(errors.hasErrors()){
throw new CustomValidatorException(errors);
}
}
}
}
execution(public * *(..)) && #annotation(com.springapp.mvc.validators.CustomValidation) means, that this aspect will applied to any public methods of beans, which marked with #CustomValidation annotation. Also note, that to mark validated parameters we are using standard org.springframework.validation.annotation.Validated annotation. But of cause we could make our custom. I think other code of aspect is very simple and does not need any comments. Further code of example validator:
public class PersonValidator implements Validator {
#Override
public boolean supports(Class<?> aClass) {
return aClass==Person.class;
}
#Override
public void validate(Object o, Errors errors) {
Person person = (Person)o;
if(person.getAge()<=0){
errors.rejectValue("age", "Age is too small");
}
}
}
Now we have make tune the configuration and all ready to use:
#Configuration
#ComponentScan(basePackages = "com.mydomain")
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig{
.....
#Bean
public CustomValidatorRegistry validatorRegistry(){
CustomValidatorRegistry registry = new CustomValidatorRegistry();
registry.addValidator(new PersonValidator());
return registry;
}
}
Note, proxyTargetClass is true because we will use cglib class proxy.
Example of target method in service class:
#Service
public class PersonService{
#CustomValidation
public void savePerson(#Validated Person person){
....
}
}
Because of #CustomValidation annotation aspect will be applied, and because of #Validated annotation person will be validated. And example of usage of service in controller(or any other class):
#Controller
public class PersonConroller{
#Autowired
private PersonService service;
public String savePerson(#ModelAttribute Person person, ModelMap model){
try{
service.savePerson(person);
}catch(CustomValidatorException e){
model.addAttribute("errors", e.getBindingResult());
return "viewname";
}
return "viewname";
}
}
Keep in mind, that if you will invoke #CustomValidation from methods of PersonService class, validation will not work. Because it will invoke methods of original class, but not proxy. This means, that you can invoke this methods only from outside of class (from other classes), if you want validation to be working (eg #Transactional works same way).
Sorry for long post. My answer is not about 'simple declarative way', and possible you will do not need it. But I was curious resolve this problem.
I marked #Ken's answer as correct because it is. But I have taken it a little further and wanted to post what I have made. I hope anybody coming to this page will find it interesting. I might try to get it in front of the Spring folks to see if it might be something included in future releases.
The idea is to have a new annotation to replace #Valid. So I called it #SpringValid. Using this annotation would kick off the system put together above. Here are all the pieces:
SpringValid.java
package org.springframework.validation.annotation;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER})
#Retention(RUNTIME)
public #interface SpringValid {
}
SpringValidationAspect.java
package org.springframework.validation;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
#Aspect
#Component
public class SpringValidationAspect {
private SpringValidatorRegistry springValidatorRegistry;
#Autowired
public SpringValidationAspect(final SpringValidatorRegistry springValidatorRegistry) {
this.springValidatorRegistry = springValidatorRegistry;
}
public SpringValidatorRegistry getSpringValidatorRegistry() {
return springValidatorRegistry;
}
#Before("#target(org.springframework.validation.annotation.Validated) "
+ "&& execution(public * *(#org.springframework.validation.annotation.SpringValid (*), ..)) "
+ "&& args(validationTarget)")
public void beforeMethodThatNeedsValidation(Object validationTarget) {
validate(validationTarget);
}
private void validate(Object arg) {
List<Validator> validatorList = springValidatorRegistry.getValidatorsForObject(arg);
for (Validator validator : validatorList) {
BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
validator.validate(arg, errors);
if (errors.hasErrors()) {
throw new SpringValidationException(errors);
}
}
}
}
Spring's examples show classes annotated with #Validated so I wanted to keep that. The above aspect only targets classes with #Validated at the class-level. And, just like when you use #Valid, it looks for the #SpringValid annotation stuck to a method parameter.
SpringValidationException.java
package org.springframework.validation;
import org.springframework.validation.BindingResult;
public class SpringValidationException extends RuntimeException {
private static final long serialVersionUID = 1L;
private BindingResult bindingResult;
public SpringValidationException(final BindingResult bindingResult) {
this.bindingResult = bindingResult;
}
public BindingResult getBindingResult() {
return bindingResult;
}
}
SpringValidatorRegistry.java
package org.springframework.validation;
import org.springframework.validation.Validator;
import java.util.ArrayList;
import java.util.List;
public class SpringValidatorRegistry {
private List<Validator> validatorList = new ArrayList<>();
public void addValidator(Validator validator) {
validatorList.add(validator);
}
public List<Validator> getValidatorsForObject(Object o) {
List<Validator> result = new ArrayList<>();
for (Validator validator : validatorList) {
if (validator.supports(o.getClass())) {
result.add(validator);
}
}
return result;
}
}
Just like the first answer, a place to register all classes that implement Spring's org.springframework.validation.Validator interface.
SpringValidator.java
package org.springframework.validation.annotation;
import org.springframework.stereotype.Component;
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;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Component
public #interface SpringValidator {
}
This is just extra sauce to make it easier to register/find Validators. You could register all your Validators by hand, or you could find them via reflection. So this part is not required, I just thought it made things easier.
MyConfig.java
package com.example.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.SpringValidationAspect;
import org.springframework.validation.SpringValidatorRegistry;
import org.springframework.validation.annotation.SpringValidator;
import java.util.Map;
import javax.validation.Validator;
#Configuration
public class MyConfig {
#Autowired
private ApplicationContext applicationContext;
#Bean
public SpringValidatorRegistry validatorRegistry() {
SpringValidatorRegistry registry = new SpringValidatorRegistry();
Map<String, Object> validators =
applicationContext.getBeansWithAnnotation(SpringValidator.class);
validators.values()
.forEach(v -> registry.addValidator((org.springframework.validation.Validator) v));
return registry;
}
#Bean
public SpringValidationAspect springValidationAspect() {
return new SpringValidationAspect(validatorRegistry());
}
}
See, scan your classpath and look for #SpringValidator classes and register them. Then register the Aspect and away you go.
Here is an example of such a Validator:
MyMessageValidator.java
package com.example.validators;
import com.example.messages.MyMessage;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.SpringValidator;
#SpringValidator
public class MyMessageValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return MyMessage.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "firstField", "{javax.validation.constraints.NotNull}",
"firstField cannot be null");
MyMessage obj = (MyMessage) target;
if (obj.getSecondField != null && obj.getSecondField > 100) {
errors.rejectField(errors, "secondField", "{javax.validation.constraints.Max}", "secondField is too big");
}
}
}
And here is the service class that uses the #SpringValid annotation:
MyService.java
package com.example.services;
import com.example.messages.MyMessage;
import org.springframework.validation.annotation.SpringValid;
import org.springframework.validation.annotation.Validated;
import javax.inject.Inject;
#Validated
public class MyService {
public String doIt(#SpringValid final MyMessage msg) {
return "we did it!";
}
}
Hope this makes sense for someone at some point. I personally think it is quite useful. A lot of companies are starting to move their internal APIs away from REST and to something like Protobuf or Thrift. You can still use Bean Validation but you have to use XML, and it isn't all that nice. So I hope this will be helpful to people who want to still do programmatic validation.
Hope it helps someone. I've got it working by adding the following configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
#Configuration
public class ValidatorConfiguration {
#Bean
public MethodValidationPostProcessor getMethodValidationPostProcessor(){
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(this.validator());
return processor;
}
#Bean
public LocalValidatorFactoryBean validator(){
return new LocalValidatorFactoryBean();
}
}
The service is then annotated the same way (#Validated on the class and #Valid on the parameter) and can be injected into another bean where the method can be called directly and validation happens.
I'm working with AOP in spring:
I have written an annotation
#Retention(RetentionPolicy.RUNTIME)
public #interface TestAnnotation {
}
And I use it on a controller method:
#ResponseBody
#TestAnnotation
#RequestMapping(method = RequestMethod.PUT, value = "/path/{variable}")
public return_type controller_call(#PathVariable String variable) {
return service.methodName(variable);
}
In the advice I have written the following code:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Annotation[] annotations = joinPoint.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotations();
This lists the RequestMapping and the ResponseBody annotation but it doesn't list my TestAnnotation.
Any idea why??
For me this works, maybe you are doing something wrong. Probably your sample code does not really reflect your situation. I have replicated this situation in a plain Java + AspectJ setup, merely putting the Spring libs on the classpath, but not running with Spring AOP. It should be the same result with Spring AOP, though, because pointcut matching is just like in native AspectJ.
Sample annotation:
package de.scrum_master.app;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Retention(RetentionPolicy.RUNTIME)
public #interface TestAnnotation {}
Sample class with entry point:
package de.scrum_master.app;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
public class Application {
#ResponseBody
#TestAnnotation
#RequestMapping(method = RequestMethod.PUT, value = "/path/{variable}")
public String controller_call(#PathVariable String variable) {
return "dummy value";
}
public static void main(String[] args) {
new Application().controller_call("my/path");
}
}
Aspect with sample pointcut/advice:
package de.scrum_master.aspect;
import java.lang.annotation.Annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
#Aspect
public class MyAspect {
#Before("execution(!static * *..Application.*(..))")
public void myAdvice(JoinPoint joinPoint) throws Throwable {
System.out.println(joinPoint);
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Annotation[] annotations = joinPoint.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotations();
for (Annotation annotation : annotations)
System.out.println(annotation);
}
}
Console output:
execution(String de.scrum_master.app.Application.controller_call(String))
#org.springframework.web.bind.annotation.ResponseBody()
#de.scrum_master.app.TestAnnotation()
#org.springframework.web.bind.annotation.RequestMapping(headers=[], name=, value=[/path/{variable}], produces=[], method=[PUT], params=[], consumes=[])
I had the same problem, and the solution was to set ensure that the runtime retention policy is set and the target type is a method.
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface MyAnnotation { }