DynamoDBをローカル環境で使う
docker-composeでローカルにDynamoDBを利用できる環境を作る
あと、dynamodb-adminも利用する
セットアップ
docker-compose.yml
version: '3.8' services: dynamodb-local: container_name: dynamodb-local image: amazon/dynamodb-local:latest user: root ports: - 8000:8000 volumes: - dynamodb-local-data:/data command: -jar DynamoDBLocal.jar -sharedDb -dbPath /data networks: - dynamodb-local-network dynamodb-admin: container_name: dynamodb-admin image: aaronshaf/dynamodb-admin:latest environment: - DYNAMO_ENDPOINT=dynamodb-local:8000 ports: - 8001:8001 depends_on: - dynamodb-local networks: - dynamodb-local-network volumes: dynamodb-local-data: networks: dynamodb-local-network: driver: bridge
実行
docker-compose up -d
コンテナを起動してから下記にアクセスすると管理コンソールを操作できる
http://localhost:8000/shell/
また、下記でdynamodb-adminを操作できる
http://localhost:8001/
管理コンソールから下記のスクリプトを流してテーブルを作成する
var params = { TableName: 'AuthTokenTable', KeySchema: [ { AttributeName: 'authToken', KeyType: 'HASH', //Partition key } ], AttributeDefinitions: [ { AttributeName: 'authToken', AttributeType: 'S' }, { AttributeName: 'userId', AttributeType: 'N' } ], ProvisionedThroughput: { // Only specified if using provisioned mode ReadCapacityUnits: 1, WriteCapacityUnits: 1 }, // https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/SQLtoNoSQL.Indexes.Creating.html#SQLtoNoSQL.Indexes.Creating.DynamoDB GlobalSecondaryIndexes: [ { IndexName: 'UserIdIndex', KeySchema: [ { AttributeName: 'userId', KeyType: 'HASH', } ], Projection: { ProjectionType: 'INCLUDE', NonKeyAttributes: [ 'authToken', ], }, ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } } ], }; dynamodb.createTable(params, function(err, data) { if (err) ppJson(err); // an error occurred else ppJson(data); // successful response });
Redisの環境を作る
docker-composeでRedisを利用できる環境を作る
セットアップ
docker-compose.yml
version: '3.8' services: redis: container_name: redis image: "redis:latest" ports: - "16379:6379" volumes: - "./redis/data:/data"
実行
docker-compose up -d
Thymeleafをホットデプロイして使う
いちいちテンプレートを修正したらサーバを起動し直すのが面倒だったのでホットデプロイできるようにした
セットアップ
build.gradle
- springloaded をdependenciesに追加
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 'com.amazonaws:aws-java-sdk-dynamodb:1.12.9' implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4' implementation 'org.springframework:springloaded:1.2.8.RELEASE' runtimeOnly 'mysql:mysql-connector-java' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
application.yml
- テンプレートのキャッシュを無効化するため、「spring.thymeleaf.cache=false」の設定を追加
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 thymeleaf: cache: false mybatis: configuration: map-underscore-to-camel-case: true logging: level: org: springframework: WARN com: example: demo: domain: mapper: DEBUG
Thymeleafを使う
Thymeleafを使うため最もシンプルな構成で環境を準備する
セットアップ
build.gradle
- spring-boot-starter-thymeleaf をdependenciesに追加
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 'com.amazonaws:aws-java-sdk-dynamodb:1.12.9' implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 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/GreetingController.java
package com.example.demo.web.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; // https://tech.chakapoko.com/java/thymeleaf/getting-started-with-thymeleaf.html // https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%81%AE%E4%BF%9D%E6%8C%81 @Controller public class GreetingController { @GetMapping("/greeting") public String geeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, Model model) { model.addAttribute("name", name); return "greeting"; } }
/src/main/resources/templates/greeting.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Getting Started: Serving Web Content</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <p th:text="'Hello, ' + ${name} + '!'" /> </body> </html>
リクエストからUserAgentを取得する
リクエストからUserAgentを取得して各コントローラで持ちまわる共通的な処理を追加する。 取得部分はFilterではなく、HandlerInterceptorで行った。
FilterとInterceptorの違いは下記を参考にした。
https://meetup-jp.toast.com/698
/src/main/java/com/example/demo/interceptor/RequestHandlerInterceptor.java
package com.example.demo.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.example.demo.web.dto.request.RequestInfo; @Component public class RequestHandlerInterceptor implements HandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RequestHandlerInterceptor.class); @Override // リクエスト処理前に行う処理 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { LOGGER.info("RequestHandlerInterceptor#preHandle() called."); final String userAgent = request.getHeader("User-Agent"); final RequestInfo requestInfo = new RequestInfo(); requestInfo.setUserAgent(userAgent); request.setAttribute(RequestInfo.REQUEST_KEY, requestInfo); return true; } @Override // コントローラー処理後に行う処理 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { LOGGER.info("RequestHandlerInterceptor#postHandle() called."); } @Override // リクエスト処理後に行う処理 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { LOGGER.info("RequestHandlerInterceptor#afterCompletion() called."); } }
/src/main/java/com/example/demo/WebConfiguration.java
- コンフィグレーションに利用するInterceptorを定義する
package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.example.demo.interceptor.RequestHandlerInterceptor; @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired private RequestHandlerInterceptor requestHandlerInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(requestHandlerInterceptor); } }
/src/main/java/com/example/demo/web/dto/request/RequestInfo.java
package com.example.demo.web.dto.request; import lombok.Data; @Data public class RequestInfo { public static final String REQUEST_KEY = "com.example.demo.web.dto.request.RequestInfo.REQUEST_KEY"; private String userAgent; }
/src/main/java/com/example/demo/web/controller/UsersController.java
- コントローラのメソッドの引数にInterceptorでリクエストに設定したオブジェクトを追加
@RequestMapping(method = RequestMethod.GET) public UsersGetListResponse getList( @RequestAttribute(name = RequestInfo.REQUEST_KEY, required = true) RequestInfo requestInfo, @RequestParam(name = "page", defaultValue = "1") String page, @RequestParam(name = "limit", defaultValue = "5") String limit) { LOGGER.info("#RequestInfo : {}", requestInfo); List<UserDto> resultList = usersService.findAll(); return UsersHelper.convertToUsersGetListResponse(resultList); }
業務チェック処理を実装
リクエストパラメータの単項目/関連項目バリデーションとは別に業務的なチェック処理を追加して、エラーメッセージを返却するところまでを追加する
/src/main/java/com/example/demo/domain/mapper/UsersMapper.java
- メールアドレスが重複するレコードが登録されているか検索するメソッドを追加
Users lockByMailAddress(@Param("mailAddress") String mailAddress);
/src/main/resources/com/example/demo/domain/mapper/UsersMapper.xml
- メールアドレスが重複するレコードが登録されているか検索するSQLを追加
<select id="lockByMailAddress" resultType="com.example.demo.domain.model.Users"> SELECT ID, MAIL_ADDRESS, USER_NAME, PASSWORD, LAST_LOGIN_TIME, REGIST_TIME, UPDATE_TIME, DELETED FROM t_users WHERE MAIL_ADDRESS = #{mailAddress} AND DELETED = 0 FOR UPDATE </select>
/src/main/java/com/example/demo/domain/repository/UsersRepository.java
- メールアドレスが重複するレコードが登録されているか検索するメソッドを追加
public Users lockByMailAddress(String mailAddress) { return usersMapper.lockByMailAddress(mailAddress); }
/src/main/java/com/example/demo/service/UsersService.java
- メールアドレスが重複するレコードが存在した場合は処理を中断し、業務例外をthrow
@Transactional(readOnly = false) public UserDto create(UserDto dto) { Users duplicated = usersRepository.lockByMailAddress(dto.getMailAddress()); if (duplicated != null) { throw new BusinessException("errors.users.mailAddress.duplicated", dto.getMailAddress()); } final Users users = Users.builder() .mailAddress(dto.getMailAddress()) .userName(dto.getUserName()) .password(dto.getPassword()) .registTime(DateUtils.getThreadDateTime()) .updateTime(DateUtils.getThreadDateTime()) .deleted(0) .build(); usersRepository.create(users); LOGGER.info("#users : {}", users); return convert(users); }
/src/main/java/com/example/demo/common/utils/SystemException.java
package com.example.demo.common.utils; import lombok.Getter; @Getter public class SystemException extends RuntimeException { /** * SerialVersionUID. */ private static final long serialVersionUID = -8269072520808618210L; private String code; private Exception nested; public SystemException(String code, Exception nested) { super(); this.code = code; this.nested = nested; } public SystemException(String code) { super(); this.code = code; } }
/src/main/java/com/example/demo/common/utils/BusinessException.java
package com.example.demo.common.utils; import java.util.List; import com.google.common.collect.Lists; import lombok.AllArgsConstructor; import lombok.Getter; @Getter public class BusinessException extends RuntimeException { /** * SerialVersionUID. */ private static final long serialVersionUID = -2911618850319099751L; private List<ErrorInfo> errors = Lists.newArrayList(); public BusinessException(String code) { this(code, new Object[0]); } public BusinessException(String code, Object... params) { super(); final ErrorInfo error = new ErrorInfo(code, params); errors.add(error); } @Getter @AllArgsConstructor public static class ErrorInfo { private String code; private Object[] params = new Object[0]; } }
/src/main/java/com/example/demo/web/controller/ExceptionController.java
@ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { final List<ErrorResponse.Error> errors = Lists.newArrayList(); final List<ErrorInfo> errorInfoList = e.getErrors(); for (ErrorInfo errorInfo : errorInfoList) { errors.add(new ErrorResponse.Error(null, messageSource.getMessage(errorInfo.getCode(), errorInfo.getParams(), Locale.getDefault()))); } return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST); } @ExceptionHandler(SystemException.class) public ResponseEntity<ErrorResponse> handleSystemException(SystemException e) { String errMessage = "system error happend."; if (e.getCode() != null) { errMessage = messageSource.getMessage(e.getCode(), new Object[0], Locale.getDefault()); } LOGGER.error(errMessage, e); ErrorResponse.Error error = new ErrorResponse.Error(null, errMessage); return new ResponseEntity<>(new ErrorResponse(error), HttpStatus.INTERNAL_SERVER_ERROR); }
/src/main/resources/ValidationMessages.properties
errors.users.mailAddress.duplicated=そのメールアドレス({0})は既に登録されています
バリデーションと共通例外処理
- バリデーション処理の実装
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
- バリデーションの対象に@Validatedを付与
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
- messagesの定義
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())