17.1 为什么需要EJB
要想知道为什么要使用EJB,就需要知道"面向服务"的概念。"面向服务",是软件开发过程中,异构环境下模块调用的一个比较重要的思想。同样,面向服务也只是一种设计思想,不是一种编程技术。由"面向服务"的思想,业界提出了"面向服务的体系结构(Service Oriented Architecture, SOA)"的概念。
用一个实际案例来引入"面向服务"的概念。在某些大型应用场合,我们要在不同的运行环境之间传递数据,比如:
A公司需要从B公司的数据库中查询一些内容之后返回,进行处理,如何实现?
最简单的结构,如图17-1所示:
图17-1 最简单的两公司之间互相调用的结构 |
(点击查看大图)图17-2 改进的结构 |
该结构详述如下:B公司编写自己的程序,访问数据库,对外发布一个接口,并发布一个服务的名称。我们知道,接口里面并没有核心代码。该接口也被A公司获取,A公司网上寻找相应的B公司发布的服务名称,然后通过接口调用B公司程序里面的方法。
但是,该技术不是简单就可以实现的,因为A公司和B公司的程序,可能运行在不同的虚拟机内,甚至可能是不同的语言。EJB可以解决A公司和B公司使用的都是Java语言,但是处于不同的Java虚拟机的情况。
该问题的原型是:一个Java虚拟机内的对象能否远程调用另外一个Java虚拟机里面的对象内的方法?实际上,在Java内,该技术可以用RMI(远程方法调用)实现。而EJB的底层,就是用RMI实现的。
实际上,即使是在同一个Java虚拟机内,将某个功能以服务的形式对外发布,被该虚拟机中的另一个模块调用,也是可以大大降低耦合性的。因为模块之间打交道的,只是一个接口和一个服务名称。
不过,顺便需要提到的是,如果两个程序使用的是不同语言平台,如一个是C,一个是Java,业界中也提出了一些方法来解决数据交换问题,如WebService、CORBA等。读者可以参考相关文献。
17.2 EJB框架的基本原理
17.2.1 EJB框架简介
如前所述,EJB实际上是服务器端运行的一个对象,只不过该对象所对应的类并不被客户端所知,该对象对外发布的是一个服务名称,并提供一个可以被客户端调用的接口。通俗点说,EJB就是一个可以被客户端调用,但是并不让客户端知道源代码的类的对象。
因此,EJB并不是普通的Java Bean,普通的JavaBean是一个符合某种规范的Java类文件,只能作为一个类被调用,只有调用的时候才运行,是一个进程内组件。而EJB并不是一个单独的文件,其组成包括:
1. 类文件:实现基本方法的类,封装了需要实现的商务逻辑,数据逻辑或消息处理逻辑,具有一定的编程规范,代码不能被客户端得知。
2. 接口文件:接口是EJB组件模型的一部分,里面提供的方法一般和需要被远程调用的方法一致,一般情况下,要求类文件必须和接口中的定义保持一致性。
3. 必要的情况下,编写一些配置文件,用于描述EJB部署过程中的一些信息。
EJB可以作为一个服务被调用,可以单独运行,是一个进程级组件。EJB中还提供了一些安全管理、事务控制功能,使得我们调用EJB时,不需要太多地束缚于这些问题的编码。
EJB 定义了四种类型的组件:
1. Session Bean:会话Bean,封装业务逻辑,负责完成某个操作。根据生命周期的不同,又可以分为:
(1) Stateless Session Bean: 无状态会话Bean,不存储用户相关信息,一般说来,在服务器端,一个Bean对象可能为很多客户服务,如图17-3所示:
图17-3 无状态会话Bean的使用 |
由于一个Bean对象可能为多个客户服务,因此,一般不在对象内保存某个客户的状态,保存也没有意义。
(2) Stateful Session Bean: 有状态会话Bean,可以存储用户相关信息,在服务器端,一个Bean对象只为一个客户服务,如图17-4所示:
图17-4 有状态会话Bean的使用 |
由于一个Bean对象只为一个客户服务,因此,可以在对象内保存某个客户的状态。
2. Entity Bean:实体Bean,类似Hibernate,封装数据库中的数据,代表底层数据的持久化对象,把表中的列映射到对象的成员,主键在实体Bean中具有唯一性,一个实体Bean对象对应表中的一行,这将在下一章讲解。
3. Message Driven Bean:消息驱动Bean,是一种异步的无状态组件,和无状态会话组件具有相似性,是JMS消息的消费者,可以和JMS配合起来使用。
17.2.2 EJB运行原理
本章所讲解的EJB,特指会话Bean。
在EJB中,常用的的组件有:客户端、接口(远程接口或者本地接口)、EJB实现类、JNDI名称等。它们之间的关系如图17-5所示:
图17-5 EJB组件之间的关系 |
对于一个业务操作,其执行步骤为:
首先,服务器端将EJB发布为一个JNDI名称,并提供一个接口文件。不过,值得注意的是,如果客户端和EJB运行在同一个容器内,可以提供的是本地(Local)接口,如果运行在不同的Java虚拟机内,提供的是远程(Remote)接口。接下来步骤如下:
1. 客户端向服务器发起连接,在服务器上寻找相应的JNDI名称,如果找到,返回一个对象。
2. 客户端将该对象强制转换为接口类型。
3. 客户端调用接口中的方法,实际上调用了服务器端EJB内的方法。
因此,利用EJB编程,有以下几个步骤:
1. 编写EJB实现类。
2. 编写接口。
3. 部署到服务器中,设定JNDI名称。
4. 编写客户端,并将接口拷贝给客户端,将JNDI名称公布,客户端调用EJB。
17.3 EJB框架的基本使用方法
该部分内容使用实际案例进行讲解。以一个银行系统为例,银行系统中提供一个"根据美元计算人民币"的功能,我们知道,美元必须乘以相应的汇率才能得到人民币,而汇率可能保存在银行的数据库中,该数据库结构不能对外公开。因此,客户端必须在不知道数据库结构的情况下,调用银行系统中"根据美元计算人民币"的方法,这就可以使用EJB实现。
本例中,需要建立远程接口和实现类。因为"根据美元计算人民币"的方法,可能是被远程调用的。
17.3.1 建立EJB项目
接下来就开始编写这个项目,打开MyEclipse,新建一个EJB项目,如图17-6所示:
图17-6 新建EJB项目 |
图17-7 新建EJB项目 |
如前所述,我们需要建立Bean的实现类和Bean的接口,由于接口最终需要被客户端使用,因此,适合单独放在一个包内。此处,可以在该项目中建立接口所在包:itf;以及实现类所在的包:impl。注意,此处的命名可能不一定规范,但是主要是为了便于理解,说明问题。建立好的项目如图17-8所示:
图17-8 新建EJB项目结构 |
17.3.2 编写远程接口
远程接口提供了客户端和服务器端的通信桥梁,在里面只有一个函数,就是可能被远程调用的函数。代码如下:
Convert.java
- package itf;
- public interface Convert {
- public String getRmb(String usd);
- }
很显然,该代码非常简单。该代码被客户端使用,也很方便。
17.3.3 编写实现类
Bean的实现类运行在服务器端,包含了核心代码。在"由美元计算人民币"的方法中,本来需要查询服务器端的数据库,为了简单起见,我们给定一个汇率值,不影响知识的理解。代码如下:
ConvertBean.java
- package impl;
- import itf.Convert;
- public class ConvertBean implements Convert {
- public String getRmb(String usd){
- //从数据库查询汇率,此处简化,假如汇率是6.0
- double rate = 6.0;
- double dblUsd = Double.parseDouble(usd);
- double dblRmb = dblUsd * rate;
- String rmb = String.valueOf(dblRmb);
- return rmb;
- }
- }
该代码很简单,Bean的实现类,实现了相应的接口。
17.3.4 配置EJB
编写了EJB实现类,还无法确定该EJB是否能够被远程调用,并且无法确定该会话Bean是有状态的还是无状态的。因此,需要进行配置。
在较早版本的EJB中,需要进行比较复杂的配置,编写xml配置文件,在EJB3中,你可以选择编写配置文件,也可以将配置在代码中标明。方法是:修改ConvertBean的源代码:
ConvertBean.java
注意,在该代码类定义之前,定义了:
- package impl;
- import itf.Convert;
- import javax.ejb.Remote;
- import javax.ejb.Stateless;
- @Stateless (mappedName="ConvertBean")
- @Remote
- public class ConvertBean implements Convert {
- public String getRmb(String usd){
- //从数据库查询汇率,此处简化,假如汇率是6.0
- double rate = 6.0;
- double dblUsd = Double.parseDouble(usd);
- double dblRmb = dblUsd * rate;
- String rmb = String.valueOf(dblRmb);
- return rmb;
- }
- }
- @Stateless (mappedName="ConvertBean")
- @Remote
表示:
1. 确定该EJB是可以被远程调用的。
2. EJB的JNDI名称为"ConvertBean",客户端寻找该EJB时,所使用的名字为"ConvertBean#itf.Convert",实际上是相当于寻找里面的接口。注意,在不同厂商的服务器中,JNDI格式有所不同。
2. 该EJB是无状态的会话Bean。
编写完毕,项目结构如图17-9所示:
图17-9 EJB项目结构 |
17.3.5 部署EJB
接下来就是将EJB部署到服务器中去。默认情况下,Tomcat不支持EJB,支持EJB的服务器有WebLogic、WebSphere、JBoss等,此处我们使用WebLogic10,以及其内部配置的用户服务器域:base domain,并已经在MyEclipse中对其进行了配置绑定。具体安装过程,参考第1章的内容。
点击工具条上的"部署"按钮,如图17-10所示:
图17-10 部署按钮 |
(点击查看大图)图17-11 部署窗口 |
(点击查看大图)图17-12 部署窗口 |
在该窗口中,选择"WebLogic 10.x",在下方选择"以目录形式部署"或者"以压缩包形式部署",系统将会在最下面显示部署的路径。此处选择"以目录形式部署"。完成。
接下来运行WebLogic服务器。如果MyEclipse和WebLogic已经绑定(参考第1章),工具条上会出现WebLogic服务器的打开菜单,如图17-13所示:
(点击查看大图)图17-13 打开WebLogic |
图17-14 Domain Structure |
(点击查看大图)图17-15 显示EJB |
图17-16 EJB详细信息 |
该详细信息中,在"EJBs"下,名称"ConvertBean",注意,这并不是JNDI名称,知识该EJB实现类的类名称。
17.3.6 远程调用该EJB
该EJB被部署之后,就可以被远程调用了。很明显,要想远程调用该EJB,必须满足:
1. 得知服务器是WebLogic,因为不同的服务器连接方式可能不一样。
2. 得知服务器的IP地址和端口。
3. 拥有该EJB的远程接口的class文件,得知服务器端EJB的JNDI名称,如前所述,名称为:"ConvertBean#itf.Convert"。
建立普通的项目Prj17_Test,将远程接口拷贝到该项目中去,并且建立一个TestConvert.java,项目结构如图17-17所示:
图17-17 项目结构 |
编程步骤如下:
1. 确定连接目标:
- ……
- Hashtable table = new Hashtable();
- table.put(Context.INITIAL_CONTEXT_FACTORY,
- "weblogic.jndi.WLInitialContextFactory");
- table.put(Context.PROVIDER_URL,"t3://localhost:7001");
- ……
注意,此处用到了weblogic.jndi.WLInitialContextFactory,是WebLogic中专门负责初始化上下文对象的类,因此,本项目中,需要导入WebLogic相关开发包。方法是:右击项目名称,选择"Properties",如图17-18所示:
图17-18 选择项目属性 |
(点击查看大图)图17-19 属性窗口 |
点击"Add External JARs",找到%WebLogic安装目录%/server/lib/weblogic.jar,导入。如图17-20所示:
(点击查看大图)图17-20 导入效果 |
- ……
- Context context = new InitialContext(table);
- Convert convert = ( Convert) context.lookup(jndiName);
- ……
3. 调用接口:
- ……
- String rmb = convert.getRmb(usd);
- System.out.println(rmb);
- ……
整个文件的代码为:
- TestConvert1.java
- import itf.Convert;
- import java.util.Hashtable;
- import javax.naming.Context;
- import javax.naming.InitialContext;
- public class TestConvert1 {
- public static void main(String[] args) throws Exception{
- String usd = "1234";
- String jndiName = "ConvertBean#itf.Convert";
- Hashtable table = new Hashtable();
- table.put(Context.INITIAL_CONTEXT_FACTORY,
- "weblogic.jndi.WLInitialContextFactory");
- table.put(Context.PROVIDER_URL,"t3://localhost:7001");
- //查询服务器中的jndiName
- Context context = new InitialContext(table);
- Convert convert = ( Convert) context.lookup(jndiName);
- String rmb = convert.getRmb(usd);
- System.out.println(rmb);
- }
- }
运行,显示的效果如图17-21所示:
图17-21 显示效果 |
说明可以正常运行。
从此处可以看出,客户端没有知道服务器端的任何源代码,就可以调用服务器端的EJB对象。
17.3.7 无状态会话Bean的生命周期
接下来讲解无状态会话Bean的生命周期。限于篇幅,本节仅仅讲解无状态会话Bean的生成和消亡。
在ConvertBean.java中增加一个构造函数:
ConvertBean.java
- package impl;
- import itf.Convert;
- import javax.ejb.Remote;
- import javax.ejb.Stateless;
- @Stateless (mappedName="ConvertBean")
- @Remote
- public class ConvertBean implements Convert {
- public ConvertBean(){
- System.out.println("ConvertBean构造函数");
- }
- public String getRmb(String usd){
- //从数据库查询汇率,此处简化,假如汇率是6.0
- double rate = 6.0;
- double dblUsd = Double.parseDouble(usd);
- double dblRmb = dblUsd * rate;
- String rmb = String.valueOf(dblRmb);
- return rmb;
- }
- }
部署,然后调用TestConvert1.java,在服务器端打印的结果为:
反复运行客户端,服务器端构造函数没有调用,说明是同一个EJB对象为所有客户端服务。关于其生命周期,读者可以参考相关文档。
17.4 有状态会话Bean开发
如前所述,有状态会话Bean,可以存储用户相关信息,在服务器端,一个Bean对象只为客户服务,本节编写有状态会话Bean。
编写有状态会话Bean很简单,以上节的ConvertBean.java为例,只需将代码中的"Stateless"改为"Stateful"即可。代码为:
ConvertBean.java
- package impl;
- import itf.Convert;
- import javax.ejb.Remote;
- import javax.ejb.Stateful;
- @Stateful (mappedName="ConvertBean")
- @Remote
- public class ConvertBean implements Convert {
- public ConvertBean(){
- System.out.println("ConvertBean构造函数");
- }
- public String getRmb(String usd){
- //从数据库查询汇率,此处简化,假如汇率是6.0
- double rate = 6.0;
- double dblUsd = Double.parseDouble(usd);
- double dblRmb = dblUsd * rate;
- String rmb = String.valueOf(dblRmb);
- return rmb;
- }
- }
其中,
- @ Stateful (mappedName="ConvertBean")
- @Remote
表示该EJB是一个具有远程接口的有状态会话Bean。
部署,然后调用TestConvert1.java,在服务器端打印的结果为:
反复运行客户端,服务器端构造函数都有调用,效果如图17-22所示:
图17-22 显示效果 |
说明是一个EJB对象为相应客户端服务。不过,读者可能会提出一个问题:既然是一个EJB为一个客户服务,是否会出现大量的EJB对象消耗内存的情况呢?实际上,EJB中的"钝化"机制,会让长期不用的EJB对象,过了一段时间从内存中腾出空间,存入缓存。这是EJB的一个特性,读者可以参考相应文献。
另外,客户也可以手工让有状态会话Bean从实例池中删除。方法是:在远程接口和实现类中定义一个方法,并在实现类中为其注释为"@Remove":
Convert.java
- ……
- public interface Convert {
- ……
- public void remove();
- }
- ConvertBean.java
- ……
- public class ConvertBean implements Convert {
- ……
- @Remove
- public void remove(){
- //释放资源
- }
- }
此后,客户端通过接口调用remove方法即可。
17.5 有配置文件的EJB
观察前面的代码,我们将JNDI名称写在了源代码中:
ConvertBean.java
- ……
- @Stateful (mappedName="ConvertBean")
- @Remote
- public class ConvertBean implements Convert {
- ……
- }
实际上,将该名称写在源代码中,并不是一个好的办法。由于JNDI名称对于各个厂商具有不同的写法,因此,最好的方法是将JNDI名称写在配置文件中。
首先将"@Stateful (mappedName="ConvertBean")"改为"@Stateful"。编写配置文件的方法如下:
1. 在项目的META-INF下新建ejb-jar.xml,结构如图17-23所示:
图17-23 项目结构 |
2. 编写ejb-jar.xml,源代码为:
ejb-jar.xml
- xml version="1.0" encoding="UTF-8"?>
- <ejb-jar>
- <enterprise-beans>
- <session>
- <ejb-name>ConvertBeanejb-name>
- <mapped-name>ConvertBeanmapped-name>
- session>
- enterprise-beans>
- ejb-jar>
注意,文件中的"
编写完毕,部署,同样也可以进行访问。
17.6 编写具有本地接口的EJB
上一节讲解的是含有远程接口的EJB,该EJB可以被远程调用。前面讲过,EJB的设计,不仅仅是为了提供远程调用功能,有时候,在同一个虚拟机内,将EJB实现类的功能用接口形式公布,也可以起到降低耦合性的作用。此时,该接口适合定义为本地(Local)接口。很明显,本地接口的调用比远程接口的调用,资源消耗应该少一些。
将本例中的EJB改为本地接口版本非常简单,只需要在Bean的实现类内进行改变即可,代码如下:
ConvertBean.java
- package impl;
- import itf.Convert;
- import javax.ejb.Local;
- import javax.ejb.Stateless;
- @Stateless
- @Local
- public class ConvertBean implements Convert {
- public String getRmb(String usd){
- //从数据库查询汇率,此处简化,假如汇率是6.0
- double rate = 6.0;
- double dblUsd = Double.parseDouble(usd);
- double dblRmb = dblUsd * rate;
- String rmb = String.valueOf(dblRmb);
- return rmb;
- }
- }
其中,
- @Stateless
- @Local
表示该EJB是一个具有本地接口的无状态会话Bean。
重新部署,我们发现,原先的TestConvert1程序将无法调用该EJB。
实际上,想要访问实现本地接口的EJB,必须让客户端和服务器运行在同一个容器中。比如,在同一个EJB容器中,被另一个EJB访问。或者,在同一个项目中,被JSP或者Servlet访问,等等。和"远程调用"相比,本地调用性能更好,但是失去了远程调用的功能。具体实现,读者可以参考相应资料。