ちょっと話題の記事

第2回 Springの様々な設定記述 – AnnotationもJavaもあるんだよ

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

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

よく訓練されたアップル信者、都元です。前回は、Springのコードを見ながらDIについて学びました。DIを使うと、「オブジェクトの生成と初期化」という宣言的な記述に親和性の高い情報と、「オブジェクトの利用」という手続き的な記述に親和性の高い情報を分離できることが確認出来ました。

Springはbean(Springの管理下にあるインスタンス)の生成と初期化に関する情報(Configuration metadataと呼びます)をXMLで受け取ります。XMLの一例は前回示した通りですが、このConfiguration metadataは色々な記述方法があります。

p及びcネームスペースの使用によるSpringの設定

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:p="http://www.springframework.org/schema/p"
     xmlns:c="http://www.springframework.org/schema/c"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">

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

  <bean id="credentialsProvider" class="com.amazonaws.auth.profile.ProfileCredentialsProvider"
      c:profileName="default" />
  <bean id="amazonSQSClient" class="com.amazonaws.services.sqs.AmazonSQSClient"
      c:awsCredentialsProvider-ref="credentialsProvider" />

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

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

</beans>

まず、beans要素の属性において、4〜5行目でp及びcネームスペースの定義を追加しています。ネームスペースについて良く知らなくても、ひとまずこのように書けば、p:c:で始まる要素や属性を利用できるようになる、と理解しておいて下さい。これ以上の詳細については本稿の範疇を超えますので、興味のある方は各自「XML名前空間」について調べてみましょう。

前回はbean要素の子要素として記述したプロパティやコンストラクタ引数の定義でしが、p及びcというネームスペースの属性を利用すれば、このように書く事ができます。どちらが好きか、好みで決めれば良いでしょう。この記述であれば、ひとまずこの例だと閉じタグはbeans1つだけにできます。ハイ、そういう問題じゃないですね。

アノテーションによるSpringの設定

Springは、一部のbean定義をアノテーションで設定することができます。一部というのは、自分からアノテーションを付与できるクラス、ということです。今回の例では、SpringQueueWorkerは自分のコードなのでアノテーションを付与できますが、その他のクラスは自分のコードではありません。これらのクラスのソースコードは自分の配下にないため、勝手にアノテーションを付与できないため、アノテーションによる設定はできません。

というわけで、workerの設定をアノテーションによって行ってみます。まず、pcネームスペースの時と同じように、contextネームスペースの宣言を追加(6行目)します。次に、Springに対して「アノテーションによる設定を有効にする」と宣言(11行目)します。そしてapplicationContext.xmlのworkerの定義のうち、プロパティ(フィールド)設定部分を省略してしまいましょう。

<?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:p="http://www.springframework.org/schema/p"
    xmlns:c="http://www.springframework.org/schema/c"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">

  <context:annotation-config />
  <bean id="worker" class="jp.classmethod.example.bersesrker.berserker.SpringQueueWorker" />

  <!-- 略 -->

</beans>

続いて、SpringQueueWorkerの3つのフィールドに、@Autowiredアノテーションを付与します。さらに、この作業に伴い、これらのsetterも削除してしまって構いません。

  @Autowired
  private AmazonSQS sqs;
  
  @Autowired
  private MailSender mailSender;
  
  @Autowired
  private SimpleMailMessage mailTemplate;

以上です。最初に紹介したXML-basedに対して、このような設定方法をAnnotation-based configurationと呼びます。

autowiredというのは、敢えて日本語に訳せば「自動紐付け」でしょうか。通常、DIにあたっては、どのプロパティ(フィールド)にどのbeanを代入するのかを、設定によって明示する必要があります。しかし、autowired機能を利用すると、型情報を利用して、DIすべきbeanを自動的に判断し、代入してくれます。

