2.3. はじめてのSpring MVCアプリケーション

Spring MVCの、詳細な使い方の解説に入る前に、実際にSpring MVCに触れることで、Spring MVCを用いたWebアプリケーションの開発に対するイメージをつかむ。


2.3.1. 検証環境

本節の説明では、次の環境で動作検証している。(他の環境で実施する際は、本書をベースに適宜読み替えて設定していくこと。)

種別

プロダクト

OS

Windows 10

JVM

Java 17

IDE

Spring Tool Suite 4.17.1.RELEASE (以降「STS」と呼ぶ。設定方法はSTS4の設定手順を参照されたい。)

Build Tool

Apache Maven 3.8.6 (以降「Maven」と呼ぶ)

Application Server

Apache Tomcat 10.1.15

Web Browser

Google Chrome 117

Note

インターネット接続するためにプロキシサーバーを介する必要がある場合は、STSのProxy設定MavenのProxy設定が必要である。


2.3.2. 新規プロジェクト作成

インターネットからmvn archetype:generateを利用して、プロジェクトを作成する。

mvn archetype:generate -B^
 -DarchetypeGroupId=com.github.macchinetta.blank^
 -DarchetypeArtifactId=macchinetta-web-blank-jsp-archetype^
 -DarchetypeVersion=1.10.0.RELEASE^
 -DgroupId=com.example.helloworld^
 -DartifactId=helloworld^
 -Dversion=1.0.0-SNAPSHOT

ここではWindows上にプロジェクトの元を作成する。

