Continuing from the last time, we are talking about Bean Validation
. This time we have a Spring element: ghost :.
Validators such as @ Max
and @ Min
prepared by Bean Validation
are prepared, but since value
is defined by long
, these are used as arguments. Only integers can be specified.
It is natural because we verify the numerical value, but maybe we want to change the allowable value depending on the environment. There may be a scene like that. In such a case, it is convenient to be able to refer to the value from the property.
So, I would like to create a @MaxFromProperty
annotation that can also refer to the value set in the property.
This goal ↓
Setting allowed values
age.max=100
Check the numerical value by referring to the property
@MaxFromProperty("age.max")
private long age; //Check if it is 100 or less
You can specify the value directly
@MaxFromProperty("20")
private long age; //Check if it is 20 or less
Please note that the source and link of the completed version is attached at the end of this article, but this is not the optimal solution. So there is a better way! We are looking for opinions from those who say: bow_tone1:
Now, let's create an annotation. That said, the annotation itself has nothing to mention.
Is it enough to set the value to String
so that it can receive the property key?
MaxFromProperty.java
@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {
String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value();
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Documented
@interface List {
MaxFromProperty[] value();
}
}
This is important.
I use PropertyResolver
to resolve property values, but I needPropertySources
as an argument. In addition, property values are managed by appliedPropertySources in PropertySourcesPlaceholderConfigurer
.
So, I will put the code only in the important part
MaxFromPropertyValidator.java
//Generate resolver in constructor
public MaxFromPropertyValidator(PropertySourcesPlaceholderConfigurer configurer) {
//Get a resolver that resolves a property value
resolver = new PropertySourcesPropertyResolver(configurer.getAppliedPropertySources());
}
@Override
public void initialize(MaxFromProperty constraintAnnotation) {
max = getMaxValue(constraintAnnotation.value());
}
private long getMaxValue(String key) {
//Property value resolution
//The second argument is the default value
String value = resolver.getProperty(key, key);
try {
//Convert to number
return Long.parseLong(value);
} catch (NumberFormatException e) {
//Exception handling
}
}
Okay: v :. I'd like to do that ...: sweat:
PropertySourcesPlaceholderConfigurer
Depending on the application, multiple PropertySourcesPlaceholderConfigurer
s may be defined, so the appliedPropertySources of each PropertySourcesPlaceholderConfigurer
must be combined into one PropertySources
.
So, since MutablePropertySources
, which is the default implementation of PropertySources
, manages multiple PropertySource
s in a list, insert the contents of appliedPropertySources of each PropertySourcesPlaceholderConfigurer
into MutablePropertySources
.
Modified version of MaxFromPropertyValidator constructor
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
MutablePropertySources propertySources = new MutablePropertySources();
configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
propertySources.addLast(temp);
}));
this.resolver = new PropertySourcesPropertyResolver(propertySources);
}
That's okay: v :. I'd like to do it ...: severe:
If you actually check the operation with PropertySourcesPlaceholderConfigurer
defined in multiple beans, the expected operation may not be achieved.
Because
PropertySource
overrides ʻequals ()[https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/ core / env / PropertySource.java # L135-L138) and if the names of
PropertySource are the same,
true` is returned.PropertySourcesPlaceholderConfigurer
The system property is ʻenvironmentProperties, and the property (= local property) read from the property file is
localProperties` and the name is fixed.MutablePropertySources
](https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java# When adding PropertySource
to L104-L107), compare the already added PropertySource
with ʻequals ()and if it is
true, delete the already added
PropertySource. ,
PropertySource` is added.In other words, this implementation only adds the contents of the last loaded PropertySourcesPlaceholderConfigurer
to MutablePropertySources
: confounded :.
So ... forcibly give it a unique name and add it to MutablePropertySources
. Unfortunately, PropertySource
didn't have a method to overwrite a name likesetName ()
, so refill CompositePropertySource
with an alias and then add it to MutablePropertySources
.
Constructor modified version
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
MutablePropertySources propertySources = new MutablePropertySources();
configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
//Refill with a uniquely named CompositePropertySource and add it to MutablePropertySources
CompositePropertySource temp = new CompositePropertySource(
c.toString().concat("$").concat(p.getName()));
temp.addPropertySource(p);
propertySources.addLast(temp);
}));
this.resolver = new PropertySourcesPropertyResolver(propertySources);
There is room for improvement here, though I say it myself: innocent :.
By the way, if there are beans of PropertySourcesPlaceholderConfigurer
, configurer1
and configurer2
, and the value of the order
attribute is 1
and 2
respectively and the value of the localOverride
attribute is false
, then MutablePropertySources
To
configurer1
configurer1
configurer2
configurer2
Since they are added in the order of, system properties will be duplicated, but I think it is difficult to improve while maintaining consistency with the settings of ʻorder and
localOverride`.
PropertySourcesPlaceholderConfigurer
in the first placeSo far, we have assumed that PropertySourcesPlaceholderConfigurer
is bean-defined, but I think that some applications may not have bean-defined.
In that case, set ʻEnvironment because ʻEnvironment
is a subinterface of PropertyResolver
.
Constructor modified version
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
if (configurers != null) {
//Abbreviation
} else {
this.resolver = environment;
}
}
MaxValidator
as a reference ...Now that the process of getting the value from the property is complete, all you have to do is compare the value to be verified with the maximum value.
The @ Max
validator class in the original hibernate-validator
supports numbers expressed in Number
and CharSequence
types, so please refer to it.
This is a personal preference, but it is difficult to understand if there are multiple Java files of the validator class, so make MaxFromPropertyValidator
an abstract class and inherit it to the inner classes for Number
and CharSequence
. At that time, by overriding the ʻisValid method with
MaxFromPropertyValidatorand performing only null check, by making the concrete numerical comparison an abstract method, the class for
Number and
CharSequencecan compare the numerical value. You can concentrate on it, and you can set the qualifier of the maximum value obtained by
MaxFromPropertyValidator to
private. Finally, the class specified in
@ Constraint of the annotation class also specifies both for
Numberand
CharSequence`.
Please read the source of ↓ because it is difficult to understand if it is a sentence: stuck_out_tongue_closed_eyes:
MaxFromProperty.java
@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.NumberMaxFromPropertyValidator.class,
MaxFromPropertyValidator.CharSequenceMaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {
String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value();
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Documented
@interface List {
MaxFromProperty[] value();
}
}
MaxFromPropertyValidator.java
public abstract class MaxFromPropertyValidator<T> extends ApplicationObjectSupport
implements ConstraintValidator<MaxFromProperty, T> {
private long max;
private final PropertyResolver resolver;
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
if (configurers != null) {
MutablePropertySources propertySources = new MutablePropertySources();
configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
// named unique name.
// Because MutablePropertySources override propertySources if defined same name.
CompositePropertySource temp = new CompositePropertySource(
c.toString().concat("$").concat(p.getName()));
temp.addPropertySource(p);
propertySources.addLast(temp);
}));
this.resolver = new PropertySourcesPropertyResolver(propertySources);
} else {
this.resolver = environment;
}
}
@Override
public void initialize(MaxFromProperty constraintAnnotation) {
max = getMaxValue(constraintAnnotation.value());
}
@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
// null values are valid
if (value == null) {
return true;
}
return compareToMaxValue(value, max);
}
/**
* compare target value to maximum value
*
* @param value
* target value
* @param max
* maximum value
* @return true if value is less than or equal to max.
*/
protected abstract boolean compareToMaxValue(T value, long max);
/**
* return max value.<br>
* if no value mapped key, convert key to long.
*/
private long getMaxValue(String key) {
String value = resolver.getProperty(key, key);
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"failed to get int value from Property(key:" + key + ", value:" + value + ")");
}
}
/**
* MaxFromPropertyValidator for Number
*/
public static class NumberMaxFromPropertyValidator extends MaxFromPropertyValidator<Number> {
public NumberMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
Environment environment) {
super(configurers, environment);
}
@Override
public boolean compareToMaxValue(Number value, long max) {
// handling of NaN, positive infinity and negative infinity
if (value instanceof Double) {
if ((Double) value == Double.NEGATIVE_INFINITY) {
return true;
} else if (Double.isNaN((Double) value) || (Double) value == Double.POSITIVE_INFINITY) {
return false;
}
} else if (value instanceof Float) {
if ((Float) value == Float.NEGATIVE_INFINITY) {
return true;
} else if (Float.isNaN((Float) value) || (Float) value == Float.POSITIVE_INFINITY) {
return false;
}
}
if (value instanceof BigDecimal) {
return ((BigDecimal) value).compareTo(BigDecimal.valueOf(max)) != 1;
} else if (value instanceof BigInteger) {
return ((BigInteger) value).compareTo(BigInteger.valueOf(max)) != 1;
} else {
long longValue = value.longValue();
return longValue <= max;
}
}
}
/**
* MaxFromPropertyValidator for CharSequernce
*/
public static class CharSequenceMaxFromPropertyValidator extends MaxFromPropertyValidator<CharSequence> {
public CharSequenceMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
Environment environment) {
super(configurers, environment);
}
@Override
public boolean compareToMaxValue(CharSequence value, long max) {
try {
return new BigDecimal(value.toString()).compareTo(BigDecimal.valueOf(max)) != 1;
} catch (NumberFormatException nfe) {
return false;
}
}
}
}
For clarity
--The upper limit can be set by referring to the property file.
--It is also possible to set the upper limit directly
--You can also check fields of type CharSequence
Is divided.
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {
@Autowired
private Validator validator;
@Test
public void maxFromPropertySampleNg() {
MaxFromPropertySample sample = new MaxFromPropertySample(100);
Set<ConstraintViolation<MaxFromPropertySample>> result = validator.validate(sample);
assertThat(result.size(), is(1));
result.stream().forEach(r -> {
assertThat(r.getInvalidValue(), is(100L));
});
}
@Test
public void maxValueSetDirectlyNg() {
MaxValueSetDirectly sample = new MaxValueSetDirectly(100);
Set<ConstraintViolation<MaxValueSetDirectly>> result = validator.validate(sample);
assertThat(result.size(), is(1));
result.stream().forEach(r -> {
assertThat(r.getInvalidValue(), is(100L));
});
}
@Test
public void maxFromPropertyForCharSequenceNg() {
MaxFromPropertyForCharSequence sample = new MaxFromPropertyForCharSequence("100");
Set<ConstraintViolation<MaxFromPropertyForCharSequence>> result = validator.validate(sample);
assertThat(result.size(), is(1));
result.stream().forEach(r -> {
assertThat(r.getInvalidValue(), is("100"));
});
}
//Specify a numerical value from the property file
private static class MaxFromPropertySample {
@MaxFromProperty("max")
private long value;
public MaxFromPropertySample(long value) {
this.value = value;
}
}
//Specify the numerical value directly
private static class MaxValueSetDirectly {
@MaxFromProperty("99")
private long value;
public MaxValueSetDirectly(long value) {
this.value = value;
}
}
//You can also check Char Sequence type numbers
private static class MaxFromPropertyForCharSequence {
@MaxFromProperty("max")
private CharSequence value;
public MaxFromPropertyForCharSequence(CharSequence value) {
this.value = value;
}
}
}
JavaConfig class
TestConfig.java
@Configuration
@PropertySource("sample.properties")
public class TestConfig {
@Bean
public Validator getValidator() {
return new LocalValidatorFactoryBean();
}
}
Place sample.properties
directly under the classpath.
sample.properties
max=99
Just get the value from the property. I tried to make it, but it was hard because I had a lot of thoughts. Since the Spring mechanism is used to resolve property values, annotations created in pure Java will not work, but I think there is demand, so I hope that Hibernate will provide it someday.
By the way, the part that sets PropertyResolver
in the validator class can be used for general purposes, so it can be extended to @ Min
and @ Size
in the same way.
The code created this time is listed in GitHub, but it is not the optimal solution as described in the article, so please be careful when referring to it: stuck_out_tongue_winking_eye :.