第6回 EJBにおけるコンテナとコンポーネント
――EJBの定義と、2つのインターフェイスの役割――

丸山不二夫
稚内北星学園大学学長
(http://www.wakhok.ac.jp/)
2001/9/13

 今回から、J2EEの本体ともいえるEJB(Enterprise Java Beans)の話を始めたいと思います。EJBには、セッション・ビーン(Session Bean)とエンティティ・ビーン(Entity Bean)、メッセージ・ドリブン・ビーン(Message-driven Bean)の3種類があります。途中で追加されたMessage-driven Beanを除いて、Session BeanとEntity Beanでは、1つのビーン(Bean)を定義するのに3つのクラス・ファイルを必要とします。

 EJBに初めて触れたとき「どうして3つも定義ファイルが必要なの?」と、感じた人は多いと思います。今回は、この、1つのBeanの定義に、Bean実装クラスのほかに、HomeRemoteと呼ばれる2つのインターフェイスが、なぜJ2EEでは必要とされるのかを説明します。

 Session Beanとは

 Session Beanには、ユーザーがサーバと接続している間(セッション)に存続するものという意味があります。そしてその内容は、1人のユーザーがセッション中にサーバに対して依頼する何らかの処理をサーバ側のコンポーネントにまとめたものと考えてよいでしょう。イメージ的には、サブルーチンの呼び出しに近いもので、その機能のネットワーク上での拡大版である、RMI(Remote Method Invocation)の働きをコンポーネント化したものと考えることができます。

 Session Beanは、サーバとの対話的なセッションにおいて、セッション中保持されるべき状態を持つか否かに応じて、statefullとstatelessの2つの種類に分かれます。例えば、ECサイトで利用される「買い物カゴ」は、何がカゴの中に入っているかという状態の情報が意味を持ちますので、statefullなSession Beanになります。一方、データベースにアクセスして、J2EEに関する情報を集めよという命令に対応するBeanは、セッション中の途中の状態は意味を持ちませんので、stateless Session Beanとして実現されます。

 

 Hello EJBサンプルを見てみる

 最初に、J2EEサーバから“Hello”のメッセージを受け取る簡単なSession Beanのサンプルを紹介したいと思います。この場合、セッション途中のBeanの状態が保持される必要はありませんので、statelessなSession Beanを使います。

 まず、サーバ側ですが、次の3つのファイルから“Hello”のメッセージを送り出す1つのビーンが構成されます。3つのクラスの中を調べてみると“Hello World!”の文字列を返すという、実際の仕事を実行しているのはEJBクラスの中のsayHelloメソッドの定義部分だけであることが分かります。

 それでは、そのほかの部分は、この簡単なサンプルにとっては無駄にも思えるのですが本当に必要なのでしょうか? 残念ながら必要です。その理由は、おいおい明らかになると思います。

Remoteインターフェース
public interface Hello extends javax.ejb.EJBObject {
  public String sayHello() throws java.rmi.RemoteException;
}

Homeインターフェース
public interface HelloHome extends javax.ejb.EJBHome {
  public Hello create()
   throws java.rmi.RemoteException, javax.ejb.CreateException;
}

EJBクラス
public class HelloEJB implements javax.ejb.SessionBean {
  public String sayHello() {
   return "Hello World!";
  }
  public void ejbCreate() {}
  public void setSessionContext(SessionContext sc) {}
  public void ejbRemove() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
}

 サーバ側の設定だけではこのサンプルは動きません。クライアント側に、以下のようなコードが必要になります。「簡単」といっても、プリント文で“Hello World!”をコンソールに表示するのとはだいぶ複雑さが違います。

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;

import Hello;
import HelloHome;public class HelloClient {
  
  public static void main(String[] args) {
    try {
     Context initial = new InitialContext();
     HelloHome home = (HelloHome) PortableRemoteObject.narrow(
        initial.lookup("MyHello"), HelloHome.class);
     Hello hello = home.create();
     System.out.println("Message: " + hello.sayHello() );
    } catch (Exception ex) {
     System.err.println("Caught an unexpected exception!");
     ex.printStackTrace();
    }
  }
}

 

 deployとクライアントの実行

 サーバ側とクライアント側のコードがそろえばそれでいいわけではありません。これらのファイルをコンパイルした後で、さらに、deploytoolを使ってdeployし、クライアント用のJARファイルを生成しなければなりません。deployするためには、もちろんJ2EEサーバを立ち上げる必要があります。今回は、deployの手順は細かく説明できませんので、基本的な部分を図示しておきます。

図1 ここでは、Stateless Session Beanが選ばれていることと、3つのクラス・ファイルが指定されていることに注意してください

図2 BeanのJNDI名を指定します。これは、クライアントでのlookupの引数に対応しています

図3 Application Client Jarファイルを生成させます

 deployが終わると、HelloClient.jarというファイルが作られているはずです。このファイルに対して、次のように、%J2EE_HOME%\bin以下にあるrunclient.batコマンドを使うとクライアント・プログラムが実行できます。

runclient -client HelloClient.jar -name HelloClient

 このとき、認証用のウィンドウがポップアップして、ユーザー名とパスワードの入力を求められるはずです。特別な設定をしていなければ、双方にj2eeと打ち込めば、プログラムの実行が始まるはずです。このように、ユーザーの認証機能が自動的に組み込めるのもJ2EEの強力な機能の1つです。

 3つの定義ファイル

 今回は、前節で紹介したサンプルでSession Beanを定義しているサーバ側の3つのファイルについて少し詳しく見てみたいと思います。

 

 Remoteインターフェイス 

 まず、最初のRemoteインターフェイスについてです。ここでは、まず、次のようなEJBObjectの定義を確認しておきましょう。EJBObjectインターフェイスがRemoteインターフェイスと呼ばれるのは、この定義によっています。

public interface EJBObject extends Remote {
……
}

 EJBのRemoteインターフェイスで重要なことは、クライアントがリモートで呼び出すRemoteExceptionを投げるリモート・メソッドとして、sayHelloメソッドが置かれていることです。このように、クライアントがJ2EEサーバから呼び出すべきメソッドは、すべてリモート・メソッドとして、このRemoteインターフェイスに記述されている必要があります。ビジネス・アプリケーションを主要なターゲットとしているJ2EEでは、こうしたメソッドを一般的に、「ビジネス・メソッド」とか「ビジネス・ロジック」と呼ぶことがあります。

 

 Homeインターフェイス 

 EJBのHomeインターフェイスも、定義的には先のEJBObjectと同じように次のような形をしています。このインターフェイスは、J2EEではHomeインターフェイスと呼ばれて、Remoteインターフェイスと呼ばれることはないのですが、本来は、EJBObjectと同じくRemoteインターフェイスであることには注意が必要です。それは、この中で定義されている、createメソッドがCreateExceptionとともにRemoteExceptionを返すことにも、はっきりと現れています。このように、HomeインターフェイスがRemoteインターフェイスでもあるということは、後で見るように、3つの定義ファイルの関係を考えるうえで大きな意味を持っています。

public interface EJBHome extends Remote {
……
}

 Homeインターフェイスで重要なことは、createというメソッドの名前とこのメソッドが返すオブジェクトの型です。先のRemoteインターフェイスで出てくるメソッドは、あるビジネス・アプリケーションのビジネス・ロジックを表現するメソッドたちですから、アプリケーションに応じて変わります。ところが、Homeインターフェイスのcreateメソッドという名前はアプリケーションには依存しません。しかし、このcreateメソッドの返す型は、この例の場合にはHelloですから、ちょうど先に見たRemoteインターフェイスの型に等しくなっています。このように、createが返す型はアプリケーションによって変わり、そのときのRemoteインターフェイスの型を取ります。

「Homeインターフェイスのcreateメソッドは、Remoteインターフェイスの型を返す」

 これが、2つのインターフェイスの間の最も重要な関係になっています。同時に、この関係はクライアントの側から、J2EEのBeanのメソッドを呼び出すときにも、基本的な役割を果たします。

 

 EJBクラス 

 3番目のEJBクラスは、一見するとsayHelloメソッドが「実装」されていますので、Remoteインターフェイスを「実装」しているように思えるかもしれません。ところが、単純にそうではないのです。SessionBeanの定義は次のようになっています。

public interface SessionBean extends EnterpriseBean {
……
}

 これだけだと分からないのですが、EnterpriseBeanの定義を見ると次のようになっていて、SessionBeanが、SerializableではあるけれどもRemoteではないことが分かります。

public interface EnterpriseBean extends Serializable {
……
}

 このことは、EJBクラスのメソッドたちが、基本的にはリモートではなくローカルなメソッドとして定義されていることを見ても分かります。EJBのメソッドの実装定義と思えたEJBクラスが、実は、そのままではネットワークを通じたリモート・メソッドの呼び出しの対象にはなりえないというのは奇妙なことです。このなぞは後で解くことにして、ここではRemoteインターフェイスで定義されたメソッドは、このEJBクラスの中で、ローカルなメソッドとして「実装」されるべきことを確認しておきましょう。

 Remoteインターフェイスが必要な理由

 なぜ、2つのインターフェイスが必要かという問いに答えようとして、Session Beanを定義する3つのクラス・ファイルを見てきましたが、むしろ疑問が増えたかもしれません。Remoteインターフェイスを必要とするのは、明らかに、ネットワークを通じて、サーバ内のコンポーネントでRMIを実行しようとしているからだと思われるのですが、よく見ていくと、EJBを定義する3つのクラス・ファイルだけで、そのことを説明するのは難しいことが分かります。3つのファイルは多すぎるように見えますが、むしろ、何かが欠けているのです。

 では、RMI/IIOPを使った同じプログラムと
 比較してみる

 ここでは、サーバから“Hello World!”のメッセージを受け取る、先のEJBのサンプルと同じプログラムをRMI/IIOPのプロトコルを使って作ってみて、それと先のサンプルとの比較をしてみたいと思います。ちなみに、J2EEもこのRMI/IIOPのプロトコルを使っています。

 まず、RMIで呼び出されるべきリモート・メソッドを定義するRemoteインターフェイスですが、これは、ほとんど先のEJBのRemoteインターフェイスと同じです。

Hello Remoteインターフェイス
public interface Hello extends java.rmi.Remote {
  public String sayHello() throws java.rmi.RemoteException;
}

 サーバ側のプログラムは、次のようなものです。このクラスが、第1にPortableRemoteObjectを拡大したものであること、第2にRemoteインターフェイスであるHelloインターフェイスを実装していること、すなわち、sayHelloメソッドをリモート・メソッドとして実装していることはすぐに分かると思います。もう1つ、メイン・メソッドが、HelloServerという名前で、JNDIサーバに自分自身を登録していることも分かります。

HelloServer
import java.io.*;
import java.rmi.*;
import javax.rmi.PortableRemoteObject;
import javax.naming.*;

public class HelloServer extends PortableRemoteObject implements Hello {

  public HelloServer() throws RemoteException { }


  public String sayHello() throws RemoteException {
    return "Hello World!";
  }


  public static void main(String args[]) {
    try {
      HelloServer obj = new HelloServer();
      Context initialContext = new InitialContext();
      initialContext.rebind("HelloServer",obj);
      System.out.println("HelloServer bound in JNDI");
    } catch (Exception e) {
      System.out.println("HelloServer err: " + e.getMessage());
    }
  }


}

 問題は、こうしたRemoteインターフェイスに対して、サーバ側で対応すべきコードがEJBのサンプルではほとんど見られないということです。

 単純なRMIでのクライアント側のコードは、次のようになります。まず、JNDIからHelloServerという名前でオブジェクトを獲得して、そのオブジェクトをnarrowメソッドを使ってHello.classの型を合わせます。こうして、RemoteインターフェイスHelloを持つオブジェクトを獲得しています。次いで、そのオブジェクトで、sayHelloメソッドを呼び出しています。

 こうしたRMIでのコードを、EJBでのクライアント側のコードと比較してみてください。JNDIを通じて獲得されるオブジェクトが、EJBのクライアントではHelloHomeです。これは先にも注意しましたが、EJBでは、Remoteインターフェイスと区別されてHomeインターフェイスと呼ばれているものが、RMIのRemoteインターフェイスでもあることを考えれば、不思議ではありません。むしろ、HelloHomeをRemoteインターフェイス、createメソッドをそのRemoteインターフェイスで定義されたリモート・メソッドと割り切って、EJBのクライアント側のコードを眺めれば、その前半部分は、RMIとまったく同じコードとしてきれいに解釈できるのです。

HelloClient
import javax.naming.*;
import javax.rmi.PortableRemoteObject;


public class HelloClient {
  

  public static void main(String args[]) {
    try {
     Context initial = new InitialContext();
     Hello obj = (Hello) PortableRemoteObject.narrow(
        initial.lookup("HelloServer"), Hello.class);
     System.out.println("Message: " + obj.sayHello() );
    } catch (Exception e) {
     System.out.println("HelloClient exception: " + e.getMessage());
    }
  }


}


 

 2つのRemoteインターフェイス

 「前半」というのは、EJBクライアントの次のような部分です。JNDIを通じて、サーバが提供する、RemoteインターフェイスとしてのHelloHomeインターフェイスを持つインスタンスhomeを獲得して、そのインスタンス上でリモート・メソッドcreateを呼び出していると解釈できます。ここでの主役は、RemoteインターフェイスであるHelloHomeインターフェイスです。

HelloHome home = (HelloHome) PortableRemoteObject.narrow(
  initial.lookup("MyHello"), HelloHome.class);
Hello hello = home.create();

 EJBクライアントの「後半」部分は次のような部分です。ここでは、主役が、HelloHomeインターフェイスから、EJBのRemoteインターフェイスであるHelloインターフェイスに交代しています。前半では、Remoteインターフェイスを持つインスタンスは、JNDIを通じて獲得されましたが、ここでは、createメソッドの呼び出しを通じて獲得されています。いったんこうしたインスタンスが獲得されれば、リモート・メソッドの呼び出しが可能となるわけです。

Hello hello = home.create();
System.out.println("Message: " + hello.sayHello() );

 ここでは、先に見た、次のような命題が、前半部分と後半部分の2つのRemoteインターフェイスを結び付けるカナメの役割を果たしていることを、あらためて確認しておきましょう。

「Homeインターフェイスのcreateメソッドは、Remoteインターフェイスの型を返す」

 

 隠れているサーバ側のプログラムを想像する

 しかし、まだ釈然としないところがあります。理由ははっきりしています。RMIのサンプルの場合には、サーバ側とクライアント側の2つのプログラムが与えられ、双方がRemoteインターフェイスを共有していることがはっきり分かるのに対して、EJBのサンプルでは、2つのRemoteインターフェイスと、その2つのRemoteインターフェイスを両方利用しているクライアント・プログラムは与えられているものの、サーバ側のプログラムが明確でないのです。sayHelloメソッドを実装しているEJBクラスは、確かにサーバ側に属するのですが、残念ながら、Remoteインターフェイスとの対応を欠いています。これは、先のhello.sayHello()というシーケンスにおいて、helloがリモート・インスタンスであり、sayHelloがEJBクラスで実装されたメソッドであるなら、この組み合わせでは、メソッドの呼び出しは行えないということにほかなりません。これでは困ります。

 先のEJBのクライアント側のプログラムが動くためには、何らかの補助的なクラスの助けが必要なのです。RMIでのリモート・メソッドの呼び出しがうまく動くためには、何よりも、EJBの2つのRemoteインターフェイスについて、対応するサーバ側のプログラムの存在が不可欠です。ここで、そうしたサーバ側のプログラムを想像してみましょう。RMIの流儀に忠実に考えていくと、次のような骨組みが浮かび上がってきます。

Homeインターフェイス HelloHomeに対応するサーバ・プログラム
HelloHome_Server
public interface HelloHome extends javax.ejb.EJBHome {
  public Hello create()
   throws java.rmi.RemoteException, javax.ejb.CreateException;
}

public class HelloHome_Server extends PortableRemoteObject implements HelloHome {
  

public HelloHome_Server() throws RemoteException { }
  public Hello create()
  throws RemoteException, CreateException
  {
    return (Hello).........; // getStub()を使えばよい。
  }


}


Remoteインターフェイス Helloに対応するサーバ・プログラム
Hello_Server
public interface Hello extends javax.ejb.EJBObject {
  public String sayHello() throws java.rmi.RemoteException;
}

public class Hello_Server extends PortableRemoteObject implements Hello{
  

public Hello_Server() throws RemoteException { }
  public String sayHello() throws RemoteException {
    return "Hello World!";
  }


}

 逆に、サーバ側に2つのRemoteインターフェイスに対応するこうしたオブジェクトが存在するなら、EJBのクライアント側のプログラムの働きが、RMIの働きとしてちゃんと説明できることが分かります。

 

 生成されたファイルを調べる

 実際に、J2EEはこのような処理を行っています。いくつかの点では手直しが必要ですが、基本的なシナリオはいま述べたとおりです。J2EEは、与えられた2つのRemoteインターフェイス(J2EEでは、RemoteインターフェイスとHomeインターフェイスといいます)とEJBクラスの定義から、新たに2つのRemoteインターフェイスに対応するサーバ側のクラスのJavaコードを生成し、それをコンパイルしてクラス・ファイルを生成し、それをVMにロードしてオブジェクトを作っています。

 こうした新しいクラスの生成は、実は、アプリケーションのdeploy時に、自動的に行われています。deploy時に、コンパイル済みのクラス・ファイルしか与えていないはずなのに、Compiling wrappercode...とかCompiling RMI-IIOP code...といったメッセージが出ていたのは、そのせいです。deployには、いろんな顔があるのです。生成されたクラス・ファイルは、J2EEがインストールされたディレクトリrepository以下の次の場所に置かれています。

%J2EE_HOME%\repository\<ホスト名>\gnrtrTMP\
<Remoteインターフェイス名>

 今回のHelloの例では、次のようなクラス・ファイルが作られていることが分かります。ネーミング・ルールは、先の「想像」とは違っていますが、対応の見当はつくと思います。

 「3つも定義ファイルがあるのはなぜ?」という問いかけから始まったのですが、現実は、もっと複雑そうです。

HelloEJB_EJBObjectImpl.class <---- 先の「想像」での
Hello_Server.class
HelloHomeImpl.class <---- 先の「想像」での
HelloHome_Server.class
_HelloEJB_EJBObjectImpl_Tie.class
_HelloHomeImpl_Tie.class
_Hello_Stub.class
_HelloHome_Stub.class

 確かに複雑ですが、次のように整理すれば、EJBのHomeとRemoteの2つのインターフェイスに対応した、クライアントとサーバをRMI/IIOPで結ぶ、2つの系列のクラス群であることが見て取れるはずです。

クライアント側Stub  RMI用 Tieクラス サーバコンポーネント
_HelloHome_Stub.class _HelloHomeImpl_Tie.class    HelloHomeImpl.class
_Hello_Stub.class _HelloEJB_EJBObjectImpl_Tie.class HelloEJB_EJBObjectImpl.class

 コンテナとコンポーネント

 ようやく、J2EEのサーバ側の「仕掛け」が見えてきました。先のHelloEJBの例で説明してみましょう。

 J2EEは、サーバ内に、与えられたEJBのRemote、Home、EJBクラスの3つの定義ファイルを受け取って、それを処理するオブジェクトを作り出します。そのオブジェクトは、3つの定義ファイル内のメソッドに関する情報を、すべてピックアップします。このオブジェクトが、「コンテナ」と呼ばれているものです。J2EEは、deploy時に、Homeインターフェイスからそれに対応するサーバ側のオブジェクトを生成します。このオブジェクトは、コンテナと1対1で対応していてコンテナにセットされます。

 同時に、J2EEは、RemoteインターフェイスHelloと、EJBクラス HelloEJBの情報から、「新しい」EJBオブジェクト・クラスを生成します。先に見たdeploytoolのメッセージ、Compiling wrapper code...に出てくる「wrapper」というのは、元のEJBクラス HelloEJBのメソッド類を引き受けて、1つに包み込んだこの新しいEJBオブジェクト・クラスのことです。これが、J2EEで「コンポーネント」と呼ばれているものの実体です。このコンポーネントのインスタンスは、Homeインターフェイスでのcreateメソッドの呼び出しによって、生成されることになります。

 EJBクラスHelloEJBが、Remoteインターフェイスを実装していないにもかかわらず、hello.sayHello()という呼び出しが、EJBクライアントの側から可能なのは、sayHelloメソッドが、実は、EJBクラスHelloEJBのsayHelloメソッドではなく、RemoteインターフェイスHelloを実装した、新しく生成されたEJBオブジェクト内の、書き換えられたsayHelloメソッドであることによって説明されます。このことは、同じ名前、同じ型を持ち、ビジネス・ロジックとして同じ役割を果たすべきこの2つのメソッドが、実装のレベルでは同一のものではない可能性を示唆しています。

 こうして、コンテナとコンポーネントという、次のような「メタファー」が成り立つことになります。deploytoolのアイコンにあるような、ビンに豆が1つ入っている状態をイメージしてもいいでしょう。このメタファーは、コンテナとコンポーネントの役割を分かりやすく視覚化するとともに、EJBの3つの定義ファイルの役割も明確にしてくれます。

  • コンテナは、コンポーネントの「いれもの」です。コンテナは、最初は空です
  • コンテナには、外部との境界に、HomeとRemoteという2つのインターフェイスを
    持っています
  • Homeインターフェイスを通じて、コンテナにコンポーネントを作ったり、消したり
    することが可能です
  • コンポーネント内のメソッドは、EJBクラスで定義され、Remoteインターフェイス
    を通じて呼び出せます

 EJBクラス・ファイル中の、ビジネス・メソッド以外の、ejbで始まる名前を持つ一群のメソッドたちは、主に、コンテナとコンポーネントの関係に関連した仕事を引き受けています。

 

 コンポーネント・メソッドの書き換えと
 コンテナの役割

 先に、コンポーネントのメソッドと、元のEJBクラスのメソッドは同じではないかもしれないことを示唆しましたが、実際にコンポーネントのメソッドは、元のEJBクラスの定義から大きく書き換えられています。すこし模式的になるのですが、コンポーネント内の新しいsayHelloメソッドのつくりを見ることにしましょう。

public String sayHello() throws RemoteException { // Remoteメソッド
  …….
  HelloEJB helloejb = (HelloEJB) …… ; // 元のEJBクラスのインスタンス
  ……
  container.preInvoke(...); // メソッド呼び出しの前処理
  String s = helloejb.sayHello(); // 元のEJBクラスのメソッド呼び出し
  container.postInvoke(...); // メソッド呼び出しの後処理
  ……
  return s;
}

 メソッドは、Remoteインターフェイスからの呼び出しが可能なように、Remoteインターフェイスを実装したクラスの、リモート・メソッドに書き換えられています。Remoteインターフェイスでこのメソッドが呼ばれると、コンテナが前処理のメソッドを呼び出します。その後で元のEJBクラスのメソッドがそのまま呼び出されます。その後で、再びコンテナが後処理のメソッドを呼び出します。コンテナを主体に考えれば、こうした過程はRemoteでメソッドが呼び出されるたびに、コンテナは呼び出し前の処理と呼び出し後の処理で、コンポーネントのメソッド呼び出しを挟み込む働きをすると解釈できます。

 コンテナによるメソッドの挟み込みというテクニックは、単純ですが非常に強力なものです。ある意味では、こうした処理こそがJ2EEの心臓部分と考えてもいいものです。例えば、トランザクションの処理では、メソッドの呼び出しの前後で、いくつかの処理を行わなければなりません。エンティティ・ビーンでは、メソッドの呼び出しの前にデータベースの内容をビーンにロードして、メソッドの呼び出しの後にデータベースにビーンの内容をストアする必要があります。また、J2EEではメソッドごとにさまざまなパーミッションが設定できるのですが、こうした処理はコンテナによるメソッドの挟み込みによって実現することができます。

 メソッドの書き換えによって、このように、コンポーネントとコンテナが役割を分担することが可能になります。EJBのプログラマーがビジネス・ロジックに集中することができるのは、こうしたメカニズムによってビジネス・ロジック以外のさまざまな処理をコンテナに担わせることが可能となるからです。

連載内容
J2EEの基礎
  第1回 Java Pet Storeで、J2EEを体験する(1)
  第2回 Java Pet Storeで、J2EEを体験する(2)
 

第3回 J2EEアプリケーションと配置(deployment)

  第4回 J2EEアプリケーションを構成するコンポーネント
  第5回 データベースのブラウザを作る
第6回 EJBにおけるコンテナとコンポーネント
  第7回 J2EEのセキュリティのキホンを知る
  第8回 J2EEのトランザクション処理


連載記事一覧




Java Agile フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Java Agile 記事ランキング

本日 月間