目次


Groovy 流の Spring

第 2 回 アプリケーションの振る舞いを実行時に変更する

Groovy によって、動的に更新可能な Bean を Spring アプリケーションに追加する方法

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Groovy 流の Spring

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Groovy 流の Spring

このシリーズの続きに乞うご期待。

この 2 回連載の第 1 回では、Groovy Bean を使用して Spring アプリケーションの柔軟性を向上させる方法を説明しました。Spring の Groovy サポートは、コンパイルまたはスクリプト化された Groovy 言語 Bean を使用すること、そしてこれらの Bean を lang XML スキーマや Grails Bean Builder などのさまざまな手段で構成することを可能にします。さらに Groovy スクリプトをアプリケーションに統合するときには、Bean の作成プロセスに追加ロジック (例えば、Bean の作成時に使用する実装ストラテジーを決定するロジックなど) を組み込むことができます。また、別個のスクリプト化された Groovy Bean を使用して、デプロイメントとパッケージ化の柔軟性を向上させることもできます。

Spring の動的言語サポートでおそらく最も興味深く、最も強力な機能は、アプリケーションの実行中に動的言語スクリプトをモニターし、スクリプトに対する変更が検出された場合には、変更された Bean を自動的に Spring アプリケーション・コンテキストにリロードできる機能です。実行中のアプリケーションでの Spring Bean の自動更新には、さまざまな使用事例が考えられます。以下に、そのうちのほんの数例を挙げてみます。

  • PDF 生成 (請求書、納品書、営業報告書、投資報告書、受領書、スケジュールなど)
  • E メールのテンプレート
  • レポートの生成
  • 外部化されたビジネス・ロジック、ドメイン固有言語 (DSL)、およびルール・エンジン
  • システム管理タスク
  • ロギング・レベルの変更およびランタイム・デバッグ

上記の他にもきっと、さまざまな使用法を思い付くことでしょう。この記事では、Bean 更新機能を Spring アプリケーションに追加する方法と、Bean 更新がどのように行われるかについて詳しく説明します。記事に記載するすべてのサンプルの完全なソース・コードは、「ダウンロード」セクションからダウンロードすることができます。

更新可能な Groovy Bean

第 1 回では、PdfGenerator インターフェースを定義し、アプリケーションの CLASSPATH に配置された GroovyPdfGenerator.groovy スクリプト・ファイルに、このインターフェースを Groovy クラス (GroovyPdfGenerator) として実装しました。そしてこの Groovy スクリプトが配置されているロケーションを指定して、Groovy ベースの pdfGenerator Bean を構成しました。リスト 1 に、インターフェースとその実装、および lang XML スキーマを使用した構成を記載します。

リスト 1. PdfGenerator インターフェース、およびその実装と構成
// PdfGenerator.java
public interface PdfGenerator {
    byte[] pdfFor(Invoice invoice);
}

// GroovyPdfGenerator.groovy
class GroovyPdfGenerator implements PdfGenerator {

    String companyName

    public byte[] pdfFor(Invoice invoice) {
        ...
    }

}

// applicationContext.xml
<lang:groovy id="pdfGenerator"
             script-source="classpath:groovierspring/GroovyPdfGenerator.groovy">
    <lang:property name="companyName" value="Groovy Bookstore"/>
</lang:groovy>

ここまでのところ、至って順調に運んでいます。Groovy で実装した pdfGenerator という名前の Bean は、アプリケーションの CLASSPATH に常駐します。Spring アプリケーション・コンテキストが作成されるときには、Spring がスクリプトを読み取り、それを Java クラスにコンパイルしてアプリケーション・コンテキストで GroovyPdfGenerator をインスタンス化するので、pdfGenerator に依存する他のすべてのクラスは、このインスタンスを単に依存関係として宣言することができます。すると、Spring がいつものようにクラスとインスタンスを接続します。

