Spring Boot: repository does not autowire in the custom validator - java

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);
}
}

Related

Setting key specific TTL with #TimeToLive for Redis Spring Caching triggers no invalidation

I have use case where single entries need to removed from the cache at a specific time. The TTL needs to be set on a key and not on a cache level
Following this spring redis documentation I tried to implement key specific TTL but it does not work. There is no event happing, I used a listener to check that and there is only an event happing when the cache ttl runs out.
The cached object has a field annotated with #TimeToLive from
org.springframework.data.redis.core.TimeToLive looking at the documentation this should trigger an expire event after the time has run out
Cached object
#Data
#NoArgsConstructor
#AllArgsConstructor
public class BrandResponse {
#TimeToLive
private Long ttl;
#NotBlank
private String id;
}
Used dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
Enable Key Space Events
#SpringBootApplication
#ServletComponentScan
#EnableAsync
#EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class KikaRestApiApplication {
public static void main(String[] args) {
SpringApplication.run(KikaRestApiApplication.class, args);
}
}
The default TTL for the cache is 5 minutes .entryTtl(Duration.ofMinutes(5))
Cache setup
#Configuration
#EnableCaching
public class RedisCachingConfiguration {
private final KikaApiProperties kikaApiProperties;
#Value("${spring.redis.host}")
private String host;
#Value("${spring.redis.port}")
private Integer port;
public RedisCachingConfiguration(KikaApiProperties kikaApiProperties) {
this.kikaApiProperties = kikaApiProperties;
}
#Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.disableCachingNullValues()
.serializeValuesWith(
SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
#Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
return new JedisConnectionFactory(configuration);
}
#Bean
public RedisTemplate<String, Idmap> redisTemplate() {
RedisTemplate<String, Idmap> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setEnableTransactionSupport(true);
return redisTemplate;
}
}
Is there something I am missing of does #TimeToLive not work together with spring-redis caching.
as per documentation
TimeToLive marks a single numeric property on aggregate root to be
used for setting expirations in Redis. The annotated property
supersedes any other timeout configuration.
RedisHash marks Objects as aggregate roots to be stored in a Redis
hash.
Default ttl unit is second.
#TimeToLive(unit = TimeUnit.SECONDS)
you can use other values like MINUTES.
Use #RedisHash on BrandResponse.
All the best
Below is my working code
#RedisHash
#Data
#NoArgsConstructor
#AllArgsConstructor
public class BrandResponse {
#TimeToLive(unit = TimeUnit.SECONDS )
private Long ttl;
#NotNull
#Id
private String id;
}
#Repository
public interface BrandRepository extends JpaRepository<BrandResponse, String> {
}
public interface CacheService {
void add(BrandResponse response);
boolean exists(String id);
}
#Service
public class RedisCacheServiceImpl implements CacheService {
#Autowired
private BrandRepository brandRepository;
#Override
public void add(BrandResponse response){
this.brandRepository.save(response);
}
#Override
public boolean exists(String id){
return !this.brandRepository.findById(id).isEmpty();
}
}
#Configuration
#EnableCaching
public class RedisCachingConfiguration {
private String host ="192.168.1.59";
private Integer port = 6379;
#Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
#Bean
JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConFactory
= new JedisConnectionFactory();
jedisConFactory.setHostName(host);
jedisConFactory.setPort(port);
return jedisConFactory;
}
#Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
}
I used two data sources one for Redis another for db
#SpringBootApplication
#EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP,
basePackages = {"com.c4c.authn.core.repository.redis"})
#EnableJpaRepositories(basePackages = {"com.c4c.authn.core.repository.db"})
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
Unit test
public class RedisCacheServiceImplTest extends BaseServiceTest {
#Autowired
private CacheService cacheService;
#Test
public void test_add_ok() throws InterruptedException {
this.cacheService.add(new BrandResponse(5l, "ID1"));
assertTrue(this.cacheService.exists("ID1"));
Thread.sleep(6000);
assertFalse(this.cacheService.exists("ID1"));
}
}

Dependency not autowired in JSR-303 Custom Validator

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;
}
}

Javax validation constraints not recognized by hibernate [duplicate]