C:\work>mvn archetype:generate -B^
More?  -DarchetypeGroupId=com.github.macchinetta.blank^
More?  -DarchetypeArtifactId=macchinetta-web-blank-jsp-archetype^
More?  -DarchetypeVersion=1.10.0.RELEASE^
More?  -DgroupId=com.example.helloworld^
More?  -DartifactId=helloworld^
More?  -Dversion=1.0.0-SNAPSHOT
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:3.2.1:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.2.1:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.2.1:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] Archetype repository not defined. Using the one from [com.github.macchinetta.blank:macchinetta-web-blank-jsp-archetype:1.10.0.RELEASE] found in catalog local
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: macchinetta-web-blank-jsp-archetype:1.10.0.RELEASE
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.example.helloworld
[INFO] Parameter: artifactId, Value: helloworld
[INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
[INFO] Parameter: package, Value: com.example.helloworld
[INFO] Parameter: packageInPathFormat, Value: com/example/helloworld
[INFO] Parameter: package, Value: com.example.helloworld
[INFO] Parameter: ProjectName, Value: Helloworld
[INFO] Parameter: groupId, Value: com.example.helloworld
[INFO] Parameter: artifactId, Value: helloworld
[INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
[INFO] Project created from Archetype in dir: C:\work\helloworld
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.859 s
[INFO] Finished at: 2023-12-27T09:05:15+09:00
[INFO] ------------------------------------------------------------------------

STSのメニューから、[File] -> [Import] -> [Maven] -> [Existing Maven Projects] -> [Next]を選択し、archetypeで作成したプロジェクトを選択する。

New MVC Project Import

Root Directoryに C:\work\helloworldを設定し、Projectsにhelloworldのpom.xmlが選択された状態で、 [Finish] を押下する。

New MVC Project Import

Package Explorerに、次のようなプロジェクトが生成される。

workspace

Spring MVCの設定方法を理解するために、生成されたSpring MVCの設定ファイルについて、簡単に説明する。

  • src/main/com/example/helloworld/config/web/SpringMvcConfig.java

package com.example.helloworld.config.web;

import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.web.codelist.CodeListInterceptor;
import org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor;
import org.terasoluna.gfw.web.exception.SystemExceptionResolver;
import org.terasoluna.gfw.web.logging.TraceLoggingInterceptor;
import org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor;

/**
 * Configure SpringMVC.
 */
@ComponentScan(basePackages = { "com.example.helloworld.app" }) // (2)
@EnableAspectJAutoProxy
@EnableWebMvc // (1)
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
        argumentResolvers.add(authenticationPrincipalArgumentResolver());
    }

    /**
     * Configure {@link PageableHandlerMethodArgumentResolver} bean.
     * @return Bean of configured {@link PageableHandlerMethodArgumentResolver}
     */
    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        return new PageableHandlerMethodArgumentResolver();
    }

    /**
     * Configure {@link AuthenticationPrincipalArgumentResolver} bean.
     * @return Bean of configured {@link AuthenticationPrincipalArgumentResolver}
     */
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations(
                "/resources/", "classpath:META-INF/resources/").setCachePeriod(
                        60 * 60);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        addInterceptor(registry, traceLoggingInterceptor());
        addInterceptor(registry, transactionTokenInterceptor());
        addInterceptor(registry, codeListInterceptor());
    }

    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link HandlerInterceptor}
     */
    private void addInterceptor(InterceptorRegistry registry,
            HandlerInterceptor interceptor) {
        registry.addInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }

    /**
     * Configure {@link TraceLoggingInterceptor} bean.
     * @return Bean of configured {@link TraceLoggingInterceptor}
     */
    @Bean
    public TraceLoggingInterceptor traceLoggingInterceptor() {
        return new TraceLoggingInterceptor();
    }

    /**
     * Configure {@link TransactionTokenInterceptor} bean.
     * @return Bean of configured {@link TransactionTokenInterceptor}
     */
    @Bean
    public TransactionTokenInterceptor transactionTokenInterceptor() {
        return new TransactionTokenInterceptor();
    }

    /**
     * Configure {@link CodeListInterceptor} bean.
     * @return Bean of configured {@link CodeListInterceptor}
     */
    @Bean
    public CodeListInterceptor codeListInterceptor() {
        CodeListInterceptor codeListInterceptor = new CodeListInterceptor();
        codeListInterceptor.setCodeListIdPattern(Pattern.compile("CL_.+"));
        return codeListInterceptor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp"); // (3)
    }

    /**
     * Configure {@link RequestDataValueProcessor} bean.
     * @return Bean of configured {@link CompositeRequestDataValueProcessor}
     */
    @Bean("requestDataValueProcessor")
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CompositeRequestDataValueProcessor(csrfRequestDataValueProcessor(), transactionTokenRequestDataValueProcessor());
    }

    /**
     * Configure {@link CsrfRequestDataValueProcessor} bean.
     * @return Bean of configured {@link CsrfRequestDataValueProcessor}
     */
    @Bean
    public CsrfRequestDataValueProcessor csrfRequestDataValueProcessor() {
        return new CsrfRequestDataValueProcessor();
    }

    /**
     * Configure {@link TransactionTokenRequestDataValueProcessor} bean.
     * @return Bean of configured {@link TransactionTokenRequestDataValueProcessor}
     */
    @Bean
    public TransactionTokenRequestDataValueProcessor transactionTokenRequestDataValueProcessor() {
        return new TransactionTokenRequestDataValueProcessor();
    }

    /**
     * Configure {@link SystemExceptionResolver} bean.
     * @param exceptionCodeResolver Bean defined by ApplicationContext#exceptionCodeResolver
     * @see com.example.helloworld.config.app.ApplicationContext#exceptionCodeResolver()
     * @return Bean of configured {@link SystemExceptionResolver}
     */
    @Bean("systemExceptionResolver")
    public SystemExceptionResolver systemExceptionResolver(
            ExceptionCodeResolver exceptionCodeResolver) {
        SystemExceptionResolver bean = new SystemExceptionResolver();
        bean.setExceptionCodeResolver(exceptionCodeResolver);
        bean.setOrder(3);

        Properties exceptionMappings = new Properties();
        exceptionMappings.setProperty("ResourceNotFoundException",
                "common/error/resourceNotFoundError");
        exceptionMappings.setProperty("BusinessException",
                "common/error/businessError");
        exceptionMappings.setProperty("InvalidTransactionTokenException",
                "common/error/transactionTokenError");
        exceptionMappings.setProperty(".DataAccessException",
                "common/error/dataAccessError");
        bean.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();
        statusCodes.setProperty("common/error/resourceNotFoundError", String
                .valueOf(HttpStatus.NOT_FOUND.value()));
        statusCodes.setProperty("common/error/businessError", String.valueOf(
                HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/transactionTokenError", String
                .valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/dataAccessError", String.valueOf(
                HttpStatus.INTERNAL_SERVER_ERROR.value()));
        bean.setStatusCodes(statusCodes);

        bean.setDefaultErrorView("common/error/systemError");
        bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return bean;
    }

    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.helloworld.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link HandlerExceptionResolverLoggingInterceptor}
     */
    @Bean("handlerExceptionResolverLoggingInterceptor")
    public HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        HandlerExceptionResolverLoggingInterceptor bean = new HandlerExceptionResolverLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }

    /**
     * Configure messages logging AOP advisor.
     * @param handlerExceptionResolverLoggingInterceptor Bean defined by #handlerExceptionResolverLoggingInterceptor
     * @see #handlerExceptionResolverLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor handlerExceptionResolverLoggingInterceptorAdvisor(
            HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))");
        return new DefaultPointcutAdvisor(pointcut, handlerExceptionResolverLoggingInterceptor);
    }
}

項番

説明

(1)

@EnableWebMvcアノテーションを定義することにより、Spring MVCのデフォルト設定が行われる。デフォルトの設定については、Spring Framework Documentation -Enable MVC Configuration-を参照されたい。

(2)

Spring MVCで使用するコンポーネントを探すパッケージを定義する。

(3)

JSP用のViewResolverを指定し、JSPファイルの配置場所を定義する。


次に、Welcomeページを表示するためのControllerについて、簡単に説明する。

  • com.example.helloworld.app.welcome.HelloController

package com.example.helloworld.app.welcome;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Handles requests for the application home page.
 */