話が面白くなってくるのは、ここからです。例えば、PDF 生成コードに対して頻繁に変更を加えることから、アプリケーションの実行中にコードを変更した場合には、その変更内容が即時に適用されるようにしたいとします。Spring でこのような機能を追加するのは、わけのないことで、Bean を定義する <lang:groovy> 要素に refresh-check-delay 属性を追加すればよいだけです。この属性は、基礎となる Groovy スクリプトに変更があるかどうかのチェックを Spring が行う間隔をミリ秒単位で指定します。このチェックによってスクリプトの変更が検出されると (例えば、前回のチェック以降に .groovy スクリプトのタイムスタンプが変更されているなど)、Spring はそのスクリプトを読み取ってコンパイルし、古くなった pdfGenerator Bean を新しい Bean に置き換えます。この置き換えが行われても、pdfGenerator を使用するすべての Bean は、この変更について一切認識することはありません。

リスト 2 に記載する pdfGenerator Bean の構成では、更新チェック遅延が 10 秒 (10,000 ミリ秒) に設定されています。refresh-check-delay を追加した後は、基礎となる GroovyPdfGenerator.groovy スクリプト・ファイルが変更されると、Spring が Bean を自動更新に応じて構成します。

リスト 2. スクリプト化 Bean 定義に追加された refresh-check-delay
<lang:groovy id="pdfGenerator"
             script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"
refresh-check-delay="10000">
    <lang:property name="companyName" value="Refreshable Groovy Bookstore"/>
</lang:groovy>

ここで、例えばアプリケーションの実行中に GroovyPdfGenerator.groovy スクリプトに変更が加えられたとします。すると Spring は変更を検出し、pdfGenerator Bean を実行時にリロードします。これによって再起動が必要になることはありません。注意する点として、更新チェックが行われるのは、遅延時間が経過してからメソッドが更新可能 Bean で呼び出された場合だけです。例えば、pdfGenerator Bean の更新チェック遅延は 10 秒に設定されている一方、この Bean でのメソッド呼び出しが 50 秒間行われなかったとすると、Spring は 10 秒ごとではなく、50 秒後のメソッド呼び出しの時点で更新が必要かどうかをチェックすることになります。別の言い方をすれば、Spring はスクリプトに変更がないか積極的にポーリングするのではなく、今回呼び出されるメソッドが前回呼び出されてからどれぐらいの時間が経過したかを判断し、その経過時間が更新チェック遅延を超えているかどうかを計算するということです。経過時間が更新チェックの遅延時間を超えている場合にのみ、Spring はスクリプトで変更が行われているかどうかをチェックし、それによって更新が必要かどうかを判断します。一方、pdfGenerator Bean に大きな負荷がかかっていて、この Bean のメソッドが 1 秒間に何度も呼び出されているとします。refresh-check-delay が 10 秒に設定されている場合、Bean の使用回数に関わらず、Bean をリロードできるのは最短でも 10 秒間隔です。そのため、Spring が Groovy スクリプトを盛んにポーリングしてシステム・リソースを浪費しているかどうかを心配する必要はありません。実際、このように浪費することはないからです。

Spring アプリケーションで複数のスクリプト化された Groovy Bean を使用している場合、<lang:defaults> 要素を使用することで簡単に、これらの Bean すべてに対して更新チェック遅延のデフォルト値を設定することができます (リスト 3 を参照)。

リスト 3. デフォルト更新チェック遅延の設定
<lang:defaults refresh-check-delay="20000"/>

リスト 3 のように <lang:defaults> を使用すると、スクリプト化されたすべての動的言語 Bean (Groovy、JRuby、BeanShell などで作成された Bean) の更新チェック遅延が 20 秒に設定されます。個々の Bean のデフォルト値を変更するには、デフォルトとは異なる値を設定する Bean ごとに refresh-check-delay 属性を追加すればよいだけです。さらに、refresh-check-delay を負の値に設定することによって、個別のスクリプト化 Bean に対して自動更新の振る舞いを無効にすることもできます (リスト 4 を参照)。

リスト 4. 個々の Bean のデフォルト更新チェック遅延の変更
<lang:defaults refresh-check-delay="20000"/>

<lang:groovy id="pdfGenerator"
             script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"
             refresh-check-delay="60000">
    <lang:property name="companyName" value="Refreshable Groovy Bookstore"/>
</lang:groovy>

<lang:groovy id="invoiceEmailer"
             script-source="classpath:groovierspring/GroovyInvoiceEmailer.groovy"
             refresh-check-delay="-1"/>

リスト 4 を見るとわかるように、デフォルト更新チェック遅延は 20 秒に設定されている一方、pdfGenerator Bean の更新チェック遅延は 60 秒となるように構成されています。さらに invoiceEmailer Bean では一貫して更新チェックが行われないようになっています。

