本文面向的观众是对j2ee技术有兴趣的入门者。
java语言
java语言最早被称为oak,它是为了实现嵌入式的消费类电子产品应用而产生的,它的作者是james gosling.ed frank, patrick naughton, jonathan payne, chris warth在随后的几年时间中为java语言加入了大量的特性,并把java语言的目标做了一个重新的定位,定位于适合internet的语言。
java语言是一种多用途的语言、并发的语言、以类为基础,面向对象的语言。它的设计尽可能的做到和操作系统是无关的,也就是java所宣传的那句话:“一次编写,到处运行。”java的设计参考了c和c++语言,因此熟悉c和c++的程序员对java语言上手很快,而java设计的原则是能够利用java语言快捷的编写应用,所以我们可以发现,在java语言中,并没有那些c和c++中的复杂的机制。最明显的就是c中被大量使用的指针,由于它的随意性,被java以引用来代替了。而c++中的操作符重载、模板、泛型的特性也因为使用比较复杂,java也不予采用。但是目前java仍然不断的推出新的特性,以满足应用的发展。例如在新推出的jdk1.4中,java语言就能够支持assertment机制和perl语言中最有用的正则表达式机制。
java语言主要由以下五种元素组成:标识符、关键字、文字、运算符和分隔符。这五种元素有着不同的语法含义和组成规则,它们互相配合,共同完成java语言的语意表达。
1:标识符。变量,类和方法都需要一定的名称,我们将这种名称叫做标识符。
2:关键字。关键字是java语言本身使用的标识符,它有其特定的语法含义。所有的java关键字将不能被用作标识符。
3:数据类型。java有着不同的数据类型。比较值得一提的是字符串数据类型,字符串数据类型是用一对双引号括起来的字符序列,字符串数据实际上是由string类所实现,而不是c语言中所用的字符数组。每一个字符串数据将产生一个string类的新的实例,用户不必对字符串与类这个概念发生关系而感到担心,由于类的特性,你不必担心如何去实现它们,它们会自己照顾好自己,需要说明的是字符串在java里作为类只是出于安全的考虑。
4:运算符。任何语言都有自己的运算符,java语言也不例外,如+、-、*、/等都是运算符,运算符的作用是与一定的运算数据组成表达式来完成相应的运算。对不同的数据类型,有着不同的运算符。
5:分隔符。分隔符用来使编译器确认代码在何处分隔。‘’‘’‘;’‘:’都是java语言的分隔符。
学习 java 语言很简单,毕竟 java 语言也只包含五十多个关键词(keyword)与几十个算符(operator),再加上 java 语法(syntax)也很简单,所以一般人可以很快就学会 java 语言。
危险的是,很多人认为已经完全掌控 java 语言,但其实对于内部的运作机制仍不能掌握,这些盲点有时候会让你无法完全掌控 java 语言。
克服这些盲点的方式是看「the java language specification, 2nd ed.」(没有中文版)来彻底弄懂 java 程序语言,并看「inside the java virtual machine, 2nd ed.」来彻底掌握 java 虚拟机器的运作方式。
学会了语言,并不代表就可以设计出好的对象导向系统架构。想要成为对象导向的专家,往往需要:
(1) 多看相关的书,特别是 design pattern 和 refactoring 的书。
(2) 多观摩别人的程序(例如 java api 的 design 与 implementation)
(3) 多写程序。
学会 java 语言之后,还需要学会一些 api 才能写出有用的程序。
java 的 api 非常多,必须规划好一个学习路径,才不会在浩瀚的 api 大海中迷失。必备的 api 包括了:io、new io、collection framework、network、rmi、jaxp…… 等。
至于其它的 api,就看你的需求而定,大致上分成:
* gui 类:javabean -> swing -> javahelp -> java2d -> image io -> jai -> java 3d ……
* enterprise 类:jdbc -> jdo -> servlet -> jsp -> ejb -> jms -> jta/jts……
* j2me 类(这一类不是我的专长,无法提供学习顺序建议)
java语言通常都是根据java虚拟机规范(the java virtual machine specification)中的定义,编译为字节码指令集和二进制格式。因此我们接下来就讨论java虚拟机(jvm)
jvm
我们已经谈过java语言的语法类似于c和c++,但是摒弃了c和c++中复杂、疑惑和不安全的特性。java语言最早是用来构建消费类网络设备的软件的,因此它要支持多主机的架构,并要求能够提供安全的软件组件。为了满足这些需求,编译好的代码就必须能够通过网络来传播,能够在任何客户端上运行,同时还要保证客户端是足够安全的。
java虚拟机是java和java 2 平台的基石。它能够保证java语言和硬件、操作系统无关,保证编译后的代码最小,并保护用户不受恶意程序的攻击。java虚拟机到底是什么呢。其实它就是一台不实际存在的计算机。和真实的计算机类似,它也有自己的指令集,并可以在运行环境中分配内存区域。使用虚拟机机制来实现编程语言并不是java的创举,这已经是非常普遍的做法了,最著名的许你就莫过于ucsd pascal的p-code机。
只要浏览器检测到目前所处理的web文件内容含有一个java applet,浏览器将会为这个java小程序另外开一个jvm,执行这个java应用小程序。在jvm中执行的java小程序可以得到充分安全的保护。如同我们上面所说,jvm是一个自给自足的作业环境,就像是一台独立的计算机一样。例如,在jvm运作的applet,无法存取主机操作系统。优点是:
1. 系统中立。java应用程序可以在任何jvm中运作,不论该系统使用何种硬件、软件。
2. 安全。正因jvm跟操作系统没有任何接触,java程序很难损害到其它档案或应用程序。
缺点是,由于在jvm运作的程序独立在操作系统之外,也就无法享受操作系统各项特殊功能。
java技术之所以在今天得到了如此广阔的应用,其中它的安全性是不能不提的。不同于其它技术(例如microsoft的activex)中安全性作为附加设计和补丁,java从设计之初便考虑到了安全性。因此java的安全性是在语言层次实现的。java的安全性由下列三个方面保证:
1、 语言特性(包括数组的边界检查、类型转换、取消指针型变量)。
2、 资源访问控制(包括本地文件系统访问、socket连接访问)。
3、 代码数字签名(通过数字签名来确认代码源以及代码是否完整)。
java的源代码是先编译成为一种字节码的中间代码,存放这种代码的文件就是class的文件。真正执行的时候是将class文件装载到jvm(虚拟机)中,然后由jvm解释执行的。所以数组的上下界检查及合法的类型转换是通过jvm得到保证的。java通过一个类装载器类(classloader)将虚拟机代码文件(即class文件)装载到jvm中,当完成装载后,一个被称做安全管理器(securitymanager)的类开始运行,例如当一个applet的class文件被缺省的类装载器装载到jvm中后,jvm会立即为它装载一个securitymanager的子类appletsecurity,由这个管理器来验证操作。代码的所有动作(例如文件读写)都要先经过验证,只有被该安全管理器接受的动作才能完成,否则就会抛出securityexception异常。
对于jdk1.0,权限被笼统的划分为两大块。一是拥有所有的权限,一个是仅拥有"沙箱"(sandbox)权限,这也是普通的applet所拥有的权限。这时本地文件读写或是与源主机(orignal server)以外的主机连接都是被禁止的。这种划分的最大问题就是缺乏灵活性。例如我们希望一个applet在用户信任的情况下能够对本地文件系统的某个目录进行读写,但并不要通过socket与其它主机连接。这是jdk1.0的权限划分就不能达到要求。jdk1.1后改进了权限的划分,引入了权限集(permissionset)的概念。
由于我们的文章并不是讨论jvm,因此,我们只是对jvm做一个简单的介绍。如果需要详细了解的,可以参考「the javatm tmvirtual machine specification」。
客观的看待java
相对于其他编程语音,java有一个无庸置疑的优点:用户以及编译器第一次不必了解生成可执行代码的特定cpu细节。java引入了一个编译代码中间层,叫做字节代码,并使用一个虚拟抽象的机器,而不是一个真实的机器。当java编译器结束了一个源文件的编译后,你所得到的不是可以立即在一个给定平台上运行的代码,而是可以在任何真实的平台上运行的字节代码,唯一的条件就是这个平台要理解和支持java.这些发展包含着一个文化的变革。作为一个开发人员,你只需要确定java虚拟机(jvm)提供的抽象层,不同的os销售商负责执行代码层,从而将中立于平台的字节代码映射到主机平台的机构中。在这种情况下,java似乎是统一分布式计算机世界的领袖候选人了。“编写一次,永远运行”(并且无论在哪里)就成为java诱人但却真实的口号。
但我们平心而论,java的跨平台并不是一个非常诱人的特性?跨平台理论的发展很好地证明了这一点。我们看到,将java代码从一个平台移植到另一个平台?java这个语言最重要和最受吹捧的特点?并不象宣传的那样容易。任何java平台都有其自己的虚拟机,它可以理解通用的字节代码,并且及时地将其编译为本地代码。矛盾由此产生,不同虚拟机的执行也很不相同,这一点足以使代码的移植比预期耗费多得多的时间,而且基本上不是自动的。在企业用户的角度上来说,也很少会有企业会频繁的更换平台,因此这个特性是否能够带来高价值是很难评价的。
那么,java模型的好处在哪里呢?首先,java是一种先进的、面向对象的语言,包含了预防常见错误的内置功能,并在仅仅一两个对象中携带了许多经常需要用到的功能。与c++相比,java更易于读写,不容易出错,而且更加美观,但是它速度较慢也不太灵活。想实现在任何软件和硬件平台上都可虚拟移植,java尽可能少地使用了公分母模型,也就是说放弃了将每个平台开发到极限的能力。第二,虚拟机的概念本身就是可移植和可共用的,因此对于分布式环境来说是理想的。java对于为非windows平台开发代码是最好的语言。
那么对于windows平台来说,java又怎么样呢?让java适应windows是不可能的,这是由于sun的许可约束问题。但是java实在是太吸引人了,microsoft比谁都能更清楚这一点。microsoft在以前推出的visual j++证明了这一点,但是可惜的是,microsoft又犯了霸权的老毛病,visual j++并不好用。因此,microsoft又一次采取了“拿来主义”的手法,很好地利用了java 的众多特性,隆重推出了windows平台的新锐力量,它就是相当简单但十分强大的面向对象的c#编程语言。c#超过了c++,它天生就包含了。net框架类库中的所有类,并使语法简单化。说到这里已经有一些离题了,不过java也不是说在windows平台上就不能够使用,jdk和大部分的ide都支持windows平台。
java技术的架构――j2me、j2se和j2ee
通常我们以 jdk(sun 所开发的一套 java 开发工具)的版本来定义 java 的版本。jdk 1.0 版于 1996 年初公开,jdk 1.1 版于 1997 年初公开,jdk 1.2 版于 1998 年底公开。基于市场行销的考量,sun 在 jdk 1.2 版公开后旋即将 java 改名为「java 2」,将 jdk 改名为「java 2 software development kit(以下简称 j2sdk)」。j2sdk(原称 jdk)1.3 于 2000 年 4 月公开,此版本仍称做「java 2」。目前 j2sdk 1.4 也已经公开了,大家可以到sun的官方java站点上查阅到大量的jdk1.4的信息。
java 技术根据硬件平台与适用环境的差异,分成几个分支。jdk 1.1 的时代,适用于一般消费性电子产品等,嵌入式系统的 java 平台是 personaljava 与 embeddedjava,此二者并无明确的界线,大致上来说,运算资源、内存、以及显示装置比较丰富者,使用 personaljava,例如 set-top box、视讯电话 …… 等;反之,资源较有限者使用 embeddedjava,例如呼叫器、行动电话 …… 等。除了 pc 使用的 java 平台、ia 使用的 personaljava 与 embeddedjava 平台之外,javacard 也是一个 java 平台,使用于 smart card(ic card)上。
java 2 出现后,推翻了先前的 personaljava 与 emeddedjava 的分法,改分成 java 2 platform enterprise edition(简称 j2ee)、java 2 platform standard edition(简称 j2se)、java 2 platform micro edition(简称 j2me)。j2ee 适用于服务器,目前已经成为企业运算、电子商务等领域中相当热门的技术;j2se 适用于一般的计算机;j2me 适用于消费性电子产品。除了这三者之外,javacard 依然是独立的一套标准。
目前,java技术的架构包括三个方面:
j2ee(java 2 platform enterprise edition )?企业版 (j2ee) 是为面向以企业为环境而开发应用程序的解决方案。
j2se(java 2 platform stand edition)?标准版 (j2se) 为桌面开发和低端商务应用提供了可行的解决方案。
j2me(java 2 platform micro edition )?小型版(j2me)是致力于消费产品和嵌入式设备的最佳解决方案
j2ee
j2ee已经成为开发商创建电子商务应用的事实标准。正是认识到j2ee平台作为一种可扩展的、全功能的平台,可以将关键的企业应用扩展到任何web浏览器上并可适合多种不同的internet数据流、可连接到几乎任何一种传统数据库和解决方案、使企业经理根据多家企业所提供的产品和技术开发和部署最佳的解决方案进而降低开发网络化应用的费用和复杂性这一巨大优势,很多厂家都表示将对j2ee给予支持,并将j2ee技术作为大型btob市场和海量交易处理的安全稳定的端到端平台。j2ee技术的基础就是j2se标准版,它巩固了标准版中的许多优点。其最终目的就是成为一个能够使企业开发者大幅缩短投放市场时间的体系结构。它为灵活配置各种多层企业应用软件,特别是b2b、b2c等电子商务应用,提供了强大的服务功能。最近又新加了connector api服务,使企业应用的开发和部署有了一系列成熟的技术。
j2se
j2se是java 2平台的标准版, 它适用于桌面系统,提供corba标准的orb技术,结合java的rmi支持分布式互操作环境。它运行在java虚拟机上。在引入了java idl后, j2se支持iiop通信。它是高可移植性、异构性的实现环境和健壮平台,也是实现可伸缩性、可移植性、分布式异构互操作应用软件开发的标准平台。
j2me
j2me提供了http高级internet协议,使移动电话能以client/server方式直接访问internet的全部信息,不同的client访问不同的文件,此外还能访问本地存储区,提供最高效率的无线交流。j2me是java 2平台的微型版,它分成cdc(connected device configuration)和cldc(connected limited device configuration)两部分。cdc运行在连接虚拟机上,为手提式计算机一类较复杂的移动设备提供应用平台;cldc运行在核心虚拟机(kvm)上,它实现midp(mobile information device profile)移动信息设备应用平台,即针对手机之类的设备建立移动计算平台。
在小型的j2me(java 2 micro edition)方面,主要是应用在内存容量小、体积也较小的电子装置上。小至智能卡、行动电话,个人数字助理都是运用j2me的最佳平台。java在palm的应用上,palmos 4.0内含kjava,sun也推出针对palmos使用的j2me版本。所以,以既有的java程序设计知识,就可以在palm pda上开发出palm的各式各样应用系统。java和palm这两个标准平台的结合,将是下一波pda应用的趋势。java在手机的应用上,nokia、motorola、ericsson 都将推出利用j2me技术的新手机,所以java程序设计师有更多的平台可供施展。此种结合j2me及无线通讯技术的无线开放应用平台,将提供行动商务极佳的解决方案。
在中型的j2se(java 2 standard edition)方面,sun推出一个新的解决方案,称为java web start.原先的java applet是在webbrowser 中间开出一块方形区域来执行java程序,但是这样在执行效能和兼容性上都受限于原有的 web browser.现在新推出的java web start则是在操作系统上直接执行的java application,但是可以在网页上激活。如此一来既可和网页结合,在执行上也更快、更有效率。并且,sun和ibm都将推出支持64位运算的java版本,这对一般计算机上执行的客户端java应用系统的开发将会是一大利器。
另外在大型的j2ee(java 2 enterprise edition)应用上,可以说"j2ee"已经成为服务器运算环境的标准。java servlets、jsp(java serverpages)、ejb(enterprise javabeans)、javamail、jdbc、jms等,都是各家厂商产品开发的重点方向。j2ee兼容的是一般intel个人计算机(linux、windows……)、麦金塔以及各家高效能高稳定度的unix伺服主机,未来必定成为服务器运算市场上的主要选择之一。
除了以上这三大java组合之外,java和xml的整合也是未来的重点。sun公司已经推出java处理xml的标准延伸api - java api for xml parsing (jaxp),可以让各家所制作的xml解析器有接口上的标准。所以在java程序中,只要了解一套api(jaxp)就可以完全处理xml文件,让xml的应用更加方便。java这个跨平台的开发环境,加上xml这个跨平台的资料格式,此种跨平台优势组合势将成为未来讯息传递及资料交换的主要应用技术,如虎添翼地结合成一个最佳的跨平台解决方案。
藉由j2se (java 2 standard edition)可以开发在pc上的应用软件,藉由j2me (java 2 micro edition) 可以跨足更广大的家电、智能卡、电子装置等市场,再藉由j2ee (java 2 enterprise edition ) 可以整合伺服主机运算环境。java技术的应用范围几乎已经无所不在,java技术更可以在网际网络及电子商务各领域中,提供全方位的解决方案。
随着应用领域的不同,java 有许多 api(application programming interface),这些 api 分成三大类:
? java core api:由 sun 制定的基本 api,任何 java 平台都必须提供。
? java standard extension api (javax):由 sun 制定的扩充 api,java 平台可以选择性地提供或加装。
? 厂商或组织所提供的 api:由各家公司或组织所提供。
其中 core api 和 standard extension api 已经逐渐涵盖了大部份的信息应用领域,例如多媒体、数据库、web、企业运算、语音、实时系统、网络、电话、影像处理、加解密、gui、分布式运算 ……。如果你有某项需求尚未有标准的 java api 可遵循,你可以向 sun 提出制定新 api 的请求。经过审核之后,你的要求可能会通过、驳回 …… 等。如果通过,就会开始进入制定 api 的程序。java api 的制定过程因为公开,且经过许多业界技术领先公司的共同参与,所以相当完善而优异。
ejb的生态环境
在sun公司提供的ejb规范中,我们一个完整的基于ejb的分布式计算结构由六个角色组成,这六个角色可以由不同的开发商提供,每个角色所作的工作必须遵循sun公司提供的ejb规范,以保证彼此之间的兼容性。
ejb组件开发者: 开发并销售 ejb.应用组合者: 将不同的 ejb 搭建成应用。
部署者: 使用相应工具在运行环境下配置 ejb. ejb 服务器提供者: 开发并销售 ejb 服务器ejb 容器供应商: 开发并销售 ejb 容器系统管理员: 监视运行时情况
1、ejb组件开发者(enterprise bean provider)
ejb组件开发者负责开发执行商业逻辑规则的ejb组件,开发出的ejb组件打包成ejb-jar文件。ejb组件开发者负责定义ejb的remote和home接口,编写执行商业逻辑的ejb class,提供部署ejb的部署文件(deployment descriptor)。部署文件包含ejb的名字,ejb用到的资源配置,如jdbc等。ejb组件开发者是典型的商业应用开发领域专家。
ejb组件开发者不需要精通系统级的编程,因此,不需要知道一些系统级的处理细节,如事务、同步、安全、分布式计算等。
2、应用组合者(application assembler)
应用组合者负责利用各种ejb组合一个完整的应用系统。应用组合者有时需要提供一些相关的程序,如在一个电子商务系统里,应用组合者需要提供jsp(java server page)程序。
应用组合者必须掌握所用的ejb的home和remote接口,但不需要知道这些接口的实现。
3、部署者(deployer)
部署者负责将ejb-jar文件部署到用户的系统环境中。系统环境包含某种ejb server和ejb container.部署者必须保证所有由ejb组件开发者在部署文件中声明的资源可用,例如,部署者必须配置好ejb所需的数据库资源。
部署过程分两步:部署者首先利用ejb container提供的工具生成一些类和接口,使ejb container能够利用这些类和接口在运行状态管理ejb. 部署者安装ejb组件和其他在上一步生成的类到ejb container中。 部署者是某个ejb运行环境的专家。
某些情况下,部署者在部署时还需要了解ejb包含的业务方法,以便在部署完成后,写一些简单的程序测试。
4、ejb 服务器提供者(ejb server provider)
ejb 服务器提供者是系统领域的专家,精通分布式交易管理,分布式对象管理及其它系统级的服务。ejb 服务器提供者一般由操作系统开发商、中间件开发商或数据库开发商提供。
在目前的ejb规范中,假定ejb 服务器提供者和ejb 容器提供者来自同一个开发商,所以,没有定义ejb 服务器提供者和ejb容器提供者之间的接口标准。
5、ejb 容器提供者(ejb container provider)
ejb 容器提供者提供以下功能:
提供ejb部署工具为部署好的ejb组件提供运行环境 .ejb容器负责为ejb提供交易管理,安全管理等服务。
ejb 容器提供者必须是系统级的编程专家,还要具备一些应用领域的经验。ejb 容器提供者的工作主要集中在开发一个可伸缩的,具有交易管理功能的集成在ejb 服务器中的容器。ejb 容器提供者为ejb组件开发者提供了一组标准的、易用的api访问ejb 容器,使ejb组件开发者不需要了解ejb服务器中的各种技术细节。
ejb容器提供者负责提供系统监测工具用来实时监测ejb容器和运行在容器中的ejb组件状态。
6、系统管理员(system administrator)
系统管理员负责为ejb服务器和容器提供一个企业级的计算和网络环境。
系统管理员负责利用ejb 服务器和容器提供的监测管理工具监测ejb组件的运行情况。
将责任分离的另一个好处是在代码级上,可以将基于ejbs的系统逻辑的分派给更适合的专家。sun的ejb规范建议使用几个独立的角色,对于确定运作环境的责任链是非常重要的。举例说,ejb提供者是由商业专家和分析人员扮演的角色,他们确定一个组织内的最佳信息流程。但是仍旧有second domain expert,如应用程序汇编人员,他们集成不同的ejb组件并确保它可以确保满足应用程序的需求。
还有两种角色归入到系统级的部分,第一个是配置人员,他们负责实际的安装和配置基于ejb的系统。这需要有设置目录服务和集成现有应用程序的经验。第二个是系统管理员,他们要提供全天的监视和支持,确保应用程序正常运作。尽管系统管理员这个角色不需要是java编程专家,但是他需要能够应付以下问题:
设置java virtual machine (jvm)并关联系统环境参数(如:classpath)
使用java archive (jar)命令保存类文件懂得web服务器和servlet的工作原理。
要能通过监视运行中程序的状态确定优化方法。
很明显,有些角色是可以交叉的,比如系统管理员和配置人员。尽管配置人员可能是将类文件复制到服务器而系统管理员需要确定配置人员是否复制到了正确的位置。
ejb技术的基础是另外两种技术:rmi-iiop和jndi.
要想了解ejb,一定要先了解rmi-iiop和jndi.因此,我们在介绍ejb细节之前,先了解这两项技术。我们的介绍比较基本,因此大多数组织只要了解这些就已经够了。
java rmi-iiop
java rmi-iiop(java remote method invocation over the internet inter-orb protocol)是j2ee的网络机制。java rmi-iiop允许你编写分布式对象,使得对象的通信范围能够在内存中,跨java虚拟机,跨物理设备。
remote method invocation
rpc(remote procedure call)是一台机器的进程调用另一台机器的进程的过程。而remote method invocation则比rpc的概念更进一步,允许分布式对象间的通信。rmi-iiop允许调用远程对象的方法,而不仅仅是过程。这有利于面向对象编程。java rmi (remote method invocation 远程方法调用)是用java在jdk1.1中实现的,它大大增强了java开发分布式应用的能力。java作为一种风靡一时的网络开发语言,其巨大的威力就体现在它强大的开发分布式网络应用的能力上,而rmi就是开发百分之百纯java的网络分布式应用系统的核心解决方案之一。其实它可以被看作是rpc的java版本。但是传统rpc并不能很好地应用于分布式对象系统。而java rmi 则支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。
remote method invocation决不简单,需要考虑几个问题:
marshalling和unmarshalling.在不同机器间通过网络传递变量(包括java基本类型和对象),如果目标机器表示数据的方式和原机器不同该怎么办?例如二进制库不同。因此marshalling和unmarshalling就是传递变量的过程。
变量传递方法。变量有两种传递方法:pass-by-value和pass-by-reference.
对于前者,你的目标方法只需使用一份copy,但对于后者,远程方法对变量的任何修改都会影响到源数据。
网络和机器的不稳定。需要有一种机制保证一个jvm崩溃之后,不会影响系统的正常运作。
在 java 分布式对象模型中,remote object 是这样一种对象:它的方法可以从其它 java 虚拟机(可能在不同的主机上)中调用。该类型的对象由一种或多种 remote interfaces(它是声明远程对象方法的 java 接口)描述。远程方法调用 (rmi) 就是调用远程对象上远程接口的方法的动作。更为重要的是,远程对象的方法调用与本地对象的方法调用语法相同。
remote interface
rmi-iiop遵循了接口和实现的原则。你写的所有网络代码都是应用于接口,而不是实现。实际上,你必须使用rmi-iiop中的范例,没有其它的选择。直接在你的对象实现上执行远程调用是不可能的,你只能在对象类的接口上单独进行这一操作。
所以我们在使用rmi-iiop时,你必须建立一个客户接口,叫做remote interface.这个远程接口应该扩展java.rmi.remote接口。
remote object implementation
远程对象和客户机的物理位置并不是很重要。可以运行在同一地址空间或是跨internet运行。
为了使对象成为一个远程对象,你需要执行一下步骤:
继承javax.rmi.portableremoteobject.portableremoteobject是进行远程调用的基类,当你的远程对象调用构造器时,portableremoteobject对象的构造器也会自动被调用。
不继承javax.rmi.portableremoteobject.如果你的远程对象需要继承其它的类,而java不允许多重继承,因此你不能继承portableremoteobject.这时,你需要手动调用javax.rmi.portableremoteobject.exportobject()。
stub和skeletons
我们来看看在rmi-iiop背后隐藏的网络架构。rmi-iiop的一个好处就是你可以不用管你要调用的对象是本地的还是远程的。这就叫做local/remote transparency.
rmi应用程序通常包括两个独立的程序:服务器程序和客户机程序。典型的服务器应用程序将创建多个远程对象,使这些远程对象能够被引用,然后等待客户机调用这些远程对象的方法。而典型的客户机程序则从服务器中得到一个或多个远程对象的引用,然后调用远程对象的方法。rmi为服务器和客户机进行通信和信息传递提供了一种机制。
在与远程对象的通信过程中,rmi使用标准机制:stub和skeleton.远程对象的stub担当远程对象的客户本地代表或代理人角色。
调用程序将调用本地stub的方法,而本地stub将负责执行对远程对象的方法调用。
在rmi中,远程对象的stub与该远程对象所实现的远程接口集相同。
调用stub的方法时将执行下列操作:
(1) 初始化与包含远程对象的远程虚拟机的连接;
(2) 对远程虚拟机的参数进行编组(写入并传输);
(3) 等待方法调用结果;
(4) 解编(读取)返回值或返回的异常;
(5) 将值返回给调用程序。
为了向调用程序展示比较简单的调用机制,stub将参数的序列化和网络级通信等细节隐藏了起来。在远程虚拟机中,每个远程对象都可以有相应的skeleton(在jdk1.2环境中无需使用skeleton)。
skeleton负责将调用分配给实际的远程对象实现。
它在接收方法调用时执行下列操作:
(1) 解编(读取)远程方法的参数;
(2) 调用实际远程对象实现上的方法;
(3) 将结果(返回值或异常)编组(写入并传输)给调用程序。
stub和skeleton由rmic编译器生成。
要实现local/remote transparency可没有那么简单。为了屏蔽你调用的是远端主机上的对象,rmi-iiop需要模拟一个本地对象供你调用。这个本地对象叫做stub.它负责接受本地的方法调用请求,把这些请求委托给真正实现它们的对象(可以通过网络定位)。这样就使得远程调用看起来就和本地调用一样。
利用rmi编写分布式对象应用程序需要完成以下工作:
(1) 定位远程对象。应用程序可使用两种机制中的一种得到对远程对象的引用。它既可用rmi的简单命名工具rmiregistry来注册它的远程对象,也可以将远程对象引用作为常规操作的一部分来进行传递和返回。
(2)与远程对象通信。远程对象间通信的细节由rmi处理,对于程序员来说,远程通信看起来就像标准的java方法调用。
(3)给作为参数或返回值传递的对象加载类字节码。因为rmi允许调用程序将纯java对象传给远程对象,所以,rmi将提供必要的机制,既可以加载对象的代码又可以传输对象的数据。
在rmi分布式应用程序运行时,服务器调用注册服务程序以使名字与远程对象相关联。客户机在服务器上的注册服务程序中用远程对象的名字查找该远程对象,然后调用它的方法。
定位远程对象。应用程序可使用两种机制中的一种得到对远程对象的引用。它既可用 rmi 的简单命名工具 rmiregistry 来注册它的远程对象;也可将远程对象引用作为常规操作的一部分来进行传递和返回。
与远程对象通讯。远程对象间通讯的细节由 rmi 处理;对于程序员来说,远程通讯看起来就象标准的 java 方法调用。给作为参数或返回值传递的对象加载类字节码因为 rmi允许调用程序将纯 java 对象传给远程对象,所以 rmi 将提供必要的机制,既可以加载对象的代码又可以传输对象的数据。服务器调用注册服务程序以使名字与远程对象相关联。客户机在服务器注册服务程序中用远程对象的名字查找该远程对象,然后调用它的方法。rmi 能用 java系统支持的任何 url 协议(例如 http、ftp、file 等)加载类字节码。
stub只是解决了一半的问题。我们还希望远程对象也不用考虑网络问题。因此远程对象也需要一个本地的skeleton来接受调用。skeleton接受网络调用并把调用委托给远程对象实现。
你的j2ee服务器应当提供一种方法来产生必须的stub和skeleton,以减轻你的对网络问题考虑的负担。典型的是通过命令行工具来完成,例如sun的j2ee参考实现包就使用了一个名为rmic(rmi compiler)的工具来产生stub和skeleton类。你应当把stub部署在客户机上,并把skeleton部署在服务器上。
对象序列化和变量传递
在rmi分布式应用系统中,服务器与客户机之间传递的java对象必须是可序列化的对象。不可序列化的对象不能在对象流中进行传递。对象序列化扩展了核心java输入/输出类,同时也支持对象。对象序列化支持把对象编码以及将通过它们可访问到的对象编码变成字节流;同时,它也支持流中对象图形的互补重构造。序列化用于轻型持久性和借助于套接字或远程方法调用(rmi)进行的通信。序列化中现在包括一个 api(application programming interface,应用程序接口),允许独立于类的域指定对象的序列化数据,并允许使用现有协议将序列化数据域写入流中或从流中读取,以确保与缺省读写机制的兼容性。
为编写应用程序,除多数瞬态应用程序外,都必须具备存储和检索 java对象的能力。以序列化方式存储和检索对象的关键在于提供重新构造该对象所需的足够对象状态。
存储到流的对象可能会支持 serializable(可序列化)或 externalizable(可外部化)接口。
对于java对象,序列化形式必须能标识和校验存储其内容的对象所属的 java类,并且将该内容还原为新的实例。
对于可序列化对象,流将提供足够的信息将流的域还原为类的兼容版本。
对于可外部化对象,类将全权负责其内容的外部格式。
序列化 java 对象的目的是:
提供一种简单但可扩充的机制,以序列化方式维护 java对象的类型及安全属性;具有支持编组和解编的扩展能力以满足远程对象的需要;具有可扩展性以支持 java 对象的简单持久性;只有在自定义时,才需对每个类提供序列化自实现;允许对象定义其外部格式。
java.rmi.remote 接口
在 rmi 中,远程接口是声明了可从远程 java 虚拟机中调用的方法集。远程接口必须满足下列要求:
远程接口至少必须直接或间接扩展 java.rmi.remote 接口。
远程接口中的方法声明必须满足下列远程方法声明的要求:远程方法声明在其 throws 子句中除了要包含与应用程序有关的异常(注意与应用程序有关的异常无需扩展 java.rmi.remoteexception )之外,还必须包括 java.rmi.remoteexception 异常(或它的超类,例如java.io.ioexception 或 java.lang.exception )。
远程方法声明中,作为参数或返回值声明的(在参数表中直接声明或嵌入到参数的非远程对象中)远程对象必须声明为远程接口,而非该接口的实现类。
java.rmi.remote 接口是一个不定义方法的标记接口:
public interface remote
远程接口必须至少扩展 java.rmi.remote 接口(或其它扩展java.rmi.remote 的远程接口)。然而,远程接口在下列情况中可以扩展非远程接口:
远程接口也可扩展其它非远程接口,只要被扩展接口的所有方法(如果有)满足远程方法声明的要求。
例如,下面的接口 bankaccount 即为访问银行帐户定义了一个远程接口。它包含往帐户存款、使帐户收支平衡和从帐户取款的远程方法:
public interface bankaccount extends java.rmi.remote
{
public void deposit(float amount)
throws java.rmi.remoteexception;
public void withdraw(float amount)
throws overdrawnexception, java.rmi.remoteexception;
public float getbalance()
throws java.rmi.remoteexception;
}
下例说明了有效的远程接口 beta.它扩展非远程接口 alpha(有远程方法)和接口 java.rmi.remote:
public interface alpha
{
public final string okay = "constants are okay too";
public object foo(object obj)
throws java.rmi.remoteexception;
public void bar() throws java.io.ioexception;
public int baz() throws java.lang.exception;
}
public interface beta extends alpha, java.rmi.remote {
public void ping() throws java.rmi.remoteexception;
}
remoteexception 类
java.rmi.remoteexception 类是在远程方法调用期间由 rmi 运行时所抛出的异常的超类。为确保使用 rmi 系统的应用程序的健壮性,远程接口中声明的远程方法在其 throws 子句中必须指定 java.rmi.remoteexception(或它的超类,例如 java.io.ioexception 或 java.lang.exception)。
当远程方法调用由于某种原因失败时,将抛出 java.rmi.remoteexception 异常。远程方法调用失败的原因包括:
通讯失败(远程服务器不可达或拒绝连接;连接被服务器关闭等。)
参数或返回值传输或读取时失败协议错误
remoteexception 类是一个已检验的异常(必须由远程方法的调用程序处理并经编译器检验的异常),而不是 runtimeexception.
remoteobject 类及其子类
rmi 服务器函数由 java.rmi.server.remoteobject 及其子类java.rmi.server.remoteserver、java.rmi.server.unicastremoteobject和 java.rmi.activation.activatable 提供。
java.rmi.server.remoteobject 为对远程对象敏感的 java.lang.object方法、hashcode、 equals 和 tostring 提供实现。
创建远程对象并将其导出(使它们可为远程客户机利用)所需的方法由类unicastremoteobject 和 activatable 提供。子类可以识别远程引用的语义,例如服务器是简单的远程对象还是可激活的远程对象(调用时将执行的远程对象)。java.rmi.server.unicastremoteobject 类定义了单体(单路传送)远程对象,其引用只有在服务器进程活着时才有效。类 java.rmi.activation.activatable 是抽象类,它定义的 activatable远程对象在其远程方法被调用时开始执行并在必要时自己关闭。
实现远程接口
实现远程接口的类的一般规则如下:
该类通常扩展 java.rmi.server.unicastremoteobject,因而将继承类java.rmi.server.remoteobject 和java.rmi.server.remoteserver 提供的远程行为。
该类能实现任意多的远程接口。
该类能扩展其它远程实现类。
该类能定义远程接口中不出现的方法,但这些方法只能在本地使用而不能在远程使用。
例如,下面的类 bankacctimpl 实现 bankaccount 远程接口并扩展java.rmi.server.unicastremoteobject 类:
package mypackage;
import java.rmi.remoteexception;
import java.rmi.server.unicastremoteobject;
public class bankaccountimpl extends unicastremoteobject implements
bankaccount
{
private float balance = 0.0;
public bankaccountimpl(float initialbalance)
throws remoteexception
{
balance = initialbalance;
}
public void deposit(float amount) throws remoteexception
{
...
}
public void withdraw(float amount) throws overdrawnexception,
remoteexception
{
...
}
public float getbalance() throws remoteexception
{
...
}
}
注意:必要时,实现远程接口的类能扩展除java.rmi.server.unicastremoteobject 类以外的其它一些类。但实现类此时必须承担起一定的责任,即导出对象(由 unicastremoteobject 构造函数负责)和实现从 java.lang.object 类继承的 hashcode、 equals 和tostring 方法的正确远程语义(如果需要)。
远程方法调用中的参数传递
传给远程对象的参数或源于它的返回值可以是任意可序列化的 java 对象。这包括 java 基本类型, 远程?java 对象和实现 java.io.serializable 接口的非远程 java 对象。有关如何使类序列化的详细信息,参见 java“对象序列化规范”。本地得不到的作为参数或返回值的类,可通过 rmi 系统进行动态下载。
传递非远程对象
非远程对象将作为远程方法调用的参数传递或作为远程方法调用的结果返回时,是通过复制传递的;也就是使用 java 对象序列化机制将该对象序列化。因此,在远程对象调用过程中,当非远程对象作为参数或返回值传递时,非远程对象的内容在调用远程对象之前将被复制。从远程方法调用返回非远程对象时,将在调用的虚拟机中创建新对象。
传递远程对象
当将远程对象作为远程方法调用的参数或返回值传递时,远程对象的 stub 程序即被传递出去。作为参数传递的远程对象仅能实现远程接口。
引用的完整性
如果一个对象的两个引用在单个远程方法调用中以参数形式(或返回值形式)从一个虚拟机传到另一个虚拟机中,并且它们在发送虚拟机中指向同一对象,则两个引用在接收虚拟机中将指向该对象的同一副本。进一步说就是:在单个远程方法调用中,rmi 系统将在作为调用参数或返回值传递的对象中保持引用的完整性。
类注解
当对象在远程调用中被从一个虚拟机发送到另一个虚拟机中时,rmi 系统在调用流中用类的信息 (url) 给类描述符加注解,以便该类能在接收器上加载。在远程方法调用期间,调用可随时下载类。
参数传输
为将 rmi 调用的参数序列化到远程调用的目的文件里,需要将该参数写入作为java.io.objectoutputstream 类的子类的流中。
objectoutputstream 子类将覆盖 replaceobject 方法,目的是用其相应的 stub 类取代每个远程对象。
对象参数将通过 objectoutputstream 的 writeobject 方法写入流中。
而objectoutputstream 则通过 writeobject 方法为每个写入流中的对象(包含所写对象所引用的对象)调用 replaceobject 方法。
rmiobjectoutputstream子类的 replaceobject 方法返回下列值:
如果传给 replaceobject 的对象是 java.rmi.remote 的实例,则返回远程对象的 stub 程序。远程对象的 stub 程序通过对java.rmi.server.remoteobject.tostub方法的调用而获得。
如果传给 replaceobject 的对象不是 java.rmi.remote 的实例,则只返回该对象。
rmi 的 objectoutputstream 子类也实现 annotateclass 方法,该方法用类的位置注解调用流以便能在接收器中下载该类。
有关如何使用 annotateclass的详细信息,参见“动态类加载”一节。
因为参数只写入一个 objectoutputstream,所以指向调用程序同一对象的引用将在接收器那里指向该对象的同一副本。在接收器上,参数将被单个objectinputstream 所读取。
用于写对象的 objectoutputstream(类似的还有用于读对象的objectinputstream )的所有其它缺省行为将保留在参数传递中。例如,写对象时对 writereplace 的调用及读对象时对 readresolve 的调用就是由 rmi的参数编组与解编流完成的。
与上述 rmi 参数传递方式类似,返回值(或异常)将被写入objectoutputstream的子类并和参数传输的替代行为相同。
定位远程对象
我们专门提供了一种简单的引导名字服务器,用于存储对远程对象的已命名引用。使用类 java.rmi.naming 的基于 url 的方法可以存储远程对象引用。客户机要调用远程对象的方法,则必须首先得到该对象的引用。对远程对象的引用通常是在方法调用中以返回值的形式取得。rmi 系统提供一种简单的引导名字服务器,通过它得到给定主机上的远程对象。java.rmi.naming 类提供基于统一资源定位符 (url) 的方法,用来绑定、再绑定、解开和列出位于某一主机及端口上的名字-对象对。
j2ee的十三种技术
java数据库连接(jdbc)
jdbc api以一个统一的方式访问各种数据库。与odbc类似,jdbc将开发者和私有数据库之间的问题隔离开来。由于它建立在java上,因此jdbc可以提供平台无关的数据库访问。
jdbc定义了4种不同的驱动,具体来说,包括有:
类型1:jdbc-odbc桥
在jdbc刚产生时,jdbc-odbc桥是非常有用的。通过它,开发者可以使用jdbc来访问一个odbc数据源。缺点是,它需要在客户机器上安装有一个odbc驱动,该机器通常是应该运行微软windows系统的。使用这一类的驱动器,你就会失去jdbc平台无关的好处。此外,odbv驱动器需要客户端的管理。
类型2:jdbc-native驱动桥
jdbc-native驱动桥提供了一个建筑在本地数据库驱动上的jdbc接口――没有使用odbc.jdbc驱动将标准的jdbc调用转变为对数据库api的本地调用。使用类型2的驱动也会失去jdbc平台无关性的好处,并且需要安装客户端的本地代码。
类型3:jdbc-network桥
jdbc-network桥不需要客户端的数据库驱动。它们使用网络-服务器中层来访问一个数据库。这会引出诸如负载均衡、连接池等技术,数据缓冲也是可能的。由于类型3的驱动通常可带来相对小的下载时间,它是平台无关的,并且不需要客户端的安装和管理,因此很适合用作internet的应用。
类型4:纯java驱动
类型4使用纯java数据库驱动来提供直接的数据库访问。由于类型4驱动运行在客户端,并且直接访问数据库,因此运行在这个模式暗示要使用一个两层的体系。要在一个n层的体系中使用类型4的驱动,可以通过一个包含有数据访问代码的ejb,并且让该ejb为它的客户提供一个数据库无关的服务。
java命名和目录接口(java naming and directory interface,jndi)
jndi是java naming and directory interface 的简写,中意为:java命名及目录接口,它是为了对高级网络应用开发中的使用的目录基础结构的访问。实际上这个目录是一个特殊的数据库,提供了对存储数据的快速访问,不象传统的目录服务访问方式-你必须提供不同的api接口去访问不同的目录服务(如:ldap,nis,ads等),而它提供了一种标准的api来访问类型不同的目录。据说,使用完整的sdk可以开发那些jndi还不支持的目录服务提供者。
jndi是j2ee的一个api,提供了一套标准的接口,以定位用户、机器、网络、对象、以及服务。例如,你可以使用jndi来定位内部网中的一台打印机,你也可以使用它来定位java对象或连接到一个数据库。jndi可以用于ejb、rmi-iiop、jdbc中。它是网络查找定位的标准方法。 jndi api被用来访问命名和目录服务。它提供一个相容的模式来访问和操作企业范围大的资源,例如一个应用服务器中的dns、ldap、本地文件系统或者对象。
在jndi中,一个目录结构中的每一个节点被称为context.每一个jndi的名字都是与一个context相对的,没有一个绝对名字的概念。一个应用可以使用initialcontext类来得到它的第一个context:
context ctx = new initialcontext();
通过这个初始的context,应用就可以经过目录树定位到需要的资源或者对象。例如,假定你已经在weblogic server中配置了一个ejb,并且在myapp.myejb中绑定了home接口。ejb的客户端,在得到这样一个初始的context后,然后就可以使用以下的代码来定位到home接口:
myejbhome home = ctx.lookup( "myapp.myejb" );
一旦你得到你所需对象的一个引用――在这个例子中,就是ejb的home接口――然后你可以调用它上面的方法。为了在一个context中查找到一个对象,jndi还提供方法可以做到:
插入或者绑定一个对象到一个context中。在你配置一个ejb时,这是非常有效的方法;
从一个context中移去一个对象
列出一个context中的所有对象
创建和删除subcontexts
企业java beans(enterprise java beans,ejb)
j2ee其中一个引人注目的技术是ejb.它提供了一个架构来开发和配置到客户端的分布式商业逻辑,因此可以明显减少开发扩展性、高度复杂企业应用的难度。ejb规范定义了ejb组件应该如何及何时与它们的容器交互。由容器来负责提供普通的服务,例如目录服务、事务管理、安全、资源池和容错。
ejb规范定义了三类基本的bean:
会话beans(session beans):会话beans为业务流程建模,由于他们通常表示执行某个动作,因此可以把它们当作是动词。这个执行的动作可以是任何事情,例如增加数量,访问数据库,调用其它系统,调用其它企业bean.我们可以举出很多的例子,包括一个计价引擎,一个工作流引擎,一个目录引擎,一个信用卡认证中心,或一个网上证券交易引擎。
实体beans(entity beans):这是持久保存数据的代表――典型的是存储在数据库中――因此在服务器崩溃后数据仍然存在。多个客户端可以使用ejb来表示同样的数据。实体beans为企业数据建模,由于它们表示数据对象(就是缓存数据库信息的java对象),因此可以把它们当作名词。实体beans的例子包括一种产品,一项订单,一个雇员,一张信用卡,或一支股票。会话beans典型的方式是通过实体beans来实现业务目标的,例如一个证券交易引擎(会话beans)处理股票(实体beans)。
message-driven beans:message-driven beans也表示动作,这一点上它类似于会话beans.它们之间的不同点在于你只能够通过发送消息给message-driven beans的方式来调用它们。message-driven beans的例子包括了接受股票交易消息的beans,信用认证消息,或工作流消息。这些message-driven beans也可以调用其它的企业beans.
接着,我们讨论无状态和有状态
无状态的beans(stateless beans):这是一个单一使用的服务,不维护任何的状态,在服务器崩溃时也不再存在,而且生存期也相对地短。例如,一个无状态的session bean可能用作执行温度转换。
有状态的bean:它提供了一个传统的与客户端交互的方法,存储客户端的状态。在线购物车就是这样一个有状态session ean的典型例子。有状态session beans在服务器崩溃时也不再存在,而且生存期也相对地短,并且每个实例只可以用在一个单一的线程中。
javaserver pages (jsps)
或许你已经对微软的active server pages (asps)非常熟悉;jsp也是类似的技术,不过它是平台无关的。它们都是设计来帮助web内容开发者使用相对较少的代码就可以创建动态的网页。web设计者即使不懂得编程,也可以使用jsp来创建动态的网页。javaserver page是html代码和java代码的混合。在客户请求页面的时候,服务器就会处理java代码,然后返回html页面给浏览器。
你可以也听过jhtml,它是一个旧的标准,现在已经被jsp取代了。weblogic server不但支持jsp,还支持jhtml.不过,在默认设置下,weblogic server是不支持jsp的(对于5.1版本)。你必须编辑weblogic.properties来激活web服务器,对于jspservlet来说,也是这样。
java servlets
servlets提供的功能大部分jsp相同,它采用的是一个有点不同的方法。jsp中大部分是html代码,其中只有少量的java代码,而servlets则相反,它完全使用java编写,并且产生html代码。
servlet是一个在服务器上运行的java小程序,它可以扩展web服务器的功能。这些服务器端的应用可以在被请求时动态执行,与传统web服务器上的cgi perl脚本差不多。cgi脚本和servlet的一个主要不同是:cgi脚本对于每次请求都启动一个全新的进程――需要额外的系统开销――而servlet的执行只要在servlet引擎内启动一个独立的线程就性了。因此servlet的扩展性也更好。
在开发servlet时,你通常都要扩展javax.servlet.http.httpservlet类,并且覆盖它的一些方法。感兴趣的方法包括有:
service(): 作为command-specific方法的一个调度程序
doget(): 处理来自一个客户的http get请求
dopost(): 处理来自一个客户的http post请求
还有一些其它的方法来处理不同类型的http请求――可参考httpservlet api的文本来得到更多相关的信息。
java idl/corba
通过java的idl支持,开发者可以将java与corba集成。他们可以创建能配置在一个corba orb中的java对象,也可以创建作为配置在其它orb内的corba对象客户端的java类。对于通过java将你的新应用和以前的系统集成,后者提供了一个另外的方法。
java事务体系(jta)/java事务服务(jts)
jta定义了一个标准的api,应用可以通过它来访问事务监控器。
jts是corba ots事务监控器的一个基本实现。jts指定了一个事务管理器的实现(transaction manager),这个管理器在一个高级别上支持java事务api(jta)规范,并且在一个低级别上实现了omg ots规范的java映射。一个jts事务管理器为应用服务器、资源管理器、standalone应用和通信资源管理器提供事务服务。
javamail和javabeans激活架构(javabeans activation framework,jaf)
javamail是一个用来访问邮件服务器的api.javamail api提供了一套抽象类来模型化一个邮件系统。支持smtp和imap服务器。
javamail通过使用javabeans activation framework (jaf) 来处理mime加密的邮件附件。mime字节流和java对象间可以互相转化。大多数的应用无需要直接使用jaf.
java信使服务(java messaging service,jms)
jms是一个用来和面向信息的中层通信的api.它不但支持点对点的域,也支持发布/订阅域,并且提供对担保信息传送、事务信息传送、持久信息和durable subscribers的支持。对于将你的应用和以前的backend系统集成,jms提供了另外一个方法。
扩展标记语言(extensible markup language,xml)
xml是一个用来定义其它标记语言的的语言。它可被用作商业之间的数据共享。xml的发展是与java分开的;不过,它的目标和java类似,都是为了与平台无关。通过将java与xml结合,你可以得到一个完全平台无关的解决方案。多个公司都为在java和xml间开发一个紧密的集成而工作。
闽公网安备 35060202000074号