https://www.baeldung.com/spring-mvc-custom-validator
https://www.baeldung.com/spring-boot-bean-validation
8자리 이상 11자리 이하의 전화번호 값이 제대로 들어왔는지 확인하는 Validation 을 만들어본다
The New Annotation
new @interface to define our annotation
@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Constraint annotation을 사용하여 필드 유효성 검사 클래스를 정의했다.
message() 는 interface에 표시되는 오류 메세지이고,
추가된 코드들은 Spring 표준을 준수한 코드들이다
Creating a Validator
validator class that enforces the rules of our validation
public class ContactNumberValidator implements
ConstraintValidator<ContactNumberConstraint, String> {
@Override
public void initialize(ContactNumberConstraint contactNumber) {
}
@Override
public boolean isValid(String contactField,
ConstraintValidatorContext cxt) {
return contactField != null && contactField.matches("[0-9]+")
&& (contactField.length() > 8) && (contactField.length() < 14);
}
}
ConstraintValidator 인터페이스를 구현하고, isValid 메소드를 구현한다
isValid 메소드 안에 유효성 검사 규칙을 정의한다
ConstraintValidator는 주어진 개체에 대해 주어진 제약 조건의 유효성을 검사하는 논리를 정의한다.
구현은 다음 제한 사항을 준수해야 한다.
- 객체는 매개변수화되지 않은 유형으로 확인되어야 한다.
- 개체의 일반 매개변수는 제한되지 않은 와일드카드 유형이여야 한다.
Applying Validation Annotation
검증 할 필드에 애노테이션을 붙여준다.
@ContactNumberConstraint
private String phone;
@ContactNumberConstraint. In our controller, we created our mappings and handled any errors:
검증에 성공하면 그대로 넘어가고
그게 아닐 경우에는 검증 실패 (result.hasErrors()) 시킨다
@Controller
public class ValidatedPhoneController {
@GetMapping("/validatePhone")
public String loadFormPage(Model m) {
m.addAttribute("validatedPhone", new ValidatedPhone());
return "phoneHome";
}
@PostMapping("/addValidatePhone")
public String submitForm(@Valid ValidatedPhone validatedPhone,
BindingResult result, Model m) {
if(result.hasErrors()) {
return "phoneHome";
}
m.addAttribute("message", "Successfully saved phone: "
+ validatedPhone.toString());
return "phoneHome";
}
}
Tests
Now let's test our controller to check if it's giving us the appropriate response and view:
@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
this.mockMvc.
perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}
@Test
public void
givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() {
this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone").
accept(MediaType.TEXT_HTML).
param("phoneInput", "123")).
andExpect(model().attributeHasFieldErrorCode(
"validatedPhone","phone","ContactNumberConstraint")).
andExpect(view().name("phoneHome")).
andExpect(status().isOk()).
andDo(print());
}
In the test, we're providing a user with the input of “123,” and as we expected, everything's working and we're seeing the error on the client side.
Custom Class Level Validation
defined at the class level to validate more than one attribute of the class
클래스 수준의 유효성 검사로, 두개 이상의 fields 를 연관하여 검사할 수 있다
Creating the Annotation
The annotation will have two parameters, field and fieldMatch, that represent the names of the fields to compare:
@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {
String message() default "Fields values don't match!";
String field();
String fieldMatch();
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
FieldsValueMatch[] value();
}
}
List sub-interface for defining multiple FieldsValueMatch annotations on a class.
보면 받아오는 값이 배열로 되어있다.
해당 애노테이션이 붙은 값들을 모두 가져와서 검증할 수 있다
Creating the Validator
FieldsValueMatchValidator class that will contain the actual validation logic:
public class FieldsValueMatchValidator
implements ConstraintValidator<FieldsValueMatch, Object> {
private String field;
private String fieldMatch;
public void initialize(FieldsValueMatch constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
public boolean isValid(Object value,
ConstraintValidatorContext context) {
Object fieldValue = new BeanWrapperImpl(value)
.getPropertyValue(field);
Object fieldMatchValue = new BeanWrapperImpl(value)
.getPropertyValue(fieldMatch);
if (fieldValue != null) {
return fieldValue.equals(fieldMatchValue);
} else {
return fieldMatchValue == null;
}
}
}
isValid에서 받아온 값들이 같은지 체크하는 로직을 작성했다.
Applying the Annotation
add two @FieldsValueMatch annotations on the NewUserForm class, one for email values, and one for password values:
@FieldsValueMatch.List({
@FieldsValueMatch(
field = "password",
fieldMatch = "verifyPassword",
message = "Passwords do not match!"
),
@FieldsValueMatch(
field = "email",
fieldMatch = "verifyEmail",
message = "Email addresses do not match!"
)
})
public class NewUserForm {
private String email;
private String verifyEmail;
private String password;
private String verifyPassword;
// standard constructor, getters, setters
}
Spring MVC에서 모델의 유효성을 검사하기 위해 @Valid 로 주석이 달린 NewUserForm 객체를 수신 하고 유효성 검사 오류가 있는지 확인하는 /user POST 매핑으로 컨트롤러를 생성한다.
@Controller
public class NewUserController {
@GetMapping("/user")
public String loadFormPage(Model model) {
model.addAttribute("newUserForm", new NewUserForm());
return "userHome";
}
@PostMapping("/user")
public String submitForm(@Valid NewUserForm newUserForm,
BindingResult result, Model model) {
if (result.hasErrors()) {
return "userHome";
}
model.addAttribute("message", "Valid form");
return "userHome";
}
}
Testing the Annotation
To verify our custom class-level annotation, let's write a JUnit test that sends matching information to the /user endpoint, then verifies that the response contains no errors:
public class ClassValidationMvcTest {
private MockMvc mockMvc;
@Before
public void setup(){
this.mockMvc = MockMvcBuilders
.standaloneSetup(new NewUserController()).build();
}
@Test
public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML).
.param("email", "john@yahoo.com")
.param("verifyEmail", "john@yahoo.com")
.param("password", "pass")
.param("verifyPassword", "pass"))
.andExpect(model().errorCount(0))
.andExpect(status().isOk());
}
}
Then we'll also add a JUnit test that sends non-matching information to the /user endpoint and asserts that the result will contain two errors:
@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML)
.param("email", "john@yahoo.com")
.param("verifyEmail", "john@yahoo.commmm")
.param("password", "pass")
.param("verifyPassword", "passsss"))
.andExpect(model().errorCount(2))
.andExpect(status().isOk());
}
'Web > spring' 카테고리의 다른 글
[Spring] Async Support with Spring MVC and Spring Security (0) | 2023.04.06 |
---|---|
[Spring] A Guide To Spring Redirects (0) | 2023.04.06 |
[Spring Security] Handle Spring Security Exceptions (0) | 2023.04.04 |
[Spring] HandlerAdapters in Spring MVC (0) | 2023.04.04 |
[Spring Test] JUnit 5 Conditional Test Execution with Annotations (0) | 2023.04.03 |