@Controller // (4)
public class HelloController {

    private static final Logger logger = LoggerFactory
            .getLogger(HelloController.class);

    /**
     * Simply selects the home view to render by returning its name.
     */
    @GetMapping(value = "/") // (5)
    public String home(Locale locale, Model model) {
        logger.info("Welcome home! The client locale is {}.", locale);

        Date date = new Date();
        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,
                DateFormat.LONG, locale);

        String formattedDate = dateFormat.format(date);

        model.addAttribute("serverTime", formattedDate); // (6)

        return "welcome/home"; // (7)
    }

}

項番

説明

(4)

@Controllerアノテーションを付けることで、DIコンテナにより、コントローラクラスが自動で読み込まれる。前述「Spring MVCの設定ファイルの説明(2)」の設定により、component-scanの対象となっている。

(5)

HTTPメソッドがGETで、”/“というResource(もしくはRequest URL)にアクセスする際に実行される。

(6)

Viewに渡したいオブジェクトをModelに設定する。

(7)

View名を返却する。前述「Spring MVCの設定ファイルの説明(3)」の設定により、WEB-INF/views/welcome/home.jspがレンダリングされる。


最後に、Welcomeページを表示するためのViewについて簡単に説明する。

  • src/main/webapp/WEB-INF/views/welcome/home.jsp

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>
<body>
    <div class="container">
        <div id="wrapper">
            <h1 id="title">Hello world!</h1>
            <p>The time on the server is ${serverTime}.</p>
        </div>
        <jsp:include page="../layout/footer.jsp" />
    </div>
</body>
</html>

項番

説明

(8)
前述の「Controllerの説明(6)」でModelに設定したオブジェクト(serverTime)は、HttpServletRequestに格納される。
そのため、JSPで${serverTime}と記述することで、Controllerで設定した値を画面に出力することができる。

Warning

${XXX}の記述は、XSS対象になる可能性があるので、文字列を出力する場合はHTMLエスケープする必要がある。

詳細はXSS対策を参照されたい。


2.3.3. サーバーを起動する

STSで、”helloworld”プロジェクトを右クリックして、Run As -> Run On Server -> localhost -> Tomcat v10.1 Server at localhost -> Finishを実行し、helloworldプロジェクトを起動する。
ブラウザにhttp://localhost:8080/helloworld/を入力し、実行すると下記の画面が表示される。
Hello World

2.3.4. エコーアプリケーションの作成

続いて、簡単なアプリケーションを作成する。作成するのは、次の図のようなテキストフィールドに、名前を入力すると メッセージを表示する、いわゆるエコーアプリケーションである。

Form of Echo Application
Output of Echo Application

2.3.4.1. フォームオブジェクトの作成

まずは、テキストフィールドの値を受け取るための、フォームオブジェクトを作成する。
com.example.helloworld.app.echoパッケージにEchoFormクラスを作成する。プロパティを1つだけ持つ、単純なJavaBeanである。
package com.example.helloworld.app.echo;

import java.io.Serializable;

public class EchoForm implements Serializable {

