Javaのシリアライズを使ったセッションの保存と復元方法

Javaプログラミングにおいて、セッション管理はWebアプリケーションの状態を保持するために不可欠な技術です。セッションはユーザーとサーバー間のインタラクションを管理し、ユーザーがWebサイトを移動しても一貫した体験を提供するために使用されます。セッション管理にはさまざまな方法がありますが、その中でもJavaのシリアライズを利用する方法は、セッションデータを永続化し、再利用するための強力な手段です。

シリアライズとは、Javaオブジェクトの状態をバイトストリームとして保存し、後でその状態を復元できるプロセスを指します。これにより、セッションデータを効率的に保存し、サーバーが再起動された場合や分散環境においても、ユーザーのセッション情報を保つことが可能になります。本記事では、Javaのシリアライズを用いたセッションの保存と復元の方法を詳しく解説し、シリアライズの利点や欠点、セキュリティリスク、実際の応用例についても紹介します。シリアライズの基本概念を理解し、セッション管理におけるベストプラクティスを学びましょう。

目次

シリアライズとは何か


シリアライズとは、Javaオブジェクトの状態をバイトストリームに変換して保存または転送するプロセスを指します。このプロセスを逆にして、バイトストリームから元のJavaオブジェクトを再構築することをデシリアライズと呼びます。シリアライズを利用することで、メモリ上のオブジェクトを永続化したり、ネットワークを介して他のシステムに転送することが可能になります。

シリアライズの用途


シリアライズは主に以下の用途で使用されます:

永続化


オブジェクトの状態をファイルシステムやデータベースに保存し、後で再利用することができます。これにより、アプリケーションを停止した後でも、再起動時に前回の状態を復元することが可能になります。

データ転送


オブジェクトの状態をネットワーク越しに別のJava Virtual Machine (JVM) に送信することができます。例えば、分散システムやリモートメソッド呼び出し(RMI)での通信に利用されます。

キャッシュ


オブジェクトをシリアライズしてキャッシュに保存し、後で同じオブジェクトを再利用することで、パフォーマンスを向上させることができます。

シリアライズを使うことで、オブジェクトの永続化やネットワーク通信が簡単に実現でき、特にセッション管理においては、その柔軟性と効率性が大きな利点となります。次のセクションでは、セッション管理の重要性について詳しく見ていきましょう。

セッション管理の重要性


セッション管理は、Webアプリケーションがユーザーごとの状態を維持し、個々のユーザー体験をパーソナライズするために不可欠です。Webは本質的にステートレスなプロトコルであるため、サーバーは各リクエストを独立して処理します。したがって、ユーザーのセッション情報を保持する仕組みが必要となります。これにより、ユーザーはサイトを移動する際に再認証を求められることなく、個別のデータや設定を利用し続けることができます。

セッション管理の主な役割

ユーザー認証


セッション管理の最も重要な役割の一つは、ユーザー認証情報の維持です。ログイン情報や認証トークンをセッションに保存することで、ユーザーが再度ログインする必要なく、認証済みとして扱うことができます。

ユーザーデータの維持


ショッピングカートの中身、ユーザーの設定、フォームの入力内容など、特定のユーザーに固有のデータを保持することができます。これにより、ユーザーはブラウジングを中断しても、再度ログインすることで前回の続きから作業を再開できます。

パーソナライズ


ユーザーの行動履歴や好みに基づいて、個別の体験を提供することができます。例えば、商品推薦システムやカスタマイズされたダッシュボードなどです。

シリアライズを使用したセッション管理のメリット


Javaのシリアライズを利用することで、セッション情報を簡単に保存し、アプリケーションの再起動時やサーバー間でセッションを移動させる際にも、データの損失を防ぐことができます。また、セッションを永続化することで、メモリ消費を抑え、サーバーのスケーラビリティを向上させることが可能です。

次のセクションでは、Javaのシリアライズを用いた具体的なセッション保存の方法について詳しく説明します。

シリアライズを使ったセッションの保存方法


Javaでセッションを保存するためにシリアライズを利用することは、セッション情報を効率的に永続化する方法の一つです。これにより、サーバーの再起動や障害時にも、セッションデータを保持し続けることが可能です。ここでは、セッションの保存方法について具体的な手順を解説します。

