blog

Springハンズオン Spring MVCの高度なテクニック

Extensions - Spring MVC環境を素早くセットアップします。この便利な基本クラスでは、基本的な環境が必要であること、そしてSpring MVCのためのSp......

Dec 17, 2020 · 27 min. read
シェア

Spring MVCの上級テクニック

Spring MVCコンフィギュレーションの代替

DispatcherServletに加えて、追加のServletとフィルタが必要になるかもしれません。DispatcherServlet自体の追加設定が必要になるかもしれません。コンフィギュレーションを従来の web.xml に追加する必要があります。

カスタムDispatcherServletの構成

以下のプログラムの外観からは必ずしも明らかではありませんが、Abstablt-AnnotationConfigDispatcherServletInitializerは、実際には見た目以上のことを実現しています。SpittrWebAppInitializer に書かれている 3 つのメソッドは、オーバーライドする必要がある抽象メソッドだけです。しかし、実際にはもっと多くのメソッドがあり、オーバーロードすることで追加の設定を行うことができます。

@Override
prtected void customizeRegistration(Dynamic registration) {
 registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

customizeRegistration()メソッド内のServletRegistration.Dynamicの助けを借りて、setLoadOnStartup()を呼び出してロードオン起動の優先順位を設定したり、settingInitParameter()を呼び出して初期化パラメータを設定するなど、いくつかのタスクを達成することができます。) は初期化パラメータを設定し、 setMultipartConfig() をコールして Servlet 3.0 のマルチパートのサポートを設定します。前の例では、マルチパートのサポートを設定し、アップロードされたファイルの一時保存ディレクトリを "/tmp/spittr/uploads" に設定しました。

サーブレットとフィルタの追加

Javaベースのイニシャライザの利点の1つは、いくつでもイニシャライザ・クラスを定義できることです。そのため、Web コンテナに追加のコンポーネントを登録したい場合は、新しいイニシャライザを作成するだけです。これを行う最も簡単な方法は、SpringのWebApplicationInitializerインターフェイスを実装することです。

package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynameic;
import org.「スプリング」フレームワーク.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
 @Override
 public void onStartup(ServletContext servletContext) throws ServletException {
 Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
 myServlet.addMapping("/custom/**");
 }
}

上のプログラムはかなり基本的なServlet登録イニシャライザークラスです。これはServletを登録し、パスにマッピングします。この方法でDispatcherServletを手動で登録することも可能です。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
 javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
 filter.addMappingForUrlPatterns(null, false, "/custom/*");
}
@Override
protected Filter[] getServletFilters() {
 return new Filter[] {
 new MyFilter();
 }
}

ご覧のように、このメソッドはjavax.servlet.Filterの配列を返します。ここでは1つのFilterのみを返しますが、実際には任意の数のFilterを返すことができます。マッピングパスをここで宣言する必要はありません。

アプリケーションがServlet 3.0コンテナにデプロイされるのであれば、Springはweb.xmlファイルを作成しなくてもServlet、Filters、Listenerを登録する方法をいくつか提供しています。しかし、上記のオプションを取りたくない場合でも可能です。アプリケーションをServlet 3.0をサポートしないコンテナにデプロイする必要があると仮定すると、従来の方法でweb.xmlを介してSpring MVCを設定することは完全に可能です。

web.xmlでDispatcherServletを宣言します。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://..//-ce" xmlns="http://..///ee"
 xmlns:web="http://..////-__.sd"
 xsi:schemaLocation="http://../// http://..////-__.sd" version="2.5">
 <context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>/WEB-INF/"春"/root-context.xml</param-value>
 </context-param>
 <listener>
 <listener-class>org.「スプリング」フレームワーク.web.context.ContextLoaderListener</listener-class>
 </listener>
 <servlet>
 <servlet-name>appServlet</servlet-name>
 <servlet-class>org.「スプリング」フレームワーク.web.servlet.DispatcherServlet</servlet-class>
 <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
 <servlet-name>appServlet</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
</web-app>

前述したように、ContextLoaderListenerとDispatcherServletはそれぞれSpringアプリケーションコンテキストをロードします。コンテキストパラメータ contextConfigLocation は、ContextLoaderListener によってロードされるルートアプリケーションコンテキストを定義する XML ファイルのアドレスを指定します。上のアプリケーションで示されているように、ルートコンテキストは"/WEB-INF/spring/root-context.xml "からBean定義をロードします。