Grails Bean Builder を使用して更新可能 Groovy Bean を構成する場合

第 1 回では、Spring Bean をプログラムで定義するには、Grails Bean Builder (「参考文献」を参照) を使用できることを説明しました。このように Bean Builder を使用して Bean を定義している場合には、比較的簡単に Bean に自動更新機能を追加することができます。ただし自動更新機能を追加するとなると、さらに多くの Spring 内部構造が公開されることになります。これは、Bean Builder には <lang:groovy> シンタックス・シュガーに相当するものがないためです。リスト 5 に、スクリプト化されたすべての Bean にデフォルト更新チェック遅延を追加する方法、そして個々の Bean に更新遅延を設定する方法を示します。

リスト 5. Grails Bean Builder を使用した更新可能 Groovy Bean の構成
def builder = new grails.spring.BeanBuilder()
builder.beans {
    scriptFactoryPostProcessor(ScriptFactoryPostProcessor) {
defaultRefreshCheckDelay = 20000
    }
    pdfGenerator(GroovyScriptFactory,
                 'classpath:groovierspring/GroovyPdfGenerator.groovy') { bean ->
        companyName = 'Refreshable Bean Builder Bookstore'
bean.beanDefinition.setAttribute(
            ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE, 60000)
    }
}

リスト 5 の Bean Builder 構成は、論理的にはリスト 4 に記載した pdfGenerator Bean の構成と同じですが、ここでは ScriptFactoryPostProcessor Bean の defaultRefreshCheckDelay プロパティーを使用して、スクリプト化されたすべての Bean にデフォルト更新チェック遅延を設定しています。Bean Builder を使用して個々の Bean に更新チェック遅延を設定するには、基礎となる Spring Bean 定義に属性を設定しなければなりません。Spring では基礎となる詳細を <lang:groovy> XML ベースの構成を使用して自動的に処理しますが、Bean Builder を使用する場合には、開発者がこの詳細を処理しなければならないためです。また、pdfGenerator Bean 定義で属性を設定するには、この Bean でクロージャーに bean 引数を宣言する必要がある点にも注意してください。

Groovy Bean のカスタマイズ

これまでは、更新可能 Bean 機能によって Groovy Bean が実行時に自動的に更新されるようにすることで、実行時のアプリケーションを一層動的にする方法を説明してきました。Spring の Groovy サポートでは、カスタマイズという方法で Groovy Bean をさらに柔軟にすることもできます。ここで言うカスタマイズとは、カスタム・ロジックを Groovy Bean の作成プロセスに注入する方法です。新しく作成した GroovyObject では、GroovyObjectCustomizer インターフェース (リスト 6 を参照) を使用してカスタム・ロジックを実行することができます。

リスト 6. GroovyObjectCustomizer インターフェース
public interface GroovyObjectCustomizer {
    void customize(GroovyObject goo);
}

GroovyObjectCustomizer は、Spring が Groovy Bean を作成した後に呼び出すコールバックです。このインターフェースを使うことで、Groovy Bean に追加のロジックを適用することも、オブジェクトのメタクラス置換などといったメタプログラミングのマジックを行うこともできます (「参考文献」を参照)。リスト 7 に記載する実装は、Groovy Bean でのメソッド実行にかかった時間を書き込みます。

リスト 7. パフォーマンスをログに記録する GroovyObjectCustomizer
public class PerformanceLoggingCustomizer implements GroovyObjectCustomizer {

    public void customize(GroovyObject goo) {
        DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) {
            @Override
            public Object invokeMethod(Object object, String method, Object[] args) {
                long start = System.currentTimeMillis();
                Object result = super.invokeMethod(object, method, args);
                long elapsed = System.currentTimeMillis() - start;
                System.out.printf("%s took %d millis on %s\n", method, elapsed, object);
                return result;
            }
        };
        metaClass.initialize();
        goo.setMetaClass(metaClass);
    }
}

