EJB3能做什么(第一部分)

发布    |      

现在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反模式

在下一个问题中,我们将考虑远程客户端...


回到顶部