この記事はtomita@atuwebがお届けします。

開発経験はほとんどPHPのみ という状態の私がJava + Spring Frameworkの開発に携わるに当たって詰まった点、苦労したことを振り返ってみました。
下の記事の続きな感じですね。

[Java]Spring Frameworkで1年開発したので主要なポイントをおさらいする
2014年にはStrutsの脆弱性が見つかり、Java界隈に激震が走った様子を、私はWebの端っこから見て感じていました。 そ...

正直に、最初は

  • Java知らねーなー
  • Springってどうやって動いているの?
  • Mavenって何?

ってことの連続でした。
みんな悩んで大きくなるんですよ!

知識不足ではありますが、これからSpring Frameworkで開発をされる方の一助になれば幸いです。

バージョンは以下の通りです。

  • Java 7
  • Spring 4.0
  • Maven 3

動作チェック

初めに時間がかかってしまったのは「どうやってサーバ上でチェックするか」です。

ローカルPCでサーブレットコンテナを起動する方法としては次の3つがあります。

  • Eclipseにサーブレットコンテナ用のプラグインを設定する
  • ローカルでサーブレットコンテナを立ち上げる
  • Mavenのサーブレットコンテナプラグインを利用する

上から難易度が高くMavenを利用する方法がおすすめです。

Eclipseからサーブレットコンテナを起動する

Spring用IDEであるSTSにはデフォルトでサーバがセットアップされており、簡単にTomcatを起動させることができます。

しかし、私が当時手元にあったWindowsマシンはこれを起動するのに4 – 5分もかかってしまうような激重さ。
何か修正してチェックするにも非常に効率が悪く、気持ちはしぼむ一方でした。

途中であきらめてしまったので、なぜそんなに時間がかかってしまうのか、ボトルネックがどこなのかは把握できていません。
Eclipse + JavaVMでメモリがカツカツだったのでしょうか。。。

また、Eclipse上でJettyを起動させるためにRun-Jetty-RunというEclipseプラグインがありますが、情報が少なく、これもうまくセッティングできませんでした。

Mavenのサーブレットコンテナプラグイン

Mavenからサーブレットコンテナを起動します。

利用方法とっても簡単。
pom.xmlのにプラグインを設定します。

<plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>9.1.3.v20140225</version>
    <configuration>
        <contextPath>/</contextPath>
        <scanIntervalSeconds>0</scanIntervalSeconds>
        <connectors>
            <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
                <port>8080</port>
                <maxIdleTime>60000</maxIdleTime>
            </connector>
        </connectors>
    </configuration>
</plugin>

そしてmvnコマンドを実行します。

$ cd /path/to/project
$ mvn jetty:run

これでhttp://localhost:8080/にアクセスすればOKです。

ポートを変更する場合はmvnオプション-Djetty.port=8080と指定することができます。

また、Tomcatならtomcat7-maven-pluginを使ってtomcat7:runしてください。

設定がXML

SpringのキモはDIですが、DIするにはまずXMLと格闘しなくてはなりません。

そのほかにもストレージの接続やAspectなど、さまざまな設定をXMLで行わなければならず、正直そのわけのかからなさに面食らってしまいました。
PHPで開発していると、「.php」以外の拡張子は「.ini」や「.js」程度ですからねw

設定をミスった場合にも「サーブレットコンテナを起動して初めてエラーが出る」ため、ここも非効化できずに苦しみました。

私が、「サーバを起動する前にXMLをチェックする方法」を知らないだけかもしれません。
ぜひとも教えて下さい、、、

マルチプロジェクトでXMLを合わせる

過去のプロジェクトは、以下の記事のように1プロジェクトを複数に分割しておりました。

[Java][Spring]Springのオレオレプロジェクト構成をチラ見せ!
Spring Frameworkで開発した、稼働実績があるプロジェクトの構成をご紹介いたします。 バージョンは以下の通りです。...

親(core)はEntityやORMを、子(web)はMVCという感じです。

この場合、親に環境別の設定を定義してしまうと、子から環境を選択できなくなってしまいます。
しかし、「どうやって子から親のXMLを参照するのか」がさっぱりわかりませんでした。

答えは「classpathからimportする」です。


具体例として、私のプロジェクトで行ったDAOの設定を一部ご紹介いたします。

1.子/src/main/webapp/WEB-INF/spring/spring.xml

<beans:import resource="classpath:config/dao.xml" />

