チュートリアル(Todoアプリケーション REST編)
********************************************************************************
.. only:: html
.. contents:: 目次
:depth: 3
:local:
|
はじめに
================================================================================
このチュートリアルで学ぶこと
--------------------------------------------------------------------------------
* \ |framework_name|\による基本的なRESTful Webサービスの構築方法
|
対象読者
--------------------------------------------------------------------------------
* \ :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\を実施している。
|
検証環境
--------------------------------------------------------------------------------
| 本チュートリアルは以下の環境で動作確認している。
| REST Clientとして、Google Chromeの拡張機能を使用するため、Web BrowserはGoogle Chromeを使用する。
.. tabularcolumns:: |p{0.30\linewidth}|p{0.70\linewidth}|
.. list-table::
:header-rows: 1
:widths: 30 70
* - 種別
- プロダクト
* - REST Client
- \ :url_talend_api_tester:`Talend API Tester <>`\ |talend_api_tester_version|
* - 上記以外のプロダクト
- \ :doc:`./TutorialTodo`\ または
\ :doc:`./TutorialTodoThymeleaf`\と同様
|
環境構築
================================================================================
Java, STS, Maven, Google Chromeについては、\ :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\ を実施する事でインストール済みの状態である事を前提とする。
|
Talend API Testerのインストール
--------------------------------------------------------------------------------
RESTクライアントとして、Chromeの拡張機能である「Talend API Tester」をインストールする。
\ :url_talend_api_tester:`Talend API Tester <>`\ にアクセスし、「Chromeに追加」を押下する。
.. figure:: ./images_TutorialREST/install-dev-http-client1.png
:width: 80%
|
「拡張機能を追加」を押下する。
.. figure:: ./images_TutorialREST/install-dev-http-client2.png
:width: 40%
|
Chromeの右上の拡張機能のマークを押下して拡張機能一覧を開くと、Talend API Testerが追加されている。
.. figure:: ./images_TutorialREST/install-dev-http-client3.png
:width: 40%
|
| Talend API Testerをクリックする。
| 以下の画面が表示されるので、「Use Talend API Tester - Free Edition」を押下する。
| この画面は、ブラウザのアドレスバーに「chrome-extension://aejoelaoggembcahagimdiliamlcdmfm/index.html」を入力する事で開く事もできる。
.. figure:: ./images_TutorialREST/install-dev-http-client4.png
:width: 80%
|
以下の画面が表示されれば、インストール完了となる。
.. figure:: ./images_TutorialREST/install-dev-http-client5.png
:width: 80%
|
プロジェクト作成
--------------------------------------------------------------------------------
本チュートリアルでは、「\ :doc:`./TutorialTodo`\ 」または「\ :doc:`./TutorialTodoThymeleaf`\ 」で作成したプロジェクトに対して、RESTful Webサービスを追加する手順となっている。
そのため、「\ :doc:`./TutorialTodo`\ 」または「\ :doc:`./TutorialTodoThymeleaf`\ 」で作成したプロジェクトが残っていない場合は、再度「\ :doc:`./TutorialTodo`\ 」または「\ :doc:`./TutorialTodoThymeleaf`\ 」を実施してプロジェクトを作成してほしい。なお、\ :ref:`TutorialREST_systemexception_handling`\ で\ ``todo-infra.properties``\ を編集する操作があるため、MyBatis3用のブランクプロジェクトから作成したTODOプロジェクトを使用すること。
.. note::
再度「\ :doc:`./TutorialTodo`\ 」または「\ :doc:`./TutorialTodoThymeleaf`\ 」を実施する場合は、ドメイン層の作成まで行えば本チュートリアルを進める事ができる。
|
REST APIの作成
================================================================================
本チュートリアルでは、todoテーブルで管理しているデータ(以降、「Todoリソース」と呼ぶ)をWeb上に公開するためのREST APIを作成する。
.. tabularcolumns:: |p{0.20\linewidth}|p{0.10\linewidth}|p{0.30\linewidth}|p{0.15\linewidth}|p{0.25\linewidth}|
.. list-table::
:header-rows: 1
:widths: 20 10 30 15 25
* - | API名
- | HTTP
| メソッド
- | パス
- | ステータス
| コード
- | 説明
* - | GET Todos
- | GET
- | \ ``/api/v1/todos``\
- | 200
| (OK)
- | Todoリソースを全件取得する。
* - | POST Todos
- | POST
- | \ ``/api/v1/todos``\
- | 201
| (Created)
- | Todoリソースを新規作成する。
* - | GET Todo
- | GET
- | \ ``/api/v1/todos/{todoId}``\
- | 200
| (OK)
- | Todoリソースを一件取得する。
* - | PUT Todo
- | PUT
- | \ ``/api/v1/todos/{todoId}``\
- | 200
| (OK)
- | Todoリソースを完了状態に更新する。
* - | DELETE Todo
- | DELETE
- | \ ``/api/v1/todos/{todoId}``\
- | 204
| (No Content)
- | Todoリソースを削除する。
.. tip::
パス内に含まれている\ ``{todoId}``\ は、パス変数と呼ばれ、任意の可変値を扱う事ができる。
パス変数を使用する事で、\ ``GET /api/v1/todos/123``\ と\ ``GET /api/v1/todos/456``\ を同じAPIで扱う事ができる。
本チュートリアルでは、Todoを一意に識別するためのID(Todo ID)をパス変数として扱っている。
|
API仕様
--------------------------------------------------------------------------------
| HTTPリクエストとレスポンスの具体例を用いて、本チュートリアルで作成するREST APIのインタフェース仕様を示す。
| 本質的ではないHTTPヘッダー等は例から除いている。
|
GET Todos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ **[リクエスト]**\
.. code-block:: bash
> GET /todo/api/v1/todos HTTP/1.1
\ **[レスポンス]**\
作成済みのTodoリソースのリストをJSON形式で返却する。
.. code-block:: bash
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
<
[{"todoId":"9aef3ee3-30d4-4a7c-be4a-bc184ca1d558","todoTitle":"Hello World!","finished":false,"createdAt":"2014-02-25T02:21:48.493"}]
|
POST Todos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ **[リクエスト]**\
新規作成するTodoリソースの内容(タイトル)をJSON形式で指定する。
.. code-block:: bash
> POST /todo/api/v1/todos HTTP/1.1
> Content-Type: application/json
> Content-Length: 29
>
{"todoTitle": "Study Spring"}
\ **[レスポンス]**\
作成したTodoリソースをJSON形式で返却する。
.. code-block:: bash
< HTTP/1.1 201
< Content-Type: application/json;charset=UTF-8
<
{"todoId":"d6101d61-b22c-48ee-9110-e106af6a1404","todoTitle":"Study Spring","finished":false,"createdAt":"2014-02-25T04:05:58.752"}
|
GET Todo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ **[リクエスト]**\
| パス変数「\ ``todoId``\ 」に、取得対象のTodoリソースのIDを指定する。
| 下記例では、パス変数「\ ``todoId``\ 」に\ ``9aef3ee3-30d4-4a7c-be4a-bc184ca1d558``\ を指定している。
.. code-block:: bash
> GET /todo/api/v1/todos/9aef3ee3-30d4-4a7c-be4a-bc184ca1d558 HTTP/1.1
\ **[レスポンス]**\
パス変数「\ ``todoId``\ 」に一致するTodoリソースをJSON形式で返却する。
.. code-block:: bash
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
<
{"todoId":"9aef3ee3-30d4-4a7c-be4a-bc184ca1d558","todoTitle":"Hello World!","finished":false,"createdAt":"2014-02-25T02:21:48.493"}
|
PUT Todo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ **[リクエスト]**\
| パス変数「\ ``todoId``\ 」に、更新対象のTodoのIDを指定する。
| PUT Todoでは、Todoリソースを完了状態に更新するだけなので、リクエストBODYを受け取らないインタフェース仕様にしている。
.. code-block:: bash
> PUT /todo/api/v1/todos/9aef3ee3-30d4-4a7c-be4a-bc184ca1d558 HTTP/1.1
\ **[レスポンス]**\
パス変数「\ ``todoId``\ 」に一致するTodoリソースを完了状態(\ ``finished``\ フィールドを\ ``true``\ )に更新し、JSON形式で返却する。
.. code-block:: bash
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
<
{"todoId":"9aef3ee3-30d4-4a7c-be4a-bc184ca1d558","todoTitle":"Hello World!","finished":true,"createdAt":"2014-02-25T02:21:48.493"}
|
DELETE Todo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ **[リクエスト]**\
パス変数「\ ``todoId``\ 」に、削除対象のTodoリソースのIDを指定する。
.. code-block:: bash
> DELETE /todo/api/v1/todos/9aef3ee3-30d4-4a7c-be4a-bc184ca1d558 HTTP/1.1
\ **[レスポンス]**\
DELETE Todoでは、Todoリソースの削除が完了した事で返却するリソースが存在しなくなった事を示すために、レスポンスBODYを返却しないインタフェース仕様にしている。
.. code-block:: bash
< HTTP/1.1 204
|
エラー応答
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| REST APIでエラーが発生した場合は、JSON形式でエラー内容を返却する。
| 以下に代表的なエラー発生時のレスポンス仕様について記載する。
| 下記以外のエラーパターンもあるが、本チュートリアルでは説明は割愛する。
\ :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\ では、エラーメッセージはプログラムの中でハードコーディングしていたが、本チュートリアルでは、エラーメッセージはエラーコードをキーにプロパティファイルから取得するように修正する。
\ **[入力チェックエラー発生時のレスポンス仕様]**\
.. code-block:: bash
< HTTP/1.1 400
< Content-Type: application/json;charset=UTF-8
<
{"code":"E400","message":"[E400] The requested Todo contains invalid values.","details":[{"code":"NotNull","message":"todoTitle may not be null.",target:"todoTitle"}]}
\ **[業務エラー発生時のレスポンス仕様]**\
.. code-block:: bash
< HTTP/1.1 409
< Content-Type: application/json;charset=UTF-8
<
{"code":"E002","message":"[E002] The requested Todo is already finished. (id=353fb5db-151a-4696-9b4a-b958358a5ab3)"}
\ **[リソース未検出時のレスポンス仕様]**\
.. code-block:: bash
< HTTP/1.1 404
< Content-Type: application/json;charset=UTF-8
<
{"code":"E404","message":"[E404] The requested Todo is not found. (id=353fb5db-151a-4696-9b4a-b958358a5ab2)"}
\ **[システムエラー発生時のレスポンス仕様]**\
.. code-block:: bash
< HTTP/1.1 500
< Content-Type: application/json;charset=UTF-8
<
{"code":"E500","message":"[E500] System error occurred."}
|
REST API用のDispatcherServletを用意
--------------------------------------------------------------------------------
まず、REST API用のリクエストを処理するための\ ``DispatcherServlet``\ の定義を追加する。
|
web.xmlの修正
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| REST API用の設定を追加する。
.. tabs::
.. group-tab:: Java Config
.. tabs::
.. group-tab:: JSP
\ ``src/main/webapp/WEB-INF/web.xml``\
.. code-block:: xml
logbackDisableServletContainerInitializer
true
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
ch.qos.logback.classic.servlet.LogbackServletContextListener
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
com.example.todo.config.app.ApplicationContextConfig
com.example.todo.config.web.SpringSecurityConfig
org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener
MDCClearFilter
org.terasoluna.gfw.web.logging.mdc.MDCClearFilter
MDCClearFilter
/*
exceptionLoggingFilter
org.springframework.web.filter.DelegatingFilterProxy
exceptionLoggingFilter
/*
XTrackMDCPutFilter
org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter
XTrackMDCPutFilter
/*
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
CharacterEncodingFilter
/*
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
restApiServlet
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.example.todo.config.web.SpringMvcRestConfig
1
restApiServlet
/api/v1/*
appServlet
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.example.todo.config.web.SpringMvcConfig
1
appServlet
/
*.jsp
false
UTF-8
false
/WEB-INF/views/common/include.jsp
500
/WEB-INF/views/common/error/systemError.jsp
404
/WEB-INF/views/common/error/resourceNotFoundError.jsp
java.lang.Exception
/WEB-INF/views/common/error/unhandledSystemError.html
30
true
COOKIE
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 初期化パラメータ「\ ``contextConfigLocation``\ 」に、REST用のSpring MVC設定ファイルを指定する。
| 本チュートリアルでは、クラスパス上にある「\ :file:`com.example.todo.config.web.SpringMvcRestConfig`\ 」を指定している。
* - | (2)
- | \ ````\ 要素に、REST API用の\ ``DispatcherServlet``\ にマッピングするURLのパターンを指定する。
| 本チュートリアルでは、\ ``/api/v1/``\ から始まる場合はリクエストをREST APIへのリクエストとしてREST API用の\ ``DispatcherServlet``\ へマッピングしている。
.. group-tab:: Thymeleaf
\ ``src/main/webapp/WEB-INF/web.xml``\
.. code-block:: xml
logbackDisableServletContainerInitializer
true
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
ch.qos.logback.classic.servlet.LogbackServletContextListener
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
com.example.todo.config.app.ApplicationContextConfig
com.example.todo.config.web.SpringSecurityConfig
org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener
MDCClearFilter
org.terasoluna.gfw.web.logging.mdc.MDCClearFilter
MDCClearFilter
/*
exceptionLoggingFilter
org.springframework.web.filter.DelegatingFilterProxy
exceptionLoggingFilter
/*
XTrackMDCPutFilter
org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter
XTrackMDCPutFilter
/*
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
CharacterEncodingFilter
/*
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
restApiServlet
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.example.todo.config.web.SpringMvcRestConfig
1
restApiServlet
/api/v1/*
appServlet
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.example.todo.config.web.SpringMvcConfig
1
appServlet
/
500
/common/error/systemError
404
/common/error/resourceNotFoundError
java.lang.Exception
/WEB-INF/views/common/error/unhandledSystemError.html
30
true
COOKIE
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 初期化パラメータ「\ ``contextConfigLocation``\ 」に、REST用のSpring MVC設定ファイルを指定する。
| 本チュートリアルでは、クラスパス上にある「\ :file:`com.example.todo.config.web.SpringMvcRestConfig`\ 」を指定している。
* - | (2)
- | \ ````\ 要素に、REST API用の\ ``DispatcherServlet``\ にマッピングするURLのパターンを指定する。
| 本チュートリアルでは、\ ``/api/v1/``\ から始まる場合はリクエストをREST APIへのリクエストとしてREST API用の\ ``DispatcherServlet``\ へマッピングしている。
.. group-tab:: XML Config
.. tabs::
.. group-tab:: JSP
\ ``src/main/webapp/WEB-INF/web.xml``\
.. code-block:: xml
logbackDisableServletContainerInitializer
true
ch.qos.logback.classic.servlet.LogbackServletContextListener
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
classpath*:META-INF/spring/applicationContext.xml
classpath*:META-INF/spring/spring-security.xml
org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener
MDCClearFilter
org.terasoluna.gfw.web.logging.mdc.MDCClearFilter
MDCClearFilter
/*
exceptionLoggingFilter
org.springframework.web.filter.DelegatingFilterProxy
exceptionLoggingFilter
/*
XTrackMDCPutFilter
org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter
XTrackMDCPutFilter
/*
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
CharacterEncodingFilter
/*
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
restApiServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:META-INF/spring/spring-mvc-rest.xml
1
restApiServlet
/api/v1/*
appServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:META-INF/spring/spring-mvc.xml
1
appServlet
/
*.jsp
false
UTF-8
false
/WEB-INF/views/common/include.jsp
500
/WEB-INF/views/common/error/systemError.jsp
404
/WEB-INF/views/common/error/resourceNotFoundError.jsp
java.lang.Exception
/WEB-INF/views/common/error/unhandledSystemError.html
30
true
COOKIE
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 初期化パラメータ「\ ``contextConfigLocation``\ 」に、REST用のSpring MVC設定ファイルを指定する。
| 本チュートリアルでは、クラスパス上にある「\ :file:`META-INF/spring/spring-mvc-rest.xml`\ 」を指定している。
* - | (2)
- | \ ````\ 要素に、REST API用の\ ``DispatcherServlet``\ にマッピングするURLのパターンを指定する。
| 本チュートリアルでは、\ ``/api/v1/``\ から始まる場合はリクエストをREST APIへのリクエストとしてREST API用の\ ``DispatcherServlet``\ へマッピングしている。
.. group-tab:: Thymeleaf
\ ``src/main/webapp/WEB-INF/web.xml``\
.. code-block:: xml
logbackDisableServletContainerInitializer
true
ch.qos.logback.classic.servlet.LogbackServletContextListener
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
classpath*:META-INF/spring/applicationContext.xml
classpath*:META-INF/spring/spring-security.xml
org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener
MDCClearFilter
org.terasoluna.gfw.web.logging.mdc.MDCClearFilter
MDCClearFilter
/*
exceptionLoggingFilter
org.springframework.web.filter.DelegatingFilterProxy
exceptionLoggingFilter
/*
XTrackMDCPutFilter
org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter
XTrackMDCPutFilter
/*
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
CharacterEncodingFilter
/*
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
restApiServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:META-INF/spring/spring-mvc-rest.xml
1
restApiServlet
/api/v1/*
appServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:META-INF/spring/spring-mvc.xml
1
appServlet
/
500
/common/error/systemError
404
/common/error/resourceNotFoundError
java.lang.Exception
/WEB-INF/views/common/error/unhandledSystemError.html
30
true
COOKIE
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 初期化パラメータ「\ ``contextConfigLocation``\ 」に、REST用のSpring MVC設定ファイルを指定する。
| 本チュートリアルでは、クラスパス上にある「\ :file:`META-INF/spring/spring-mvc-rest.xml`\ 」を指定している。
* - | (2)
- | \ ````\ 要素に、REST API用の\ ``DispatcherServlet``\ にマッピングするURLのパターンを指定する。
| 本チュートリアルでは、\ ``/api/v1/``\ から始まる場合はリクエストをREST APIへのリクエストとしてREST API用の\ ``DispatcherServlet``\ へマッピングしている。
|
spring-mvc-restの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| REST用のSpring MVC設定ファイルを作成する。
| REST用のSpring MVC設定ファイルは以下のような定義となる。
.. tabs::
.. group-tab:: Java Config
.. figure:: ./images_TutorialREST/add-spring-mvc-rest_JavaConfig.png
\ ``src/main/java/com/example/todo/config/web/SpringMvcRestConfig.java``\
.. code-block:: java
package com.example.todo.config.web;
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.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor;
import org.terasoluna.gfw.web.logging.TraceLoggingInterceptor;
/**
* Configure SpringMVCRest.
*/
@ComponentScan(basePackages = {"com.example.todo.api"}) // (2)
@EnableAspectJAutoProxy // (4)
@EnableWebMvc
@Configuration
public class SpringMvcRestConfig implements WebMvcConfigurer {
/**
* Configure {@link PropertySourcesPlaceholderConfigurer} bean.
* @param properties Property files to be read
* @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
*/
// (1)
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
@Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
bean.setLocations(properties);
return bean;
}
/**
* {@inheritDoc}
*/
// (3)
@Override
public void addInterceptors(InterceptorRegistry registry) {
addInterceptor(registry, traceLoggingInterceptor());
}
/**
* Common processes used in #addInterceptors.
* @param registry {@link InterceptorRegistry}
* @param interceptor {@link HandlerInterceptor}
*/
// (3)
private void addInterceptor(InterceptorRegistry registry, HandlerInterceptor interceptor) {
registry.addInterceptor(interceptor).addPathPatterns("/**")
.excludePathPatterns("/resources/**", "/*/*.html");
}
/**
* Configure {@link TraceLoggingInterceptor} bean.
* @return Bean of configured {@link TraceLoggingInterceptor}
*/
// (3)
@Bean
public TraceLoggingInterceptor traceLoggingInterceptor() {
return new TraceLoggingInterceptor();
}
/**
* Configure messages logging AOP.
* @param exceptionLogger Bean defined by ApplicationContextConfig#exceptionLogger
* @see com.example.todo.config.app.ApplicationContextConfig#exceptionLogger()
* @return Bean of configured {@link HandlerExceptionResolverLoggingInterceptor}
*/
// (4)
@Bean(name = "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
*/
// (4)
@Bean
public Advisor handlerExceptionResolverLoggingInterceptorAdvisor(
HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(
"execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))");
return new DefaultPointcutAdvisor(pointcut, handlerExceptionResolverLoggingInterceptor);
}
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- \ アプリケーション層のコンポーネントでプロパティファイルに定義されている値を参照する必要がある場合は、\ ``PropertySourcesPlaceholderConfigurer.class``\ を使用してプロパティファイルを読み込む必要がある。
* - | (2)
- REST API用のパッケージ配下のコンポーネントをスキャンする。
本チュートリアルでは、REST API用のパッケージを\ ``com.example.todo.api``\ にしている。
画面遷移用のControllerは、\ ``app``\ パッケージ配下に格納していたが、REST API用のControllerは、\ ``api``\ パッケージ配下に格納する事を推奨する。
* - | (3)
- \ Controllerの処理開始、終了時の情報をログに出力するために、共通ライブラリから提供されている\ ``TraceLoggingInterceptor``\ を定義する。
* - | (4)
- \ Spring MVCのフレームワークでハンドリングされた例外を、ログ出力するためのAOP定義を指定する。
.. group-tab:: XML Config
.. figure:: ./images_TutorialREST/add-spring-mvc-rest.png
\ ``src/main/resources/META-INF/spring/spring-mvc-rest.xml``\
.. code-block:: xml
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- \ アプリケーション層のコンポーネントでプロパティファイルに定義されている値を参照する必要がある場合は、\ ````\ 要素を使用してプロパティファイルを読み込む必要がある。
* - | (2)
- \ JSONの型変換を有効にするために、\ ````\ を設定する
Spring MVCのデフォルト設定ではアプリケーションのクラスパスに応じて使用可能な\ ``HttpMessageConverter``\ が自動的に登録されるが、ここではリソースの形式をJSONに限定したいため、register-defaults属性を\ ``false``\ に設定し、上で定義した\ ``MappingJackson2HttpMessageConverter``\ のみを登録している。
* - | (3)
- REST API用のパッケージ配下のコンポーネントをスキャンする。
本チュートリアルでは、REST API用のパッケージを\ ``com.example.todo.api``\ にしている。
画面遷移用のControllerは、\ ``app``\ パッケージ配下に格納していたが、REST API用のControllerは、\ ``api``\ パッケージ配下に格納する事を推奨する。
* - | (4)
- \ Controllerの処理開始、終了時の情報をログに出力するために、共通ライブラリから提供されている\ ``TraceLoggingInterceptor``\ を定義する。
* - | (5)
- \ Spring MVCのフレームワークでハンドリングされた例外を、ログ出力するためのAOP定義を指定する。
|
REST API用のSpring Securityの定義追加
--------------------------------------------------------------------------------
| ブランクプロジェクトでは、CSRF対策といった、Spring Securityのセキュリティ対策機能が有効になっている。
| REST APIを使って構築するWebアプリケーションでも、セキュリティ対策機能は必要である。ただし、本チュートリアルの目的として、
| セキュリティ対策の話題は本質的ではないため、機能を無効化し、説明も割愛する。
| 以下の設定を追加する事で、Spring Securityのセキュリティ対策機能を無効化することができる。
.. tabs::
.. group-tab:: Java Config
\ ``src/main/java/com/example/todo/config/web/SpringSecurityConfig.java``\
.. tabs::
.. group-tab:: JSP
.. code-block:: java
package com.example.todo.config.web;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
import java.util.LinkedHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;
/**
* Bean definition to configure SpringSecurity.
*/
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
/**
* Configure ignore security pattern.
* @return Bean of configured {@link WebSecurityCustomizer}
*/
// (1)
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(antMatcher("/resources/**"),
antMatcher("/api/v1/**"));
}
/**
* Configure {@link SecurityFilterChain} bean.
* @param http Builder class for setting up authentication and authorization
* @return Bean of configured {@link SecurityFilterChain}
* @throws Exception Exception that occurs when setting HttpSecurity
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults());
http.logout(Customizer.withDefaults());
http.exceptionHandling(ex -> ex.accessDeniedHandler(accessDeniedHandler()));
http.addFilterAfter(userIdMDCPutFilter(), AnonymousAuthenticationFilter.class);
http.sessionManagement(Customizer.withDefaults());
http.authorizeHttpRequests(authz -> authz.requestMatchers(antMatcher("/**")).permitAll());
return http.build();
}
/**
* Configure {@link AccessDeniedHandler} bean.
* @return Bean of configured {@link AccessDeniedHandler}
*/
@Bean("accessDeniedHandler")
public AccessDeniedHandler accessDeniedHandler() {
LinkedHashMap, AccessDeniedHandler> errorHandlers =
new LinkedHashMap<>();
// Invalid CSRF authenticator error handler
AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
invalidCsrfTokenErrorHandler
.setErrorPage("/WEB-INF/views/common/error/invalidCsrfTokenError.jsp");
errorHandlers.put(InvalidCsrfTokenException.class, invalidCsrfTokenErrorHandler);
// Missing CSRF authenticator error handler
AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
missingCsrfTokenErrorHandler
.setErrorPage("/WEB-INF/views/common/error/missingCsrfTokenError.jsp");
errorHandlers.put(MissingCsrfTokenException.class, missingCsrfTokenErrorHandler);
// Default error handler
AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
defaultErrorHandler.setErrorPage("/WEB-INF/views/common/error/accessDeniedError.jsp");
return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
}
/**
* Configure {@link DefaultWebSecurityExpressionHandler} bean.
* @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
*/
@Bean("webSecurityExpressionHandler")
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
return new DefaultWebSecurityExpressionHandler();
}
/**
* Configure {@link UserIdMDCPutFilter} bean.
* @return Bean of configured {@link UserIdMDCPutFilter}
*/
@Bean("userIdMDCPutFilter")
public UserIdMDCPutFilter userIdMDCPutFilter() {
return new UserIdMDCPutFilter();
}
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | REST API用のSpring Securityのセキュリティ機能を無効にする定義を追加する。
| \ ``WebSecurity.class``\ の\ ``ignoring``\ メソッドに、REST API用のリクエストパスのURLパターンを指定している。
| 本チュートリアルでは\ ``/api/v1/``\ で始まるリクエストパスをREST API用のリクエストパスとして扱う。
.. group-tab:: Thymeleaf
.. code-block:: java
package com.example.todo.config.web;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
import java.util.LinkedHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;
/**
* Bean definition to configure SpringSecurity.
*/
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
/**
* Configure ignore security pattern.
* @return Bean of configured {@link WebSecurityCustomizer}
*/
// (1)
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(antMatcher("/resources/**"),
antMatcher("/api/v1/**"));
}
/**
* Configure {@link SecurityFilterChain} bean.
* @param http Builder class for setting up authentication and authorization
* @return Bean of configured {@link SecurityFilterChain}
* @throws Exception Exception that occurs when setting HttpSecurity
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults());
http.logout(Customizer.withDefaults());
http.exceptionHandling(ex -> ex.accessDeniedHandler(accessDeniedHandler()));
http.addFilterAfter(userIdMDCPutFilter(), AnonymousAuthenticationFilter.class);
http.sessionManagement(Customizer.withDefaults());
http.authorizeHttpRequests(authz -> authz.requestMatchers(antMatcher("/**")).permitAll());
return http.build();
}
/**
* Configure {@link AccessDeniedHandler} bean.
* @return Bean of configured {@link AccessDeniedHandler}
*/
@Bean("accessDeniedHandler")
public AccessDeniedHandler accessDeniedHandler() {
LinkedHashMap, AccessDeniedHandler> errorHandlers =
new LinkedHashMap<>();
// Invalid CSRF authenticator error handler
AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
invalidCsrfTokenErrorHandler.setErrorPage("/common/error/invalidCsrfTokenError");
errorHandlers.put(InvalidCsrfTokenException.class, invalidCsrfTokenErrorHandler);
// Missing CSRF authenticator error handler
AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
missingCsrfTokenErrorHandler.setErrorPage("/common/error/missingCsrfTokenError");
errorHandlers.put(MissingCsrfTokenException.class, missingCsrfTokenErrorHandler);
// Default error handler
AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
defaultErrorHandler.setErrorPage("/common/error/accessDeniedError");
return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
}
/**
* Configure {@link DefaultWebSecurityExpressionHandler} bean.
* @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
*/
@Bean("webSecurityExpressionHandler")
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
return new DefaultWebSecurityExpressionHandler();
}
/**
* Configure {@link UserIdMDCPutFilter} bean.
* @return Bean of configured {@link UserIdMDCPutFilter}
*/
@Bean("userIdMDCPutFilter")
public UserIdMDCPutFilter userIdMDCPutFilter() {
return new UserIdMDCPutFilter();
}
}
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | REST API用のSpring Securityのセキュリティ機能を無効にする定義を追加する。
| \ ``WebSecurity.class``\ の\ ``ignoring``\ メソッドに、REST API用のリクエストパスのURLパターンを指定している。
| 本チュートリアルでは\ ``/api/v1/``\ で始まるリクエストパスをREST API用のリクエストパスとして扱う。
.. group-tab:: XML Config
\ ``src/main/resources/META-INF/spring/spring-security.xml``\
.. tabs::
.. group-tab:: JSP
.. code-block:: xml
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | REST API用のSpring Securityのセキュリティ機能を無効にする定義を追加する。
| \ ````\ 要素の\ ``pattern``\ 属性に、REST API用のリクエストパスのURLパターンを指定している。
| 本チュートリアルでは\ ``/api/v1/``\ で始まるリクエストパスをREST API用のリクエストパスとして扱う。
.. group-tab:: Thymeleaf
.. code-block:: xml
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | REST API用のSpring Securityのセキュリティ機能を無効にする定義を追加する。
| \ ````\ 要素の\ ``pattern``\ 属性に、REST API用のリクエストパスのURLパターンを指定している。
| 本チュートリアルでは\ ``/api/v1/``\ で始まるリクエストパスをREST API用のリクエストパスとして扱う。
|
REST API用パッケージの作成
--------------------------------------------------------------------------------
REST API用のクラスを格納するパッケージを作成する。
| REST API用のクラスを格納するルートパッケージのパッケージ名は\ ``api``\ として、配下にリソース毎のパッケージ(リソース名の小文字)を作成する事を推奨する。
| 本チュートリアルで扱うリソースのリソース名はTodoなので、\ ``com.example.todo.api.todo``\ パッケージを作成する。
.. figure:: ./images_TutorialREST/make-package-for-rest.png
.. note::
作成したパッケージに格納するクラスは、通常以下の3種類となる。
作成するクラスのクラス名は、以下のネーミングルールとする事を推奨する。
* \ ``[リソース名]Resource``\
* \ ``[リソース名]RestController``\
* \ ``[リソース名]Helper``\ (必要に応じて)
本チュートリアルで扱うリソースのリソース名がTodoなので、
* \ ``TodoResource``\
* \ ``TodoRestController``\
を作成する。
本チュートリアルでは、\ ``TodoRestHelper``\ は作成しない。
|
Resourceクラスの作成
--------------------------------------------------------------------------------
| Todoリソースを表現する\ ``TodoResource``\ クラスを作成する。
| 本ガイドラインでは、REST APIの入出力となるJSON(またはXML)を表現するJava Beanを\ **Resourceクラス**\ と呼ぶ。
\ ``src/main/java/com/example/todo/api/todo/TodoResource.java``\
.. code-block:: java
package com.example.todo.api.todo;
import java.io.Serializable;
import java.time.LocalDate;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class TodoResource implements Serializable {
private static final long serialVersionUID = 1L;
private String todoId;
@NotNull
@Size(min = 1, max = 30)
private String todoTitle;
private boolean finished;
private LocalDate createdAt;
public String getTodoId() {
return todoId;
}
public void setTodoId(String todoId) {
this.todoId = todoId;
}
public String getTodoTitle() {
return todoTitle;
}
public void setTodoTitle(String todoTitle) {
this.todoTitle = todoTitle;
}
public boolean isFinished() {
return finished;
}
public void setFinished(boolean finished) {
this.finished = finished;
}
public LocalDate getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDate createdAt) {
this.createdAt = createdAt;
}
}
.. note::
DomainObjectクラス(本チュートリアルでは\ ``Todo``\ クラス)があるにも関わらず、Resourceクラスを作成する理由は、クライアントとの入出力で使用するインタフェース上の情報と、業務処理で扱う情報は必ずしも一致しないためである。
これらを混同して使用すると、アプリケーション層の影響がドメイン層におよび、保守性を低下させる。DomainObjectとResourceクラスは別々に作成し、Mapstructを利用してデータ変換を行うことを推奨する。
ResourceクラスはFormクラスと役割が似ているが、FormクラスはHTMLの\ ``