Я создал пример пользовательского ограничения для изучения реализации проверки спящего компонента. Само ограничение довольно простое; учитывая конкретную строку и перечисление, валидатор использует регулярное выражение, чтобы проверить, соответствует ли строка определенному шаблону (выбранному через перечисление EmpType). У меня есть следующее:
НомерСотрудника.java
package com.lmig.beanvalidation.domain.customconstraints.employeenumber;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
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.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//implementation of ConstraintValidator interface, i.e. the class that performs custom logic to validate the value
@Constraint(validatedBy = { EmployeeNumberValidator.class})
public @interface EmployeeNumber {
//Validation message for failures
String message() default "{EmployeeNumber.standard}";
//allows "grouping"; can choose under what circumstances this will fire via interfaces
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
EmpType value() default EmpType.STANDARD;
}
EmpType.java
package com.lmig.beanvalidation.domain.customconstraints.employeenumber;
public enum EmpType {
ADMIN,STANDARD;
}
EmployeeNumberValidator.java
package com.lmig.beanvalidation.domain.customconstraints.employeenumber;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
/**
* Backing validator class for EmployeeNumber bean validation annotation
*/
public class EmployeeNumberValidator implements ConstraintValidator<EmployeeNumber, String> {
private static Pattern STANDARD_PATTERN = Pattern.compile("E\\d{6}");
private static Pattern ADMIN_PATTERN = Pattern.compile("A\\d{6}");
protected EmpType empType;
/**
* checks if emp number is not blank, and matches either standard or admin employee number format based on empType
*
* @param empNum String to check for validity
* @param constraintValidatorContext context for validation annotation
*
* @return true if empNum matches empType specific regex, false if empNum is blank or does not match any regex
*/
@Override
public boolean isValid(final String empNum, final ConstraintValidatorContext constraintValidatorContext) {
boolean isValid;
if (StringUtils.isBlank(empNum)) {
isValid = false;
} else if (empType.equals(EmpType.STANDARD)) {
isValid = isStandardNumberValid(empNum);
} else {
isValid = isAdminNumberValid(empNum);
if (!isValid) {
constraintValidatorContext.disableDefaultConstraintViolation();;
constraintValidatorContext
.buildConstraintViolationWithTemplate("{EmployeeNumber.admin}")
.addConstraintViolation();
}
}
return isValid;
}
/**
* Compares empNum against Standard employee number pattern regex
* @param empNum string to compare against standard pattern regex
*
* @return true if match, false otherwise
*/
private boolean isStandardNumberValid(final String empNum) {
final Matcher matcher = STANDARD_PATTERN.matcher(empNum);
return matcher.matches();
}
/**
* Compares empNum against Admin employee number pattern regex
* @param empNum string to compare against admin pattern regex
*
* @return true if match, false otherwise
*/
private boolean isAdminNumberValid(final String empNum) {
final Matcher matcher = ADMIN_PATTERN.matcher(empNum);
return matcher.matches();
}
}
Использование следующих пользовательских сообщений:
ValidationMessages.properties
EmployeeNumber.standard=Standard employee Number must start with E, followed by 6 digits
EmployeeNumber.admin=Admin employee number must start with A, followed by 6 digits
И следующий тестовый класс:
Сотрудникнумбервалидатортест
package com.lmig.beanvalidation.domain.customconstraints.employeenumber;
import static com.lmig.beanvalidation.testutils.ValidatorUtils.getErrorMessagesFromSet;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.junit.Test;
public class EmployeeNumberValidatorTest {
private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private final Validator validator = factory.getValidator();
@Test
public void test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
EmpNumTestClass input = new EmpNumTestClass("E123456", "A123456");
final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validate(input);
final List<String> actualMessages = getErrorMessagesFromSet(errors);
assertThat(actualMessages).isEmpty();
}
@Test
public void test_standardEmployeeNumber_employeeNumberIsInvalid_standardEmployeeNumberValidationFailure() {
final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
"standardEmpNum", "invalid");
final List<String> actualMessages = getErrorMessagesFromSet(errors);
assertThat(actualMessages).hasSize(1).contains("Standard employee Number must start with E, followed by 6 digits");
}
@Test
public void test_adminEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
"adminEmpNum", "A123456");
final List<String> actualMessages = getErrorMessagesFromSet(errors);
assertThat(actualMessages).isEmpty();
}
@Test
public void test_adminEmployeeNumber_employeeNumberIsInvalid_adminEmployeeNumberValidationFailure() {
final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
"adminEmpNum", "invalid");
final List<String> actualMessages = getErrorMessagesFromSet(errors);
assertThat(actualMessages).hasSize(1).contains("Admin employee number must start with A, followed by 6 digits");
}
class EmpNumTestClass {
@EmployeeNumber(value = EmpType.STANDARD)
private String standardEmpNum;
@EmployeeNumber(value = EmpType.ADMIN)
private String adminEmpNum;
EmpNumTestClass(final String standardEmpNum, final String adminEmpNum) {
this.standardEmpNum = standardEmpNum;
this.adminEmpNum= adminEmpNum;
}
}
}
Я получаю исключение нулевого указателя в else if в классе EmployeeNumberValidator при выполнении любого из тестов:
javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:177)
at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:68)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:380)
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:169)
at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidatorTest.test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned(EmployeeNumberValidatorTest.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.NullPointerException
at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:35)
at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:14)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:171)
... 33 more
Я протестировал вариант этого на объекте домена. Макет теста аналогичен приведенному выше, но вместо того, чтобы использовать в тесте внутренний класс, он использует класс из дерева папок src и работает без проблем. Я не могу найти никакой разницы между тестами, кроме использования внутреннего класса (как часть попытки протестировать варианты проверки EmpType, не загрязняя или иным образом не манипулируя классом домена src таким образом, чтобы он не использовался в производственном сценарии) . Я пробовал несколько подходов к этому (видно в тестовом файле; большинство использует validateValue, я тестировал validateProperty и стандартную функцию проверки с использованием заполненного объекта), но получаю ту же ошибку для всех вариантов, которые я использовал.
Упомянутая ошибка подразумевает, что empType не установлен, но я не уверен, почему это так, учитывая, что я передал значение для обеих аннотаций, присутствующих во внутреннем классе теста, и чтобы подтвердить, что я пытался передать EmpType значение в классе src и выполнение тестов для него, и не встретился с нулевым указателем. Есть ли какое-то ограничение на использование внутреннего класса с этими аннотациями, о которых я еще не читал? Я использую финальную версию 6.0.14 hibernate-validator.
Основываясь на нескольких примерах (включая официальные документы hibernate), переменная empType не устанавливается стандартным установщиком или конструктором, значение берется из аннотации и должно быть инициализировано таким образом. До сих пор я не видел примеров, которые бы обеспечивали какой-либо механизм мутатора. Для справки: docs.jboss.org/hibernate/stable/validator/reference/en-US/… . Из любопытства, почему, на ваш взгляд, переменная empType и последующий нулевой указатель не имеют смысла?
@Tom для дальнейшего использования: dolszewski.com/java/custom-parametrized-validation-annotation , dzone.com/articles/…
Это не имеет смысла, потому что вам это не нужно. Вы получите значение как empNum и сможете проверить это без empType.
«значение взято из аннотации и должно быть инициализировано таким образом»: no empType не будет инициализирован, а значение будет присвоено empNum. Удалите empType (или преобразуйте в локальную переменную) и работайте только с empNum (или преобразуйте вручную, преобразуйте строку из empNum в запись перечисления и назначьте ее empType)
Это для примера, план состоит в том, чтобы интегрировать это в проект, над которым я сейчас работаю, поэтому будут случаи, когда эта конкретная структура и логика примеров не будут такими простыми, т.е. перечисление, которое не может быть напрямую соотнесено с проверяемым значением, поэтому в любом случае мне все еще нужно решить эту конкретную проблему для будущих приложений. Я понимаю, откуда вы взялись за этот конкретный пример, но мои варианты использования не всегда будут такими.
Проблема в том, что empType не будет инициализирован автоматически, поэтому, если вам это нужно, вам нужно инициализировать его вручную. Больше этого нет.
Итак, как он инициализируется для других случаев, таких как тот, который я упомянул в вопросе, который действительно работает?
Какой из них вы имеете в виду? Это: dolszewski.com/java/custom-parametrized-validation-annotation ? Если да, посмотрите, где он инициализирует свою переменную. У вас нет метода initialize, чтобы сделать то же самое.




Вы читали Stacktrace? Это приводит к строке, где возникает исключение, и эта строка содержит переменную
empType, которая не инициализирована и в любом случае не имеет смысла.