リスト 7PerformanceLoggingCustomizer は、パフォーマンスの時間計測ロジックを追加するために、GroovyObject のメタクラスを置き換えて invokeMethod をオーバーライドします。ここで必要となってくる作業は、カスタマイザーを構成して、カスタマイザーが 1 つまたは複数の Groovy Bean に適用されるようにすることです。リスト 8 に、<lang:groovy>customizer-ref 属性を使用してカスタマイザーを既存の Groovy Bean に追加する方法を説明します。

リスト 8. Groovy オブジェクト・カスタマイザーの構成
<bean id="performanceLoggingCustomizer"
      class="groovierspring.PerformanceLoggingCustomizer"/>

<lang:groovy id="pdfGenerator"
    refresh-check-delay="60000"
    script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"
customizer-ref="performanceLoggingCustomizer">
    <lang:property name="companyName" value="Customized Groovy Bookstore"/>
</lang:groovy>

これで、GroovyPdfGenerator のメソッドが呼び出されると、標準出力に以下のような出力が表示されるようになります (読者は今、ロギング・フレームワークを使用した方がよいのではないかと思っているかもしれませんが、おそらくそれが正論です)。

pdfFor took 18 millis on groovierspring.GroovyPdfGenerator@f491a6

Groovy Bean にカスタマイズを追加するのは至って簡単ですが、実際のカスタマイズ・ロジック、つまり Groovy Bean の作成時の Bean に対する操作内容を実装するという部分はそれほど簡単ではありません。上記のリストでは、<lang:groovy> とその customizer-ref 属性を使用して構成する方法を説明しましたが、Grails Bean Builder を使用すれば、簡単に Spring Bean をビルドすることができます。リスト 9 に、peformanceLoggingCustomizer Bean を追加する方法を説明します。

リスト 9. Grails Bean Builder を使用した Groovy オブジェクト・カスタマイザーの追加
builder.beans {
performanceLoggingCustomizer(PerformanceLoggingCustomizer)
    scriptFactoryPostProcessor(ScriptFactoryPostProcessor) {
        defaultRefreshCheckDelay = 20000
    }
    pdfGenerator(GroovyScriptFactory,
                 'classpath:groovierspring/GroovyPdfGenerator.groovy',
performanceLoggingCustomizer) { bean ->
        companyName = 'Refreshable Bean Builder Bookstore'
        bean.beanDefinition.setAttribute(
            ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE, 60000)
    }
}

Groovy 流のデータベース

JAR とは別に、Spring ではインライン・スクリプト、そして Spring の Resource 抽象化によってロードされたスクリプトをサポートします (「参考文献」を参照)。インライン・スクリプトと Resource ベースのスクリプト (具体的には、CLASSPATH リソース) については第 1 回で取り上げましたが、今回は更新可能 Bean を使用して一層動的な振る舞いを追加しています。Spring が動的言語 Bean をロード、コンパイル、更新するために依存しているのは、リスト 10 (Javadoc は省略) に記載する ScriptSource インターフェースです。

リスト 10. ScriptSource インターフェース
public interface ScriptSource {

    String getScriptAsString() throws IOException;

    boolean isModified();

    String suggestedClassName();
}

ScriptSource が定義するメソッドは、スクリプトのソース・コードを取得するメソッド、スクリプトが変更されているかどうかを判断するメソッド、そしてスクリプト用に提案するクラス名を返すメソッドの 3 つです。Spring にはこのインターフェースの 2 つの実装、StaticScriptSourceResourceScriptSource が用意されています。StaticScriptSource は、Spring 構成ファイルにスクリプトを定義するときに使用し、ResourceScriptSourceResource からスクリプト (例えば、ファイルに配置されたスクリプト、CLASSPATH にあるスクリプト、または URL にあるスクリプトなど) をロードするときに使用します。

静的スクリプトと Resource ベースのスクリプトによって、スクリプトは広範な場所で定義することができますが、その一方、データベースをスクリプトのロケーションとして使用したいという場合もあります。これにはもっともな理由がいくつかあります。例えば、多くの組織では、ファイル・システムによる実動マシンへのアクセスを許可していなかったり、デプロイメントを WAR または EAR ファイルとして行わなければならない場合もあります。その上、データベースはほとんどの組織がすでに使い慣れているトランザクション・リソースです。データベースという手段を使用すれば、ファイル・システムやサーバーなどの具体的詳細を知る必要なく、比較的簡単にデータ・アクセスやセキュリティーを一元化することができます。さらに、スクリプトをデータベースに保管すれば、ユーザーにスクリプトの編集を許可することによって、スクリプトをアプリケーションから更新することが可能になります (言うまでもなく、アクティブ・コードをデータベースに保管するときには、セキュリティー上考えられる問題を検討し、アプリケーションを適切に保護することが重要です)。