DispatcherServletはServlet名に基づいてファイルを見つけ、そのファイルに基づいてアプリケーションコンテキストをロードします。上記のアプリケーションでは、サーブレット名はappServletなので、DispatcherServletは"/WEB-INF/appServlet-context.xml "ファイルからアプリケーションコンテキストをロードします。

<servlet>
 <servlet-name>「スプリング」サーブレット</servlet-name>
 <servlet-class>org.「スプリング」フレームワーク.web.servlet.DispatcherServlet</servlet-class>
 <init-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>/WEB-INF/"春"/appServlet/servlet-context.xml</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>

もちろん、上記の全ては、DispatcherServletとContextLoaderListenerがXMLからそれぞれのアプリケーションコンテキストをロードする方法を説明しています。しかし、ここでのほとんどの内容では、XMLのコンフィギュレーションよりもJavaのコンフィギュレーションを使用することを優先しています。そのため、Spring MVCが起動時に@Configurationアノテーションを持つクラスからコンフィギュレーションをロードする必要があります。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://..//-ce" xmlns="http://..///ee"
 xmlns:web="http://..////-__.sd"
 xsi:schemaLocation="http://../// http://..////-__.sd" version="2.5">
 <context-param>
 <param-name>contextClass</param-name>
 <param-value>org.「スプリング」フレームワーク.web.context.support.AnnotationConfigWebApplicationContext</param-value>
 </context-param>
 <context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>com.habuma.spitter.config.RootConfig</param-value>
 </context-param>
 <listener>
 <listener-class>org.「スプリング」フレームワーク.web.context.ContextLoaderListener</listener-class>
 </listener>
 <servlet>
 <servlet-name>appServlet</servlet-name>
 <servlet-class>org.「スプリング」フレームワーク.web.servlet.DispatcherServlet</servlet-class>
 <init-param>
 <param-name>contextClass</param-name>
 <param-value>org.「スプリング」フレームワーク.web.context.support.AnnotationConfigWebApplicationContext</param-value>
 </init-param>
 <init-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>com.habuma.spitter.config.WebConfig</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
 <servlet-name>appServlet</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
</web-app>

マルチパートフォームでのデータ処理

Spittrアプリケーションは2つの場所でファイルのアップロードを必要とします。新規ユーザーがアプリにサインアップするとき、個人情報と関連付けられるイメージをアップロードできるようにします。ユーザーが新しいSpittleを送信するとき、テキストメッセージに加えて写真をアップロードすることができます。

典型的なフォーム送信の結果は、"&"文字で区切られた複数の名前と値のペアという単純なリクエストです。例えば、Spittrアプリケーションで登録フォームを送信する場合、リクエストは次のようになります:

firstName=Charles&lastName=Xavier&email=professorx@40xmen.org&username=professor&password=letmein01

この形式のエンコーディングはシンプルで、典型的なテキストベースのフォーム送信には十分ですが、イメージのアップロードのようなバイナリデータの転送には十分ではありません。対照的に、マルチパート形式のデータはフォームを複数の部分に分割し、それぞれが入力フィールドに対応します。通常のフォームの入力フィールドでは、テキストデータは対応するセクションに置かれますが、ファイルがアップロードされる場合、次のマルチパートのリクエストボディに示されるように、対応するセクションはバイナリにすることができます:

------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"
Charles
------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"
Xavier
------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="email"
charles@xmen.com
------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="username"
professorx
------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="password"
letmein01
------WebKitFormBoundaryqgkabn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg
	[[ Binary image data goes here]]
------WebKitFormBoundaryqgkabn8IHJCuNmiW--

このマルチパートリクエストでは、profilePictureセクションがリクエストの残りの部分とは明らかに異なることがわかります。このマルチパートリクエストでは、profilePicture セクションがリクエストの残りの部分と明らかに異なっていることがわかります。残りのコンテンツに加えて、それは JPEG イメージであることを示す、それ自身の Content-Type ヘッダを持っています。必ずしも明らかではありませんが、profilePicture セクションのリクエストボディはバイナリデータであり、単純なテキストではありません。

マルチパートパーサの設定

DispatcherServletは、マルチパートリクエストデータの解析機能を実装していません。このタスクはSpringのMultipartResolverポリシーインターフェイスの実装に委譲され、実装クラスがマルチパートリクエストの内容を解析します。Spring 3.1 以降、Spring には MultipartResolver の組み込み実装が2つあります:

  • CommonsMultipartResolver: Jakarta Commons FileUploadを使ってマルチパートリクエストを解析します。