シリアライズ可能なオブジェクトを作成する


まず、セッションに保存するオブジェクトは、Serializableインターフェースを実装している必要があります。このインターフェースを実装することで、Javaオブジェクトはシリアライズが可能になります。

import java.io.Serializable;

public class UserSession implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userId;
    private String userName;

    // コンストラクタ、ゲッター、セッターなど
}

この例では、UserSessionクラスがシリアライズ可能なオブジェクトとなり、セッションに保存することができます。

セッションオブジェクトのシリアライズ


次に、HttpSessionオブジェクトを取得し、そのセッションにシリアライズ可能なオブジェクトを保存します。

HttpSession session = request.getSession();
UserSession userSession = new UserSession("12345", "John Doe");

// セッションにオブジェクトを保存
session.setAttribute("userSession", userSession);

このコードは、HttpServletRequestからセッションを取得し、UserSessionオブジェクトをセッションに保存しています。

セッションデータの永続化


サーバーの設定やフレームワークによっては、セッションデータを自動的にディスクに書き込むように設定できます。例えば、Apache Tomcatでは、context.xmlファイルでManagersaveOnRestart属性を設定することができます。

<Manager className="org.apache.catalina.session.PersistentManager"
         saveOnRestart="true"/>

これにより、サーバーが再起動された場合でも、セッションデータがディスクから復元されます。

シリアライズの活用例


シリアライズを用いたセッション保存は、特に大規模なWebアプリケーションや、セッション情報の可用性が求められるシステムで有用です。例えば、ユーザーの購入情報や設定データを永続化することで、予期せぬシステムダウンにも柔軟に対応できます。

次のセクションでは、シリアライズされたセッションデータを復元する方法について詳しく説明します。

シリアライズされたセッションの復元方法


シリアライズを利用して保存されたセッションデータを復元することは、セッションの永続性を維持し、サーバーの再起動や障害時にユーザーの状態を正確に再現するために重要です。ここでは、Javaでシリアライズされたセッションデータを復元する具体的な方法について説明します。

セッションデータの自動復元


多くのWebサーバーやアプリケーションコンテナは、セッションデータを自動的にディスクに保存し、サーバー再起動時に自動で復元する機能を備えています。例えば、Apache Tomcatでは、前述のPersistentManagerを利用して、サーバーの再起動時にセッションがディスクから自動的に読み込まれます。この場合、特別なコードを書く必要はありません。

プログラムによるセッションデータの復元


自動復元が利用できない場合や、手動でセッションデータを管理したい場合には、シリアライズされたデータを明示的に読み込んで復元する必要があります。以下の例は、シリアライズされたセッションデータをファイルから読み込み、オブジェクトを復元する方法を示しています。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import javax.servlet.http.HttpSession;

public class SessionHandler {