例えば、Groovy スクリプトをリレーショナル・データベースに保管するとします。Spring 2.5 では新しいタイプのスクリプトを作成できるようになっていますが、それには独自の ScriptSource を作成し、一部の Spring クラスを継承する必要があります。具体的には、独自の ScriptSource 実装を定義し、Spring の ScriptFactoryPostProcessor を変更して、このクラスが新しいタイプの ScriptSource を処理する方法を認識するようにしなければなりません。

リスト 11 が実装する DatabaseScriptSource は、Spring JDBC を使用してリレーショナル・データベースからスクリプトをロードします。

リスト 11. DatabaseScriptSource の実装
public class DatabaseScriptSource implements ScriptSource {

    private final String scriptName;
    private final JdbcTemplate jdbcTemplate;
    private Timestamp lastKnownUpdate;

    private final Object lastModifiedMonitor = new Object();

    public DatabaseScriptSource(String scriptName, DataSource dataSource) {
        this.scriptName = scriptName;
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public String getScriptAsString() throws IOException {
        synchronized (this.lastModifiedMonitor) {
            this.lastKnownUpdate = retrieveLastModifiedTime();
        }
        return (String) jdbcTemplate.queryForObject(
                "select script_source from groovy_scripts where script_name = ?",
                new Object[]{ this.scriptName }, String.class);
    }

    public boolean isModified() {
        synchronized (this.lastModifiedMonitor) {
            Timestamp lastUpdated = retrieveLastModifiedTime();
            return lastUpdated.after(this.lastKnownUpdate);
        }
    }

    public String suggestedClassName() {
        return StringUtils.stripFilenameExtension(this.scriptName);
    }

    private Timestamp retrieveLastModifiedTime() {
        return (Timestamp) this.jdbcTemplate.queryForObject(
                "select last_updated from groovy_scripts where script_name = ?",
                new Object[]{ this.scriptName }, Timestamp.class);
    }
}

リスト 11 の DatabaseScriptSource はかなり単純明快なものですが、このクラスが要求するデータベース・テーブルの構造という点では、さらに一般化することもできます。前提とするテーブルの名前は groovy_scripts で、テーブルを構成する列は script_namescript_source、および last_updated 列です。このクラスは、groovy_scripts テーブルからのスクリプトのロードと、変更のチェックをサポートします。

ここで、Spring に DatabaseScriptSource を認識するように教え込まなければなりません。それには、ScriptFactoryPostProcessor を継承し、スクリプト・ソース・ロケーター (例えば、classpath:groovierspring/GroovyPdfGenerator.groovy) を ScriptSource に変換する convertToScriptSource メソッドをオーバーライドします。リスト 12 に、ScriptFactoryPostProcessor でのデフォルト実装を記載します。

リスト 12. ScriptFactoryPostProcessorconvertToScriptSource メソッド
protected ScriptSource convertToScriptSource(
        String beanName, String scriptSourceLocator, ResourceLoader resourceLoader) {

    if (scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX)) {
        return new StaticScriptSource(
                scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()), beanName);
    }
    else {
        return new ResourceScriptSource(resourceLoader.getResource(scriptSourceLocator));
    }
}

ご覧のように、このデフォルト実装はインライン・スクリプトとリソース・ベースのスクリプトしか処理しません。そこで、ScriptFactoryPostProcessor のサブクラスを新規に作成し、DatabaseScriptSource を使用して convertToScriptSource をオーバーライドすることで、データベースからもスクリプトをロードするようにします (リスト 13 を参照)。

リスト 13. CustomScriptFactoryPostProcessor の実装
public class CustomScriptFactoryPostProcessor extends ScriptFactoryPostProcessor {

    public static final String DATABASE_SCRIPT_PREFIX = "database:";

    private DataSource dataSource;