一般的に言えば、この2つのうち StandardServletMultipartResolver が望ましいソリューションでしょう。Servletによって提供される機能サポートを使用し、他のプロジェクトに依存する必要はありません。アプリケーションをServlet 3.0コンテナにデプロイする必要がある場合や、Spring 3.1以降を使用していない場合は、CommonsMultipartResolverが必要になるでしょう。

Servlet 3.0でのマルチパートリクエストの解析

Servlet 3.0互換のStandardServletMultipartResolverには、コンストラクタのパラメータも設定するプロパティもありません。そのため、以下のようにSpringアプリケーションコンテキストでBeanとして宣言するのはとても簡単です:

@Bean
public MultipartResolver multipartResolver() throws IOException {
 return new StandardServletMultipartResolver();
}

StandardServletMultipartResolverの制約を設定します。ただ、SpringでStandardServletMultipartResolverを設定する代わりに、サーブレットでマルチパートの設定を指定する必要があります。少なくとも、ファイルのアップロード処理中に書き込まれる一時ファイルのパスを指定する必要があります。StandardServlet-MultipartResolverはこの最小限の設定なしでは動作しません。具体的には、web.xml または Servlet 初期化クラスの DispatcherServlet 構成の一部として、マルチパートの仕様を含める必要があります。

DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/up;oads"));
@Override
protected void custonizeRegistration(Dynamic registration) {
 registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

これまでのところ、MultipartConfigElementコンストラクタは、アップロードされたファイルが一時的に書き込まれるファイルシステム内の絶対ディレクトリを指定する1つのパラメータのみで使用されてきました。しかし、他のコンストラクタを使用して、アップロードされるファイルのサイズを制限することができます。一時パスの場所に加えて、他のコンストラクタが受け付けるパラメータは以下のとおりです:

  • アップロードされるファイルの最大サイズ。デフォルトでは制限はありません。
  • マルチパートリクエスト全体の最大サイズ。デフォルトは無制限です。
  • アップロード中、ファイルサイズが指定された最大容量に達すると、一時ファイルパスに書き込まれます。デフォルト値は 0 で、アップロードされたファイルはすべてディスクに書き込まれます。

ファイルサイズを2MB以下に制限し、リクエスト全体を4MB以下に制限し、すべてのファイルがディスクに書き込まれるようにしたいとします。以下のコードでは、MultipartConfigElement.Switch を使用してこれらのしきい値を設定します:

@Override
protect void customizeRegistration(Dynamic registration) {
 registration.setMultipartConfig(new MultipartConfigElement("tmp/spittr/uploads", 2097152, 4194304, 0));
}

より伝統的な web.xml を使って MultipartConfigElement を設定する場合は、以下のようになります:

<servlet>
 <servlet-name>appServlet</servlet-name>
 <servlet-class>org.「スプリング」フレームワーク.web.servlet.DispatcherServlet</servlet-class>
 <load-on-startup>1</load-on-startup>
 <multipart-config>
 <location>/tmp/spittr/uploads</location>
 <max-file-size>2097152</max-file-size>
 <max-request-size>4194304</max-request-size>
 </multipart-config>
</servlet>

Jakarta Commons FileUpload マルチパート・パーサの設定

通常、StandardServletMultipartResolver が最良の選択ですが、Servlet 3.0 以外のコンテナにアプリケーションをデプロイする必要がある場合は、別の方法が必要になります。MultipartResolver の実装を自分で書くこともできます。Springには、StandardServletMultipartResolverの代替として使える組み込みのCommonsMultipartResolverがあります。

CommonsMultipartResolverをSpring Beanとして宣言する最も簡単な方法は次の通りです:

@Bean
public MultipartResolver multipartResolver() {
 return new CommonsMultipartResolver();
}

StandardServletMultipartResolverとは異なり、CommonsMultipart-Resolverは一時ファイルのパスを強制しません。デフォルトでは、このパスはServletコンテナの一時ディレクトリです。しかし、uploadTempDirプロパティを設定することで、別の場所を指定することができます:

@Bean
public MultipartResolver multipartResolver() throws IOException {
 CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
 multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
 return multipartResolver;
}

実際には、他のマルチパートのアップロード詳細も同じ方法で、つまり CommonsMultipartResolver プロパティを設定することで指定できます。例えば、以下の設定は、前のセクションで MultipartConfigElement を介して設定した StandardServletMultipartResolver と同等です:

@Bean
public MultipartResolver multipartResolver() throws IOException {
 CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
 multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
 multipartResolver.setMaxUploadSize(2097152);
 multipartResolver.setMaxInMemorySize(0);
 return multipartResolver;
}

ここでは、最大ファイルサイズが 2MB に設定され、最大メモリサイズが 0 バイトに設定されています。これら 2 つのプロパティは MultipartConfigElement の 2 番目と 4 番目のコンストラクタ引数に直接対応しており、2MB より大きなファイルはアップロードできず、すべてのファイルはサイズに関係なくディスクに書き込まれることを示しています。しかしながら、MultipartConfigElement とは異なり、マルチパートリクエスト全体の最大サイズを設定することはできません。

マルチパートリクエストの処理

Spittr アプリケーションの登録時にユーザがイメージをアップロードできるようにすると仮定すると、ユーザがアップロードするイメージを選択できるようにフォームを修正し、アップロードされたイメージを受け取るように SpitterController の processRegistration() メソッドを修正する必要があります。Thymeleaf 登録フォームビューの次のコードスニペットは、フォームに必要な変更をハイライトしたものです:

<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
...
 <label>Profile Picture</label>
 <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" /><br/>
...
</form>
このタグは enctype 属性を multipart/form-data に設定し、フォームデータではなくマルチパートデータとして送信するようブラウザに指示します。マルチパートでは、各入力フィールドはパートに対応します。

登録フォームの既存の入力フィールドに加えて、新しい入力フィールドが追加され、タイプはfileで、ユーザーがアップロードするイメージファイルを選択できるようになっています。accept属性は、ファイルタイプをJPEG、PNG、GIFイメージに制限するのに使われます。そのname属性に基づいて、イメージデータはマルチパートリクエストのprofilePicture部分に送られます。

ここで、アップロードされたイメージを受け付けるように processRegistration() メソッドを変更する必要があります。これを行うひとつの方法は、バイト配列のパラメータを追加し、それを @RequestPart でアノテーションすることです。以下はその例です:

@RequestMapping(value="/register", method=POST)
 public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors) {
 	...
 }

