話題の記事

第1回 はじめてのSpring Framework

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

【求人のご案内】Javaアプリ開発エンジニア募集

今やすっかりAWS屋、しかもアプリではなくインフラ寄りのプロダクトばかり触っている都元です。しかし元々はサーバサイドアプリ屋ということで、ボスのAWSへの想いとは裏腹に、ぼちぼちとサーバサイドJavaの話も出して行こうと思っています。

というわけで、Spring Frameworkについて色々書いて行こうと思うのですが、どう考えても1回で終わる内容ではないため、シリーズ形式(連載)とさせて頂きたいと思います。ただ、書くネタは無限にありそうなので、回数は反響に応じて調整しようかな、と思っています。ギブミー・いいね。

Javaフレームワークの世界

Javaはフレームワークがいっぱいあることが利点でもあり欠点でもあります。多くの言語にはデファクトと言えるフレームワークが存在します。あまり知らない分野なので深く触れてヤケドしたくはないのですが、例えばRubyだったらRailsでしょうし、PythonだったらDjangoでしょう、PHPだったらCakePHPでしょう的な、だいたいの流れがあるように感じます。しかし、Javaと言えば? Strutsと言われてしまうのでしょうかw 個人的にWicketは好きですが、残念ながらメジャーだとは到底思えません。JSFは「JavaEE標準」ではありますが「デファクトスタンダード=事実上の標準」の座を押さえてはいないでしょう。そうなんです。今ひとつ絞りきれないのです。

また、先に挙げた他言語のフレームワークの多くはフルスタックフレームワークです。つまり、HTMLテンプレート処理や、画面遷移コントローラから、データの永続化まで、Webアプリに必要とされる幅広い機能を抽象化し、統合して提供しています。それに対して多くのJavaフレームワークは、単体機能での提供になります。前述のStrutsはコントローラ専門ですし、Wicketはテンプレート+コントローラ、Hibernateは永続化専門です。つまり、機能毎に複数のフレームワークを併用して統合していく必要があります。これは柔軟である反面、正直メンドクサイことでもあります。

Spring Framework

そんな中でのSpring Frameworkですが。Springも、デファクトの地位を確立しているフレームワークではありません。しかし、個人的に非常に有力なフレームワークとして、長い間注目しています。では、Springはどのレイヤに対するフレームワークなのでしょうか。

広義のSpringは、フルスタックフレームワークとも言える(微妙に足りない部分はあるのですが…)と思います。Springは、1つのフレームワークではなく、複数のコンポーネントの集合体です。その全体を「1つのフルスタックフレームワーク」だと捉える考え方です。

狭義のSpringは、IoCコンテナ(Inversion of Controlコンテナ)、またの名をDIコンテナ(Dependency Injectionコンテナ)のことを指します。本シリーズではDIコンテナで用語統一します。前述の通り、Springは多くのコンポーネントから成っています。つまり「様々なレイヤに対するフレームワークコンポーネント」を各種取り揃えています。これには例えば以下のようなものがあります。

  • spring-webmvc: 画面遷移コントローラ
  • spring-jdbc: データアクセス
  • spring-security: 認証や権限回り
  • spring-batch: バッチ処理

適当に4つほど挙げましたが、この他にも、OAuthの実装やソーシャルなサービスとの連携だったり、正直数えきれないほどのコンポーネントがあります。

狭義のSpringであるDIコンテナは、これらのコンポーネントを組み合わせて「統合するためのフレームワーク」という位置づけです。統合と言われても抽象的でさっぱりイメージが沸かないかもしれません。「O/Rマッパ」や「コントローラ」等は目的が具体的で認識しやすい機能ですが、「何をもってして "統合できた" と考えるのか」がとても曖昧です。説明するのも正直難しいです。

最初のうちは、DIコンテナの具体的な機能を知っても、統合についてを意識出来ないまま過ごす時期があるかもしれません。しかし、これらの機能を利用するうちに、結果としていつの間にか「各フレームワークが上手く統合できている」と感じられるようになるはずです。従って、まずは統合という側面を考えずに、DIコンテナには便利機能が色々あるんだなぁ、という認識から始めれば良いと思います。

ちなみに、DIコンテナによる統合の対象は、上に挙げたようなSpring製のコンポーネントだけではありません。外部のフレームワークを統合するのにも非常に役に立つフレームワークです。フルSpring体制で全てを作り上げても構いませんし、一部だけ外部のフレームワークと入れ替えても構いません。数多くの外部のフレームワーク群を採用し、これらの統合にDIコンテナを使うだけでも構いません。

