摘要:
EJB与CORBA的集成能力对于集成基于JAVA或非JAVA的应用来说是很重要的。本文描述了如何实现一个EJB与一个CORBA的C++应用相集成。它阐述了几个重要的集成问题,尤其是那些EJB采用JAVA固有的或是用户定义的对象作为参数或返回值的方法时涉及的问题。
EJB对于用JAVA来开发关键业务应用程序是非常重要的。但是,业务应用不是孤立存在的,当今,企业需要集成各种应用。从而,把基于EJB的解决方案与现有的应用系统集成起来就变得越来越重要了。
在本文中,我将说明如何从一个非JAVA语言编写的应用中访问EJB。更加特别地是,我将讨论从一个CORBA的C++客户端访问会话和实体Bean(它使用同步的IIOP协议进行通信)。我没有提到消息驱动Bean,尽管你可能想从其它语言编写的应用中使用MOM产品来访问它们。
1. RMI-IIOP
会话Bean和实体Bean使用远程方法调用(RMI)来进行同步通信。J2EE1.3要求JAVA客户端使用RMI-IIOP。RMI-IIOP采用CORBA的IIOP协议,这使得RMI-IIOP与CORBA相兼容。换句话说,不是基于JAVA开发的客户端可以通过CORBA与EJB进行通信。
要实现这点,你必须使用符合J2EE1.3的应用服务器。以前的EJB规范没有要求你去用RMI-IIOP协议。而是,应用服务器采用了RMI-JRMP或是其它私有协议。另外,你必须使用符合CORBA2.3.1或更高版本的ORB。以前的CORBA版本没有实现与RMI-IIOP协议进行互操作所必需的规范,尤其是后来集成中CORBA规范和JAVA到IDL的语言映射规范中的用值传递对象的规范(可以参看CORBA/IIOP规范2.6版,在“值类型语义”一章)。
值类型语言增加了用值来传递对象的概念,是由RMI引来,加入到CORBA中的。CORBA最初并不支持这项功能;但是,这个概念对于实现JAVA与CORBA之间的互操作是至关重要的。
JAVA到IDL语言映射规范定义了如何把JAVA接口映射到CORBA的IDL语言。这个定义使CORBA分布对象可以访问本来不具有CORBA的IDL的EJB(还有那些RMI-IIOP分布对象)。特别的是,这个规范定义了一个JAVA的RMI子集,叫RMI/IDL,它可以让你
映射到IDL,用IIOP(或是更通用,是GIOP协议)作为通信的底层协议。
2. RMI/IDL
许多RMI/IDL数据类型遵循一定的约束;我们来看一下那些最重要的类型。更详细的信息,请参看JAVA到IDL的语言映射规范。
表1显示了JAVA基本类型到IDL的映射。
表1:JAVA到IDL的映射
Java
OMG IDL
void
void
boolean
boolean
char
wchar
byte
octet
short
short
int
long
long
long long
float
float
double
double
JAVA包映射为IDL的模块。RMI/IDL中的远程接口映射为IDL的接口并具有相对应的名字。但是,那些用JavaBean命名方式用来只读或读写属性的方法被映射为IDL的属性。后面我将提到这个。
JAVA中可序列化的对象映射为CORBA的值类型。值类型为CORBA提供了用值来进行传递的语义。值类型是属于本地的,不能被远程调用。它们不注册到ORB中,也不需要标识符,因为它们的值就是它们的标识符。更详细的信息,请参阅《Professional J2EE EAI》和CORBA/IIOP规范2.6版。
就象我已经提到过的,所有的JAVA或序列化的对象,包括JAVA固有的和用户定义的,都将映射为值类型。但是,这个规则也有一些例外----例如,当你想把java.lang.String映射到IDL时。如果把它定义为常量(final static),这个对象将被映射为IDL的wstring。在其它情况下,包括作为方法的参数或返回值,该对象都被映射为值类型CORBA::WStringValue。这个值类型是CORBA模块的一部分,它的IDL定义如下:
valuetype WStringValue wstring;
这等同于下面的IDL定义:
valuetype WStringValue {
public wstring data;
};
但是,要记住,第一种定义能够更干净地映射到JAVA。表2列出其它特殊的映射情况。
表2:其它重要的特殊映射情况
Java
OMG IDL
java.lang.Object
::java::lang::_Object
java.lang.String
::CORBA::WStringValue or wstring
java.lang.Class
::javax::rmi::CORBA::ClassDesc
java.io.Serializable
::java::io::Serializable
java.io.Externalizable
::java::io::Externalizable
java.rmi.Remote
::java::rmi::Remote
org.omg.CORBA.Object
Object
3. 实现集成
后面我将返回来讨论值类型,先讨论用户定义的类,再讨论内嵌的类,如Vectors、Collections和Enumerations。现在,让我们看一下CORBA和EJB集成的基本方式。首先,我们需要一个EJB。在第一个例子中,我们使用一个简单的会话Bean,它只使用简单的数据类型作为方法的参数和返回值。我们没有强行去用值类型。(注意:从CORBA客户端访问实体Bean跟访问会话Bean的过程一样。)
这个方式是最简单的;但是,你不能把它用在复杂的接口上。它的好处是:你可以使用不支持值类型的ORB。许多CORBA产品都是这样(不支持值类型),尤其是那些不是用C++实现的产品。
这个例子中,我将使用C++版本的ORBacus4.1.0作为CORBA的ORB,使用VC++6.0作为编译客户端代码的编译器。为部署这个例子中的EJB,我将使用JBoss3.0.0。你可以从网上下载ORBacus4.1.0(也可以从IONA网站上下载)和JBoss3.0.0。
你可以使用任何支持CORBA2.3.1或更高版本的ORB产品(只要它支持到C++的映射)、一个相对应的C++编译器和一个支持J2EE1.3规范的应用服务器。理论上讲不需要修改代码;但是,如果你使用其它产品,小的改动可能是必要的。
4. EJB会话Bean
让我们简单地看一下这个名叫CorbaEai的会话Bean。在我们的第一个例子中,这个简单的远程接口包含一个计算两个整数之和的简单方法。
package eai;
import java.rmi.RemoteException;
import javax.ejb.EJBObject;
public interface CorbaEai extends EJBObject {
public int sum(int a, int b) throws RemoteException;
}
在你熟悉了集成过程之后,我将告诉你如何扩展这个接口以包含使用用户定义的对象和内嵌对象的方法。
要转这个例子,你必须把会话Bean部署到一个应用服务器上。这要求你定义Home接口、实现这个Bean的实现类、定义部署描述器、创建jar文件并最终部署这个EJB。我不写这些步骤了,但是你可以下载这个例子的源码包。
5. 开发CORBA客户端
要开发CORBA客户端,我们需要完成以下步骤:
l 从会话Bean的Home接口和组件远程接口生成IDL
l 简化生成的IDL
l 编译IDL接口,生成相应的编程语言的代码----我们这个例子中是C++----来生成桩及其它必需的映射
l 确定如何使用JNDI作为CORBA的名字服务
l 开发C++的客户端和支持值类型
l 创建客户端
5.1. 生成IDL
为从JAVA接口生成IDL,你可以使用任何支持JAVA语言到OMG的IDL映射规范的工具。例如:
l rmic编译器,由JSDK1.3或更版本提供,使用-idl选项;
l java2idl,由VisiBroker提供;
l rmic和ejbc(使用-idl)编译器,由BEA的WebLogic提供
其它的应用服务器提供类似的工具。需要明确的是,从EJB接口生成IDL接口要比从RMI-IIOP接口生成复杂得多。首先,EJB接口继承于EJBObjec和EJBHome接口,它们定义了一些基本方法。因为IDL接口也必须继承于这些接口,所以其工具也必须为这些接口生成IDL接口。其次,EJB的home接口中的方法至少抛出CreateException异常,所以其工具也必须把这个异常和一些其它用户定义的异常映射到IDL。
这儿我用的是JSDK1.3.1提供的rmic,注意要把包含要映射的接口的jar包包含在classpath中(对于JBoss来说,要包含$JBOSSHOME\client\ jboss-j2ee.jar)。这儿需要映射的是eai.CorbaEai和eai.CorbaEaiHome两个类:
rmic -idl -classpath "%classpath%;corbaeai.jar" eai.CorbaEai eai.CorbaEaiHome -d ./idl
rmic编译器在目录./idl下生成IDL文件,共有26个。
5.2. 简化生成的IDL的文件
用C++来实现所有的生成的接口和值类型是很费时间的。但是我们并不需要所有的生成的接口,因为有一些方法我们没有去用。所以我们可以通过简化生成的IDL接口文件来节省一大部分工作。
对于这第一个例子,我们不去用EJBObject和EJBHome两个接口上的方法,我们也不需要EJBMetaData。我们也不需要特殊的RemoveException异常,这样,我们就可以不用RemoteExceprion和RemoteEx两个IDL接口。我们还可以除去下列IDL接口:java.io、java.lang和java.rmi。
为进一步简化IDL文件,我们把所需要的IDL接口放在一个文件中。经过简化以后,得到以下IDL文件CorbaEai.idl:
#include "OB/orb.idl" //ORBacus中的orb.idl在OB目录下。
module javax {
module ejb {
valuetype CreateException {
};
#pragma ID CreateException "RMI:javax.ejb.CreateException:FD98A9711F66DF7F:575FB6C03D49AD6A"
exception CreateEx {
CreateException value;
};
};
};
module eai {
interface CorbaEai {
long sum(
in long arg0,
in long arg1 );
};
#pragma ID CorbaEai "RMI:eai.CorbaEai:0000000000000000"
interface CorbaEaiHome {
::eai::CorbaEai create( ) raises (
::javax::ejb::CreateEx );
};
#pragma ID CorbaEaiHome "RMI:eai.CorbaEaiHome:0000000000000000"
};
5.3. 编译IDL接口
接下来,我们把IDL接口映射到客户端的编程语言。我们在客户端用C++,这儿我们用ORBacus的idl编译器(你可以用任何支持CORBA2.3.1或更高版本规范的IDL到C++的编译器)。
如果你仔细看IDL的文件,你会发现它包含了一个文件orb.idl。在我们的例子中,我们用ORBacus自带的orb.idl文件,它在$ORBacusHome/idl/OB目录下。因此,在使用IDL编译器时,我们要指定包含orb.idl的路径。我们用ORBacus所带的IDL编译器进行编译,因为我们只是编写客户端,所以不需要服务方的骨架文件:
idl -IE:\OOC\idl --no-skeletons CorbaEai.idl
会生成stub文件CorbaEai.h和CorbaEai.cpp。
5.4. 使用JNDI作一个CORBA名字服务
在我们开发C++客户端之前,先考虑一下如何获得会话Bean的Home接口CorbaEaiHome的初始引用。CorbaEaiHome已经注册到应用服务器的JNDI上,所以C++的客户端必须访问JNDI名字服务。
有一些应用服务器,就象BEA的WebLogic,提供了一个跟CORBA名字服务相兼容的接口去访问JNDI。为测试这一点,我们可以用WebLogic的host2ior工具,它将输出这个名字服务的使用IIOP和IIOP/SSL的IOR。对我们这个例子来说,我们只对非安全的IIOP感兴趣。JBoss也提供一个CORBA名字服务,从JBoss的启动日志中可以看到这个服务的IOR。
CORBA名字服务是一个有着标准接口的CORBA分布对象,从技术上讲,它和其它CORBA对象一样。因此,我们可以通过IOR直接连接到JNDI。
但是,一个更好的方法是通过用一个关键字NameServie析取初始引用来得到名字服务的引用。在本例中,我们在启动客户端时指出所要用的名字服务,我们通过命令行实现,在后面将会提到。
5.5. 开发C++客户端
我们开发一个简单的C++客户端来调用会话Bean CorbaEai上的sum()方法。假定你熟悉CORBA。客户端的主程序Client.cpp如下:
#include <OB/CORBA.h>
#include <OB/CosNaming.h>
#include <CorbaEai.h>
#ifdef HAVE_STD_IOSTREAM
using namespace std;
#endif
int run(CORBA::ORB_ptr orb, int argc, char* argv[])
{
//
// Get naming service
//
CORBA::Object_var obj;
obj = orb -> resolve_initial_references("NameService");
CosNaming::NamingContext_var nc =
CosNaming::NamingContext::_narrow(obj.in());
//
// Resolve names with the Naming Service
//
CosNaming::Name name;
name.length(1);
name[0].id = CORBA::string_dup("Corba-Eai");
name[0].kind = CORBA::string_dup("");
cout << "Resolved `CorbaEaihome´" << endl;
CORBA::Object_var aObj = nc -> resolve(name);
eai::CorbaEaiHome_var ceaihome = eai::CorbaEaiHome::_narrow(aObj.in());
assert(!CORBA::is_nil(ceaihome.in()));
cout << "Creating a new instance" << endl;
eai::CorbaEai_var ceai = ceaihome->create();
cout << "Result: "<< ceai->sum((CORBA::Long)15,(CORBA::Long)20)<< endl;
return EXIT_SUCCESS;
}
int main(int argc, char* argv[], char*[])
{
int status = EXIT_SUCCESS;
CORBA::ORB_var orb;
try
{
orb = CORBA::ORB_init(argc, argv);
status = run(orb, argc, argv);
}
catch(const CORBA::Exception& ex){
cerr << ex << endl;
status = EXIT_FAILURE;
}
if(!CORBA::is_nil(orb)){
try {
orb -> destroy();
}
catch(const CORBA::Exception& ex){
cerr << ex << endl;
status = EXIT_FAILURE;
}
}
return status;
}
5.6. 建立和运行客户端
我们用VC++6.0建立C++的客户端。源文件是idl编译生成的客户端的stub文件CorbaEai.h和CorbaEai.cpp和主程序文件Client.cpp。生成可执行文件CorbaEai.exe。
按照以下步骤运行本例子:
l 启动应用服务器,在我们的例子中是JBoss3.0。为了启动IIOP,所以用all模式来启动JBoss3.0.0(可能集群服务启动不起来,如果起不来,可以先把集群服务屏蔽掉)。
l 部署会话Bean CorbaEai到JBoss应用服务器上。
l 使用下面的命令启动C++客户端程序:
CorbaEai ?CORBInitRef NameService=corbaloc::localhost:8683/JBoss/Naming/root
注意在本例中,EJB应用服务器和客户端在同一台机器上。如果要进行远程通信,需要用一个真正的名字或是IP地址代替localhost。
6.值类型
这个例子还是十分的不完美。它没有为Java的可序列化对象实现值类型,这就意味着我们不能捕捉到远程异常(因为正象IDL中所表示的,它们被映射成值类型)。另外,在第一个例子中我还限制了方法只使用原始的数据类型。
为了理解CORBA是如何处理Java的可序列化的类,你必须看一下CORBA的值类型。Java的可序列化对应着值类型。每一个被作为参数、返回值或异常传自于/传递到CORBA客户端的Java可序列化的对象都应该用客户端的编程语言再实现一次,在我们的例子中就是C++。这需要一定的工作。不幸的是,我不知道那些对Java的固有类型,如Remote、Throwable、EJBObject和EJBHome完全支持的CORBA实现。据我所知,只有IBM的WebSphere应用服务器提供了一个值类型的库,它包含了诸如Integer、Float、Vector、Exception和OutputStream等这些常用的Java类型的C++值类型的实现。根据WebSphere4.0应用服务器的文档,它的值类型库只支持WebSphere的C++ ORB。
对每一个值类型,IDL到C++的编译器生成一个指针类型的定义_ptr和一个_var类。_var类自动管理为对象引用动态分配的内存。一个转换操作符让你也可以把一个_var赋给_ptr。还需要提醒的是,原始的Java构造器不能映射到IDL。但是,当IDL映射到C++时,构造器变成工厂类的init方法。
为实现值类型(比如用C++),你需要:
l 实现一个值类型的类,它要继承于值类型的基类(由IDL编译器生成)和缺省的值引用计数的基类。对于值类型,你必须手工实现引用的计数。
l 实现工厂类和工厂方法。
l 把工厂注册到ORB。
让我们看一下CreateException值类型的例子。首先,我们声明它的实现类CreateExceptionImpl:
class CreateExcepionImpl : public virtual ::javax::ejb::OBV_CreateException,
public virtual CORBA::DefaultValueRefCountBase
{
我们至少必须实现构造器和_copy_value()方法,它简单地一个新的值类型类的实例:
public:
CreateExceptionImpl() : OBV_CreateException() { }
virtual ~CreateExceptionImpl() { }
CORBA::ValueBase* _copy_value() {
return new CreateExceptionImpl();
}
我们也能够为其它一些方法提供实现,如message()、localizedMessage()和toString():
CORBA::WStringValue* message() {
return new CORBA::WStringValue(CORBA::wstring_dup(L”javax::ejb::CreateException”));
}
CORBA::WStringValue* localizedMessage() {
return new CORBA::WStringValue(CORBA::wstring_dup(L”javax::ejb::CreateException”));
}
CORBA::WStringValue* toString() {
return new CORBA::WStringValue(CORBA::wstring_dup(L”javax::ejb::CreateException”));
}
};
接下来,我们定义工厂类CreateExceptionFactory。我们至少要实现create_for_unmarshal()方法,但是我们还要实现create__()方法。这两个方法都返回一个新实现的类实例:
class CreateExceptionFactory: public ::javax::ejb::CreateException_init
{
public:
CreateExceptionFactory() { }
virtual ~CreateExceptionFactory() { }
javax::ejb::CreateException* create__() {
return new CreateExceptionImpl();
}
CORBA::ValueBase* create_for_unmarshal() {
return new CreateExceptionImpl();
}
};
最后,我们把工厂注册到ORB上。ORB接口提供register_value_factory()方法,它接收库ID号和工厂实例作为参数。我们可以从IDL中得到库ID号(它由#pragma指示)。下面的例子示范了如何注册CreateException:
orb->register_value_factory(“RMI:javax.ejb.CreateException:FD98A9711F66DF7F:575FB6C03D49AD6A”, (CORBA::ValueFactory)new CreateExceptionFactory);
我们简单地实现了在CORBA客户端中所需要的所有值类型。尽管这看起来有些复杂,但是你将会发现这个过程并不是很难而且你可以定义模板类来自动生成它。
7.开发一个更高级的CORBA客户端
现在你对CORBA的值类型已经熟悉了,你可以为具有更复杂接口的EJB开发CORBA客户端。
在这个例子中,我们用几个使用了用户自定义对象和固有的Java对象的方法来扩展CorbaEai会话Bean的接口。首先,我们添加sumObj()方法来将两个Integer对象相加:
public Integer sumObj(Integer num1, Integer num2) throws RemoteException;
然后我们为一个可序列化的类MyType添加一个getter/setter操作对:
public MyType getMyType() throws RemoteException;
public void setMyType(MyType mt) throws RemoteException;
MyType类非常简单,它包含两个属性(为简单起见,我没有列出相对应的getter/setter操作方法):
package eai;
import java.io.Serializable;
public final class MyType implements serializable {
public int a;
public double b;
}
最后,我们添加一个返回Java向量的方法getVector()和一个返回Java集合的方法getCollection():
public Vector getVector() throws RemoteException;
public Collection getCollection() throws RemoteException;
这些方法代表了大多数基于EJB的应用中的典型方法。完整的例子可以参见源代码2。
开发这个CORBA客户端的过程跟第一个例子相似。首先,我们生成IDL接口。我们使用跟前面一样的命令;但是,这次我们不再简化IDL。
注意,为会话Bean CorbaEai远程组件接口生成的IDL接口遵循RMI/IDL映射属性访问方法的规则。使用JavaBean命名方式来读写/只读属性的方法没有映射到IDL操作,而是映射到IDL属性。在我们的例子中,我们有getMyType()和setMyType()读写属性的方法和getVector()、getCollection()的只读方法。这些方法映射到下面的IDL属性:
attribute ::eai::MyType myType;
readonly attribute ::java::util::Vector vector;
readonly attritube ::java::util::Collection collection;
为了从C++访问这些属性,你必须知道它们是如何从IDL映射到C++(或者你选择的其它语言)。对C++,每一个属性映射到两个重载的和属性一样名字的C++函数,一个用来设置属性,一个用来读取属性。只读属性只映射到一个读取函数。更详细的信息请参阅C++语言映射。
其它方法,如sum()和sumObj(),都是一对一地映射:
long sum(in long arg0, in long arg1);
::java::lang::Integer sumObj(in ::java::lang::Integer arg0, in ::java::lang::Integer arg1);
接下来,我们用ORBacus的idl编译器把IDL接口编译成C++。你可以一个一个地来编译所有的IDL文件,也可以把所有的IDL文件放在一个文件中编译。
现在,我们可以编写C++客户端的代码。第一,按照我前面解释的,为所有有关的值类型提供值类型的实现(值类型的类和工厂类)。在例子中,我们为下面这些值类型定义实现:
l MyType
l Integer
l Vector
l CreateException
l RemoveException
我们还要把这些值类型注册到ORB。
你也许要奇怪为什么我们没有为Collection定义一个值类型。你知道,Collection是一个接口,它由类AbstractCollection实现,提供了一个框架性的实现。JavaSDK为更多的特定接口提供了实现,如List。为了理解这种类和接口之间的层次关系,请看以下的UML图:
Collection的接口和实现类层次
你可以把Vector看成Collection接口的一个可能的实现,所以我们用它来访问Collection。
实际上C++用来调用CorbaEai会话Bean方法的代码是比较简单的。为调用sumObj()方法,我们先创建两个Integer值类型并插入数值,然后调用方法、输出结果:
::java::lang::Integer_var javaInt1 = new IntegerImpl();
::java::lang::Integer_var javaInt2 = new IntegerImpl();
javaInt1->value(15);
javaInt2->value(20);
::java::lang::Integer_var javaIntR = ceai->sumObj(javaInt1, javaInt2);
cout << "sumObj(): " << javaIntR->value() << endl;
为了调用getMyType()和setMyType()方法,回忆一下它们是如何被映射到IDL属性的。所以,我们用C++重载的方法myType()。下面的代码调用会话Bean上的getMyType()方法:
::eai::MyType_var mt = ceai->myType();
cout << "getMyType(): " << mt << endl;
为了调用setMyType()方法,我们先创建一个MyType实例并填充必要的数值;剩下的就是调用它了:
cout << "setMyType()... " << endl;
::eai::MyType_var mt2 = new MyTypeImpl();
mt2->a((CORBA::Long)9);
mt2->b((CORBA::Double)9.9);
ceai->myType(mt2);
注意,这儿的方法名字已经变了,因为我们用的是JavaBean的命名方式(get/set方法)。否则,方法就不会改变。
对getVector()方法也是一样,它被映射到一个只读的IDL属性。所以,我们用C++的vector()方法来访问它:
::java::util::Vector_ptr vec = ceai->vector();
cout << "getVector(): " << vec << endl;
这个过程中最有趣的事情也许是我们如何访问Collection的过程。调用会话Bean的getCollection()方法(在C++中是collection())后,我们把它显式的转换为一个Vector:
::java::util::Collection_ptr coll = ceai->collection();
::java::util::Vector_ptr cvec = ::java::util::Vector::_downcast(coll);
cout << "Collection: " << cvec << endl;
最后,注意,我们可以应用那些从EJBObject继承来的远程组件接口方法。如最重要的方法remove()。所以,我们可以得出结论我们的例子中有以下一行:
ceai->remove();
理想的集成
在本文中,我已经说明了在CORBA和EJB的的集成是可能的。但是,用非Java开发一个EBJ的客户端并不象你所希望的那么简单,尤其是当你使用那接收和返回对象或是用户定义类型的方法。这是应用EJB组件的绝大多数情况,所以你必须使用CORBA的值类型。最主要的问题是你还要必须实现Java固有的类型。我希望CORBA的销售商尽快提供值类型的实现,如IBM的WebSphere应用服务器。我也希望不同的CORBA产品之间的互操作问题能够尽快解决。
无论如何,当你把一个现有的不是基于Java的应用跟新的基于J2EE的方案进行集成时,EJB和CORBA之间的互操作是十分重要和有用的。
译者说明:
l 在第7节中,采用值类型,当用rmic把JAVA接口映射到IDL时,我使用了-noValueMethods,因为如果带着该参数编译出来的IDL文件要多一些,再映射到C++后,很难编译过去。
l 即使使用-noValueMethods选项,最后生成的C++程序还是不能完全进行正常地编译和运行(涉及到Java固有的那些类型:Integer、Vector、Collection),所以只好将一些语句屏蔽掉了。
<!-- google_ad_client = "pub-8054039502712326"; google_ad_width = 160; google_ad_height = 600; google_ad_format = "160x600_as"; google_ad_channel =""; google_color_border = "336699"; google_color_bg = "FFFFFF"; google_color_link = "000000"; google_color_url = "006699"; google_color_text = "000000"; //-->
更多资源:
参与论坛讨论: http://www.java-cn.com/forum/index.jsp
更多技术文章: http://www.java-cn.com/technology/index.jsp
JAVA中文站: 我们要做中国最好的JAVA门户网站,记住我们的域名: JAVA-CN.COM | JAVA-CN.NET | JAVA-CN.ORG本文网址: http://www.java-cn.com/technology/technology_detail.jsp?id=3560
本文地址:http://com.8s8s.com/it/it9645.htm