I am trying to add #NotNull constraint into my Person object but I still can #POST a new Person with a null email. I am using Spring boot rest with MongoDB.
Entity class:
import javax.validation.constraints.NotNull;
public class Person {
#Id
private String id;
private String username;
private String password;
#NotNull // <-- Not working
private String email;
// getters & setters
}
Repository class:
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {
}
Application class:
#SpringBootApplication
public class TalentPoolApplication {
public static void main(String[] args) {
SpringApplication.run(TalentPoolApplication.class, args);
}
}
pom.xml
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
...
When I #POST a new object via Postman like:
{
"username": "deadpool",
"email": null
}
I still get STATUS 201 created with this payload:
{
"username": "deadpool",
"password": null,
"email": null
....
....
}
I had the same problem, but just enabling validation didn't work for me, this did work with both JPA and MongoDb to save anyone else spending ages on this. Not only does this get validation working but I get a nice restful 400 error rather than the default 500.
Had to add this to my build.gradle dependencies
compile('org.hibernate:hibernate-validator:4.2.0.Final')
and this config class
#Configuration
public class CustomRepositoryRestConfigurerAdapter extends RepositoryRestConfigurerAdapter {
#Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
validatingListener.addValidator("afterCreate", validator());
validatingListener.addValidator("beforeCreate", validator());
validatingListener.addValidator("afterSave", validator());
validatingListener.addValidator("beforeSave", validator());
}
}
i found it better to make my own version of #NotNull annotation which validates empty string as well.
#Documented
#Constraint(validatedBy = NotEmptyValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface NotEmpty {
String message() default "{validator.notEmpty}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, Object> {
#Override
public void initialize(NotEmpty notEmpty) { }
#Override
public boolean isValid(Object obj, ConstraintValidatorContext cxt) {
return obj != null && !obj.toString().trim().equals("");
}
}
You can either use the following code for validating
#Configuration
#Import(value = MongoAutoConfiguration.class)
public class DatabaseConfiguration extends AbstractMongoConfiguration
{
#Resource
private Mongo mongo;
#Resource
private MongoProperties mongoProperties;
#Bean
public ValidatingMongoEventListener validatingMongoEventListener() {
return new ValidatingMongoEventListener(validator());
}
#Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Override
public Mongo mongo() throws Exception {
return mongo;
}
}
Normally, the #RestRepository will resolve into a controller than handles validation by itself, except if you Override the default behavior or it by including some #HandleBeforeSave, #HandleBeforeCreate, ... into your code.
A solution is to remove the #HandleBeforeSave, #HandleBeforeCreate, ...
and then spring will handle the validation again.
Or if you want to keep them, you can provide a handler for any object validation like this:
#Component
#RepositoryEventHandler
public class EntityRepositoryEventHandler {
#Autowired
private Validator validator;
#HandleBeforeSave
#HandleBeforeCreate
public void validate(Object o) {
Set<ConstraintViolation<Object>> violations = this.validator.validate(o);
if (!violations.isEmpty()) {
ConstraintViolation<Object> violation = violations.iterator().next();
// do whatever your want here as you got a constraint violation !
throw new RuntimeException();
}
}
}

Full validation test in Spring Boot, injection failing

Hello everyone I wanted to tested the full validation of a Request in my Spring Boot application I mean no testing one validator at a time but all of them on the target object)
First I have my object :
public class UserCreationRequest {
#JsonProperty("profileId")
#NotNull
#ValidProfile
private Integer profileId;
}
Then my Validator (#ValidProfile):
#Component
public class ProfileValidator implements ConstraintValidator<ValidProfile, Integer> {
#Autowired
private IProfileService profileService;
#Autowired
private IUserRestService userRestService;
#Override
public void initialize(ValidProfile constraintAnnotation) {
}
#Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
RestUser restUser = userRestService.getRestUser();
ProfileEntity profileEntity = profileService.getProfile(value, restUser.getAccountId());
return profileEntity != null;
}
}
Now I write my unit test :
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {ValidationTestConfiguration.class})
public class UserCreationRequestValidationTest {
private static LocalValidatorFactoryBean localValidatorFactory;
#Autowired
private IUserService userService;
#Autowired
private IProfileService profileService;
#Autowired
private IUserRestService restService;
#BeforeClass
public static void createValidator() {
localValidatorFactory = new LocalValidatorFactoryBean();
localValidatorFactory.setProviderClass(HibernateValidator.class);
localValidatorFactory.afterPropertiesSet();
}
#AfterClass
public static void close() {
localValidatorFactory.close();
}
#Test
public void validateUserCreationRequestStringfields() {
UserCreationRequest userCreationRequest = new UserCreationRequest();
/* Here fill test object*/
when(userService.getUser(any(Integer.class), any(Integer.class))).thenReturn(new UserEntity());
when(profileService.getProfile(any(Integer.class), any(Integer.class))).thenReturn(new ProfileEntity());
when(restService.getRestUser()).thenReturn(new RestUser());
Set<ConstraintViolation<UserCreationRequest>> violations
= localValidatorFactory.validate(userCreationRequest);
assertEquals(violations.size(), 8);
}
}
and my TestConfiguration is like that :
#Configuration
public class ValidationTestConfiguration {
#Bean
#Primary
public IProfileService profileService() {
return Mockito.mock(IProfileService.class);
}
#Bean
#Primary
public IUserRestService userRestService() { return Mockito.mock(IUserRestService.class); }
}
On execution I can see that in the test itself the injection works :
restService is mapped to "Mock for IUserRestService"
But in my validator it is not injected, userRestService is null.
Same thing for ProfileService
I tried several things seen here, nothing works (code is running, only test conf is failing)
This is because you do not produce the Validator bean so it can be injected.
As you manually instantiate the LocalValidatorFactoryBean, it cannot access to the spring DI defined for this test.
You should produce instead a bean for the Validator, or even reference an existing spring configuration to do so.

Spring REST validation on custom annotation

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.

Categories

Resources