ここが起点です。
さっそく、子プロジェクトから親プロジェクトのXMLを読みにいっています。

2. 親src/main/resources/config/dao.xml

<import resource="classpath:config/datasource-common.xml" />
<import resource="classpath:config/datasource.xml" />
<import resource="classpath:config/orm.xml" />

1行目で接続定義用の抽象オブジェクトを定義します。
2行目で、子に定義した環境別の接続定義をロードします。

3. 親src/main/resources/config/datasource-common.xml

<bean id="abstractDbcpDataSource" abstract="true" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="username" value="username" />
    <property name="password"  value="****" />
    <property name="initialSize"  value="10" />
    :
</bean>

親と子で参照が行ったり来たりしますが、しっかり読み込むことができます。

どうして動作するのかは、正直に今もよく分かっていません。
とっつきにくいですねー。

DIする

Springではコンポーネントの呼び出しにDIを活用します。
newもしますが、newよりDIするのがSpring Frameworkの醍醐味(?)です。

DIするのはとても簡単な3ステップです。

  • 1.springのXML定義に以下を入れます。
<context:annotation-config />
<context:component-scan base-package="net.atuweb.example.web.*" />
  • 2.DIされるほうのクラスを用意します。
// @ConponentでもDI可能
@Service
public class BuzService {
    public void bar() {
        // 素敵な処理
    }
}
  • 3.コントローラーで欲しいオブジェクトを@Autowiredアノテーション付きで宣言します。
@Controller
public class FooController {

    @Autowired
    private BuzService buzService;

    @RequestMapping(value = "/foo/index")
    public String charaGachaRatio() {
        // BuzServiceがDIされて即利用可能!
        buzService.bar();
        :
    }
}

たったこれだけです。
DIについては特にXMLなどの設定はありません。

わかればそれだけのことですが、慣れるまでは黒魔法のように感じますね。

DIはシングルトン

デフォルトではDIされたインスタンスはシングルトンです。
起動時にインスタンスが生成され、以降使いまわされます。

シングルトンであることは、個人的にはGoodな点だと思いっていますが、シングルトンであることを意図せず実装 するのはNo Goodですよね。

私はDI仕様把握をおろそかにしてしまい、見事にハマった落とし穴でした。

私の出した不具合は、「DIされたオブジェクトのメンバ変数を、複数のユーザから更新、参照する」といったケースで、「アクセスが重なった場合にユーザAのレスポンスが、ユーザBにも届いてしまった」感じです。
あああぁ。

そこで、@Scopeアノテーションの出番です。
@ScopeはDIされたインスタンスのライフサイクルを指定しますので、これを利用してDIするオブジェクトのスコープを適切に設定しましょう。

@Scopeのプロパティは以下の通りです。(ほぼ、テックノートさんの記事のままです。)

プロパティ値 説明
singleton コンテナに対してBeanのインスタンスを1つだけ生成
prototype Beanの呼び出し毎にインスタンスを生成
request 1回のHTTPリクエスト毎にインスタンスを生成
session HTTPセッション毎にインスタンスを生成

たとえば「リクエストパラメーターをひきまわすオブジェクト」など、1リクエストのみで破棄したいクラスに対し@Scope("request")を設定することで、リクエスト単位のインスタンスが生成され、DIコンテナがしっかり管理してくれます。

しかしながら闇雲に@Scope("prottype")すると、インスタンス生成コストが高くなり、メモリ消費量(new領域の消費)が増えてしまいますのでご注意ください。

コントローラーや大きなサービスクラスはシングルトンにしますが、ここでは極力Daoなどのデータアクセスオブジェクトやユーザ依存のオブジェクトをDIしないように設計します。

そして、Daoやユーザ固有の情報はスコープを狭めるか、素直にNewしたインスタンスを使うなどで対処するといい感じです。

参考

テックノート
springのシングルトン問題を@Scopeを使って回避する
http://javatechnology.net/spring/spring-singleton-scope/

@Componentと@Serviceの違いは?

@Autowiredについて調べると、大きく次の2つがあると気づきます。

  • @Autowired + @Component
  • @Autowired + @Service

どちらもDIコンテナに管理してもらうためのアノテーションであることに違いはありません。

私はざっくり、「同じコンポーネントだけれども、サービス層には役割をより明確にするために@Serviceをつかう」と理解しております。

サーブレット・フィルタよりAOP

