end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

SpringBoot for java における validation練習

@Required(msg = @Msg(key = "errors.required"),
                     arg0 = @Arg(key = "LOGIN ID", resource = false))
@Maxbytelength(maxbytelength=20,
               msg =@Msg(key = "errors.maxbytelength"),
                         arg0 = @Arg(key = "LOGIN ID", resource = false),
                 arg1 = @Arg(key = "20", resource = false))
public String loginId;

SEASAR2 では、上記のように validation を実施していましたが、 このまま Spring Boot へ移植できないものかと思い、試し始めました。

試した結果、なんとなく理解しましたが、今後、気が向いたら、以下のTODOを調べます。

  • {0} の記載で、エラーメッセージにフィールド名を埋め込めるようです。(試したが不明)
  • 各明細行等の為に、ネストなvalidationがある。(試してない)
  • 更に複雑なルールには、独自validation作成が必要。(試してない)

上記以外のポイントは、各srcのcommentに記載しています。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring</name>
  <description>Bukkenkoutei written from SEASAR2 to SpringBoot</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
        <dependency>
      <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
          <exclusion><!--【以下はLOG4J2の為】-->
             <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
          </exclusion>
        </exclusions>
    </dependency>
        <dependency><!--【LOMBOK】 -->
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    <dependency><!--【for JSP】 -->
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <dependency> <!--【VALIDATION】 -->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency><!--【ログ出力は LOG4J2】-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
        <dependency><!--【今回は CustomeStringTrimmerEditor より参照】-->
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

src/main/resources/application.properties

# 昔ながらのJSPを使用する為
spring.mvc.view.prefix= /WEB-INF/view/
spring.mvc.view.suffix= .jsp

# MessageSource が参照するfileを指定
spring.messages.basename=messages,validation_messages
spring.messages.cache-seconds=-1
spring.messages.encoding=UTF-8

src/main/resources/log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
  <Properties>
    <Property name="app_name">demo</Property>
    <Property name="date">%d{yyyy-MM-dd HH:mm:ss.SSS}</Property>
    <Property name="daily_log">logs/app${app_name}_%d{yyyy-MM-dd}.log</Property>
  </Properties>
  <appenders>
    <Console
      name="Console"
      target="SYSTEM_OUT"
    >
      <PatternLayout
       pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
    </Console>
    <RollingFile
      name="File"
      fileName="logs/app.log"
      filePattern="${daily_log}.gz">
      <PatternLayout
       pattern="${date}, [${app_name}], [ %-5level ], %logger{10}, %msg %n" />
      <Policies>
        <TimeBasedTriggeringPolicy />
      </Policies>
    </RollingFile>
  </appenders>
  <loggers>
    <root level="info">
      <appender-ref ref="Console" />
      <appender-ref ref="File" />
    </root>
  </loggers>
</configuration>

src/main/resources/messages_ja.properties

messages.properties も、空ファイルで必要

login.pleaseinput= 名前を入力してください
honorific.title = {0}さん

src/main/resources/validation_messages_ja.properties

validation_messages.properties も、空ファイルで必要

# {0}による フィールド名埋め込み方法は不明でした
javax.validation.constraints.NotNull.message= {0} は必須項目です
javax.validation.constraints.Size.message= {0}は {min} ~ {max} 文字で入力ください
# EL式により、以下のようにも記載できます。
javax.validation.constraints.Size.message_el = \
   ${min==max ? min += '文字で入力ください' : \
     min==1   ? max += '文字以内で入力ください' : \
                min += '~' += max += '文字で入力ください'}

src/main/webapp/WEB-INF/view/login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form method="post" action="/login">

<hr/>
${msg}
<hr/>

<div>LOGIN ID 1<input type="text" name="loginId1"></div>
<div>LOGIN ID 2<input type="text" name="loginId2"></div>
<div>LOGIN ID 3<input type="text" name="loginId3"></div>