…と言ってしまうと、やはりJavaのフレームワークは選択が大変だということになってしまいますが。Javaのその一面は正直認めざるを得ないかな、と思っています。それはさておき、Springは素晴らしいJavaフレームワーク、という、ごく個人的な想いを本シリーズにぶつけつつ、その様々な機能をご紹介しようと思います。

DIコンテナ

では早速。DIコンテナとは一体なんでしょうか。Java用語は、なんだかコンテナという言葉が大好きですね。Webコンテナ(Servlet/JSPコンテナ)とかEJBコンテナとか。containerという英単語は、容器・入れ物という意味なので、これだけではさっぱり理解できません。私も正解かどうか不安なのですが、ざっくり言ってしまえば「◯◯をホスティングする環境」のことを◯◯コンテナと呼ぶ、といった理解をしています。つまり、DIコンテナという言葉は「DI機能をホスティングする環境」ということでしょう。

要するに、DIと呼ばれる機能を提供してくれるものなので、単純にDIフレームワークと言っても良いものだと思います。よしわかった。DIってどんな機能だ。…そろそろ眠くなってきませんか。多分最初は説明を聞いてもなにが嬉しいのかはサッパリ理解できないものなので、もうDIという言葉の意味はさておき、実例いきましょう。

インスタンスの生成管理

public class Main {
  
  public static void main(String[] args) {
    Main main = new Main();
    main.execute();
  }
  
  private void execute() {
    System.out.println("foo");
  }
}
public class SpringMain {
  
  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml")) {
      SpringMain main = context.getBean(SpringMain.class);
      main.execute();
    }
  }
  
  private void execute() {
    System.out.println("foo");
  }
}

// このコードの実行には、applicationContext.xml というファイルが必要になります(後述)

2つのコードの違いに注目してください。前者がDIコンテナを利用しないコード、後者がDIコンテナを利用したコードです。ConfigurableApplicationContextがまさに「DIコンテナ」だと思ってもらって構いません。今の所は、ただ複雑になっただけで、まだDIコンテナを使った嬉しさはありません。ちなみにtryで囲っているのは、ConfigurableApplicationContextを自動的にcloseするためです。Java7のtry-with-resourcesの構文ですね。(これは本質ではない話ですが)

さて、これらのコードの違いとして着目すべきは、メインクラスのインスタンス生成、すなわち new を自前で行っているか、DIコンテナに任せているか、というところです。Javaアプリケーションは、その実行に際してに様々なインスタンスを生成し、利用し、そして破棄(GC)していきます。その際、以下のような課題があります。

  • インスタンスをどのタイミングで誰が生成するのか
  • インスタンスをどのタイミングで誰が初期化するのか
  • 生成・初期化したインスタンスは誰が保持するのか
  • どこかで生成・初期化済みのインスタンスを必要とする場合、どのように手に入れるのか
  • インスタンスはいつ破棄される(GC対象となる)のか

通常、このようなインスタンス及びそのライフサイクル管理は、アプリケーション自身で行います。これに対して、インスタンス管理と、そのライフサイクル管理をしてくれる、というのがDIコンテナののが主な役割です。つまり、インスタンスの「入れ物」的な働きを担っているため、コンテナと呼ばれるのだと思います。

DIコンテナを使った場合、コンテナに対して「このクラスのインスタンスくれー」と要求(getBean)すると、くれるのです。確かに、コンテナに対してgetBeanメソッドを呼ぶことによって、メインクラスを返してくれているようです。ちなみに、Springが管理する(≒Springによって生成された)インスタンスのことを、Spring用語では「bean」と呼びます。

サンプルプロジェクト berserker v1.0

2015-07-13追記:このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。初回実行時には色々表示も異なり、実行開始まで時間が掛かります。execute1タスクが、Mainクラスの実行、execute2タスクがSpringMainクラスの実行です。ログ出力は少々異なりますが、どちらもfooという出力がされていますね。

$ git clone https://github.com/classmethod-sandbox/berserker.git
$ cd berserker
$ git checkout 1.0
$ ./gradlew execute1
:compileJava
:processResources
:classes
:execute1
foo

BUILD SUCCESSFUL

Total time: 1.853 secs

$ ./gradlew execute2
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:execute2
4 08, 2016 4:54:18 午後 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@c4437c4: startup date [Fri Apr 08 16:54:18 JST 2016]; root of context hierarchy
4 08, 2016 4:54:18 午後 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from class path resource [applicationContext.xml]
foo
4 08, 2016 4:54:18 午後 org.springframework.context.support.ClassPathXmlApplicationContext doClose
情報: Closing org.springframework.context.support.ClassPathXmlApplicationContext@c4437c4: startup date [Fri Apr 08 16:54:18 JST 2016]; root of context hierarchy