登録フォームが送信されると、profilePicture プロパティに、リクエストの対応する部分のデータを含むバイト配列が渡されます。ユーザがファイルを選択せずにフォームを送信した場合、この配列は空になります。イメージデータが取得されると、processRegistration() メソッドの残りのタスクは、ファイルを任意の場所に保存することです。

マルチパートファイルの受け入れ

アップロードされたファイルの生のバイトを使用するのは単純ですが、機能が制限されます。そこで、SpringはMultipartFileインターフェイスも提供し、マルチパートデータを扱うためのリッチなオブジェクトを提供します。次のプログラムは、MultipartFileインターフェースの概要を示しています。

package org.「スプリング」フレームワーク.web.multipart;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public interface MultipartFile {
 String getName();
 String getOriginalFilename();
 String getContentType();
 boolean isEmpty();
 long getSize();
 byte[] getBytes() throws IOException;
 InputStream getInputStream() throws IOException;
 void transferTo(File dest) throws IOException, IllegalStateException;
}

見ての通り、MultipartFileはアップロードされたファイルのバイト数を取得する方法を提供しますが、提供する機能はこれだけにとどまらず、元のファイル名、サイズ、コンテンツタイプを取得することもできます。また、ファイルデータをストリームとして読み込むためのInputStreamも提供します。

これに加えて、MultipartFile は便利な transferTo() メソッドを提供しており、アップロードされたファイルをファイルシステムに書き込むことができます。サンプルとして、以下のコードを process-Registration() メソッドに追加すると、アップロードされたイメージファイルをファイルシステムに書き込むことができます:

profilePicture.c(new File('/data/spittr/' + profilePicture.b()));

ローカル・ファイル・システムへのファイルの保存は非常に簡単ですが、ファイルの管理が必要です。十分な容量を確保し、ハードウェアが故障した場合にファイルをバックアップし、クラスタ内の複数のサーバー間でイメージファイルの同期を行う必要があります。

Amazon S3へのファイル保存

もう一つの選択肢は、これらの処理を誰かに任せることです。数行のコードを追加するだけで、イメージをクラウドに保存することができます。例えば、以下のプログラムリストに示されているsaveImage()メソッドは、アップロードされたファイルをAmazon S3に保存することができ、processRegistration()の中で呼び出すことができます。