    @Required
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    protected ScriptSource convertToScriptSource(String beanName,
                                                 String scriptSourceLocator,
                                                 ResourceLoader resourceLoader) {
        if (scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX)) {
            return new StaticScriptSource(
                scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()), beanName);
        }
        else if (scriptSourceLocator.startsWith(DATABASE_SCRIPT_PREFIX)) {
            return new DatabaseScriptSource(
                scriptSourceLocator.substring(DATABASE_SCRIPT_PREFIX.length()),
                dataSource);
        }
        else {
            return new ResourceScriptSource(
                resourceLoader.getResource(scriptSourceLocator));
        }
    }

}

このリストの CustomScriptFactoryPostProcessorScriptFactoryPostProcessor と似ていますが、スクリプト・ソース・ロケーターが database で始まる場合には、データベースをベースとしたスクリプト (例えば、database:groovierspring/GroovyPdfGenerator.groovy) を使用できるようになっている点が異なります。理想を言えば、このメカニズムを一層柔軟にしたいところですが (囲み記事「プラガブル・スクリプト・ソース・ロケーターへの移行」を参照)、とりあえずは、これで Groovy スクリプトをデータベースに保管するために必要な機能が用意できました。

残る作業は、データベースから読み込まれるように pdfGenerator Bean を構成するだけです。そのためにはまず、リスト 13 に記載した CustomScriptFactoryPostProcessor を使用して scriptFactoryPostProcessor Bean を定義します。次に、データベース・スクリプト・ソース・ロケーターを使用して pdfGenerator Bean を定義してください。pdfGenerator Bean を定義するには、<bean/> 構文をそのまま使うことも、より明確な <lang:groovy> 構文を使うこともできます。<lang:groovy> を使用すると、Spring は ScriptFactoryPostProcessor Bean が scriptFactoryPostProcessor という名前のアプリケーション・コンテキストにあるかどうかをチェックし、まだ存在していない場合には自動的に作成します。scriptFactoryPostProcessor がすでに定義されている場合には、Spring はその Bean を使用します。この仕組みによって、独自のカスタム実装を置き換えられるというわけです。リスト 14 に、新しい構成を記載します。

リスト 14. データベース pdfGenerator Bean の構成
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/GroovierSpringDataSource"/>

<bean id="scriptFactoryPostProcessor"
      class="groovierspring.CustomScriptFactoryPostProcessor">
    <property name="dataSource" ref="dataSource"/>
</bean>

<lang:groovy id="pdfGenerator"
             refresh-check-delay="60000"
             script-source="database:groovierspring/GroovyPdfGenerator.groovy">
    <lang:property name="companyName" value="Database Groovy Bookstore"/>
</lang:groovy>

リスト 14 のコードは、今まで見てきたコードと比べ、それほど複雑にはなっていません。さらに dataSource Bean を定義している理由は、scriptFactoryPostProcessor Bean には DataSource の注入が必要だからです。その他に違う点と言えば、CLASSPATH ベースのスクリプトからデータベースに常駐するスクリプトに変更されていることぐらいです。Grails Bean Builder を使用する方向に傾いているのであれば、データ・ソースとカスタム ScriptFactoryPostProcessor Bean の構成には Grails Bean Builder を簡単に使用することができます。

これで、Groovy スクリプトをデータベースからロードできるようになっただけでなく、データベース内の Groovy スクリプトが変更された後に Groovy スクリプトを更新することもできるようになりました。これで、すでに柔軟性に優れた Spring の Groovy サポートが、ますます柔軟かつ動的になったということです。ここでは、独自の ScriptSource 実装を追加して、スクリプトを任意の場所からロードできるようにする方法も説明しました。

Groovy スクリプトが正常に機能しなくなった場合には

アプリケーションのテストは徹底的に行うべきだという意見には誰もが賛成すると思いますが、その方法については意見が分かれるかもしれません。例えば、コードの 100 パーセントを網羅する必要があるのか、それが果たして可能なのか、あるいは時間の無駄になるだけなのかなどの意見は、人によって違うでしょう。個人的な見方がどうであれ、Spring の動的言語のサポートを利用することによって、実行中の実動システムに変更をデプロイし、その変更内容を即時に適用できるとなると、テストの重要性は飛躍的に大きくなります。