    private static final long serialVersionUID = 1L;

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

2.3.4.2. Controllerの作成

次に、Controller(com.example.helloworld.app.echo.EchoController)クラスを作成する。

package com.example.helloworld.app.echo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("echo")
public class EchoController {

    @ModelAttribute // (1)
    public EchoForm setUpEchoForm() {
        EchoForm form = new EchoForm();
        return form;
    }

    @GetMapping // (2)
    public String index(Model model) {
        return "echo/index"; // (3)
    }

    @PostMapping(value = "hello") // (4)
    public String hello(EchoForm form, Model model) {// (5)
        model.addAttribute("name", form.getName()); // (6)
        return "echo/hello";
    }
}

項番

説明

(1)
@ModelAttributeというアノテーションを、メソッドに付加する。このアノテーションがついたメソッドの返り値は、自動でModelに追加される。
Modelの属性名を、@ModelAttributeで指定することもできるが、デフォルトでは、クラス名の先頭を小文字にした値が、属性名になる。この場合は、echoFormである。フォームの属性名は、次に説明するform:form タグmodelAttribute属性の値に一致している必要がある。
(2)
メソッドに付加した@GetMappingアノテーションのvalue属性に何も指定しない場合、クラスに付加した@RequestMappingのルートにマッピングされる。この場合、<contextPath>/echoにGETメソッドを使用してアクセスすると、indexメソッドが呼ばれる。
(3)
View名でecho/indexを返すので、ViewResolverにより、WEB-INF/views/echo/index.jspまたはWEB-INF/views/echo/index.htmlがレンダリングされる。
(4)
メソッドに付加した@PostMappingアノテーションのvalue属性にhelloを指定しているので、この場合、<contextPath>/echo/helloにPOSTメソッドを使用してアクセスするとhelloメソッドが呼ばれる。
(5)
引数に、EchoFormには(1)によりModelに追加されたEchoFormオブジェクトが渡される。
(6)
フォームで入力されたnameを、Viewにそのまま渡す。

Note

@GetMappingアノテーションもしくは@PostMappingアノテーションをメソッドに指定する場合は、クライアントから送信されたデータの扱い方によって変えるのが一般的である。

  • データをサーバに保存する場合(更新系の処理の場合)は、@PostMappingアノテーション(POSTメソッド)。

  • データをサーバに保存しない場合(参照系の処理の場合)は、@GetMappingアノテーション(GETメソッド)。

エコーアプリケーションでは、

  • indexメソッドはデータをサーバに保存しない処理なのでGETメソッド@GetMappingアノテーション

  • helloメソッドはデータをModelオブジェクトに保存する処理なので@PostMappingアノテーション

を指定している。


2.3.4.3. Viewの作成

最後に、入力画面と、出力画面のViewを作成する。それぞれのファイルパスは、View名に合わせて、次のようになる。

入力画面 (src/main/webapp/WEB-INF/views/echo/index.jsp) を作成する。

<!DOCTYPE html>
<html>
<head>
<title>Echo Application</title>
</head>
<body>
    <%-- (1) --%>
    <form:form modelAttribute="echoForm" action="${pageContext.request.contextPath}/echo/hello">
        <form:label path="name">Input Your Name:</form:label>
        <form:input path="name" />
        <input type="submit" />
    </form:form>
</body>
</html>

項番

説明

(1)
タグライブラリを利用し、HTMLフォームを構築している。modelAttribute属性に、Controllerで用意したフォームオブジェクトの名前を指定する。
タグライブラリはSpring Framework Documentation -The Form Tag-を参照されたい。

Note

<form:form>タグのmethod属性を省略した場合は、POSTメソッドが使用される。

出力されるHTMLは、

<!DOCTYPE html>
<html>
<head>
<title>Echo Application</title>
</head>
<body>
    <form id="echoForm" action="/helloworld/echo/hello" method="post">
        <label for="name">Input Your Name:</label>
        <input id="name" name="name" type="text" value=""/>
        <input type="submit" />
        <input type="hidden" name="_csrf" value="43595f38-3edd-4c08-843b-3c31a00d2b15" />
    </form>
</body>
</html>

となる。


出力画面 (src/main/webapp/WEB-INF/views/echo/hello.jsp) を作成する。

<!DOCTYPE html>
<html>
<head>
<title>Echo Application</title>
</head>
<body>
    <p>
        Hello <c:out value="${name}" /> <%-- (2) --%>
    </p>
</body>
</html>

項番

説明

(2)
Controllerから渡されたnameを出力する。c:outタグにより、XSS対策を行っている。

Note

ここではXSS対策を標準タグのc:outで実現したが、より容易に使用できるf:h()関数を共通ライブラリで用意している。

詳細は、XSS対策を参照されたい。

これでエコーアプリケーションの実装は完了である。
サーバーを起動し、http://localhost:8080/helloworld/echoにアクセスするとフォームが表示される。

2.3.4.4. 入力チェックの実装

ここまでのアプリケーションでは、入力チェックを行っていない。
Spring MVCでは、Bean Validationをサポートしており、アノテーションベースな入力チェックを、簡単に実装することができる。
例として、エコーアプリケーションで名前の入力チェックを行う。

EchoFormnameフィールドに、入力チェックルールを指定するアノテーションを付与する。

package com.example.helloworld.app.echo;

import java.io.Serializable;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class EchoForm implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull // (1)
    @Size(min = 1, max = 5) // (2)
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

項番

説明

(1)
@NotNullアノテーションをつけることで、HTTPリクエスト中にnameパラメータがあることを確認する。
(2)
@Size(min = 1, max = 5)をつけることで、nameのサイズが、1以上5以下であることを確認する。

入力チェックが実行されるように修正し、入力チェックでエラーが発生した場合の処理を実装する。

package com.example.helloworld.app.echo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("echo")
public class EchoController {