private void saveImage(MultipartFile image) throws ImageUploadException {
 try {
 AWSCredentials awsCredentials = new AWSCredentials(s3AccessKey, s2SecretKey);
 S3Service s3 = new RestS3Service(awsCredentials);
 S3Bucket bucket = s3.getBucket("spittrImages");
 S3Object imageObject = new S3Object(image.getOriginalFilename());
 imageObject.setDataInputStream(image.getInputStream());
 imageObject.setContentLength(image.getSize());
 imageObject.setContentType(image.getContentType());
 
 AccessControlList acl = new AccessControlList();
 acl.setOwner(bucket.getOwner());
 acl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ);
 imageObject.setAcl(acl);
 
 s3.putObject(bucket, imageObject);
 } catch (Exception e) {
 throw new ImageUploadException("Unable to save image", e);
 }
}

AWS認証情報の準備ができたら、saveImage()メソッドはJetS3tのRestS3Serviceインスタンスを作成し、それを通してS3ファイルシステムを操作することができます。spitterImagesバケットへの参照を取得し、イメージを格納するために使用されるS3Objectオブジェクトを作成します。

putObject()メソッドを呼び出してイメージデータをS3に書き込んだ後、saveImage()メソッドはS3Objectのパーミッションを設定します。これは重要です - これがなければ、イメージはアプリケーションのユーザーからは見えません。最後に、何か問題が発生するとImageUploadExceptionがスローされます。

アップロードされたファイルを部品として受け入れる場合

アプリケーションをServlet 3.0コンテナにデプロイする必要がある場合、MultipartFileの代わりに、javax.servlet.http.Partをコントローラメソッドのパラメータとして受け取ることができます。MultipartFile の代わりに Part を使用する場合、 processRegistration() のメソッドシグネチャは次のようになります:

@RequestMapping(value="/register", method=POST)
public String processRegistration(@RequestPart("profilePicture") Part profilePicture, @Valid Spitter spitter, Errors errors) {
 ...
}

本体に関する限り、Part インターフェイスは MultipartFile とあまり変わりません。次のプログラムでは、Part インタフェースのいくつかのメソッドが実際に MultipartFile に対応していることがわかります。

package javax.servlet.http;
import java.io.*;
import java.util.*;
public interface Part {
 public InputStream getInputStream() throws IOException;
 public String getContentType();
 public String getName();
 public String getSubmittedFileName();
 public long getSize();
 public void write(String fileName) throws IOException;
 public void delete() throws IOException;
 public String getHeader(String name);
 public Collection<String> getHeaders(String name);
 public Collection<String> getHeaderNames();
}

多くの場合、Part のメソッド名は MultipartFile のメソッド名と同じです。getSubmittedFileName() は getOriginalFilename() に対応しています。同様に、write() は transferTo() に対応し、アップロードされたファイルをファイルシステムに書き込むことができます:

profilePicture.write('/data/spittr/' + profilePicture.c());

コントローラのメソッドが、Part パラメータ形式でファイルのアップロードを受け付けるように書かれている場合は、 MultipartResolver を設定する必要はありません。MultipartResolver が必要になるのは、 MultipartFile を使用する場合のみです。

例外処理

何が起ころうと、良かろうと悪かろうと、Servlet リクエストの出力は Servlet レスポンスです。リクエストの処理中に例外が発生した場合でも、その出力は Servlet レスポンスです。例外は何らかの方法でレスポンスに変換されなければなりません。

Springは例外をレスポンスに変換する方法をいくつか提供しています:

  • 特定のSpring例外は、指定されたHTTPステータスコードに自動的にマッピングされます。
  • 例外に @ResponseStatus アノテーションを追加することで、例外を HTTP ステータスコードにマッピングできます。
  • メソッドに @ExceptionHandler アノテーションを追加して、そのメソッドで例外を処理できるようにします。

例外を処理する最も簡単な方法は、例外を HTTP ステータス・コードに対応付け、それをレスポンスに含めることです。次に、例外を HTTP ステータス・コードにマップする方法を見てみましょう。

例外の HTTP ステータスコードへのマッピング

デフォルトでは、Springはいくつかの例外を自動的に適切なステータスコードに変換します。

BindException400 - Bad Request
ConversionNotSupportedException500 - Internal Server Error
HttpMediaTypeNotAcceptableException406 - Not Acceptable
HttpMediaTypeNotSupportedException415 - Unsupported Media Type
HttpMessageNotReadableException400 - Bad Request
HttpMessageNotWritableException500 - Internal Server Error
HttpRequestMethodNotSupportedException405 - Method Not Allowed
MethodArgumentNotValidException400 - Bad Request
MissingServletRequestParameterException400 - Bad Request
MissingServletRequestPartException400 - Bad Request
NoSuchRequestHandlingMethodException404 - Not Found
TypeMismatchException400 - Bad Request