更新可能 Bean 機能を使用することに決めた場合には、確固たるストラテジーによって、新しいコードが正しく、期待通りに動作することを確実にしなければなりません。その方法を実質的に決定するのは、以下の状況です。

  • そのシステムがどれだけ重要なのか
  • 何かを壊してしまった場合、どのような影響があるか
  • 問題が生じたときに、どれだけ迅速に修正できるか

それぞれの状況にはさらに考慮しなければならない事項もあると思いますが、肝心な点は、Bean 更新機能は強力ながらも、危険が伴う可能性もあるということです。そのため、この機能は責任を持って使用しなければなりません。発生する可能性のある主な問題には、スクリプトのコンパイル・エラーとランタイム・エラーの 2 種類があります。

スクリプトのコンパイル・エラー

例えば、スクリプトに対して、コンパイルできなくなるような変更を実行時に行ったとします。Spring がこの変更を検出して Bean をリロードしようとすると、元の例外 (Groovy の MultipleCompilationErrorsException など) をラップした ScriptCompilationException がスローされます。このエラーが発生すると、Spring は行おうとしていた Bean のリロードをキャンセルし、何事もなかったかのように変更前の Bean をそのままの状態で維持します。アプリケーションに必要なのは、この ScriptCompilationException に適切に対応することです。大抵は、何らかのエラー・メッセージを表示し、開発者またはオペレーション・スタッフに (E メール・メッセージやインスタント・メッセージなどで) 通知することになります。もちろんスクリプトに対して変更を行っている当の本人が、アプリケーションをモニターしてスクリプトが正常にコンパイルされること、新しい Bean が古い Bean を置き換えることを確認しなければならないことは言うまでもありません。

ただし、コンパイルされないスクリプトはデプロイ済みの Bean には全く影響を与えないため、すべてが台無しになるわけではありません。コンパイル例外の原因となった問題を修正した後、再試行してください。Bean が正常にコンパイルされると、Spring はアプリケーション・コードにはわからないように既存の Bean を新しい Bean に置き換えます。新旧 Bean の置き換えが行われた場合、アプリケーションを再デプロイしたり、再起動しなくても変更が適用されます。

実行時のスクリプト・エラー

実行時のスクリプト・エラーには、コンパイル済みコードによってスローされるランタイム・エラーと同じ問題があります。その問題とは、これらのエラーが原因でアプリケーション内に発生した障害の状態はほとんどの場合、ユーザー・レベルにまで伝搬し、ユーザーがどんなアクションを実行しようとしても、その試行は失敗に終わってしまうことです。例えば GroovyPdfGenerator に対し、コンパイルはできるけれども、PDF を生成しようとするとランタイム例外をスローするような変更を加えたとします。この場合、pdfGenerator を使用するコードが例外を処理するか、または例外を伝搬しなければなりません。そして大抵の場合は、ユーザーが PDF の生成失敗 (そして、できるだけ早く修正しなければならないこと) を通知するエラー・メッセージを受け取ることになります。

ただし、スクリプトのコンパイル・エラーと同じく、ランタイム・スクリプト・エラーが発生したとしても解決の糸口はあります。実際、スクリプトは実行時に変更可能なため、コンパイル済みコードよりも修正は簡単です。スクリプト内に存在する問題が何であれ、それを修正してスクリプトをリロードすれば、もう問題にはなりません。このように、ある観点から見ると、実行時にコードを変更できるということは、変更する上での柔軟性をもたらすだけでなく、エラーが発生した場合の柔軟性ももたらします。しかし、Spring アプリケーションのすべての Bean を更新可能 Bean にするべきだと言っているわけではありません。物事の常として、ほどほどが最善であるように、更新 Bean もほどほどに使用するのが最善です。

スクリプトのセキュリティー

最後になりましたが、セキュリティーは決して軽視できない考慮事項です。スクリプトをセキュアにして、許可されたユーザーまたは管理者だけがスクリプトを変更できるようにしなければなりません。これはある意味、アプリケーションの他のあらゆる部分をセキュアにすることと同じです。例えば、ほとんどのアプリケーションでは、データ保全性を確実にするために、ユーザーが利用できる機能をそれぞれの役割や特権に応じて制限する必要があります。しかし一方で、このような制限を行えるということが、システムに侵入してデータだけでなくシステムの振る舞いを変更する新たな攻撃の道を、ハッカーに対して開いてしまうことにもなりかねません。確実に言えることは、アプリケーションの中で攻撃の対象となる部分を減らさなければならず、そのためには設計のトレード・オフと同じく、利用できる機能を制限することによる利点と欠点をよく見定めなければならないということです。

