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.7
Web Browser Google Chrome 108

Note

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


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

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

mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype^
 -DarchetypeVersion=5.8.1.RELEASE^
 -DgroupId=com.example.helloworld^
 -DartifactId=helloworld^
 -Dversion=1.0.0-SNAPSHOT

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

C:\work>mvn archetype:generate -B^
More?  -DarchetypeGroupId=org.terasoluna.gfw.blank^
More?  -DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype^
More?  -DarchetypeVersion=5.8.1.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.1.2:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.1.2:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.1.2:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] Archetype repository not defined. Using the one from [org.terasoluna.gfw.blank:terasoluna-gfw-web-blank-archetype:5.8.1.RELEASE] found in catalog remote
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: terasoluna-gfw-web-blank-archetype:5.8.1.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: version, Value: 1.0.0-SNAPSHOT
[INFO] Parameter: groupId, Value: com.example.helloworld
[INFO] Parameter: artifactId, Value: helloworld
[INFO] Project created from Archetype in dir: C:\work\helloworld
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.278 s
[INFO] Finished at: 2021-07-20T14:49:33+09:00
[INFO] ------------------------------------------------------------------------
C:\work>

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/resources/META-INF/spring/spring-mvc.xml)について、簡単に説明する。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <context:property-placeholder
        location="classpath*:/META-INF/spring/*.properties" />

    <!-- (1) Enables the Spring MVC @Controller programming model -->
    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean
                class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
            <bean
                class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

    <mvc:default-servlet-handler />

    <!-- (2) -->
    <context:component-scan base-package="com.example.helloworld.app" />

    <mvc:resources mapping="/resources/**"
        location="/resources/,classpath:META-INF/resources/"
        cache-period="#{60 * 60}" />

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean class="org.terasoluna.gfw.web.codelist.CodeListInterceptor">
                <property name="codeListIdPattern" value="CL_.+" />
            </bean>
        </mvc:interceptor>
        <!--  REMOVE THIS LINE IF YOU USE JPA
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" />
        </mvc:interceptor>
            REMOVE THIS LINE IF YOU USE JPA  -->
    </mvc:interceptors>

    <!-- (3) Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
    <!-- Settings View Resolver. -->
    <mvc:view-resolvers>
        <mvc:jsp prefix="/WEB-INF/views/" />
    </mvc:view-resolvers>

    <bean id="requestDataValueProcessor"
        class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor">
        <constructor-arg>
            <util:list>
                <bean
                    class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
                <bean
                    class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
            </util:list>
        </constructor-arg>
    </bean>

    <!-- Setting Exception Handling. -->
    <!-- Exception Resolver. -->
    <bean id="systemExceptionResolver"
        class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
        <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
        <!-- Setting and Customization by project. -->
        <property name="order" value="3" />
        <property name="exceptionMappings">
            <map>
                <entry key="ResourceNotFoundException" value="common/error/resourceNotFoundError" />
                <entry key="BusinessException" value="common/error/businessError" />
                <entry key="InvalidTransactionTokenException" value="common/error/transactionTokenError" />
                <entry key=".DataAccessException" value="common/error/dataAccessError" />
            </map>
        </property>
        <property name="statusCodes">
            <map>
                <entry key="common/error/resourceNotFoundError" value="404" />
                <entry key="common/error/businessError" value="409" />
                <entry key="common/error/transactionTokenError" value="409" />
                <entry key="common/error/dataAccessError" value="500" />
            </map>
        </property>
        <property name="excludedExceptions">
            <array>
            </array>
        </property>
        <property name="defaultErrorView" value="common/error/systemError" />
        <property name="defaultStatusCode" value="500" />
    </bean>
    <!-- Setting AOP. -->
    <bean id="handlerExceptionResolverLoggingInterceptor"
        class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor">
        <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>
    <aop:config>
        <aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor"
            pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" />
    </aop:config>

</beans>
項番 説明
(1)
<mvc:annotation-driven>要素を定義することにより、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("/") // (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ページを表示するためのJSP (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 id="wrapper">
        <h1>Hello world!</h1>
        <p>The time on the server is ${serverTime}.</p> <%-- (8) --%>
    </div>
</body>
</html>
項番 説明
(8)

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

ただし、${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”がレンダリングされる。
(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. JSPの作成

最後に、入力画面と、出力画面のJSPを作成する。それぞれのファイルパスは、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タグを追加する。

以上で、入力チェックの実装は完了である。
実際に、次のような場合、エラーメッセージが表示される。
  • 名前を空にして送信した場合
  • 5文字より大きいサイズで送信した場合
Validation Error (name is empty)
Validation Error (name's size is over 5)

出力される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>

となる。


2.3.4.5. まとめ

この章では、

  1. mvn archetype:generateを利用したブランクプロジェクトの作成方法
  2. Spring MVCの基本的な設定方法
  3. 最も簡易な、画面遷移方法
  4. 画面間での値の引き渡し方法
  5. シンプルな入力チェック方法

を学んだ。

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