BUILD SUCCESSFUL

Total time: 1.752 secs

さて、そんなん、わざわざSpringに生成管理してもらわなくても、単純に自分でnewすりゃいいじゃん、と思うかもしれません。では次の例を見てみましょう。

インスタンスの初期化管理

public class QueueWorker {
  
  private static final String QUEUE_URL="...";
  
  public static void main(String[] args) {
    QueueWorker main = new QueueWorker();
    
    ClasspathPropertiesFileCredentialsProvider provider = new ClasspathPropertiesFileCredentialsProvider("aws.properties");
    AmazonSQSClient amazonSQSClient = new AmazonSQSClient(provider);
    main.sqs = amazonSQSClient;
    
    JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
    mailSender.setHost("smtp.example.com");
    mailSender.setPort(25);
    mailSender.setUsername("daisuke");
    mailSender.setPassword("p@ssw0rd");
    main.mailSender = mailSender;
    
    SimpleMailMessage mailTemplate = new SimpleMailMessage();
    mailTemplate.setSubject("QueueWorker sample");
    mailTemplate.setFrom("daisuke@example.com");
    main.mailTemplate = mailTemplate;
    
    main.execute();
  }
  
  private AmazonSQS sqs;
  private MailSender mailSender;
  private SimpleMailMessage mailTemplate;
  
  private void execute() {
    ReceiveMessageResult result = sqs.receiveMessage(new ReceiveMessageRequest(QUEUE_URL));
    List<SimpleMailMessage> mails = Lists.newArrayList();
    for (Message message : result.getMessages()) {
      SimpleMailMessage mail = new SimpleMailMessage(mailTemplate);
      mail.setTo(message.getAttributes().get("to"));
      mail.setText(message.getBody());
      mails.add(mail);
      sqs.deleteMessage(new DeleteMessageRequest(QUEUE_URL, message.getReceiptHandle()));
    }
    mailSender.send(mails.toArray(new SimpleMailMessage[mails.size()]));
  }
}

このプログラムは、Amazon SQSからメッセージを取り出し、その内容に従ってメールを送信するプログラムです。executeメソッドの実行に際しては、AmazonSQSMailSender等、適切に設定されたいくつかのオブジェクトが必要です。言い換えれば、QueueWorkerAmazonSQSMailSenderに依存しています。というわけで、事前にこれらのインスタンスを生成し、初期化し、フィールドに代入してからexecuteを呼んでいます。

しかし、この「生成」や「初期化」のコードは、このプログラムの本質ではありません。本質は「実行」の部分(executeメソッド)にありますが、それがぼやけてしまいます。また、設定はプログラム(手続き)として書くのではなく、設定ファイル(宣言)として書いた方がスッキリするのではないでしょうか。

そこでSpringにインスタンスを管理してもらうと、このようなプログラム(下記に示したmainメソッド以外は同じ)になります。

  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml")) {
      SpringQueueWorker main = context.getBean(SpringQueueWorker.class);
      main.sqs = context.getBean(AmazonSQS.class);
      main.mailSender = context.getBean(MailSender.class);
      main.mailTemplate = context.getBean(SimpleMailMessage.class);
      main.execute();
    }
  }

そして、プログラム上から消えてしまった生成と初期化の情報は、Springの設定ファイルであるapplicationContext.xmlの中に移動しました。Javaが手続き(処理)を担い、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"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="worker" class="jp.classmethod.example.berserker.SpringQueueWorker" />

  <bean id="amazonSQSClient" class="com.amazonaws.services.sqs.AmazonSQSClient">
    <constructor-arg>
      <bean class="com.amazonaws.auth.profile.ProfileCredentialsProvider">
        <constructor-arg value="default"/>
      </bean>
    </constructor-arg>
  </bean>

  <bean id="javaMailSenderImpl" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="smtp.example.com" />
    <property name="port" value="25" />
    <property name="username" value="daisuke" />
    <property name="password" value="p@ssw0rd" />
  </bean>

  <bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
    <property name="subject" value="QueueWorker sample"/>
    <property name="from" value="daisuke@example.com"/>
  </bean>

</beans>

ちなみに、ソフトウェア設計の世界ではしばしば「手続き的(procedural)プログラミング」「宣言的(declarative)プログラミング」という言葉が出てきます。前者は「まずAについてfooを実行、次にBについてbarを実行…」というように、処理順序を意識した思考でプログラミングすることを表します。後者は「Aはfooであり、そしてBはbarである」というように、各要素がどんな性質を持っているのかを、順序を問わず定義していくという思考でプログラミングをします。

