I have a Spring Boot app that contains an User class - all fields have standard JSR-303 annotations (#NotNull, #Size, etc.) and validation works fine.
However when I add a custom validation to User, I can't get a dependency injected into a custom validator:
#Component
public class UniqueUsernameValidator implements
ConstraintValidator<UniqueUsername, String> {
#Autowired
private UserRepository userRepository;
#Override
public boolean isValid(String username, ConstraintValidatorContext context) {
// implements logic
}
#UniqueUsername annotation is declared as:
#Documented
#Retention(RUNTIME)
#Target({FIELD, ANNOTATION_TYPE, PARAMETER})
#Constraint(validatedBy = UniqueUsernameValidator.class)
#interface UniqueUsername {
String message() default "{com.domain.user.nonUniqueUsername}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
The annotated field:
#NotBlank
#Size(min = 2, max = 30)
#UniqueUsername
private String username;
And the validator usage:
#Service
public final class UserService {
private final UserRepository userRepository;
private final Validator validator;
public UserService(UserRepository userRepository, Validator validator)
{
this.userRepository = userRepository;
this.validator = validator;
}
public void createUser(User user) {
Set<ConstraintViolation<User>> validate = validator.validate(user);
// logic...
}
}
The problem is that UserRepository is not being autowired in UniqueUsernameValidator. Field is always null.
I am using a LocalValidatorFactoryBean.
Does anyone have any idea why autowiring's not working?
#Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
#PostMapping("/user/new")
public String createUser(#ModelAttribute("newUser") User newUser, BindingResult bindingResult,
Model model) {
userService.createUser(newUser);
// omitted
}
You need to add #Valid annotation in front of entity class in the public String createUser(#ModelAttribute("newUser") User newUser) in front of User.
#RequestBody #Valid User user
The UserRepository implementation needs an Annotation like "#Repository" or "#Component" or "#Service". Your UserService gets the repository instance via constructor. Maybe there was a "new UserRepositoryDao()" call used. And in your validator you are trying to autowire. I guess it's either not annotated as service OR not loaded in the spring context path or as a spring bean in your spring.xml
I have dealt with the same problem a few months ago. Instead of autowiring repository, pass the service which already uses the very same repository through the annotation.
Declare the annotation to accept the field required to be unique and a service performing the validation.
#Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueUsernameValidator.class)
#Documented
public #interface UniqueUsername {
String message() default "{com.domain.user.nonUniqueUsername}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends UniqueUsernameValidation> service(); // Validating service
String fieldName(); // Unique field
}
Use it on the POJO like:
#NotBlank
#Size(min = 2, max = 30)
#UniqueUsername(service = UserService.class, fieldName = "username")
private String username;
Notice the service passed into annotation (Class<? extends UniqueUsernameValidation> service()) must implement UniqueUsernameValidation interface.
public interface UniqueUsernameValidation {
public boolean isUnique(Object value, String fieldName) throws Exception;
}
Now make the passed UserService implement the interface above and override it's only method:
#Override
public boolean isUnique(Object value, String fieldName) throws Exception {
if (!fieldName.equals("username")) {
throw new Exception("Field name not supported");
}
String username = value.toString();
// Here is the logic: Use 'username' to find another username in Repository
}
Don't forget to UniqueUsernameValidator which processes the annotation:
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, Object>
{
#Autowired
private ApplicationContext applicationContext;
private UniqueUsernameValidation service;
private String fieldName;
#Override
public void initialize(UniqueUsername unique) {
Class<? extends UniqueUsernameValidation> clazz = unique.service();
this.fieldName = unique.fieldName();
try {
this.service = this.applicationContext.getBean(clazz);
} catch(Exception ex) {
// Cant't initialize service which extends UniqueUsernameValidator
}
}
#Override
public boolean isValid(Object o, ConstraintValidatorContext context) {
if (this.service !=null) {
// here you check whether the username is unique using UserRepository
return this.service.isUnique(o, this.fieldName))
}
return false;
}
}
Related
I have a custom validator that validates data against DB using repository:
#Constraint(validatedBy = DataValidator.class)
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidator {
String message() default "some message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
#Component
public class DataValidator implements ConstraintValidator<CustomValidator, String> {
#Autowired
private DataRepository repository;
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
var data = repository.findDataByValue(value);
//validation logic with result in 'isValid' variable
return isValid;
}
}
I have entity with a field that is annotated with DataValidator:
#Entity
#Table(name = "custom_data")
public class Data {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#DataValidator
#NotBlank(message = "Value is mandatory")
#Column
private String value;
Spring Boot dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
When I call repository.save(data) from the rest controller, my validator is called, but its repository field is null.
What configuration did I miss that DataRepository bean was injected to RestController correctly, but wasn't injected into DataValidator?
Try it this way.
#Configuration
public class DataValidator implements ConstraintValidator<CustomValidator, String> {
private static final DataValidator holder = new DataValidator();
#Bean
public static DataValidator bean(DataRepository repository) {
holder.repository = repository;
return holder;
}
private DataRepository repository;
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
var data = holder.repository.findDataByValue(value);
//validation logic with result in 'isValid' variable
return isValid;
}
}
I found a solution that will autowire the bean, but you need to call a validator manually.
First of all, add the following to application.properties to disable automatic validation trigger on data persistence:
spring.jpa.properties.javax.persistence.validation.mode=none
Create #Configuration class and describe Validator bean, configure validator factory for it:
#Bean
public Validator validator(AutowireCapableBeanFactory beanFactory) {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()
.constraintValidatorFactory(new SpringConstraintValidatorFactory(beanFactory))
.buildValidatorFactory();
return validatorFactory.getValidator();
}
Add validator to the class where you want to use it and and call its validate method directly:
#RestController
public class DataController {
#Autowired
private DataRepository repository;
#Autowired
private Validator validator;
#PostMapping("/doSomething")
public Data doSomething(#RequestBody Data data) {
var validationResult = validator.validate(data);
//validation result processing
return repository.save(data);
}
}
or if you use validator within the REST endpoint as in this example, usage of the #Valid annotation is more correct, to my mind. Then you don't need to declare Validator bean:
#RestController
public class DataController {
#Autowired
private DataRepository repository;
#PostMapping("/doSomething")
public Data doSomething(#Valid #RequestBody Data data) {
return repository.save(data);
}
}
My question is related to bean validation. Apparently SpringBoot comes with two different validation mechanisms, one JSR-303 compilant (in javax.validation package) and the other provided by Spring framework (in org.springframework.validation package).
While it is easy to enforce JSR-303 validation via #Validated and #Valid annotations, I couldn't find the proper way for Spring's ones.
Take the following User bean.
#Entity
public class User {
#Column
#Id
private String id;
#Column(unique = true)
#Username
private String username;
// [...]
}
Where #Username constraint is defined as follows. Basically, it's just a composition of #Pattern and #Size costraints.
#Constraint(validatedBy = {})
#Documented
#Pattern(regexp = "[A-Za-z0-9_]+")
#Retention(RUNTIME)
#Size(max = 24, min = 3)
#Target(FIELD)
public #interface Username {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
User beans are stored in a repository named UserRepository, defined as follows.
#Repository
public interface UserRepository extends CrudRepository<User, String>, JpaSpecificationExecutor<User> {
}
To access the repository I wrote a service, shown below.
#Transactional
#Service
public class UserService {
#Autowired
private UserRepository userRepository;
#Validated
public void create(#Valid User user) {
userRepository.save(user);
}
}
Until now, things are pretty neat and everything works. With just a couple of annotations I've achieved everything.
Now, I have this other validator (not JSR-303).
// [...]
import org.springframework.validation.Validator;
#Component
public class UniqueUsernameValidator implements Validator {
#Autowired
private UserRepository userRepository;
#Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
User user = (User) target;
if (userRepository.count(where(usernameIsEqualTo(user.getUsername()))) > 0) {
errors.rejectValue("username", "username.exists");
}
}
}
Ideally, I would like to have it enforced via the same #Validated and #Valid annotations but until now I've been unlucky. I guess it is definitely possible given that the documentation of the class org.springframework.validation.Validator says
This interface is totally divorced from any infrastructure or context; that is to say it is not coupled to validating only objects in the web tier, the data-access tier, or the whatever-tier. As such it is amenable to being used in any layer of an application, and supports the encapsulation of validation logic as a first-class citizen in its own right.
#Valid is a JSR-303 bean validation annotation, and it won't call your UniqueUsernameValidator Spring Validator.
If you want to do that in your service, you need to invoke it manually:
public void create(#Valid User user) {
Errors errors = new BeanPropertyBindingResult(user, "user");
// Validator component has been previously injected into the service.
uniqueUsernameValidator.validate(user, errors);
if (errors.hasErrors()) {
throw new RuntimeException(errors.toString());
}
userRepository.save(user);
}
I'm using Spring version 4.1.6.RELEASE. I have the following custom ConstraintValidator.
public class CountryCodeValidator implements ConstraintValidator<CountryCode, String> {
private List<String> allowedCountries;
#Autowired
public void setAllowedCountries(#Value("${allowed.countries}")String countries) {
allowedCountries = Arrays.asList(countries.split(","));
}
#Override
public void initialize(CountryCode constraintAnnotation) { }
#Override
public boolean isValid(String countryCode, ConstraintValidatorContext ctx) {
return null != countryCode && allowedCountries.contains(countryCode.toUpperCase());
}
}
and I have the following annotation
#Documented
#Constraint(validatedBy = CountryCodeValidator.class)
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface CountryCode {
String message() default "{CountryCode}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
I'm using this in a request object like the following
#CountryCode(message = MessageKeys.COUNTRY_CODE_INVALID)
private String countryCode;
When I run the application, everything is working as expected. However in the unit test for the controller it is failing.
My unit test code is like the following
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = "classpath:/test-context.xml")
public class ControllerTest {
#InjectMocks
private Controller controller;
#Before
public void init() {
MockitoAnnotations.initMocks(this);
}
#Test
public void testEndpoint() {
Request request = new Request();
RestAssuredMockMvc.given().standaloneSetup(controller).contentType(MediaType.APPLICATION_JSON_VALUE).body(request)
.post("/endpoint").then().statusCode(HttpStatus.OK.value());
}
}
When I run the code I could not get #Value("${allowed.countries}"), therefore I'm receiving a null pointer exception in the isValid method.
What am I missing?
Thanks in advance
I know how to validate #PathVariable from https://stackoverflow.com/a/35404423/4800811
and it worked as expected with standard annotations but not with the customized one using a Repository bean. Maybe the bean is not initialized and I end up with NullPointerException when accessing the end point has #PathVariable validated. So how to get that work?
My Controller:
#RestController
#Validated
public class CustomerGroupController {
#PutMapping(value = "/deactive/{id}")
public HttpEntity<UpdateResult> deactive(#PathVariable #CustomerGroupEmpty String id) {
}
}
My custom validator:
public class CustomerGroupEmptyValidator implements ConstraintValidator<CustomerGroupEmpty, String>{
#Autowired
private CustomerRepository repository;
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// NullPointerException here (repository == null)
if (value!=null && !repository.existsByCustomerGroup(value)) {
return false;
}
return true;
}
}
My Custom Annotation:
#Documented
#Constraint(validatedBy = CustomerGroupEmptyValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomerGroupEmpty {
String message() default "The customer group is not empty.";
Class<?>[] groups() default {};
Class<? extends Payload> [] payload() default {};
}
code in this post is correct, only mistake is that validator need to override initialize method as well. Probably user123 incorrect configure repository bean, the simply way to check this is define it manually in configuration class
I'm trying to add some extra validation logic on my REST beans using annotations. This is just an example, but the point is that the annotation is to be used on multiple REST resource objects / DTO's.
I was hoping for a solution like this:
public class Entity {
#NotNull // JSR-303
private String name;
#Phone // Custom phonenumber that has to exist in a database
private String phoneNumber;
}
#Component
public class PhoneNumberValidator implements Validator { // Spring Validator
#Autowired
private PhoneRepository repository;
public boolean supports(Class<?> clazz) {
return true;
}
public void validate(Object target, Errors errors) {
Phone annotation = // find fields with annotations by iterating over target.getClass().getFields().getAnnotation
Object fieldValue = // how do i do this? I can easily get the annotation, but now I wish to do a call to repository checking if the field value exists.
}
}
Did you try JSR 303 bean validator implementations like hibernate validator
e.g. is available here http://www.codejava.net/frameworks/spring/spring-mvc-form-validation-example-with-bean-validation-api
Maven Module A:
public interface RestValidator<A extends Annotation, T> extends ConstraintValidator<A, T>
public interface PhoneValidator extends RestValidator<PhoneNumber, String>
#Target(FIELD)
#Retention(RUNTIME)
#Constraint(validatedBy = PhoneValidator.class) // This usually doesnt work since its a interface
public #interface PhoneNumber {
// JSR-303 required fields (payload, message, group)
}
public class Person {
#PhoneNumber
private String phoneNumber;
}
Maven Module B:
#Bean
LocalValidatorFactoryBean configurationPropertiesValidator(ApplicationContext context, AutowireCapableBeanFactory factory) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setConstraintValidatorFactory(factory(context, factory));
return factoryBean;
}
private ConstraintValidatorFactory factory(final ApplicationContext context, final AutowireCapableBeanFactory factory) {
return new ConstraintValidatorFactory() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
if (RestValidator.class.isAssignableFrom(key)) {
return context.getBean(key);
} else {
return factory.createBean(key);
}
}
#Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
if (!(instance instanceof RestValidator<?, ?>)) {
factory.destroyBean(instance);
}
}
};
}
#Bean
WebMvcConfigurerAdapter webMvcConfigurerAdapter(final LocalValidatorFactoryBean validatorFactoryBean) {
return new WebMvcConfigurerAdapter() { // Adds the validator to MVC
#Override
public Validator getValidator() {
return validatorFactoryBean;
}
};
}
Then I have a #Component implementation of PhoneValidator that has a Scope = Prototype.
I hate this solution, and I think Spring SHOULD look up on Interface implementations by default, but I'm sure some people that are a lot smarter than me made the decision not to.