表7の例外は通常、DispatcherServletの処理に問題があったり、チェックが行われた結果、Spring自身がスローするものです。例えば、DispatcherServletがリクエストを処理する適切なコントローラメソッドを見つけられない場合、NoSuchRequestHandlingMethodExceptionがスローされ、404ステータスコードのレスポンスが返されます。

これらのビルトインマッピングは便利ですが、アプリケーションからスローされる例外には役に立ちません。幸いなことに、Springは@ResponseStatusアノテーションによってHTTPステータスコードに例外をマッピングする仕組みを提供しています。

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
 @PathVariable("spittleId") long spittleId, 
 Model model) {
 Spittle spittle = spittleRepository.findOne(spittleId);
 if (spittle == null) {
 throw new SpittleNotFoundException();
 }
 model.addAttribute(spittle);
 return "spittle";
}

ここでは、SpittleオブジェクトをSpittleRepositoryからIDで取得します。findOne()メソッドがSpittleオブジェクトを返すことができれば、Spittleはモデルに入れられ、spittleという名前のビューがレスポンスへのレンダリングを担当します。しかし、findOne()メソッドがnullを返した場合、SpittleNotFoundExceptionがスローされます。SpittleNotFoundExceptionは、以下のようにチェックされない単純な例外です:

package spittr.web;
public class SpittleNotFoundException extends RuntimeException {
}

リクエストを処理するために spittle() メソッドが呼び出され、指定された ID に対して得られた結果が NULL の場合、SpittleNotFoundException は 500 ステータス・コードのレスポンスを生成します。実際、マッピングされていない例外が発生した場合、レスポンスは500のステータスコードを持ちますが、このデフォルトの動作はSpittleNotFoundExceptionをマッピングすることで変更できます。

SpittleNotFoundExceptionがスローされる場合、これは要求されたリソースが見つからないシナリオです。リソースが見つからなかった場合、HTTPステータスコード404が最も正確なレスポンスステータスコードです。したがって、@ResponseStatus アノテーションを使用して、SpittleNotFoundException を HTTP ステータス・コード 404 にマッピングします。

package spittr.web;
import org.「スプリング」フレームワーク.http.us;
import org.「スプリング」フレームワーク.web.bind.annotation.ResponseStatus;
@ResponseStatus(value=HttpS._ND, reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

ResponseStatusアノテーションを導入すると、 コントローラメソッドがSpittleNotFound-Exceptionをスローした場合、 レスポンスのステータスコードは404になります。

例外処理用のメソッドの記述

例として、ユーザが作成しようとしているSpittleが既に作成されているSpittleと全く同じテキストを持っていると仮定すると、SpittleRepositoryのsave()メソッドはDuplicateSpittle例外をスローします。つまり、SpittleControllerのsaveSpittle()メソッドはこの例外を処理する必要があります。以下のプログラムに示すように、saveSpittle() メソッドはこの例外を直接処理することができます。

@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
 try {
 spittleRepository.save(new Spittle(null, form.getMessage(), new Date(), 
 form.getLongitude(), form.getLatitude()));
 return "redirect:/spittles";
 } catch (DuplicateSpittleException e) {
 return "error/duplicate";
 }
}

実行しても問題はありませんが、このメソッドは少し複雑です。このメソッドは2つのパスを持つことができ、それぞれの出力は異なります。saveSpittle()メソッドが正しいパスにのみフォーカスし、他のメソッドが例外を処理するようにした方がシンプルでしょう。

@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
 spittleRepository.save(new Spittle(null, form.getMessage(), new Date(), 
 form.getLongitude(), form.getLatitude()));
 return "redirect:/spittles";
}

ご覧のとおり、saveSpittle() メソッドはよりシンプルです。Spittleを正常に保存することだけにフォーカスしているため、実行パスが1つしかなく、理解しやすくなっています。

@ExceptionHandler(DuplicateSpittleException.class)
public String handleNotFound() {
 return "error/duplicate";
}

handleDuplicateSpittle()メソッドには@ExceptionHandlerアノテーションが追加されており、DuplicateSpittleException例外がスローされたときに委譲されます。これは、リクエストを処理するメソッドと一貫性のある String を返し、レンダリングされる論理ビューの名前を指定することで、重複エントリを作成しようとしていることをユーザーに伝えることができます。

ExceptionHandler アノテーションを使用したメソッドの興味深い点は、 同じコントローラ内のすべてのハンドラメソッドがスローした例外を処理できることです。つまり、 handleDuplicateSpittle() メソッドは saveSpittle() からコードを取得して作成されているにもかかわらず、 SpittleController 内のすべてのメソッドからスローされる DuplicateSpittleException 例外を処理することができます。DuplicateSpittleException をスローする可能性のあるすべてのメソッドに例外処理コードを追加する代わりに、 このメソッドひとつですべてをカバーできます。