<button type="submit">SUBMIT</button>
</form>
</body>
</html>

src/main/java/jp.end0tknr/MySpringApplication.java

package jp.end0tknr;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;


@SpringBootApplication
public class MySpringApplication {

    public static void main(String[] args) {
        SpringApplication.run(MySpringApplication.class, args);
    }

    // validation error 時のメッセージを
    // validation_messages.properties ファイルから参照する為
    @Autowired
    private MessageSource msgSrc;
    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean localValidatorFactoryBean
        = new LocalValidatorFactoryBean();

        localValidatorFactoryBean.setValidationMessageSource(msgSrc);
        return localValidatorFactoryBean;
    }
}

src/main/java/jp.end0tknr/CustomeStringTrimmerEditor.java

package jp.end0tknr;

import java.beans.PropertyEditorSupport;

import org.apache.commons.lang3.StringUtils;

// <input type="text">からpostされた値を全角空白含め、
// 予め、trimする為のclassです
public class CustomeStringTrimmerEditor extends PropertyEditorSupport {
    private final boolean emptyAsNull;

    public CustomeStringTrimmerEditor(boolean emptyAsNull) {
        this.emptyAsNull = emptyAsNull;
    }

    @Override
    public String getAsText() {
        Object value = this.getValue();
        return value != null ? value.toString() : "";
    }

    @Override
    public void setAsText(String text) {
        if (text == null) {
            this.setValue((Object) null);
        } else {
            String value = StringUtils.strip(text);

            if (this.emptyAsNull && "".equals(value)) {
                this.setValue((Object) null);
            } else {
                this.setValue(value);
            }
        }
    }

}

src/main/java/jp.end0tknr/LoginAction.java

package jp.end0tknr;

import java.util.Locale;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class LoginAction {

    static protected Logger logger = LogManager.getLogger(LoginAction.class);
    @Autowired
    MessageSource msgsrc;


    @RequestMapping(value="/login", method=RequestMethod.GET)
    public ModelAndView index(ModelAndView modelview) {

        // messages.properties 内のメッセージへの値の埋め込み
        String msg_1 = msgsrc.getMessage(
                "honorific.title", new String[]{"ゲスト"}, Locale.JAPAN);
        String msg_2 = msgsrc.getMessage(
                "login.pleaseinput", null, Locale.JAPAN);

        // jsp にある ${msg} へ埋め込み
        modelview.addObject("msg",msg_1 +" "+msg_2);

        modelview.setViewName("login");
        return modelview;
    }

    @RequestMapping(value="/login", method=RequestMethod.POST)
    public ModelAndView send(
            @Validated LoginForm loginForm,
            BindingResult errors,
            ModelAndView modelview) {

        logger.info("START This method.");

        String msg = "";

        // validation errorがある場合、メッセージ等を取得
        if(errors.hasErrors()){
            logger.error("This method has error.");

            for (FieldError fieldErr : errors.getFieldErrors()) {
                msg += "Field:" + fieldErr.getField();
                msg += " x Code:" + fieldErr.getCode();
                msg += " x Msg:" + fieldErr.getDefaultMessage();
                msg += "<hr/>";
            }
        }

        logger.info("MSG" + msg);

        modelview.addObject("msg",msg);
        modelview.setViewName("login");
        return modelview;
    }


}

src/main/java/jp.end0tknr/LoginForm.java

package jp.end0tknr;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginForm {
    @Autowired
    MessageSource msgsrc;

    @NotNull(message="{javax.validation.constraints.NotNull.message}")
    @Size(min=1, max=3, message="{0}は、{min}~{max}文字で入力してください")
    public String loginId1;

    @NotNull
    @Size(min=1, max=3, message="{javax.validation.constraints.Size.message}")
    public String loginId2;

    @NotNull
    @Size(min=1, max=3, message="{javax.validation.constraints.Size.message_el}")
    public String loginId3;
}