现在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反模式
在下一个问题中,我们将考虑远程客户端...