ExceptionHandlerアノテーションでマークされたメソッドは、同じコントローラクラス内のすべてのハンドラメソッドの例外を処理できます。Spring 3.2では、コントローラの通知クラスで定義するだけで、例外を処理できるようになりました。

コントローラへの通知の追加

コントローラクラスの特定の切り出しを、アプリケーション全体のすべてのコントローラに適用できれば、もっと簡単です。たとえば、複数のコントローラで例外を処理する場合は、 @ExceptionHandler アノテーションで指定したメソッドが便利です。しかし、特定の例外が複数のコントローラクラスでスローされる場合は、 すべてのコントローラメソッドで同じ @ExceptionHandler メソッドを繰り返す必要があります。あるいは、重複を避けるために基底コントローラクラスを作成し、 すべてのコントローラクラスはこのクラスを継承して汎用的な @ExceptionHandler メソッドを継承する必要があります。

Spring 3.2では、このような問題に対する新しい解決策として、コントローラ通知(Controller Notification)が導入されました。コントローラ通知とは、@ControllerAdviceアノテーションを持つクラスで、次のようなメソッドを1つ以上持ちます:

  • ExceptionHandler アノテーションのメソッド
  • InitBinder アノテーションを持つメソッド。
  • ModelAttribute アノテーションを持つメソッド。

ControllerAdvice アノテーションを指定したクラスでは、 アプリケーション全体のすべてのコントローラで @RequestMapping アノテーションを指定したメソッドに上記のメソッドが適用されます。

ControllerAdvice アノテーション自身はすでに @Component を使用しているので、 @ControllerAdvice アノテーションでアノテーションされたクラスは、 @Component でアノテーションされたクラスと同様に自動的にコンポーネントスキャンで取得されます。

ControllerAdvice のもっとも有用なシナリオのひとつは、すべての @ExceptionHandler メソッドをひとつのクラスにまとめて、 すべてのコントローラの例外を一箇所で一貫して処理できるようにすることです。たとえば、アプリケーション全体のすべてのコントローラに DuplicateSpittleException ハンドラメソッドを適用したいとします。これは @ControllerAdvice アノテーションを使用したクラスです。

package spittr.web;
import org.「スプリング」フレームワーク.web.bind.annotation.ControllerAdvice;
import org.「スプリング」フレームワーク.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class AppWideExceptionHandler {
 @ExceptionHandler(DuplicateSpittleException.class)
 public String duplicateSpittleHandler() {
 return "error/duplicate";
 }
}

これで、コントローラのメソッドが DuplicateSpittleException をスローした場合は、 そのメソッドがどのコントローラにあるかにかかわらず、 この duplicateSpittleHandler() メソッドがコールされて例外が処理されるようになります。ExceptionHandler アノテーションを付けたメソッドも、 @RequestMapping アノテーションを付けたメソッドと同じように書くことができます。上のアプリケーションで示したように、論理ビュー名として "error/duplicate" を返します。

リダイレクトリクエスト間でのデータの受け渡し

POST リクエストを処理した後は、しばしばリダイレクトを行うのがベストプラクティスです。これは、ユーザーがブラウザの更新ボタンや戻る矢印をクリックしたときに、 クライアントが危険な POST リクエストを再実行しないようにするためです。

prefix "redirect:" の威力は、コントローラメソッドが返すビュー名で発揮されます。コントローラメソッドが返す文字列が "redirect:" で始まっている場合は、 その文字列はビューを探すために使用されるのではなく、 ブラウザをリダイレクトパスに導くために使用されます。

return 'redirect:/spitter/' + spitter.a();

redirect: "はリダイレクトをとてもシンプルにします。Springがリダイレクトをこれ以上シンプルにすることはできない、と思うかもしれません。しかしちょっと待ってください。Springはリダイレクトのために他にもいくつかの機能を提供しています。

一般的に、ハンドラメソッドが完了すると、そのメソッドで指定されたモデルデータがリクエストにコピーされ、リクエストの属性として使用され、レンダリングのためにビューに転送されます。コントローラのメソッドとビューは同じリクエストを処理するので、 リクエストのプロパティは転送処理中も保持されます。

