1. アノテーションによるバリデーション処理の実装
2. 独自メッセージの使用
3. 独自バリデーター(単項目/関連項目)の作成
1. @RestControllerAdviceを使って共通の例外ハンドラーを作成
/build.gradle
- spring-boot-starter-validationを追加
plugins {
id 'org.springframework.boot' version '2.5.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
implementation 'org.slf4j:slf4j-api'
implementation 'ch.qos.logback:logback-core'
implementation 'ch.qos.logback:logback-classic'
implementation 'com.google.guava:guava:30.1.1-jre'
implementation 'org.apache.commons:commons-lang3'
implementation 'commons-beanutils:commons-beanutils:1.9.2'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
/src/main/java/com/example/demo/web/controller/UsersController.java
package com.example.demo.web.controller;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.UsersService;
import com.example.demo.service.dto.UserDto;
import com.example.demo.web.controller.helper.UsersHelper;
import com.example.demo.web.dto.request.UsersCreateRequest;
import com.example.demo.web.dto.request.UsersUpdateRequest;
import com.example.demo.web.dto.response.UsersCreateResponse;
import com.example.demo.web.dto.response.UsersGetListResponse;
import com.example.demo.web.dto.response.UsersGetResponse;
import lombok.RequiredArgsConstructor;
@CrossOrigin
@RestController
@RequestMapping(value = "/users", produces = "application/json;charset=UTF-8")
@RequiredArgsConstructor
public class UsersController {
private static final Logger LOGGER = LoggerFactory.getLogger(UsersController.class);
private final UsersService usersService;
@RequestMapping(method = RequestMethod.GET)
public UsersGetListResponse getList(@RequestParam(name = "page", defaultValue = "1") String page,
@RequestParam(name = "limit", defaultValue = "5") String limit) {
List<UserDto> resultList = usersService.findAll();
return UsersHelper.convertToUsersGetListResponse(resultList);
}
@RequestMapping(method = RequestMethod.GET, value = "{id}")
public UsersGetResponse get(@PathVariable("id") Long id) {
LOGGER.info("#id : {}", id);
UserDto result = usersService.findById(id);
return UsersHelper.convertToUsersGetResponse(result);
}
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public UsersCreateResponse create(@Validated @RequestBody UsersCreateRequest request) {
LOGGER.info("#UsersCreateRequest : {}", request);
UserDto userDto = UsersHelper.convertFromUsersCreateRequest(request);
UserDto result = usersService.create(userDto);
return UsersHelper.convertToUsersCreateResponse(result);
}
@RequestMapping(method = RequestMethod.PUT, value = "{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void update(@PathVariable("id") Long id, @Validated @RequestBody UsersUpdateRequest request) {
LOGGER.info("#id : {}", id);
LOGGER.info("#UsersUpdateRequest : {}", request);
UserDto userDto = UsersHelper.convertFromUsersUpdateRequest(id, request);
usersService.update(userDto);
}
@RequestMapping(method = RequestMethod.DELETE, value = "{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable("id") Long id) {
LOGGER.info("#id : {}", id);
usersService.delete(id);
}
}
/src/main/java/com/example/demo/web/dto/request/UsersCreateRequest.java
package com.example.demo.web.dto.request;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import com.example.demo.common.validation.MaxLength;
import com.example.demo.common.validation.Same;
import lombok.Data;
@Data
@Same(target="confirmPassword", ref="password", message="{errors.users.password.notSame}")
public class UsersCreateRequest {
@NotEmpty
@MaxLength(15)
private String userName;
@NotEmpty
@Email
@MaxLength(256)
private String mailAddress;
@NotEmpty
@Size(min = 8, max = 15)
private String password;
@NotEmpty
@Size(min = 8, max = 15)
private String confirmPassword;
}
/src/main/java/com/example/demo/web/dto/request/UsersUpdateRequest.java
package com.example.demo.web.dto.request;
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import com.example.demo.common.validation.MaxLength;
import com.example.demo.common.validation.Same;
import lombok.Data;
@Data
@Same(target="confirmPassword", ref="password", message="{errors.users.password.notSame}")
public class UsersUpdateRequest {
@MaxLength(15)
private String userName;
@Email
@MaxLength(256)
private String mailAddress;
@Size(min = 8, max = 15)
private String password;
@Size(min = 8, max = 15)
private String confirmPassword;
@Pattern(regexp = "[01]")
private String deleted;
}
/src/main/java/com/example/demo/web/controller/ExceptionController.java
package com.example.demo.web.controller;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import com.example.demo.web.dto.response.ErrorResponse;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@RestControllerAdvice
public class ExceptionController {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class);
private final MessageSource messageSource;
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(HttpServletRequest request, Exception e) {
LOGGER.error(e.getMessage(), e);
final ErrorResponse Errors = new ErrorResponse(new ErrorResponse.Error(null, e.getMessage()));
return new ResponseEntity<>(Errors, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final List<ErrorResponse.Error> errors = Lists.newArrayList();
BindingResult bindingResult = e.getBindingResult();
if (bindingResult != null) {
List<ObjectError> objectErrors = bindingResult.getAllErrors();
if (objectErrors != null) {
for (ObjectError objectError : objectErrors) {
LOGGER.info("{}", objectError);
LOGGER.info("code: {}", objectError.getCode());
if (objectError instanceof FieldError) {
final FieldError fieldError = (FieldError) objectError;
errors.add(new ErrorResponse.Error(fieldError.getField(),
messageSource.getMessage(objectError, Locale.getDefault())));
} else {
errors.add(new ErrorResponse.Error(null, messageSource.getMessage(objectError, Locale.getDefault())));
}
}
}
}
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
final List<ErrorResponse.Error> errors = Lists.newArrayList();
errors.add(
new ErrorResponse.Error(e.getName(), messageSource.getMessage("typeMismatch." + e.getRequiredType().getName(),
new String[] { e.getName(), e.getRequiredType().getName() }, Locale.getDefault())));
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
}
/src/main/java/com/example/demo/web/dto/response/ErrorResponse.java
package com.example.demo.web.dto.response;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.builder.RecursiveToStringStyle;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
private final List<Error> errors;
public ErrorResponse(List<Error> errors) {
this.errors = errors;
}
public ErrorResponse(Error error) {
this.errors = Collections.singletonList(error);
}
@Data
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Error {
private String field;
private String message;
}
public List<Error> getErrors() {
return errors;
}
@Override
public String toString() {
return new ReflectionToStringBuilder(this, new RecursiveToStringStyle()).toString();
}
}
/src/main/java/com/example/demo/common/validation/MaxLength.java
package com.example.demo.common.validation;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import org.apache.commons.lang3.StringUtils;
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Documented
@Repeatable(MaxLength.List.class)
@Constraint(validatedBy = { MaxLength.Validator.class })
public @interface MaxLength {
String message() default "{com.example.demo.common.validation.MaxLength.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int value();
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Documented
@interface List {
MaxLength[] value();
}
class Validator implements ConstraintValidator<MaxLength, String> {
private int maxLength;
public void initialize(MaxLength constraintAnnotation) {
this.maxLength = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value))
return true;
return value.length() <= this.maxLength;
}
}
}
/src/main/java/com/example/demo/common/validation/Same.java
package com.example.demo.common.validation;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Objects;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Repeatable(Same.List.class)
@Constraint(validatedBy = { Same.Validator.class })
public @interface Same {
String message() default "{com.example.demo.common.validation.Same.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String target();
String ref();
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@interface List {
Same[] value();
}
class Validator implements ConstraintValidator<Same, Object> {
private String targetField;
private String refField;
private String message;
public void initialize(Same constraintAnnotation) {
this.targetField = constraintAnnotation.target();
this.refField = constraintAnnotation.ref();
this.message = constraintAnnotation.message();
}
@Override
public boolean isValid(Object rootObject, ConstraintValidatorContext context) {
BeanWrapper beanWrapper = new BeanWrapperImpl(rootObject);
Object targetValue = beanWrapper.getPropertyValue(targetField);
Object refValue = beanWrapper.getPropertyValue(refField);
if (ObjectUtils.isEmpty(targetValue) || ObjectUtils.isEmpty(refValue)) {
return true;
}
if (Objects.equals(targetValue, refValue)) {
return true;
}
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode(targetField).addConstraintViolation();
return false;
}
}
}
/src/main/resources/application.yml
server:
servlet:
context-path: /sample
spring:
messages:
basename: "application-messages,ValidationMessages"
encoding: "UTF-8"
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:33060/sample
username: mysql-app
password: mysql-app
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
org:
springframework: WARN
com:
example:
demo:
domain:
mapper: DEBUG
/src/main/resources/ValidationMessages.properties
# provided validation messages
javax.validation.constraints.NotBlank.message={0}は必須です
javax.validation.constraints.NotEmpty.message={0}は必須です
javax.validation.constraints.NotNull.message={0}は必須です
javax.validation.constraints.Email.message={0}はメールアドレス形式で入力してください
javax.validation.constraints.Pattern.message={0}の形式が不正です
javax.validation.constraints.Min.message={0}は{value}以上の数を入力してください
javax.validation.constraints.Max.message={0}は{value}以下の数を入力してください
javax.validation.constraints.Size.message={0}は{min}桁から{max}桁の範囲で入力してください
# original validation messages
com.example.demo.common.validation.MaxLength.message={0}は{value}桁以内で入力してください
com.example.demo.common.validation.Same.message=値が一致しません
# typemismatch messages
typeMismatch="{0}" is invalid.
typeMismatch.int="{0}" must be an integer.
typeMismatch.double="{0}" must be a double.
typeMismatch.float="{0}" must be a float.
typeMismatch.long="{0}" must be a long.
typeMismatch.short="{0}" must be a short.
typeMismatch.boolean="{0}" must be a boolean.
typeMismatch.java.lang.Integer="{0}" must be an Integer.
typeMismatch.java.lang.Double="{0}" must be a Double.
typeMismatch.java.lang.Float="{0}" must be a Float.
typeMismatch.java.lang.Long="{0}" must be a Long.
typeMismatch.java.lang.Short="{0}" must be a Short.
typeMismatch.java.lang.Boolean="{0}" is not a Boolean.
typeMismatch.java.util.Date="{0}" is not a Date.
typeMismatch.java.lang.Enum="{0}" is not a valid value.
# error messages
errors.users.password.notSame=パスワードが一致しません
/src/main/resources/application-messages.properties
# label
userName=ユーザー名
mailAddress=メールアドレス
password=パスワード
confirmPassword=パスワード(確認用)
はまった点
- エラーメッセージにプロパティ名を埋め込めれなくて超ハマった。。。
messageSource.getMessage(objectError, Locale.getDefault())