    public static void restoreSession(HttpSession session, String filepath) {
        try (FileInputStream fileIn = new FileInputStream(filepath);
             ObjectInputStream in = new ObjectInputStream(fileIn)) {

            UserSession userSession = (UserSession) in.readObject();
            session.setAttribute("userSession", userSession);

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、シリアライズされたUserSessionオブジェクトを指定されたファイルパスから読み込み、現在のセッションに復元しています。

デシリアライズの手順

  1. ファイルからの読み込み: FileInputStreamを使用して、シリアライズされたオブジェクトが保存されたファイルを開きます。
  2. オブジェクトの復元: ObjectInputStreamを使ってファイルからオブジェクトを読み込みます。ここで、シリアライズ時に使用されたクラスが必要です。
  3. セッションへの設定: 復元されたオブジェクトをセッションに設定します。

復元時の注意点


シリアライズされたデータを復元する際には、次の点に注意する必要があります:

  • クラスの互換性: 復元するオブジェクトのクラスが変更されていると、InvalidClassExceptionが発生することがあります。クラスのバージョン管理には注意が必要です。
  • セキュリティ: 不正なデータや攻撃のリスクを避けるため、信頼できるソースからのみデシリアライズを行うようにします。

シリアライズとデシリアライズを適切に管理することで、Javaアプリケーションのセッション管理がより強固で柔軟になります。次のセクションでは、シリアライズの利点と欠点について詳しく見ていきましょう。

シリアライズの利点と欠点


Javaのシリアライズを用いることで、オブジェクトの保存や転送が容易になりますが、シリアライズにはメリットとデメリットの両方があります。シリアライズを利用する際には、これらの特性を理解して適切に利用することが重要です。

シリアライズの利点

1. 永続化の簡便性


シリアライズを使用することで、Javaオブジェクトを簡単に永続化できます。これにより、オブジェクトの状態をファイルやデータベースに保存し、アプリケーションの再起動後もその状態を復元することが可能です。特にユーザーセッションの保存や、アプリケーションの状態のバックアップに有用です。

2. データ転送の容易さ


ネットワークを通じてオブジェクトを転送する際、シリアライズは非常に便利です。JavaのRMI(Remote Method Invocation)や他の分散システムで、シリアライズを利用してオブジェクトを別のJVMに渡すことができます。この機能により、複雑なオブジェクトのデータ転送もシンプルに実装できます。

3. キャッシュ機構への応用


オブジェクトをシリアライズしてキャッシュに保存し、後で再利用することが可能です。これにより、同じオブジェクトを繰り返し生成するコストを削減でき、アプリケーションのパフォーマンスが向上します。

シリアライズの欠点

1. パフォーマンスの低下


シリアライズとデシリアライズのプロセスはCPUとI/Oリソースを多く消費するため、大規模なオブジェクトや頻繁に行われるシリアライズ操作は、アプリケーションのパフォーマンスに悪影響を与えることがあります。特にリアルタイム性が求められるシステムでは、注意が必要です。

2. クラスの互換性の問題


シリアライズされたオブジェクトをデシリアライズする際、オブジェクトのクラスが変更されていると、互換性の問題が発生します。例えば、フィールドが追加・削除された場合やクラス名が変更された場合、InvalidClassExceptionClassNotFoundExceptionが発生することがあります。このため、シリアライズに使用するクラスのバージョン管理が必要となります。

3. セキュリティリスク


シリアライズされたデータは、オブジェクトの内部状態をそのまま保存します。信頼できないソースからのデシリアライズは、アプリケーションにセキュリティ上の脆弱性を導入する可能性があります。例えば、デシリアライズ時に不正なコードが実行される攻撃(デシリアライズ攻撃)が知られており、特に注意が必要です。

4. メモリ消費量の増加


シリアライズによってオブジェクトの全てのフィールド情報が保存されるため、シリアライズされたデータのサイズが大きくなることがあります。これにより、ストレージの消費量が増加し、効率的なメモリ管理が難しくなることがあります。

シリアライズを使用する際は、これらの利点と欠点を理解し、アプリケーションの要件に応じて適切に設計することが重要です。次のセクションでは、シリアライズにおけるセキュリティリスクとその対策について詳しく説明します。

シリアライズにおけるセキュリティリスク


シリアライズはJavaのオブジェクトを保存および転送するための強力な手段ですが、その反面、適切に管理されないとセキュリティリスクを引き起こす可能性があります。シリアライズされたデータをデシリアライズする際に発生しうるセキュリティ上の脆弱性について理解し、適切な対策を講じることが重要です。

デシリアライズ攻撃


デシリアライズ攻撃とは、悪意のあるデータを含むシリアライズされたオブジェクトがデシリアライズされることによって発生するセキュリティ脆弱性です。この攻撃により、アプリケーションが不正なコードを実行したり、機密情報が漏洩したりする可能性があります。攻撃者は、シリアライズデータにマルウェアコードを仕込むことで、サーバー上で任意のコマンドを実行させることができます。

例: RCE(Remote Code Execution)攻撃


デシリアライズ攻撃の典型的な例がRCE攻撃です。攻撃者が意図的に構造を改ざんしたシリアライズデータを送り込み、それをデシリアライズすることで、サーバー上で悪意のあるコードが実行されます。これにより、攻撃者はサーバーに対する完全なコントロールを取得できる場合があります。

セキュリティリスクの主な原因

1. 信頼できないデータのデシリアライズ


信頼できないソースからのシリアライズデータをデシリアライズすることは非常に危険です。攻撃者がシリアライズされたオブジェクトを改ざんし、意図的に悪意のあるペイロードを注入する可能性があります。

2. シリアライズ対象クラスの脆弱性


シリアライズ対象のクラスが持つ脆弱性、例えばオブジェクトの復元時に危険なメソッドが呼び出されるような設計ミスがあると、それを攻撃者に悪用される可能性があります。

セキュリティ対策

1. 信頼できるデータのみデシリアライズする


信頼できないデータソースからのデシリアライズは避けるべきです。デシリアライズするデータは、信頼できるソースからのみ受け取るようにし、外部からの入力には十分な検証を行います。

2. オブジェクト入力ストリームのカスタムフィルタを使用する


Java 8u121以降では、ObjectInputFilter APIを使用して、デシリアライズされるオブジェクトの種類を制限することができます。これにより、不正なオブジェクトがデシリアライズされるリスクを軽減できます。

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("maxdepth=5;java.base/*;!*");
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.ser"));
in.setObjectInputFilter(filter);

3. セキュアな代替手段の検討


場合によっては、シリアライズを使用する代わりにJSONやXMLなどの安全なデータフォーマットを用いてデータを保存・転送することが推奨されます。これにより、デシリアライズ攻撃のリスクを大幅に減らすことができます。

4. シリアライズ対象クラスの設計を見直す


シリアライズ対象となるクラスは、可能な限りシンプルに設計し、復元時に予期せぬメソッドが呼ばれないようにすることが重要です。また、readObjectメソッドをカスタマイズして、復元時のデータ検証を行うことも有効です。

シリアライズとデシリアライズは強力な機能ですが、適切なセキュリティ対策を講じなければ深刻な脆弱性を引き起こす可能性があります。次のセクションでは、シリアライズされたセッションの復元時に起こり得るトラブルとその対処方法について詳しく説明します。

セッション復元時のトラブルシューティング


シリアライズを利用してセッションデータを保存し、後で復元するプロセスには、いくつかの潜在的な問題が伴います。これらのトラブルを迅速に解決するためには、問題の原因を理解し、適切な対策を講じることが重要です。ここでは、セッション復元時に起こり得る一般的なトラブルとその解決策について説明します。

一般的な問題とその原因

1. `ClassNotFoundException`


問題の概要: デシリアライズ時に必要なクラスが見つからない場合、この例外がスローされます。これは、シリアライズ時に使用されたクラスが、デシリアライズ時のクラスパスに存在しない場合に発生します。

解決策:

  • 必要なクラスがデシリアライズ時のクラスパスに含まれていることを確認してください。
  • バージョン管理システムやデプロイメントのプロセスで、クラスのバージョンの整合性を保つことが重要です。

2. `InvalidClassException`


問題の概要: デシリアライズ対象のクラスがシリアライズされた時点から変更されている場合に、この例外が発生します。たとえば、フィールドの追加や削除、serialVersionUIDの変更などが原因となります。

解決策:

  • クラスに明示的にserialVersionUIDを定義することで、互換性のない変更を避けることができます。シリアライズ時とデシリアライズ時でserialVersionUIDを一致させるようにします。
private static final long serialVersionUID = 1L;
  • クラス設計を変更する際には、シリアライズ可能なオブジェクトとの互換性を慎重に検討してください。

3. `IOException`(入出力例外)


問題の概要: シリアライズまたはデシリアライズの際に、入出力操作が失敗した場合に発生します。これには、ディスクのフルやファイルアクセス権の問題が含まれます。

解決策:

  • ディスク容量を監視し、必要に応じてクリーンアップを行います。
  • アクセス権の問題がないか確認し、ファイルパスが正しいことを確認します。

4. `NotSerializableException`


問題の概要: シリアライズ可能ではないオブジェクトがセッションに保存されようとすると、この例外がスローされます。

解決策:

  • セッションに保存するすべてのオブジェクトがSerializableインターフェースを実装しているか確認します。
  • 必要に応じて、シリアライズ可能なラッパークラスを作成して、非シリアライズ可能なオブジェクトをラップします。

デバッグ方法

1. ログの活用


デシリアライズ時のエラーをトラブルシューティングするために、エラーメッセージを詳細にログに記録することが重要です。特に例外が発生した場合には、スタックトレースを完全に記録しておくと、問題の原因特定が容易になります。

2. 一貫性のチェック


シリアライズとデシリアライズの間に、オブジェクトの状態が一貫しているかどうかを確認します。フィールドの値やオブジェクトの構造が変更されていないかを検証するテストケースを作成することも有効です。

3. セキュリティ対策


特にデシリアライズ時には、信頼できないデータを処理しないようにすることが重要です。前述のObjectInputFilterのようなフィルタリング機能を利用して、不正なデータを早期に検出し、排除することが推奨されます。

これらのトラブルシューティング手法を用いることで、シリアライズされたセッションデータの復元時に発生する可能性のある問題を迅速に解決することができます。次のセクションでは、シリアライズの具体的な応用例について詳しく説明します。

シリアライズの応用例


シリアライズは、Javaプログラミングにおける非常に柔軟で強力な機能であり、さまざまな用途に利用されています。ここでは、シリアライズの具体的な応用例について説明し、その効果と利点を見ていきます。

1. HTTPセッションの永続化


Webアプリケーションでは、ユーザーのセッションデータを永続化することが求められます。シリアライズを使用することで、HTTPセッションのオブジェクトをファイルシステムやデータベースに保存し、サーバーの再起動や障害時にもセッション情報を復元することができます。

実装例


JavaのHttpSessionオブジェクトをシリアライズ可能な形で保存することで、アプリケーションの可用性を向上させることが可能です。たとえば、Tomcatなどのサーバーでは、PersistentManagerを使用してセッションデータを永続化できます。これにより、セッション情報を保持しながらも、サーバーメンテナンスを容易に行うことができます。

2. 分散システムでのデータ転送


シリアライズは、分散システム間でのデータ転送に広く利用されています。JavaのRMI(Remote Method Invocation)などの技術では、シリアライズを利用してオブジェクトをネットワーク越しに送信し、リモートサーバーでの処理を可能にします。

実装例


分散システムでシリアライズを使用する場合、オブジェクトをネットワーク経由で転送し、リモートのJVMでデシリアライズすることが可能です。これにより、異なるサーバー間でデータの整合性を保ちつつ、効率的にデータを共有できます。以下はRMIを使ったリモートオブジェクトの転送例です。

// サーバー側
RemoteObject obj = new RemoteObject();
Naming.rebind("rmi://localhost:5000/remoteObject", obj);

// クライアント側
RemoteObject stub = (RemoteObject) Naming.lookup("rmi://localhost:5000/remoteObject");

3. データベースキャッシュの実装


シリアライズはデータベースキャッシュの実装にも役立ちます。クエリ結果や計算結果をオブジェクトとしてシリアライズし、キャッシュに保存することで、同じクエリが再度実行される際にデータベースアクセスを回避し、アプリケーションのパフォーマンスを向上させます。

実装例


シリアライズされたオブジェクトをRedisやMemcachedなどのインメモリデータストアに保存することで、データベースへの負荷を大幅に削減できます。以下はシリアライズを用いたキャッシュの簡単な例です。

// シリアライズしてキャッシュに保存
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(myObject);
byte[] serializedObject = bos.toByteArray();
redisClient.set("cacheKey", serializedObject);

// キャッシュからデシリアライズして取得
byte[] bytes = redisClient.get("cacheKey");
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bis);
MyObject cachedObject = (MyObject) in.readObject();

4. ファイル保存と読み込み


Javaのシリアライズを利用してオブジェクトの状態をファイルに保存し、後で復元することができます。これにより、設定ファイルやゲームの進行状況、ユーザープロフィールなどを簡単に保存することが可能です。

実装例


ユーザーの設定情報をシリアライズしてファイルに保存し、アプリケーションの起動時にそれを読み込むことで、前回の設定状態を復元することができます。

// オブジェクトをファイルに保存
FileOutputStream fos = new FileOutputStream("userSettings.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(userSettings);
oos.close();
fos.close();

// ファイルからオブジェクトを読み込み
FileInputStream fis = new FileInputStream("userSettings.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
UserSettings loadedSettings = (UserSettings) ois.readObject();
ois.close();
fis.close();

これらの応用例を通じて、Javaのシリアライズはデータの保存、転送、および管理において非常に役立つ技術であることがわかります。ただし、セキュリティやパフォーマンスの問題にも注意を払う必要があります。次のセクションでは、シリアライズとセッション管理に関する理解を深めるための演習問題を紹介します。

演習問題


Javaのシリアライズとセッション管理についての理解を深めるために、いくつかの演習問題を用意しました。これらの問題に取り組むことで、シリアライズの実践的な応用と、セッションデータの保存および復元の方法についてより深く学ぶことができます。

問題1: シリアライズとデシリアライズの基本


課題: 以下の要件に従って、Productクラスをシリアライズおよびデシリアライズするプログラムを作成してください。

  • Productクラスにはname(String型)とprice(double型)のフィールドを持たせます。
  • Productクラスをシリアライズ可能にし、インスタンスをファイルに保存します。
  • 保存されたファイルからProductインスタンスをデシリアライズして復元し、内容をコンソールに出力します。

問題2: セッションの永続化


課題: ユーザーのログインセッションを管理するためのJavaクラスを作成し、シリアライズを使用してセッションデータをファイルに永続化してください。

  • UserSessionクラスを作成し、ユーザーIDとユーザー名をフィールドとして持たせます。
  • UserSessionインスタンスを作成し、シリアライズしてファイルに保存します。
  • ファイルからセッションデータを読み込み、デシリアライズして元のUserSessionオブジェクトを復元します。

問題3: セキュリティ対策の実装


課題: シリアライズとデシリアライズのセキュリティを強化するために、以下の要件を満たすプログラムを作成してください。

  • ObjectInputFilterを使用して、許可されたクラスのみがデシリアライズされるようにフィルタを設定します。
  • UserSessionクラスを用いて、許可されたクラスフィルタを通じてデシリアライズを行うコードを実装してください。
  • 不許可のクラスをデシリアライズしようとした場合の例外処理を実装し、適切なエラーメッセージをコンソールに出力してください。

問題4: 分散システムにおけるデータ転送


課題: RMI(Remote Method Invocation)を使用して、リモートオブジェクトをシリアライズして別のJVMに転送し、デシリアライズして使用するプログラムを作成してください。

  • サーバー側でRemoteCalculatorクラスを作成し、加算メソッドを持たせます。
  • クライアント側でRMIを使用してRemoteCalculatorオブジェクトを取得し、リモートメソッドを呼び出して加算結果を表示します。

問題5: シリアライズのトラブルシューティング


課題: 以下の状況を想定し、トラブルシューティングの手順を記述してください。

  • InvalidClassExceptionが発生する原因について説明し、その回避方法を実際のコード例を交えて示してください。
  • NotSerializableExceptionが発生する状況を再現し、その解決方法を実装してください。
  • ClassNotFoundExceptionが発生する状況を想定し、対処法をコード例とともに説明してください。

これらの演習問題に取り組むことで、Javaのシリアライズとセッション管理に関する知識を実践的に深めることができます。解答を通じて、シリアライズ技術の利点を活かしながら、セキュリティやパフォーマンスの課題にも対応できるようになります。次のセクションでは、この記事全体のまとめを行います。

まとめ


本記事では、Javaのシリアライズを使ったセッションの保存と復元方法について詳しく解説しました。シリアライズの基本概念から始まり、セッション管理の重要性やシリアライズを使用する利点と欠点、さらにはセキュリティリスクとその対策までを網羅しました。また、具体的な応用例を通してシリアライズの実践的な利用方法を学びました。

シリアライズは、Javaオブジェクトを永続化したり、ネットワーク越しにデータを転送したりするのに便利な技術です。しかし、セキュリティ上の懸念やパフォーマンスの問題も存在するため、適切な設計と実装が求められます。この記事で紹介した知識を活用し、安全かつ効率的なセッション管理とデータ保存を行うことで、Javaアプリケーションの信頼性と可用性を向上させることができます。

コメント

コメントする

目次