コントローラがリダイレクトを行うと、元のリクエストは終了し、 新しい GET リクエストが開始します。元のリクエストのモデルデータは、リクエストと共に破棄されます。新しいリクエスト属性では、モデルデータはなく、リクエスト自身がデータを計算しなければなりません。

明らかに、リダイレクトでは、モデルはデータを渡すために使われません。しかし、リダイレクトを開始したメソッドからリダイレクトを処理するメソッドにデータを渡すための他のオプションがあります:

  • パス変数やクエリパラメータの形式でデータを渡すために URL テンプレートを使うこと
  • フラッシュ属性によるデータの送信

URLテンプレートによるリダイレクト

パス変数やクエリパラメータの形でデータを渡すのはとても簡単なように見えます。例えば、新しく作成されたSpitterのユーザー名はパス変数として渡されます。 しかし、現在の記述ではユーザー名の値は直接リダイレクト文字列に接続されています。これは正しく動作しますが、問題がないわけではありません。URLやSQLクエリ文を構築する際に、Stringの連結を使用するのは危険です。

return "redirect:/spitter/{username}";

Stringを連結してリダイレクトURLを構築するだけでなく、Springはテンプレートを使ってリダイレクトURLを定義する方法も提供しています。例えば、processRegistration()メソッドの最後の行は次のように書き換えることができます:

@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitter, Model model) {
 spitterRepository.save(spitter);
 model.addAttribute("username", spitter.getUsername());
 return "redirect:/spitter/{username}";
}

例えば、processRegistration()メソッドの最後の行は次のように書き換えることができます。しかし、モデルの spitterId 属性はリダイレクト URL のプレースホルダのどれとも一致しないため、クエリパラメータとして自動的にリダイレクト URL に追加されます。

username 属性の値が habuma で spitterId 属性の値が 42 の場合、リダイレクト URL のパスは "/spitter/habuma?spitterId=42" となります。

パス変数とクエリパラメータでリダイレクト先にデータを渡すのはシンプルで簡単ですが、いくつかの制限があります。文字列や数値といった単純な値しか送ることができません。より複雑な値を URL で送信する方法はありませんが、flash 属性が役に立ちます。

flash属性の使用法

Springは、リダイレクトをまたいで生き残るデータをセッションに入れることも素晴らしい方法だと考えています。しかし、Springはこのデータを管理する必要はないと考えています。代わりに、Springはデータをflash属性として送信する機能を提供します。定義上、flash属性は次のリクエストまでこのデータを保持し、その後消えます。

Springは、Spring 3.1で導入されたModelのサブインターフェイスであるRedirectAttributesを通してフラッシュ属性を設定するメソッドを提供します。

@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitter, Model model) {
 spitterRepository.save(spitter);
 model.addAttribute("username", spitter.getUsername());
 model.addFlashAttribute("spitter", spitter);
 return "redirect:/spitter/{username}";
}

ここでは、addFlashAttribute()メソッドがspitterをキー、Spitterオブジェクトを値として呼び出されます。あるいは、key パラメータを未設定のままにしておき、値の型に基づいてキー自身を導出させることも可能です:

model.addFlashAttribute("spitter", spitter);

SpitterオブジェクトがaddFlashAttribute()メソッドに渡されるので、推測されるキーはspitterになります。

リダイレクト実行時に、すべてのフラッシュ属性がセッションにコピーされます。リダイレクト後、セッションに存在するフラッシュ属性が取り出され、セッションからモデルに転送されます。リダイレクトを処理するメソッドは、他のモデルオブジェクトと同じように、モデルからSpitterオブジェクトにアクセスすることができます。

フラッシュプロパティのフローを完成させるために、以下はshowSpitterProfile()メソッドの更新バージョンを示しています:

@RequestMapping(value="/{username}", method=GET)
public String showSpitterProfile(
 @PathVariable String username, Model model) {
 if (!model.containsAttribute("spitter")) {
 model.addAttribute(
 spitterRepository.findByUsername(username));
 }
 return "profile";
}

ご覧のように、showSpitterProfile() メソッドが最初に行うことは、spitter というキーを持つモデル・プロパティがあるかどうかをチェックすることです。モデルに spitter 属性が含まれていれば、何もする必要はありません。ここに含まれる spitter オブジェクトは、レンダリングのためにビューに渡されます。しかし、モデルに spitter 属性が含まれていない場合、showSpitterProfile() はリポジトリから spitter を検索し、モデルに格納します。

Read next

scrapyフレームワーク

スパイダーのデータ抽出は、直接辞書を得ることができますが、item クラスと比較すると、フィールドの欠落チェックが不足しています。

Dec 17, 2020 · 7 min read