Spring Boot : Custom Validation in Request Params

六眼飞鱼酱① 提交于 2020-07-04 14:00:10

问题


I want to validate one of the request parameters in my controller . The request parameter should be from one of the list of given values , if not , an error should be thrown . In the below code , I want the request param orderBy to be from the list of values present in @ValuesAllowed.

@RestController
@RequestMapping("/api/opportunity")
@Api(value = "Opportunity APIs")
@ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
        "ApplicationsApprovedCount" })
public class OpportunityController {

@GetMapping("/vendors/list")
    @ApiOperation(value = "Get all vendors")

    public ResultWrapperDTO getVendorpage(@RequestParam(required = false) String term,
            @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
            @RequestParam(required = false) String orderBy, @RequestParam(required = false) String sortDir) {

I have written a custom bean validator but somehow this is not working . Even if am passing any random values for the query param , its not validating and throwing an error.

@Repeatable(ValuesAllowedMultiple.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValuesAllowedValidator.class})
public @interface ValuesAllowed {

    String message() default "Field value should be from list of ";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String propName();
    String[] values();
}
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, Object> {

    private String propName;
    private String message;
    private String[] values;

    @Override
    public void initialize(ValuesAllowed requiredIfChecked) {
        propName = requiredIfChecked.propName();
        message = requiredIfChecked.message();
        values = requiredIfChecked.values();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {
        Boolean valid = true;
        try {
            Object checkedValue = BeanUtils.getProperty(object, propName);

            if (checkedValue != null) {
                valid = Arrays.asList(values).contains(checkedValue.toString().toLowerCase());
            } 

            if (!valid) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(message.concat(Arrays.toString(values)))
                        .addPropertyNode(propName).addConstraintViolation();
            }
        } catch (IllegalAccessException e) {
            log.error("Accessor method is not available for class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        } catch (NoSuchMethodException e) {
            log.error("Field or method is not present on class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        } catch (InvocationTargetException e) {
            log.error("An exception occurred while accessing class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        }
        return valid;
    }
}

回答1:


Case 1: If the annotation ValuesAllowed is not triggered at all, it could be because of not annotating the controller with @Validated.

    @Validated
    @ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
                        "ApplicationsApprovedCount" })
    public class OpportunityController {
    @GetMapping("/vendors/list")
    public String getVendorpage(@RequestParam(required = false) String term,..{
    }

Case 2: If it is triggered and throwing an error, it could be because of the BeanUtils.getProperty not resolving the properties and throwing exceptions.

If the above solutions do not work, you can try moving the annotation to the method level and update the Validator to use the list of valid values for the OrderBy parameter. This worked for me. Below is the sample code.


    @RestController
    @RequestMapping("/api/opportunity")
    @Validated
    public class OpportunityController {
        @GetMapping("/vendors/list")
        public String getVendorpage(@RequestParam(required = false) String term,
                @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
                @ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
                        "ApplicationsApprovedCount" }) @RequestParam(required = false) String orderBy, @RequestParam(required = false) String sortDir) {
            return "success";
        }

    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = { ValuesAllowed.Validator.class })
    public @interface ValuesAllowed {

        String message() default "Field value should be from list of ";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};

        String propName();

        String[] values();

        class Validator implements ConstraintValidator<ValuesAllowed, String> {
            private String propName;
            private String message;
            private List<String> allowable;

            @Override
            public void initialize(ValuesAllowed requiredIfChecked) {
                this.propName = requiredIfChecked.propName();
                this.message = requiredIfChecked.message();
                this.allowable = Arrays.asList(requiredIfChecked.values());
            }

            public boolean isValid(String value, ConstraintValidatorContext context) {
                Boolean valid = value == null || this.allowable.contains(value);

                if (!valid) {
                    context.disableDefaultConstraintViolation();
                    context.buildConstraintViolationWithTemplate(message.concat(this.allowable.toString()))
                            .addPropertyNode(this.propName).addConstraintViolation();
                }
                return valid;
            }
        }
    }




回答2:


You would have to change few things for this validation to work.

Controller should be annotated with @Validated and @ValuesAllowed should annotate the target parameter in method.

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("/api/opportunity")
public class OpportunityController {

    @GetMapping("/vendors/list")
    public String getVendorpage(
            @RequestParam(required = false)
            @ValuesAllowed(values = {
                    "OpportunityCount",
                    "OpportunityPublishedCount",
                    "ApplicationCount",
                    "ApplicationsApprovedCount"
            }) String orderBy,
            @RequestParam(required = false) String term,
            @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
            @RequestParam(required = false) String sortDir) {
        return "OK";
    }
}

@ValuesAllowed should target ElementType.PARAMETER and in this case you no longer need propName property because Spring will validate the desired param.

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValuesAllowedValidator.class})
public @interface ValuesAllowed {

    String message() default "Field value should be from list of ";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] values();
}

Validator:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;

public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, String> {

    private List<String> expectedValues;
    private String returnMessage;

    @Override
    public void initialize(ValuesAllowed requiredIfChecked) {
        expectedValues = Arrays.asList(requiredIfChecked.values());
        returnMessage = requiredIfChecked.message().concat(expectedValues.toString());
    }

    @Override
    public boolean isValid(String testValue, ConstraintValidatorContext context) {
        boolean valid = expectedValues.contains(testValue);

        if (!valid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(returnMessage)
                    .addConstraintViolation();
        }
        return valid;
    }
}

But the code above returns HTTP 500 and pollutes logs with ugly stacktrace. To avoid it, you can put such @ExceptionHandler method in controller body (so it will be scoped only to this controller) and you gain control over HTTP status:

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
String handleConstraintViolationException(ConstraintViolationException e) {
    return "Validation error: " + e.getMessage();
}

... or you can put this method to the separate @ControllerAdvice class and have even more control over this validation like using it across all the controllers or only desired ones.



来源:https://stackoverflow.com/questions/59422883/spring-boot-custom-validation-in-request-params

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!