现在EJB 3.0的早期草案已经发布,确实是时候放下最近的争论,开始考虑我们实际上能做什么。EJB是唯一一个针对服务器端/业务逻辑/的Java标准,因此是J2EE的核心。EJB规范委员会认识到EJB在某些雄心勃勃的目标上未能实现,需要进行全面改革。这次改革非常注重易用性,主要通过简化对bean实现者的要求。然而,规范委员会也确定了一些关键的功能性增强,这些增强有助于简化易用性并消除某些J2EE的反模式。
幸运的是,Java社区现在比五年前对构建Web和企业应用程序中存在的问题了解得更多。我们一直在“通过实践学习”。现在我们已经掌握了构建服务器端业务对象的强大、易于使用、标准方法!
规范委员会非常认真思考了如何简化某些非常常见的用例,特别是以下两种情况,我将它们规范如下
更新数据/
1. 检索数据项
2. 显示在屏幕上
3. 接受用户输入
4. 更新数据
5. 向用户报告成功
添加新数据/
1. 检索数据项
2. 显示在屏幕上
3. 接受用户输入
4. 创建一个新的数据项,与第一个数据项相关联
5. 向用户报告成功
这些情况都涉及两个完整的应用程序请求/响应周期。两者都表明,在高并发应用程序中(这两个情况都需要作为跨越两个不同ACID数据库/JTA事务的单个会话来实现),乐观锁定是必不可少的。
另一个更常见的用例,特别是在EJB 2.1中难以实现,因为查询语言有限,是以下内容
列出和报告数据/
1. 检索数据的聚合或汇总列表
2. 向用户显示
最后,我们考虑了两种标准物理架构。/colocated/ 情况——我们认为这至少对于典型的Web应用来说是最重要的——有一个表示层作为业务层的本地客户端。/remote/ 情况有一个远程客户端(例如,运行在不同物理层的servlet引擎或Swing客户端)访问业务层。
首先我们需要数据。与关系数据的交互几乎是每个Web或企业应用的核心。实现非平凡业务逻辑的应用程序受益于数据的面向对象表示(/领域模型/),充分利用面向对象技术如继承和多态。由于各种原因(我不会重复我们在Hibernate in Action中详细阐述的论点),使用完全面向对象的领域模型的应用程序需要自动解决OR不匹配问题的解决方案。EJB3集成了一个非常复杂的ORM规范,大量借鉴了CMP 2.1、Hibernate和Oracle的TopLink的经验。
我们的拍卖应用程序有用户、项目和出价。让我们将这些作为3.0风格的实体bean实现。我们首先从项目开始。
@Entity
public class Item {
private Long id;
private User seller;
private Collection<Bid> bids = new ArrayList<Bid>();
private String description;
private String shortDescription;
private Date auctionEnd;
private int version;
public Item(User seller, String desc, String shortDesc, Date end) {
this.seller = seller;
this.description = desc;
this.shortDescription = shortDesc;
this.auctionEnd = end;
}
protected Item() {}
@Id(generate=AUTO)
public Long getId() {
return id;
}
protected void setId(Long id) {
this.id = id;
}
@Column(length=500)
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Date getAuctionEnd() {
return auctionEnd;
}
protected void setAuctionEnd(Date end) {
this.auctionEnd = end;
}
@Column(length=100)
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
@JoinColumn(nullable=false, updatable=false)
public User getSeller() {
return seller;
}
protected void setSeller(User seller) {
this.seller = seller;
}
@OneToMany(cascade=ALL)
protected Collection<Bid> getBids() {
return bids;
}
protected void setBids(Collection<Bid> bids) {
this.bids = bids;
}
@Version
public int getVersion() {
return version;
}
protected void setVersion(int version) {
this.version = version;
}
public Bid bid(BigDecimal amount, User bidder) {
Bid newBid = new Bid(this, amount, bidder);
bids.add(newBid);
return bid;
}
}
最初最引人注目的是注解。不需要部署描述符!EJB 3.0将允许您在注解的使用和XML部署描述符之间进行选择,但我们预计注解将是最常见的案例。
@Entity注解告诉容器这个类是一个CMP实体bean。可选的@Column注解定义了持久属性的列映射(如果我们正在映射遗留数据,我们将在这些注解中定义列名)。@Id注解选择实体bean的主键属性。在这种情况下,实体bean有一个生成的键。可选的@OneToMany注解定义了一个一对多关联,并指定了/级联风格/为ALL。可选的@JoinColumn注解定义了关联的列映射。在这种情况下,它将关联约束为必需且不可变。@Version注解定义了容器用于乐观锁的属性。
在这里需要注意的最重要的事情可能是,几乎所有这些注解都是可选的。EJB3使用/异常配置/,这意味着规范定义了自然、有用的默认值,因此您不需要在常见情况下指定注解。例如,我们没有在持久属性上包括任何注解。
此外,除了注解外,项目对javax.ejb中的类或接口没有任何依赖。这是EJB3的一个主要目标,有助于显著减少噪声
。
实体bean不需要本地或远程接口,这也减少了噪声。
现在是时候实现出价了。
@Entity
public class Bid {
private Long id;
private Item item;
private BigDecimal amount;
private Date datetime;
private User bidder;
private boolean approved;
protected Bid() {}
public Bid(Item item, BigDecimal amount, User bidder) {
this.item = item;
this.amount = amount;
this.bidder = bidder;
this.datetime = new Date();
}
@Id(generate=AUTO)
public Long getId() {
return id;
}
protected void setId(Long id) {
this.id = id;
}
@Column(nullable=false)
public BigDecimal getAmount() {
return amount;
}
protected void setAmount(BigDecimal amount) {
this.amount = amount;
}
@Column(nullable=false)
public Date getDatetime() {
return datetime;
}
protected void setAmount(Date datetime) {
this.datetime = datetime;
}
@JoinColumn(nullable=false)
public User getBidder() {
return bidder;
}
protected void setBidder(User bidder) {
this.bidder = bidder;
}
@JoinColumn(nullable=false)
public Item getItem() {
return item;
}
protected void setItem(Item item) {
this.item = item;
}
public boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
this.approved = approved;
}
}
我将把编写用户实体bean的任务留给您!
请注意,我们的领域模型类现在是简单的JavaBean。这意味着它们可以在EJB容器外部进行测试。
关于家接口呢?嗯,EJB3已经消除了对家接口的需求,所以我们将不需要它们。
现在,让我们看看我们的第一个用例,更新一个项目。我们将假设 colocated 架构,并在一个具有本地业务接口的无状态会话bean中实现我们的业务逻辑。业务接口定义了客户端可见的操作(您的会话bean可以有任意多的业务接口)。我们将定义Auction本地接口如下
public interface Auction {
public Item getItemById(Long itemId);
public void updateItem(Item item);
}
业务接口默认是本地的,所以不需要@Local注解。
由于我们的项目实体bean也是JavaBean,我们可以直接将其传递给JSP(例如)。我们不需要任何DTOs,或复杂的方法签名。
AuctionImpl bean类实现了这个接口
@Stateless
public class AuctionImpl implements Auction {
@Inject public EntityManager em;
public Item getItemById(Long itemId) {
return em.find(Item.class, itemId);
}
public void updateItem(Item item) {
em.merge(item);
}
}
哇。真是太简单了!
@Inject注解用于指示会话bean的字段由容器设置。在EJB 3.0中,会话bean不需要使用JNDI来获取对资源或其他EJB的引用。在这种情况下,容器/injects/对EntityManager的引用,该接口用于实体bean的持久化相关操作。
请注意,容器透明地处理乐观锁。
假设我们的客户端是一个servlet。显示项目的代码可能如下所示
Long itemId = new Long( request.getParameter("itemId") );
Auction auction = (Auction) new InitialContext().lookup("Auction");
Item item = auction.getItemById(itemId);
session.setAttribute("item", item);
由于Item实体bean也只是简单的JavaBean,JSP可以直接使用它。更新Item的第二个请求可能如下所示
Item item = (Item) session.getAttribute("item");
item.setDescription( request.getParameter("description") );
item.setShortDescription( request.getParameter("shortDescription") );
Auction auction = (Auction) new InitialContext().lookup("Auction");
auction.updateItem(item);
注意,其他专家小组正在探索消除JNDI查找的方法,例如这些用于会话bean的查找。
现在让我们转向第二个用例,创建一个新的Bid。我们将在Auction中添加一个新方法
public interface Auction {
public Item getItemById(Long itemId);
public void updateItem(Item item);
public Bid bidForItem(Item item, BigDecimal amount, User bidder)
throws InvalidBidException;
public void approveBid(Long bidId);
}
当然,我们必须在AuctionImpl中实现这些新方法
@Stateless
public class AuctionImpl implements Auction {
@Inject public EntityManager em;
@Inject QueueConnectionFactory bidQueue;
...
public void approveBid(Long bidId) {
Bid bid = em.find(Bid.class, bidId);
bid.setApproved(approved);
}
public Bid bidForItem(Item item, BigDecimal amount, User bidder)
throws InvalidBidException {
String query = "select max(bid.amount) from Bid bid where "
+ "bid.item.id = :itemId and bid.approved = true";
BigDecimal maxApprovedBid =
(BigDecimal) em.createNamedQuery(query)
.setParameter( "itemId", item.getId() )
.getUniqueResult();
if ( amount.lessThan(maxApprovedBid) ) {
throw new InvalidBidException();
}
Bid bid = item.createBid(amount, bidder);
em.create(bid);
requestApproval(bid, bidQueue);
return bid;
}
private static void requestApproval(Bid bid, Queue queue) {
...
}
}
bidForItem()方法执行一个EJB QL查询,确定当前的最大批准Bid。如果新投标的金额更大,我们实例化一个新的Bid,并通过调用EntityManager.create()使其持久化。
requestApproval()方法向队列发送消息,请求后端系统验证投标人的支付能力。会话bean通过依赖注入获得JMS QueueConnectionFactory的引用。
我们需要一个消息驱动bean,BidApprovalListener,以接收后端系统的回复。当收到回复时,我们将向投标人发送电子邮件,并标记Bid为已批准。
@MessageDriven
public class BidApprovalListener implements MessageListener {
@Inject javax.mail.Session session;
@Inject Auction auction;
public void onMessage(Message msg) {
MapMessage mapMessage = (MapMessage) msg;
boolean approved = mapMessage.getBoolean("approved");
long bidId = mapMessage.getLong("bidId");
auction.approveBid(bidId);
notifyApproval(bid, session);
}
private static void notifyApproval(Bid bid, Session session) {
...
}
}
请注意,消息驱动bean通过依赖注入获取其JavaMail Session和Auction会话bean的引用。
我们的最后一个用例很简单。我们为Auction添加一个方法
public interface Auction {
public Item getItemById(Long itemId);
public void updateItem(Item item);
public Bid bidForItem(Item item, BigDecimal amount, User bidder)
throws InvalidBidException;
public void approveBid(Long bidId);
public List<ItemSummary> getItemSummaries();
}
并在AuctionImpl中实现它
@Stateless
public class AuctionImpl implements Auction {
...
public List<ItemSummary> getItemSummaries() {
String query = "select new "
+ "ItemSummary(item.shortDescription, item.id, max(bid.amount)) "
+ "from Item item left outer join item.bids bid";
return (List<ItemSummary>) em.createQuery(query)
.listResults();
}
}
ItemSummary只是一个简单的JavaBean,具有适当的构造函数,所以我们在这里不需要展示它。
EJB QL查询演示了在EJB3中执行外连接、投影和聚合是多么容易。这些(以及其他)新功能使Fast Lane Reader反模式几乎变得过时。
好了,第一部分到此结束。我们看到了以下简化
- 注解取代了复杂的XML部署描述符
- 消除home接口
- 消除对javax.ejb中类和接口的依赖
- 实体bean是简单的JavaBean,没有本地或远程接口
- 依赖注入取代了JNDI查找
- 消除DTO
- 消除Fast Lane Reader反模式
在下一个问题中,我们将考虑远程客户端...