この例では、まずsqsフィールドに@Autowiredがついています。そこでSpringはこのプロパティに何らかのbeanをDIする必要があると判断します。そこで、コンテナ内からAmazonSQS型を持つbeanを検索します。その結果、amazonSQSClientが該当するので、これをDIします。mailSendermailTemplateについても同様です。

ちなみに、該当する型を持つbeanが見つからなかった場合や、該当する方を持つbeanが複数見つかってしまった場合は、applicationContext生成の段階で例外が発生します。

コンポーネントスキャン

Annotationを利用することで、workerのbean定義をシンプルにすることができました。ここで、さらにもう一つテクニックを使う事で、XML内からworkerの定義を追い出すことができます。

ここではコンポーネントスキャンという機能を使います。Springに、指定したパッケージ(及びそのサブパッケージ)のクラスを全て走査させ、そのうち@Componentアノテーションがついたクラスをbeanとして登録する、という機能です。その他、@Service等、いくつか同等の役割を持つアノテーションがあるが、基本は@Componentです。

applicationContext.xmlに、コンポーネントスキャンの指定を追加し、スキャン対象のパッケージを指定します。その上で、workerのbean定義は削除してしまってください。

  <context:annotation-config />
  <context:component-scan base-package="jp.classmethod.example.berserker" />
  <!-- <bean id="worker" class="jp.classmethod.example.berserker.SpringQueueWorker" /> -->

続いて、SpringQueueWorker@Componentを付与し、Springに対して、このクラスがbeanであることを伝えます。

@Component
public class SpringQueueWorker {
  // 略
}

以上で、workerの定義はXMLから消えました。ただし、@Autowiredと同じく、このアノテーションも、自分の制御下にあるクラスに対してでなければ使えないので注意が必要です。

XMLレスなSpringの設定

…オーケー、落ち着くんだ。分かった、分かった。俺が悪かった。
とにかくXMLはダメなんだな? オーケー、話を聞いてくれ。

まず、君の大嫌いなapplicationContext.xmlは呪詛の言葉と共に今すぐに削除してしまってくれ。ただ、その代わりとなるものを用意しなけりゃならない。JSONを期待したかい? 残念、さやかちゃんJavaでした。

@Configuration
@ComponentScan("jp.classmethod.example.berserker")
public class AppConfig {

// ComponentScanとAutowiredによって、この記述は不要になる
//  @Bean
//  public SpringQueueWorker worker() {
//    SpringQueueWorker worker = new SpringQueueWorker();
//    worker.setSqs(amazonSQSClient());
//    worker.setMailSender(javaMailSenderImpl());
//    worker.setMailTemplate(templateMessage());
//    return worker;
//  }

  @Bean
  public AmazonSQS amazonSQSClient() {
    AmazonSQSClient amazonSQSClient =
        new AmazonSQSClient(new ClasspathPropertiesFileCredentialsProvider("aws.properties"));
    return amazonSQSClient;
  }

  @Bean
  public MailSender javaMailSenderImpl() {
    JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl();
    javaMailSenderImpl.setHost("smtp.example.com");
    javaMailSenderImpl.setPort(25);
    javaMailSenderImpl.setUsername("username");
    javaMailSenderImpl.setPassword("password");
    return javaMailSenderImpl;
  }

  @Bean
  public SimpleMailMessage templateMessage() {
    SimpleMailMessage templateMessage = new SimpleMailMessage();
    templateMessage.setSubject("QueueWorker sample");
    templateMessage.setFrom("daisuke@example.com");
    return templateMessage;
  }
}

ここまでに紹介したXML-basedやAnnotation-basedに対して、このような設定方法をJava-basedと呼びます。

ぱっと見、前回示した「Springを使わないQueueWorker」に戻っちまったじゃねえか、と思うかもしれません。通常のJavaとしてコードを解釈したら、まさにそうです。だが、このクラスはSpringの魔法が掛かってから実行されるため、素のままとは僅かに異なった挙動をすることになります。それはまた別のお話。