例えば「認証状態のチェック」など、すべてのHTTPリクエスト、レスポンスに対して一律に行いたい処理はいくつかありますね。

一律処理といえばサーブレット・フィルタですが、SpringならAspect(AOP)も利用できます。

AOPの利用方法については以下に簡単なサンプルを出しています。

[Java]Spring Frameworkで1年開発したので主要なポイントをおさらいする
2014年にはStrutsの脆弱性が見つかり、Java界隈に激震が走った様子を、私はWebの端っこから見て感じていました。 そ...

私がSpringを触った初期段階で「ログイン状態のチェック」をサーブレットフィルタで実装しようとしていましたが、サーブレットフィルタからDaoを捕まえて、、、などと考えるかなり難しいことだけよくわかりました。
その点、AspectはオブジェクトをDIすることもでき、サーブレットフィルタよりずっと手軽に扱うことができると思います。

サーブレットフィルタは「サーブレットコンテナとサーブレットの中間」に位置します。
これに対し、Aspectはサーブレット側で動作するもので、ありとあらゆるところに割り込み処理を挟むこと(ポイントカット)ができ、活用できるシーンがとても広いですね。

しかしながら、デメリットもありますよね。

例えば、処理の流れを分断してしまうものですから、処理の流れが見えなくなったり、AOPによるオブジェクトの更新が分かりにくくなったり、Aspect間の処理競合が発生したり、こちらも乱用することはお勧めできません。

HTTPレスポンスオブジェクト

HttpServletRequestは、次のコードで簡単にオブジェクトを得ることができます。

class RequestUtil {
    public static HttpServletRequest getHttpServletRequest() {
        ServletRequestAttributes request = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return request.getRequest();
    }
}

ところが、レスポンスオブジェクトは同様に扱うことができないようですね。
いろいろ調べてみましたが、結局うまく対応できず、普通にコントローラーにDIして受け取ることで解決しました。

@RequestMapping(value = "/bar/baz")
public String startGame(HttpServletResponse response) {
}

簡単にレスポンスオブジェクトを扱う方法をご存知の方は、ぜひ教えてください。

@Transactional

サービス公開後に、検証が不十分な改修をリリースした結果、トランザクションが期待通りに動作せずにデータの不整合が発生してしまいました。

Springでトランザクションを行う方法はいくつかあるようですが、私は@Transactionalを利用しています。

設定方法は、3ステップです。

  • 1.spring.xmlでトランザクションを宣言する
<tx:annotation-driven/>

txとはspring-txですね。

  • 2.トランザクションマネージャの定義(ライブラリによる?)
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
  • 3.トランザクションを行うメソッドに@Transactionalをつける
@Service
public class FooService {

    @Transactional(rollbackFor = Throwable.class)
    public void execute() {
        // ... アトミックに束ねたい処理たち ...
    }

トランザクションが動かないケース

次のようなトランザクションの呼び出しはNGでした。

  • コントローラーからサービスクラスAのメソッドを呼び出す
  • サービスクラスAからサービスクラスBの@Transactionalなメソッドを呼び出す

次の呼び出しはOKです。

  • コントローラーからサービスクラスAの@Transactionalなメソッドを呼び出す

なにやら@Transactionalを宣言したメソッドは、コントローラーから直接呼び出す必要があるようです。
この挙動はSpring4.0でも確認できました。

参考

kochabloさんのブログ
@Transactionalが動かなくてハマった話
http://ameblo.jp/kochablo/entry-11578714824.html

ロールバックが動作しないケース

@Transactionalはデフォルトで実行時例外のみがロールバック対象となってしまうようです。
実行時例外はRuntimeExceptionを継承したクラスですね。

そのため、上記サンプルのようにrollbackForを指定し、ロールバックを起こす対象のクラスを指定しましょう。
独自例外もロールバックされますから、例外をスローして処理を抜ける簡潔な処理にすることができます。

おわりに

躓く点も多かったですが、うまく組み立てることでパフォーマンスが高く、疎結合なプロジェクトを構築することが可能です。
Spring、Goodです。

Spreing習得の初期にはかなりテックノートさんに助けていただきました。(特にDIやスコープ回り)
ありがとうございました!




2017年01月18日:タイトルを「[Java]Spring Webの開発でつまづいた点を振り返る」から変更

スポンサーリンク
ad_336
ad_336
  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存
スポンサーリンク
ad_336