티스토리 뷰
Spring Advanced : Web MVC + Form Validation - Custom message 설정 (spring boot 포함)
Korean Eagle 2020. 4. 24. 02:270. 이 포스트는 Spring : Web MVC + Form Validation - with InitBinder의 연속이다.
1. Spring Validation은 다양한 에러메시지를 발생시킨다.
2. 에러 메시지를 받는 부분은 이전 포스팅에 있듯 Controller 내의 BindingResult라는 객체이다.
3. 아래 코드를 보면 bindingResult를 출력하는 부분이 있는데 이 부분에서 어떤 에러가 발생하는지 알 수 있다.
@GetMapping("/processForm")
public String processForm(@Valid @ModelAttribute Customer customer, BindingResult bindingResult) {
System.out.println("Last Name: |" + customer.getLastName() + "|");
System.out.println("BindingResult: "+ bindingResult.toString());
if (bindingResult.hasErrors()) {
return "customer-form";
}
return "customer-confirmation";
}
4. 위의 코드를 가지고 에러 유형이 Integer값을 받아야 하는데 String값이 들어오는 경우를 가정해 본다.
4-1 오류 로그는 아래와 같고 중요한 부분은
4-1-1 첫줄의 에러가 1개라는 부분(1 errors fields)과
4-1-2 유형이 적합하지 않은 타입(typeMismatch)라는 부분이다.
4-2 1 Field error in object 'customer' on field 'freePass' -> customer객체의 freePass라는 속성에서 문제발생
4-3 rejected value [qwerqwer]; -> 정수가 들어와야 하는데 문자열이 qwerqwer이 들어와 오류가 생겼다.
4-4 codes [typeMismatch.customer.freePass, -> 이 부분이 가장 중요함
4-4-1 typeMismatch라는 게 오류유형, customer라는 게 객체, freePass라는 것이 속성이다.
4-4-2 custom 메시지를 만들 때 이걸 그대로 사용한다.
BindingResult: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'customer' on field 'freePass': rejected value [qwerqwer];
codes [typeMismatch.customer.freePass,typeMismatch.freePass,typeMismatch.java.lang.Integer,
typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [customer.freePass,freePass]; arguments [];
default message [freePass]]; default message [Failed to convert property value of type
'java.lang.String' to required type 'java.lang.Integer' for property 'freePass'; nested
exception is java.lang.NumberFormatException: For input string: "qwerqwer"]
여기서 부터가 작업이다.
5. classpath 상에 messages.properties 파일을 만든다. 확장자가 properties가 되어야 한다.
5-1 MessageSource 인터페이스 구현소스의 setBasenames내에서 확장자가 .properties 파일을 읽어오기 때문이다.
5-1-1 정확히 말하면 AbstractResourceBasedMessageSource의 addaddBasenames 함수다.
5-1-2 6번 항목의 소스를 보면 property속성의 basenames가 이 함수와 매핑되는 xml 코드이다.
5-2 내용은 아래처럼 보여질 메시지의 에러타입 typeMismatch, 검증 객체 customer, 검증 속성 freePass다.
typeMismatch.customer.freePass=Invalid number
5-3 다른 에러를 처리하고 싶으면 그대로 에러를 발생시키고 출력된 에러의 code를 따면 된다.
6. Spring Web configuration xml파일에서 MessageSource 인스턴스를 등록한다.
6-0 정해진 것이어서 아래 소스를 그대로 사용하면 된다.
6-1 name에 들어가는 것이 로딩될 클래스의 호출할 함수 이름, value가 들어갈 값 즉 파일이름이 된다.
6-2 간단하게 말하면 messages.properties를 classpath루트에서 읽으라는 의미다.
6-3 classpath root 설정하는 것은 이전 포스트를 참조한다.
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource" >
<property name="basenames" value="messages" />
</bean>
7. 에러가 발생하면 자동으로 스프링이 사용자가 설정한 Invalid number라는 것을 form:error 테그에 표출한다.
1. 스프링 부트를 사용하는 경우는 보다 간단하다. resources 폴더 아래에 messages.properties 작성만 하면 된다.
1-1 Validation messages라고 표시된 부분에 어떤 형식으로 지정된 properties가 우선순위를 나타내는지를 보여준다.
1-2 2번 항목 캡처의 빨간색 네모를 보면 codes라는 것이 있고 우선순위가 지정되어야 할 코드가 주욱 나열되어 있다.
1-2-1 NotBlank.recipe.description, NotBlank.description, NotBlank.java.lang.String ...
1-2-2 이런 나열이 아래의 우선순위에서 지정한 형식과 일하는데 어떤 것을 고르는가에 따라 우선순위가 달라진다.
1-2-3 properties파일에서는 최우선 순위의 방식을 사용하였다.
1-3 아래의 properties에 보면 '{}'로 지정된 부분이 있는데 이것은 2번 항목 캡처 사진에 노란네모를 보면
1-3-1 default message에 있는 [description]이 입력한 정보, 즉 {0}, 255가 {1}, 3이 {2}에 매핑이 된다.
1-3-2 이 경우 {0} 아래 messages.properties의 첫 째 줄에 정의된 recipe.description의 값이 된다.
# Set names of properties
recipe.description=Description
# Validation Messages
# Order of precedence
# 1 code.objectName.fieldName
# 2 code.fieldName
# 3 code.fieldType (Java data type)
# 4 code
NotBlank.recipe.description=Description Cannot Be Blank
Size.recipe.description={0} must be between {2} and {1} characters long
Max.recipe.cookTime={0} must be less than {1}
URL.recipe.url=Please provide a valid URL
2. vscode 로그화면 캡처
3. 결과화면
4. 참고 타임리프 템플릿
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"
th:href="@{/webjars/bootstrap/4.5.0/css/bootstrap.min.css}">
<title>Recipe Homer</title>
</head>
<body>
<!--/*@thymesVar id="recipe" type="guru.springframework.domain.Recipe"*/-->
<div class="container-fluid" style="margin-top: 20px">
<div class="row">
<div class="col-md-6 offset-md-3">
<form th:object="${recipe}" th:action="@{/recipe/}" method="post">
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger pb-0">
<p>Please Correct Errors Below</p>
</div>
<input type="hidden" th:field="*{id}" />
<div class="card text-white bg-primary">
<div class="card-body p-0">
<div class="card-title">
<p class="m-2 font-weight-bold">Edit Recipe Information</p>
</div>
<div class="card-text bg-white text-dark m-0 p-2">
<div class="row form-group"
th:class="${#fields.hasErrors('description')} ? 'row form-group text-danger' : 'row form-group'">
<label class="col-lg-4 col-xl-3 mx-0 pr-0 col-form-label">Recipe Description:</label>
<div class="col-lg-8 col-xl-9">
<input type="text" class="form-control" th:field="*{description}" th:errorclass="is-invalid" />
<span class="small form-text text-muted invalid-feedback" th:if="${#fields.hasErrors('description')}">
<ul>
<li th:each="err : ${#fields.errors('description')}" th:text="${err}"></li>
</ul>
</span>
</div>
</div>
<div class="row form-group">
<div class="col-form-label col-md-3">
<label>Categories:</label>
</div>
<div class="col-md-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" />
<label class="form-check-label">
Cat 1
</label>
</div>
<div class="form-check" th:remove="all">
<input class="form-check-input" type="checkbox" value="" />
<label class="form-check-label">
Cat 2
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 form-group"
th:class="${#fields.hasErrors('prepTime')} ? 'col-md-4 form-group text-danger' : 'col-md-4 form-group'">
<label class="col-form-label">Prep Time:</label>
<input type="text" class="form-control" th:field="*{prepTime}" th:errorclass="is-invalid" />
<span class="small form-text text-muted invalid-feedback" th:if="${#fields.hasErrors('prepTime')}">
<ul>
<li th:each="err : ${#fields.errors('prepTime')}" th:text="${err}"></li>
</ul>
</span>
</div>
<div class="col-md-4 form-group"
th:class="${#fields.hasErrors('cookTime')} ? 'col-md-4 form-group text-danger' : 'col-md-4 form-group'">
<label class="col-form-label">Cooktime:</label>
<input type="text" class="form-control" th:field="*{cookTime}" th:errorclass="is-invalid" />
<span class="small form-text text-muted invalid-feedback" th:if="${#fields.hasErrors('cookTime')}">
<ul>
<li th:each="err : ${#fields.errors('cookTime')}" th:text="${err}"></li>
</ul>
</span>
</div>
<div class="col-md-4 form-group">
<label class="col-form-label">Difficulty:</label>
<select class="form-control" th:field="*{difficulty}">
<option th:each="difficultyValue : ${T(pe.pilseong.recipe.domain.Difficulty).values()}"
th:value="${difficultyValue.name()}"
th:text="${difficultyValue.name()}">
val
</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-4 form-group"
th:class="${#fields.hasErrors('servings')} ? 'col-md-4 form-group text-danger' : 'col-md-4 form-group'">
<label class="col-form-label">Servings:</label>
<input type="text" class="form-control" th:field="*{servings}" th:errorclass="is-invalid" />
<span class="small form-text text-muted invalid-feedback" th:if="${#fields.hasErrors('servings')}">
<ul>
<li th:each="err : ${#fields.errors('servings')}" th:text="${err}"></li>
</ul>
</span>
</div>
<div class="col-md-4 form-group">
<label class="col-form-label">Source:</label>
<input type="text" class="form-control" th:field="*{source}" />
</div>
<div class="col-md-4 form-group"
th:class="${#fields.hasErrors('url')} ? 'col-md-4 form-group text-danger' : 'col-md-4 form-group'">
<label class="col-form-label">URL:</label>
<input type="text" class="form-control" th:field="*{url}" th:errorclass="is-invalid" />
<span class="small form-text text-muted invalid-feedback" th:if="${#fields.hasErrors('url')}">
<ul>
<li th:each="err : ${#fields.errors('url')}" th:text="${err}"></li>
</ul>
</span>
</div>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="card-title mb-0">
<div class="row m-0">
<div class="col-md-10 p-0">
<p class="m-2 font-weight-bold">Ingredients</p>
</div>
<div class="col-md-2">
<a class="btn btn-success font-weight-bold" href="#" role="button">View</a>
</div>
</div>
</div>
<div class="card-text bg-white text-dark m-0 p-2">
<div class="row">
<div class="col-md-12">
<ul th:if="${not #lists.isEmpty(recipe.ingredients)}">
<li th:remove="all">1 Cup of milk</li>
<li th:remove="all">1 Teaspoon of chocolate</li>
<li th:remove="all">1 Doggy of SAXU</li>
<li th:each="ingredient : ${recipe.ingredients}" th:text="${(ingredient.getAmount() +
' ' + ingredient.uom.getDescription() +
' - ' + ingredient.getDescription())}">1 Teaspoon of Sugar
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card text-white bg-primary">
<div class="card-body p-0">
<div class="card-title">
<p class="m-2 font-weight-bold">Directions</p>
</div>
<div class="card-text bg-white text-dark m-0 p-2">
<div class="row">
<div class="col-md-12 form-group">
<textarea class="form-control" rows="3" th:field="*{directions}"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="card text-white bg-primary">
<div class="card-body p-0">
<div class="card-title">
<p class="m-2 font-weight-bold">Notes</p>
</div>
<div class="card-text bg-white text-dark m-0 p-2">
<div class="row">
<div class="col-md-12 form-group">
<textarea class="form-control" rows="3" th:field="*{note.recipeNote}"></textarea>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"
th:src="@{/webjars/jquery/3.5.1/jquery.min.js}">
</script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
th:src="@{/webjars/popper.js/1.16.0/popper.min.js}">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"
th:src="@{/webjars/bootstrap/4.5.0/js/bootstrap.min.js}">
</script>
</body>
</html>
'Spring > Spring Advanced' 카테고리의 다른 글
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- Rest
- MYSQL
- spring boot
- Spring
- 매핑
- 자바
- 설정하기
- 스프링부트
- WebMvc
- one-to-many
- hibernate
- jsp
- 외부파일
- 하이버네이트
- one-to-one
- Validation
- RestTemplate
- Spring Security
- 스프링
- crud
- Security
- mapping
- form
- 설정
- login
- 상속
- Angular
- XML
- 로그인
- Many-To-Many