そして、せっかくXMLによって「オブジェクトの初期化を宣言的な世界に分離できた」のに、再びJavaという手続きの世界に戻って来てしまったように感じると思います。それはXMLというフォーマットを捨て、Javaという文法の世界に戻って来てしまったのだから、避け様のないことです。ただし「AppConfigはあくまでも宣言的な記述の代替である」という意識は捨ててはいけません。なので、粛々とオブジェクトを生成し、必要な依存性を注入することだけを行うようにしてください。この宣言的な思想を踏み外さない限り、AppConfigの中に条件分岐やループはほぼ出て来ないはずです。(但し、絶対禁止というわけではないので、思考停止しないようにしましょう。)

まだリリースはされていませんが、Springのversion4では、Groovyによるbean定義が可能になるようです。Javaの世界ではどうしても手続き的な文法が目立ってしまいますが、Groovyの力を利用すれば、宣言的な文法を持った「Springの設定DSL」を使った記述ができるようになると思います。

最後に、mainメソッドの中のApplicationContextの生成を、以下のように変更します。これでXMLとは縁が切れました。

new AnnotationConfigApplicationContext(AppConfig.class);

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

2015-07-13追記:このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。executeタスクがSpringMainクラスの実行です。相変わらずQUEUE_URLの設定は各自で必要ですが。

$ git clone https://github.com/classmethod-sandbox/berserker.git
$ cd berserker
$ git checkout 2.0
$ ./gradlew execute
:compileJava
:processResources
:classes
:execute
4 08, 2016 4:56:53 午後 org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5197848c: startup date [Fri Apr 08 16:56:53 JST 2016]; root of context hierarchy
4 08, 2016 4:56:55 午後 org.springframework.context.annotation.AnnotationConfigApplicationContext doClose
情報: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5197848c: startup date [Fri Apr 08 16:56:53 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:48)
	at jp.classmethod.example.berserker.SpringMain.main(SpringMain.java:28)
:execute FAILED

まとめ

ちなみに、これらの設定方法は相互に排他的なものではありません。つまり、一部にXML-basedを採用し、別の一部にJava-basedを採用する、ということも可能です。冗談めかしてXML根絶という雰囲気で、Java-basedの記述方法を紹介しましたが、XML-basedも捨てたものではありません。まぁ、IDEによる補完は必須だと思いますけどね。

XMLのネームスペース定義などは、全て手入力するのれあれば「やはりXMLは嫌いだ…」ということになってしまいます。SpringのXMLはIDEの補完機能による補助を受けながら書くものです。この補助を受けるためのEclipse環境構築については、恐らく次回、そうでなくても近いうちにご紹介する予定です。

「IDEが無い環境ではどうなんだ」というのは揚げ足取りです。Springの設定記述は、Javaの記述に寄り添った環境で行うものです。素のままのテキストエディタでJavaを書いている人は、Springの採用でXML云々の前に、IDEの採用を検討した方がいいと思います。つまり、Springの設定はIDEによる補完等によるサポートを受ける前提で考えて良いものです。(Tomcatのserver.xmlのチューニングをvimでやったらシンドかったとかそーゆー話とは次元が違う、という話です。)

それでもXMLが嫌な人には、多分Springに向いていないと思うので、自分好みの世界を他に探しましょう。むしろ、既に好みの世界を見つけていると思います。別にSpringは唯一最強の選択肢ではありません。完全なJava-basedでもSpringは利用できますが、ググっても資料は多くないし、色々調べる手間も増えると思います。つまり、Springを採用する以上、XMLからは多分逃げられません。

私自身、Springを利用する時は、初回でご紹介したような基本的なXML記述も利用しますし、p及びcネームスペースも利用します。そしてアノテーションによる記述も併用しますし、Java-basedな記述も(稀にですが)使います。その場その場でケースに応じて、最も適切な記述方法を選択する、その選択がXMLの場合もある、そういった柔軟な考え方が必要です。