さて、少し話が逸れました。ここで気づいた人がいるかもしれません。AmazonSQSClientMailSenderの初期化が宣言的にできるのならば、SpringQueueWorkerの初期化(3つのフィールドへの代入)もできるのでは? そうすれば、mainメソッドはこのようになるのでは?

SpringQueueWorker main = context.getBean(SpringQueueWorker.class);
main.execute();

その通り。正解です。workerの宣言を以下のようにすればいいのです。(value属性は直接文字列で値を指定するものですが、今回はref属性でbeanのidを指定していることに注意してください)

  <bean id="worker" class="jp.classmethod.example.berserker.SpringQueueWorker">
    <property name="sqs" ref="amazonSQSClient" />
    <property name="mailSender" ref="javaMailSenderImpl" />
    <property name="mailTemplate" ref="templateMessage" />
  </bean>

但し、SpringがSpringQueueWorkerに3つのインスタンスを設定代入するためのsetterが、それぞれ必要になります。

サンプルプロジェクト berserker v1.1

2015-07-13追記:このコードを、GitHubに上げておきました。executeタスクが、SpringMainクラスの実行です。とは言え、実行する際はQUEUE_URLフィールドをきちんと定義してからでないと、例外終了します。

$ git clone https://github.com/classmethod-sandbox/berserker.git
$ cd berserker
$ git checkout refs/tags/1.1
$ ./gradlew execute
:compileJava
:processResources
:classes
:execute
4 08, 2016 4:55:30 午後 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6433a2: startup date [Fri Apr 08 16:55:30 JST 2016]; root of context hierarchy
4 08, 2016 4:55:31 午後 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from class path resource [applicationContext.xml]
4 08, 2016 4:55:33 午後 org.springframework.context.support.ClassPathXmlApplicationContext doClose
情報: Closing org.springframework.context.support.ClassPathXmlApplicationContext@6433a2: startup date [Fri Apr 08 16:55:30 JST 2016]; root of context hierarchy
Exception in thread "main" com.amazonaws.services.sqs.model.QueueDoesNotExistException: The specified queue does not exist for this wsdl version. (Service: AmazonSQS; Status Code: 400; Error Code: AWS.SimpleQueueService.NonExistentQueue; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
	at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1389)
	at com.amazonaws.http.AmazonHttpClient.executeOneRequest(AmazonHttpClient.java:902)
	at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:607)
	at com.amazonaws.http.AmazonHttpClient.doExecute(AmazonHttpClient.java:376)
	at com.amazonaws.http.AmazonHttpClient.executeWithTimer(AmazonHttpClient.java:338)
	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:287)
	at com.amazonaws.services.sqs.AmazonSQSClient.invoke(AmazonSQSClient.java:1651)
	at com.amazonaws.services.sqs.AmazonSQSClient.receiveMessage(AmazonSQSClient.java:1314)
	at jp.classmethod.example.berserker.SpringQueueWorker.execute(SpringQueueWorker.java:47)
	at jp.classmethod.example.berserker.SpringMain.main(SpringMain.java:26)
:execute FAILED

まとめ

さて、上の例でDIコンテナは何をしたのでしょうか。SpringQueueWorkerに対して、AmazonSQS,MailSender,SimpleMailMessageを、設定代入しました。言い換えると、依存性(Dependency)を注入(Injection)しました。これがDIです。

そして、依存とはインスタンスに対してのみ存在するものではありません。JavaMailSenderImplは「設定値に依存している」のです。従って、DIコンテナは、JavaMailSenderImplに設定値という依存性を注入しています。

なんとなく掴めたでしょうか。DIコンテナは、この依存性注入機能によって、設定ファイルに従って、インスタンスを設定済みの状態まで持って行く役割があります。そして、そのインスタンスをbeanとして管理し、要求に従って渡してくれるのです。

さてさて、シリーズ初回から結構重い内容になってしまいました。しかも、今どきらしからぬ重量級のXMLが登場する、という印象の悪い感じで終わるのも少々不本意ですが…。XMLをそこまで毛嫌いする必要もないと思うんです。実際、今回紹介したapplicationContext.xmlの作成は、IDEの補完機能を駆使すれば、キーストロークも少なく、3分程度で作れてしまうものです。

とは言え、このXMLによる "重さ" は、Springのまだ紹介していない機能によってより軽減され、もっとライトウェイトに扱えるようになっています。最初からあれこれと詰め込み過ぎても大変なので、今回は「SpringによるDIの基本」を説明しただけに過ぎません。

というわけで次回以降のエントリーにもご期待下さい。ブクマやいいねをいっぱい頂けるほど、次のエントリのお届けが早くなると思いますw 続きが読みたい方は、ページの上の方でぜひポチっとしていただいて、応援お願いします。