    @ModelAttribute
    public EchoForm setUpEchoForm() {
        EchoForm form = new EchoForm();
        return form;
    }

    @GetMapping
    public String index(Model model) {
        return "echo/index";
    }

    @PostMapping(value = "hello")
    public String hello(@Validated EchoForm form, BindingResult result, Model model) { // (1)
        if (result.hasErrors()) { // (2)
            return "echo/index";
        }
        model.addAttribute("name", form.getName());
        return "echo/hello";
    }
}

項番

説明

(1)
コントローラー側には、Validation対象の引数に@Validatedアノテーションを付加し、BindingResultオブジェクトを引数に追加する。
Bean Validationによる入力チェックは、自動で行われる。結果は、BindingResultオブジェクトに渡される。
(2)
hasErrorsメソッドを実行して、エラーがあるかどうかを確認する。入力エラーがある場合は、入力画面を表示するためのView名を返却する。

入力画面に、入力エラーのメッセージを表示するための実装を追加する。

  • src/main/webapp/WEB-INF/views/echo/index.jsp

<!DOCTYPE html>
<html>
<head>
<title>Echo Application</title>
</head>
<body>
    <form:form modelAttribute="echoForm" action="${pageContext.request.contextPath}/echo/hello">
        <form:label path="name">Input Your Name:</form:label>
        <form:input path="name" />
        <form:errors path="name" cssStyle="color:red" /><%-- (1) --%>
        <input type="submit" />
    </form:form>
</body>
</html>

項番

説明

(1)
入力画面には、エラーがあった場合に、エラーメッセージを表示するため、form:errorsタグを追加する。

出力されるHTMLは、

<!DOCTYPE html>
<html>
<head>
<title>Echo Application</title>
</head>
<body>
    <form id="echoForm" action="/helloworld/echo/hello" method="post">
        <label for="name">Input Your Name:</label>
        <input id="name" name="name" type="text" value=""/>
        <span id="name.errors" style="color:red">size must be between 1 and 5</span>
        <input type="submit" />
        <input type="hidden" name="_csrf" value="6e94a78d-4a2c-4a41-a514-0a60f0dbedaf" />
    </form>
</body>
</html>

となる。

以上で、入力チェックの実装は完了である。
実際に、次のような場合、エラーメッセージが表示される。
  • 名前を空にして送信した場合

  • 5文字より大きいサイズで送信した場合

Validation Error (name is empty)
Validation Error (name's size is over 5)

2.3.4.5. まとめ

この章では、

  1. mvn archetype:generateを利用したブランクプロジェクトの作成方法

  2. Spring MVCの基本的な設定方法

  3. 最も簡易な、画面遷移方法

  4. 画面間での値の引き渡し方法

  5. シンプルな入力チェック方法

を学んだ。

上記の内容が理解できていない場合は、もう一度、本節を読み、環境構築から始めて、進めていくことで理解が深まる。