本文部分内容节选自Enterprise JavaBeans 3.0 by Bill Burke & Richard Monson-Haefel
1 Overview
Apache OpenEJB 是可嵌入、轻量级的EJB3.0实现,它即可作为Standalone Server,也可以作为Embedded Server嵌入到Tomcat, JUnit, Eclipse, Intellij, Maven, Ant 等等。OpenEJB缺省使用Apache OpenJPA作为JPA实现,Apache ActiveMQ作为JMS实现。OpenEJB 被应用于Apache Geronimo应用服务器、IBM WebSphere Application Server CE和Apple's WebObjects。
OpenEJB的创建者是 David Blevins 和 Richard Monson-Haefel。David Blevins目前还活跃在OpenEJB User Froum。而Richard Monson-Haefel则是Enterprise JavaBeans前几版的作者。目前Enterprise JavaBeans由JBoss公司首席架构师Bill Burke执笔,出版了第五版。
OpenEJB 实际上包括两个部分:服务器和容器,这两者是分离的。当创建InitialContext的时候,如果使用LocalInitialContextFactory并配置了openejb.embedded.remotable=true属性,那么会启动ServiceManager。ServiceManager会在类路径上查找ServerServices。如果类路径上有openejb-ejbd jar(依赖openejb-server, openejb-client, openejb-core),那么这些services会被启动并允许远程的客户端访问。如果你希望添加更多的ServerServices以支持其它协议,例如HTTP,那么只要把openejb-httpejbd jar加入到类路径上即可。
2 EJBs
2.1 Session Bean
Session bean可以分为两种类型:stateless和stateful。Stateless session bean不维护两次方法调用间的状态。Stateful session bean维护着与客户端的会话状态。Session bean拥有一个或多个业务接口,业务接口可以是远程接口,也可以是本地接口,但是不能两者兼备。远程接口抛出的异常也会被传播到客户端,因此异常中的成员变量也必须能够被序列化。调用本地接口的方法不会引起参数和返回值的拷贝。如果远程接口和本地接口拥有相同的方法,EJB规范允许它们继承自相同的接口。需要注意的是,只要配置openejb.remotable.businessLocals=true,那么OpenEJB允许客户端远程调用本地接口。在这种情况下,本地接口的参数和返回值仍然需要可以被序列化。
以下是个stateless session bean的例子:
public interface Calculator {
int sum(int add1, int add2);
int multiply(int mul1, int mul2);
}
import javax.ejb.Local;
@Local
public interface CalculatorLocal extends Calculator{
}
import javax.ejb.Remote;
@Remote
public interface CalculatorRemote extends Calculator{
}
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
@Stateless
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public class CalculatorImpl implements CalculatorLocal, CalculatorRemote {
public int sum(int add1, int add2) {
return add1 + add2;
}
public int multiply(int mul1, int mul2) {
return mul1 * mul2;
}
@PostConstruct
private void init() {
System.out.println("#in CalculatorImpl.init()");
}
@PreDestroy
private void dispose() {
System.out.println("#in CalculatorImpl.dispose()");
}
@AroundInvoke
private Object intercept(InvocationContext ctx) throws Exception {
try {
System.out.println("#pre-invoke method: " + ctx.getMethod().getName());
return ctx.proceed();
} finally {
System.out.println("#post-invoke method: " + ctx.getMethod().getName());
}
}
}
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
public class StatelessTest {
public static void main(String args[]) throws Exception {
//
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
InitialContext ctx = new InitialContext(properties);
//
{
CalculatorLocal cl1 = (CalculatorLocal) ctx.lookup("CalculatorImplLocal");
System.out.println("cl1.sum(12, 7): " + cl1.sum(12, 7));
System.out.println("cl1.multiply(12, 7): " + cl1.multiply(12, 7));
}
//
{
Object ref1 = ctx.lookup("CalculatorImplRemote");
CalculatorRemote cr1 = (CalculatorRemote) PortableRemoteObject.narrow(ref1, CalculatorRemote.class);
System.out.println("cr1.sum(12, 7): " + cr1.sum(12, 7));
System.out.println("cr1.multiply(12, 7): " + cr1.multiply(12, 7));
}
}
}
以上例子中,CalculatorImpl 类的init() 和dispose()方法是EJB的生命周期回调方法。intercept()方法是拦截器方法,此外也可以定义拦截器类。需要注意的是,拦截器与所拦截的EJB具有相同的生命周期。
以上例子采用的是local client(embedded container)模式。META-INF/ejb-jar.xml中的内容只是包含了ejb-jar根元素,如下:
<ejb-jar/>
OpenEJB会在扫描并部署类路径上的EJB。另外一种部署的方式是编辑conf/openejb.xml文件,例如:
<openejb> <Deployments dir="./bin/yourpackage" /> <Deployments jar="./lib/another.jar" /> </openejb>
OpenEJB提供缺省的JNDI Name格式是{deploymentId}{interfaceType.annotationName},它可以通过openejb.jndiname.format定制。{deploymentId}缺省是{ejbName},而{ejbName}缺省是EJB的非限定类名。{interfaceType.annotationName}有如下取值:
因此查找CalculatorLocal接口的JNDI Name是CalculatorImplLocal。关于更多JNDI Name Formatting的介绍,请参考OpenEJB官方文档。
如果在程序的当前路径下存在conf目录,那么OpenEJB在第一次启动的时候会创建以下文件。如果需要,也可以对这些文件进行编辑以修改相关的配置。
2.2 Entity Bean
EJB3.0规范中,Entity bean是被@Entity标注的POJO。如果Entity bean可以被序列化,那么它可以脱离应用服务器而被传递到表示层。可以为Entity bean定义Entity监听器以便指定Entity bean的生命周期回调方法。关于Entity bean的更多介绍,请参考JPA相关文档。
2.3 Message Driven Bean
Message Driven Bean是无状态的,事务感知的服务器端组件。事务上下文并非从JMS的发送方传播过来,而是由容器发起(有意义的事务属性是NotSupported和Required),或者是由bean明确使用UserTransaction被发起的。如果事务是由容器管理的,JMS确认行为是在事务上下文中执行的。EJB2.1后,EJB规范支持一个可扩展的,也更为开放的MDB定义,使之可以对任何开发商的任何消息系统提供服务,而不仅仅是JMS消息系统。以下是个MDB的简单例子:
import javax.ejb.Remote;
@Remote
public interface SchedulerRemote {
void schedule(long initialExpration, long interval, String subject);
void cancel(String subject);
}
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
@Stateless
public class SchedulerImpl implements SchedulerRemote {
@Resource
private TimerService timerService;
@Resource(name="JmsConnectionFactory1")
private ConnectionFactory connectionFactory;
public void schedule(long initialExpration, long interval, String subject) {
timerService.createTimer(initialExpration, interval, subject);
}
public void cancel(String subject) {
for(Object obj : timerService.getTimers()) {
Timer timer = (Timer)obj;
if(timer.getInfo().equals(subject)) {
timer.cancel();
}
}
}
@Timeout
public void sendMessage(Timer timer) {
try {
//
Connection connection = connectionFactory.createConnection();
connection.start();
//
Session session = connection.createSession(true, 0);
Destination destination = session.createQueue((String)timer.getInfo());
MessageProducer producer = session.createProducer(destination);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//
TextMessage message = session.createTextMessage("this is a simple message, time: " + System.currentTimeMillis());
producer.send(message);
connection.close();
} catch (Exception e) {
System.err.println("failed to send message, detail: " + e.getMessage());
}
}
}
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@MessageDriven(activationConfig={
@ActivationConfigProperty(propertyName="destinationType",propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination",propertyValue="SimpleQueue")
})
public class SimpleConsumer implements MessageListener {
public void onMessage(Message msg) {
try {
TextMessage tm = (TextMessage)msg;
System.out.println("on message: " + tm.getText());
} catch(Exception e) {
e.printStackTrace();
}
}
}
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
public class MdbTestServer {
public static void main(String args[]) throws Exception {
//
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.LocalInitialContextFactory");
properties.setProperty("openejb.embedded.remotable", "true");
//
@SuppressWarnings("unused")
InitialContext ctx = new InitialContext(properties);
//
while(true) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
public class MdbTestClient {
public static void main(String args[]) throws Exception {
//
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.RemoteInitialContextFactory");
properties.setProperty(Context.PROVIDER_URL, "ejbd://localhost:4201");
properties.setProperty(Context.SECURITY_PRINCIPAL, "Kevin");
properties.setProperty(Context.SECURITY_CREDENTIALS, "password");
InitialContext ctx = new InitialContext(properties);
//
Object ref = ctx.lookup("SchedulerImplRemote");
SchedulerRemote sr = (SchedulerRemote) PortableRemoteObject.narrow(ref, SchedulerRemote.class);
sr.schedule(0L, 1000, "SimpleQueue");
//
Thread.sleep(5000);
//
sr.cancel("SimpleQueue");
}
}
conf/ users.properties文件的内容如下:
Kevin= password
conf/openejb.xml文件中的相关内容如下:
<Container id="My MDB Container " type="MESSAGE"> # The resource adapter delivers messages to the container ResourceAdapter JmsResourceAdapter1 # Specifies the message listener interface handled by this container MessageListenerInterface javax.jms.MessageListener # Specifies the activation spec class ActivationSpecClass org.apache.activemq.ra.ActiveMQActivationSpec # Specifies the maximum number of bean instances that are # allowed to exist for each MDB deployment. InstanceLimit 10 </Container> <Resource id="mysqlDataSource" type="DataSource"> JdbcDriver com.mysql.jdbc.Driver JdbcUrl jdbc:mysql://localhost:3306/ejb UserName root Password password JtaManaged true </Resource> <Resource id="mysqlDataSourceUnmanaged" type="DataSource"> JdbcDriver com.mysql.jdbc.Driver JdbcUrl jdbc:mysql://localhost:3306/ejb UserName root Password password JtaManaged false </Resource> <Resource id="JmsResourceAdapter1" type="ActiveMQResourceAdapter"> # Broker configuration URI as defined by ActiveMQ # see http://activemq.apache.org/broker-configuration-uri.html BrokerXmlConfig broker:(tcp://localhost:61616)?useJmx=false # Broker address ServerUrl vm://localhost?async=true # DataSource for persistence messages DataSource mysqlDataSourceUnmanaged </Resource> <Connector id="JmsConnectionFactory1" type="javax.jms.ConnectionFactory"> ResourceAdapter JmsResourceAdapter1 # Specifies if the connection is enrolled in global transaction # allowed values: xa, local or none TransactionSupport xa # Maximum number of physical connection to the ActiveMQ broker PoolMaxSize 10 # Minimum number of physical connection to the ActiveMQ broker PoolMinSize 0 # Maximum amount of time to wait for a connection ConnectionMaxWaitMilliseconds 5000 # Maximum amount of time a connection can be idle before being reclaimed ConnectionMaxIdleMinutes 15 </Connector>
以上例子中,SimpleConsumer订阅SimpleQueue;客户端调用SchedulerRemote接口的schedule()和cancel()方法;SchedulerImpl通过使用TimerService定期向SimpleQueue中发送TextMessage。
跟2.1中的例子不同,本例中采用的是remote client(embedded container)模式:MdbTestServer中配置了properties.setProperty("openejb.embedded.remotable", "true");