セキュリティーがより重要になる理由として考えられるのは、Spring の動的言語サポートでは、システムのデータだけでなく振る舞いも変更できるという点です。これはある程度、的を得ています。例えば悪意のあるコードを注入する SQL インジェクション攻撃や JavaScript クロスサイト・スクリプティング、あるいはシステムの振る舞いを変更または置き換えるクロスサイト・リクエスト・フォージェリーについて考えてみてください。これらに共通する重要な点は、Groovy スクリプトが更新可能な場合はなおさらのこと、Groovy スクリプトにおける適切な制御をどのようにして確実にするかを考えなければならないということです。

更新可能 Bean をどのように使用するかによっては、セキュリティー上のリスク増大よりも、実行時に振る舞いを変更できるという利点が優先される可能性もあります。例えば、顧客に値引きを提供するために度々ルールを変更しなければならない顧客向けの販売アプリケーションや、ビジネス・ルールが頻繁に変わる可能性のある保険用アプリケーションを想像してみてください。このような場合、Groovy では、販売員や保険外交員が顧客のビジネス・ニーズに合わせて変更することのできる DSL を設計することができます。また、購入額が $50 を超えた場合には 10 パーセントの値引きを提供するといったロジックを追加しなければならないとしても、ユーザーが実行中のアプリケーションで DSL のほんの一部を直接変更できるようにすることで、この種の変更に対応することも確かに可能です。あるいは、値引きの規定を変更するためのグラフィカル・エディターを設計するのも一考でしょう。

まとめ

この連載では、コンパイル済み Groovy クラスまたは動的にコンパイルされてロードされるスクリプトを使って Groovy を Spring ベースのアプリケーションに統合する方法を説明してきました。また、スクリプト化された Groovy Bean を更新可能にする方法、Groovy Bean を作成時にカスタマイズする方法、さらには Groovy Bean をリレーショナル・データベースに保管する方法についても説明しました。スクリプト・コンパイル・エラーやランタイム・エラーが実行中のアプリケーションに与えるさまざまな影響について、そして更新可能 Bean では、再デプロイメントやアプリケーションの再起動が必要になる従来のアーキテクチャーを使った場合よりも簡単に実行時のバグを修正できることを理解していただけたと思います。最後に、スクリプト化された更新可能な Bean にセキュリティー上考えられる問題について簡単に触れ、アプリケーションにはどのようなセキュリティー・レベルが必要なのかを十分に評価しなければならないことを指摘しました。

Spring と Groovy は強力な組み合わせです。Spring がアーキテクチャーとインフラストラクチャーを提供し、Groovy が動的機能を加えます。変更された Groovy スクリプトをリロードする Spring の機能によって、アプリケーションの未知の領域が拓けるはずです。しかし、「大きな力には大きな責任も伴う」ことを忘れないでください。アプリケーションの動的性質を大幅に強化すれば、当然、アプリケーションは一層柔軟かつ強力になりますが、それと同時に今まで対処する必要のなかった問題や課題が出てきます。


ダウンロード可能なリソース


関連トピック

  • Spring Framework:Spring のホーム・ページです。
  • The Spring series」(Naveen Balani 著、developerWorks、2005年): フレームワークについて紹介している 4 回の連載記事です。
  • Spring AOP: Spring リファレンス・ガイドのこの章で、Spring でのアスペクト指向プログラミングについて説明しています。
  • Spring の Resource 抽象化: Spring リファレンス・ガイドのこの章では、Spring の Resource インターフェースとそのさまざまな実装について説明しています。
  • 『Pro Spring』(Rob Harrop、Jan Machacek 共著、Apress、2005年): Spring Framework のすべての側面を網羅した本です。
  • Grails Bean Builder: Spring Bean をプログラムでビルドしてください。
  • Make script source locator mechanism pluggable: ScriptSource メカニズムを交換可能にするための Spring 機能要求について読んでください。
  • Spring: Spring の最新リリースをダウンロードしてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=368156
ArticleTitle=Groovy 流の Spring: 第 2 回 アプリケーションの振る舞いを実行時に変更する
publish-date=01062009