大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说EJB的简单介绍和使用[通俗易懂],希望能够帮助大家进步!!!
17.1 为什么需要EJB
要想知道为什么要使用EJB,就需要知道"面向服务"的概念。"面向服务",是软件开发过程中,异构环境下模块调用的一个比较重要的思想。同样,面向服务也只是一种设计思想,不是一种编程技术。由"面向服务"的思想,业界提出了"面向服务的体系结构(Service Oriented Architecture, SOA)"的概念。
用一个实际案例来引入"面向服务"的概念。在某些大型应用场合,我们要在不同的运行环境之间传递数据,比如:
A公司需要从B公司的数据库中查询一些内容之后返回,进行处理,如何实现?
最简单的结构,如图17-1所示:
| |
|:----|
| 图17-1 最简单的两公司之间互相调用的结构 |
但是,以上程序在实际操作中,是不能实现的。因为JDBC代码写在A公司部分,那就必须让A公司的程序知道B公司数据库的详细结构。在一般情况下,这是不合理的。比如,一个公司通过自己的平台向银行转账,不可能知道银行数据库的结构。于是,程序可以变为如图17-2所示结构:
| |
|:----|
| (点击查看大图)图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并不是一个单独的文件,其组成包括:
EJB可以作为一个服务被调用,可以单独运行,是一个进程级组件。EJB中还提供了一些安全管理、事务控制功能,使得我们调用EJB时,不需要太多地束缚于这些问题的编码。
EJB 定义了四种类型的组件:
(1) Stateless Session Bean: 无状态会话Bean,不存储用户相关信息,一般说来,在服务器端,一个Bean对象可能为很多客户服务,如图17-3所示:
| |
|:----|
| 图17-3 无状态会话Bean的使用 |
由于一个Bean对象可能为多个客户服务,因此,一般不在对象内保存某个客户的状态,保存也没有意义。
(2) Stateful Session Bean: 有状态会话Bean,可以存储用户相关信息,在服务器端,一个Bean对象只为一个客户服务,如图17-4所示:
| |
|:----|
| 图17-4 有状态会话Bean的使用 |
由于一个Bean对象只为一个客户服务,因此,可以在对象内保存某个客户的状态。
17.2.2 EJB运行原理
本章所讲解的EJB,特指会话Bean。
在EJB中,常用的的组件有:客户端、接口(远程接口或者本地接口)、EJB实现类、JNDI名称等。它们之间的关系如图17-5所示:
| |
|:----|
| 图17-5 EJB组件之间的关系 |
对于一个业务操作,其执行步骤为:
首先,服务器端将EJB发布为一个JNDI名称,并提供一个接口文件。不过,值得注意的是,如果客户端和EJB运行在同一个容器内,可以提供的是本地(Local)接口,如果运行在不同的Java虚拟机内,提供的是远程(Remote)接口。接下来步骤如下:
因此,利用EJB编程,有以下几个步骤:
17.3 EJB框架的基本使用方法
该部分内容使用实际案例进行讲解。以一个银行系统为例,银行系统中提供一个"根据美元计算人民币"的功能,我们知道,美元必须乘以相应的汇率才能得到人民币,而汇率可能保存在银行的数据库中,该数据库结构不能对外公开。因此,客户端必须在不知道数据库结构的情况下,调用银行系统中"根据美元计算人民币"的方法,这就可以使用EJB实现。
本例中,需要建立远程接口和实现类。因为"根据美元计算人民币"的方法,可能是被远程调用的。
17.3.1 建立EJB项目
接下来就开始编写这个项目,打开MyEclipse,新建一个EJB项目,如图17-6所示:
| |
|:----|
| 图17-6 新建EJB项目 |
弹出"New EJB Project"对话框,确定项目名称。注意,"J2EE Specification Level"中一定要选定"Java EE5.0 - EJB3",否则无法支持EJB3。把界面下方的其他勾选去掉。如图17-7所示:
| |
|:----|
| 图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
此代码由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;
}
}
注意,在该代码类定义之前,定义了:
此代码由Java架构师必看网-架构君整理
@Stateless (mappedName="ConvertBean")
@Remote
表示:
编写完毕,项目结构如图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-11 部署窗口 |
选择项目名称,点击"Add"按钮,出现如图17-12所示的界面:
| |
|:----|
| (点击查看大图)图17-12 部署窗口 |
在该窗口中,选择"WebLogic 10.x",在下方选择"以目录形式部署"或者"以压缩包形式部署",系统将会在最下面显示部署的路径。此处选择"以目录形式部署"。完成。
接下来运行WebLogic服务器。如果MyEclipse和WebLogic已经绑定(参考第1章),工具条上会出现WebLogic服务器的打开菜单,如图17-13所示:
| |
|:----|
| (点击查看大图)图17-13 打开WebLogic |
可以打开WebLogic。打开之后,在浏览器中输入:
,打开控制台,输入账号密码,登录,进入控制台,在"Domain Structure"中,点击"Deployments",如图17-14所示:
| |
|:----|
| 图17-14 Domain Structure |
在界面右方显示如图17-15所示:
| |
|:----|
| (点击查看大图)图17-15 显示EJB |
点击该EJB链接最左边的"+"号,出现如图17-16所示的界面,显示了EJB详细信息:
| |
|:----|
| 图17-16 EJB详细信息 |
该详细信息中,在"EJBs"下,名称"ConvertBean",注意,这并不是JNDI名称,知识该EJB实现类的类名称。
17.3.6 远程调用该EJB
该EJB被部署之后,就可以被远程调用了。很明显,要想远程调用该EJB,必须满足:
建立普通的项目Prj17_Test,将远程接口拷贝到该项目中去,并且建立一个TestConvert.java,项目结构如图17-17所示:
| |
|:----|
| 图17-17 项目结构 |
编程步骤如下:
……
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 选择项目属性 |
在跳出的窗口中,找到"Java Build Path",切换到"Library",如图17-19所示:
| |
|:----|
| (点击查看大图)图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);
……
……
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"。编写配置文件的方法如下:
| |
|:----|
| 图17-23 项目结构 |
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>ConvertBean</ejb-name>
<mapped-name>ConvertBean</mapped-name>
</session>
</enterprise-beans>
</ejb-jar>
注意,文件中的"<ejb-name>ConvertBean</ejb-name>"中的"ConvertBean",默认和实现类的名称相同。
编写完毕,部署,同样也可以进行访问。
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访问,等等。和"远程调用"相比,本地调用性能更好,但是失去了远程调用的功能。具体实现,读者可以参考相应资料。