JSR-299æè¿å°ååä»"Web Beans"æ¹ä¸º"Javaä¸ä¸æåä¾èµæ³¨å ¥"ãåèæåä»ç¶ä½¿ç¨"Web Beans"åç§°ï¼åèå®ç°ä»ç¶ä½¿ç¨"Web Beans RI"åç§°ãå ¶ä»çææ¡£ï¼å客ï¼è®ºåå¸åççå¯è½ä½¿ç¨æ°çå½åï¼å æ¬æ°çJSR-299åèå®ç°çåå- "Web Beans"ã
ä½ ä¹å°åç°ä¸äºæå®çæè¿çä¸äºåè½ç¼ºå¤±äº(ä¾å¦ç产è åï¼å®ç°(realization)ï¼å¼æ¥äºä»¶ï¼Java EEèµæºçXMLæ å°)
The Web Beans (JSR-299)规范为Java EE环境定义了一套服务以便开发者能够更轻松地开发应用。Web Beans在现有的包括JavaBeans和企业JavaBeans在内的Java组件类型之上增强了生命周期和交互的模型层。作为传统的Java EE编程模型的补充,Web Beans服务提供了:
一个为有状态组件而改良的并且绑定到定义明确的 上下文上的生命周期,
一个类型安全的依赖注入方法,
通过 事件通知 机制实现的交互,并且
一个更好的将拦截器绑定到组件上的方法,同时提供了一个名为装饰器(decorator)的新型拦截器,这种拦截器更加适合解决业务问题。
依赖注入和具备上下文的生命周期管理节省了开发者出于对接口的不熟悉而不得不处理下列问题所浪费时间:
这个对象的生命周期是什么?
它能够有多少个并发的客户端
它是多线程的么?
我能从哪里得到一个对象?
我需要显式地销毁它么?
当我不再直接使用它的时候,我需要将它的引用保存到哪里?
我如何才能添加一个间接层,以便在部署的时候可以更换这个对象的不同实现?
我应该如何在其他对象中共享这个对象?
一个Web Bean仅仅需要指定它所依赖的其他Web Bean的类型和语义。它不需要知道他所依赖的任何Web Bean的实际生命周期,具体的实现,线程模型或者这个Web Bean的其他客户端。更棒的是,它所依赖的Web Bean的具体实现和生命周期,线程模型可以根据部署场景而更改,却丝毫不影响任何客户端。
事件,拦截器和装饰器增强了这个模型固有的松耦合特性:
事件通知将事件的消费者和事件的产生者解耦,
拦截器将技术关注点从业务逻辑中解耦,并且
装饰器允许开发者划分业务关注点。
最重要的是,Web Beans以一种类型安全的方式提供所有的特性。Web Beans从来不使用基于字符串的标识符来决定交互的对象如何关联在一起。至于XML, 虽然它仍旧是一个选项,但也可以几乎不使用。取而代之,Web Bean使用Java对象模型的类型信息和一种新的绑定注释模式将Web Beans和它们的依赖以及拦截器,装饰器和事件消费者关联在一起。
Web Beans服务是相当普遍的,可以应用在下列Java EE环境中的组件类型中:
所有的JavaBeans,
所有的EJB, 和
所有的Servlet。
Web Beans甚至提供了必要的整合点,以便未来的Java EE规范或者其他非标准的框架中的不同组件能够和Web Beans轻松整合,使用Web Beans服务以及和任何其他类型的Web Bean交互。
Web Beans受到了包括Seam, Guice和Spring在内的很多现有的Java框架的影响。然而,Web Beans具有自己鲜明的特性:在类型安全方面优于Seam,比Spring更加具备状态和使用更少的XML, 比Guice更加具备Web和企业应用的能力。
最重要的是,Web Beans是一个JCP标准,能够干净地整合到Java EE中。Web Beans也可以整合到任何使用轻量的内嵌EJB的Java SE环境中。
你是否已经迫不及待想要开始编写你的第一个Web Bean了?或许,你仍旧抱有怀疑态度,想要知道Web Beans规范会给出什么样的圈套让你跳。好消息是你也许已经编写并且使用了好几百个或者好几千个Web Bean了。你也许甚至想不起来你写的第一个Web Bean了。
除非特殊情况,每个具有一个非参构造器的Java类都可以是一个Web Bean。这包括了每个JavaBean, 并且每个EJB3的会话Bean都是一个Web Bean。当然,你每天已经写过的JavaBean和EJB无法使用Web Beans规范定义的新服务,但是你能够通过Web Beans的XML配置将这些组件配置为Web Bean,然后将其注入到其他Web Bean中。你甚至可以不用修改已有代码就可以为其添加拦截器和装饰器。
假定我们有两个已经写好的Java类,我们已经在不同的应用中使用它们好多年了。第一个类将一个字符串解析为一个句子列表:
public class SentenceParser {
public List<String
> parse(String text) { ... }
}
第二个已有类是一个无状态的会话Bean,这个会话Bean作为一个外部系统的前端,能够将句子从一种语言翻译到另一个语言:
@Stateless
public class SentenceTranslator implements Translator {
public String translate(String sentence) { ... }
}
Translator
是本地接口:
@Local
public interface Translator {
public String translate(String sentence);
}
不幸的是,我们没有Java类能够翻译整个文档。因此,让我们写一个Web Bean来做这个工作:
public class TextTranslator {
private SentenceParser sentenceParser;
private Translator sentenceTranslator;
@Initializer
TextTranslator(SentenceParser sentenceParser, Translator sentenceTranslator) {
this.sentenceParser = sentenceParser;
this.sentenceTranslator = sentenceTranslator;
}
public String translate(String text) {
StringBuilder sb = new StringBuilder();
for (String sentence: sentenceParser.parse(text)) {
sb.append(sentenceTranslator.translate(sentence));
}
return sb.toString();
}
}
通过将其注入到一个Web Bean,Servlet或者EJB,我们能够获得一个 TextTranslator
的实例:
@Initializer
public setTextTranslator(TextTranslator textTranslator) {
this.textTranslator = textTranslator;
}
或者,我们可以直接调用Web Bean管理器的方法获得这个实例:
TextTranslator tt = manager.getInstanceByType(TextTranslator.class);
但是,等一下:TextTranslator
并没有一个无参构造器!它仍然是一个Web Bean么?好吧,一个没有无参构造器的类依然能够成为一个Web Bean,只要你在它的一个构造器上使用@Initializer
注释即可。
就像你猜到的一样, @Initializer
注释和依赖注入有关! @Initializer
可以应用到一个Web Bean的构造器或者方法上,它告诉Web Bean管理器在初始化一个Web Bean的时候去调用这个构造器或者方法。Web Beam管理器能够将其他的Web Bean注入到构造器或者方法的参数中。
在系统初始化的时候,Web Bean管理器必须验证只存在一个Web Bean能够满足每个注入点。在我们的例子中,如果没有Translator
实现如果SentenceTranslator
EJB没有被部署Web Bean管理器将会抛出一个UnsatisfiedDependencyException
异常。如果多于一个Translator
实现,Web Bean管理器将会抛出一个AmbiguousDependencyException
异常。
那么, Web Bean究竟是什么?
一个Web Bean是一个包含业务逻辑的应用类。一个Web Bean能够从Java代码中直接调用,也可以通过统一表达语言调用。一个Web Bean可以访问事务性的资源。Web Bean之间的依赖通过Web Bean管理器自动管理。大部分Web Bean是具有 状态和上下文的。Web Bean的生命周期总是通过Web Bean管理器进行管理。
让我们后退一步。"上下文"究竟意味着什么?既然Web Beans可以是有状态的,那它关系到我到底拥有哪个Bean实例。和无状态组件模型(例如无状态的会话Bean)或者一个单例模型组件(例如Servlet或者单例Bean)不同,一个Web Bean的不同客户端看到的Web Bean的状态是不同的。客户端所见的状态取决于这个客户端拥有的是哪一个Web Bean实例的引用。
然而,Web Bean像无状态或者单例模型那样,却和有状态的会话Bean不同,客户端无法通过显式地创建或者销毁它来控制实例的生命周期。取而代之,Web Bean的范围决定了:
每个Web Bean实例的生命周期,以及
哪些客户端共享Web Bean特定实例的一个引用。
给定一个Web Bean应用的线程,将可能有一个激活的上下文和Web Bean的范围关联。这个上下文可能对于该线程是唯一的(例如,如果这个Web Bean是请求范围的),或者这个上下文可能被某些其他线程共享(例如,如果这个Web Bean是会话范围的),这个上下文甚至可以被所有线程共享(例如,这个Web Bean是应用范围的)。
在同一个上下文中执行的客户端(例如,其他的Web Bean)看到的是同一个Web Bean的实例。但是不同的上下文中的客户端看到的是不同的实例。
具备上下文的模型带来的一个巨大优点是它允许有状态的Web Bean可以像服务一样使用!客户端不需要关注本身以及它使用的Web Bean的生命周期,甚至它根本不需要知道生命周期是什么。Web Bean通过传递消息来交互,Web Bean的实现定义了他们自己状态的生命周期。Web Bean是松耦合的,因为:
它们通过定义良好的公共接口交互
它们的生命周期是完全解耦的
我们能够使用一个实现相同接口,具有不同生命周期(一个不同的范围)的Web Bean替换一个Web Bean而不会影响其他的Web Bean实现。实际上,Web Bean定义了一个复杂的机制能够在部署时覆盖Web Bean的实现,我们将在第 4.2 节 “部署类型”一章阐述。
需要注意的是并非所有的Web Bean的客户端都是Web Bean。其他对象诸如Servlet或者消息驱动Bean#151;天生不可注入的, 具备上下文的对象也可以通过注入获得Web Bean的引用。
讨论的够多了。我们看些更正式些的,依据规范:
一个Web Bean包括:
一套(非空)API类型
一套(非空)绑定注释类型
一个范围
一个部署类型
可选的,一个Web Bean的名字
一套拦截器绑定类型
一个Web Bean实现
让我们看看这些术语对于Web Bean的开发者都意味着什么。
Web Bean通常通过依赖注入获得其他Web Bean的引用。任何注入的属性都要指定一个"合约",这个合约必须满足注入的Web Bean的要求。这个合约是:
一个API类型,和
一套绑定类型
一个API指的是用户定义的类或者接口。(如果Web Bean是一个EJB会话Bean,API类型是 @Local
接口或者Bean类的本地视图)。一个绑定类型表示某个客户端可见的语义,这个语义由API的某个实现而不是其他实现来满足。
绑定类型通过用户定义的注释来表现,这些注释自己需要通过@BindingType
来注释。例如,下面的注入点有一个API类型 PaymentProcessor
和绑定类型@CreditCard
:
@CreditCard PaymentProcessor paymentProcessor
如果在一个注入点没有显式的指定一个绑定类型,那么默认的绑定类型是@Current
。
对于每个注入点,Web Bean管理器都会搜索满足合约的Web Bean(实现了API并且拥有所有的绑定类型),然后将这个Web Bean注入。
下面的Web Bean拥有一个绑定类型@CreditCard
,并且实现了API类型PaymentProcessor
。因此,这个Web Bean可以被注入到这个例子的注入点中:
@CreditCard
public class CreditCardPaymentProcessor
implements PaymentProcessor { ... }
如果一个Web Bean没有显式的指定一套绑定类型,它将只有一个绑定类型:默认的绑定类型@Current
。
Web Bean定义了一个复杂但是很直观的解析算法来帮助容器确定如何处理多个Web Bean满足特定合约的情况。我们将在 第 4 章 依赖注入一章中详述。
部署类型能够让我们根据部署场景来划分我们的Web Bean。一个部署类型是一个注释,这个注释代表了一种特定的部署场景,例如@Mock
,@Staging
或者@AustralianTaxLaw
。我们通过这些注释来决定哪些Web Bean部署在哪些场景中。一个部署类型允许我们只使用一行配置就可以对一整套Web Bean进行条件化的部署。
很多Web Bean只使用默认的部署类型@Production
。在这种情况下,不需要显式的指定部署类型。在我们的例子中的三个Web Bean都拥有部署类型@Production
。
在一个测试环境中,我们有可能将SentenceTranslator
Web Bean替换为一个"模拟对象":
@Mock
public class MockSentenceTranslator implements Translator {
public String translate(String sentence) {
return "Lorem ipsum dolor sit amet";
}
}
我们需要在测试环境中激活部署类型@Mock
,这时应用会使用 MockSentenceTranslator
或者其他拥有@Mock
注释的Web Bean。
我们将在第 4.2 节 “部署类型”一章中详述这个独特而强大的特性。
范围定义了一个Web Bean实例的生命周期和可见度。Web Bean的上下文模型是可扩展的,可以适应任意范围。不过某些特定的重要的范围已经内置于规范中了,由Web Bean管理器提供这些范围。一个范围也是通过注释类型来表示的。
例如,任何一个Web应用都可能拥有 会话范围的Web Bean:
@SessionScoped
public class ShoppingCart { ... }
一个会话范围的Web Bean实例将绑定到用户会话中,它被这个会话上下文中的所有请求共享。
默认情况下,Web Bean属于一个名为依赖伪范围的特殊范围。拥有这个范围的Web Bean的范围取决于其所注入的对象的范围,它的生命周期和所注入的对象的生命周期绑定在一起。
我们将在第 5 章 范围和上下文一章中详述范围。
一个Web Bean可能有一个名字,通过名字,Web Bean可以在统一表达式语言中使用。为Web Bean指定一个名字非常简单:
@SessionScoped @Named("cart")
public class ShoppingCart { ... }
现在我们可以轻松地在任何JSF或者JSP页面中使用这个Web Bean:
<h:dataTable value="#{cart.lineItems}" var="item"> .... </h:dataTable >
甚至我们可以让Web Bean管理器来给Web Bean指定默认的名字:
@SessionScoped @Named
public class ShoppingCart { ... }
在这种情况下,Web Bean的名字默认为shoppingCart
非完整的类名,首字母改为小写
Web Bean支持EJB3定义的拦截器功能,Web Bean将其扩展,使得POJO也具备该功能。另外,Web Bean提供一个新的方式来将拦截器绑定到EJB Bean和其他Web Bean上。
可以通过@Interceptors
注释直接指定拦截器类:
@SessionScoped
@Interceptors(TransactionInterceptor.class)
public class ShoppingCart { ... }
然而,更优雅的方式或者更佳的实践是通过拦截器绑定类型来间接地绑定拦截器:
@SessionScoped @Transactional
public class ShoppingCart { ... }
我们已经看到,JavaBean, EJB和其他Java类都可以成为Web Bean。但是,确切地说,什么样的对象能够称为Web Bean?
Web Bean规范声称一个具体的Java类可以成为一个简单的Web Bean, 只要这个类:
它不是一个EE容器管理的组件,例如一个EJB,一个Servlet或者一个JPA实体,
它不是一个非静态的静态内嵌类,
它不是一个参数化类型,并且
它拥有一个无参构造器,或者构造器具有@Initializer
注释。
这样,几乎所有的JavaBean都可以是一个简单的Web Bean。
每个被简单Web Bean直接或者间接实现的接口都是这个简单Web Bean的一个API类型。这个类和它的超类也是API类型。
规范指出所有EJB3类型的会话Bean或者单例Bean都是企业级Web Bean。消息驱动Bean不是Web Beans因为它们不能被注入到其他对象中#151;但是它们可以使用大部分Web Bean的功能,包括依赖注入和拦截器。
一个企业级Web Bean的本地接口,只要没有一个通配类型参数或者类型变量,它都是这个企业级Web Bean的API类型,该接口的每个父接口也都是这个企业级Web Bean的API类型。如果EJB Bean有一个Bean类的本地视图,这个Bean类和它的每个父类也是一个API类型。
有状态的会话Bean应该声明一个无参的删除方法或者一个拥有@Destructor
注释的删除方法。Web Bean管理器将在这个有状态会话Bean实例的生命周期结束时调用这个方法。这个方法被称为企业级Web Bean的销毁方法。
@Stateful @SessionScoped
public class ShoppingCart {
...
@Remove
public void destroy() {}
}
我们什么时候使用企业级Web Bean来替代简单Web Bean呢?当我们需要EJB提供的高级的企业服务的时候,例如:
方法水平的事务管理和安全,
并发管理
实例水平的有状态会话Bean的钝化和无状态会话Bean的实例池,
远程或者Web服务调用,和
定时器以及异步方法,
在这中情况下,我们应该使用一个企业级Web Bean。当我们不需要这些服务的时候,一个简单Web Bean就足够了。
很多Web Bean(包括任何会话范围或者应用范围的Web Bean)都需要并发访问。因此,EJB3.1提供的并发管理相当有用。大部分会话范围和应用范围的Web Bean应该是EJB。
对于拥有重量级资源引用或者大量内部状态的Web Bean来说,它能够从高级的容器管理的生命周期获得好处。这些生命周期由EJB的 @Stateless
/@Stateful
/@Singleton
模型定义,它支持钝化和实例池。
最后,什么情况下应该使用方法水平的事务管理,方法水平的安全,计时器,远程方法或者异步方法应该是显而易见的。
从简单Web Bean起步最容易,然后转换到EJB上,过程很简单,只须添加一个注释:@Stateless
, @Stateful
或者 @Singleton
。
一个生产者方法由Web Bean管理器在当前上下文中没有某个Web Bean实例的时候调用,用以获得该Web Bean的一个实例。一个生产者方法能够让应用完全控制实例化的过程,而不是将初始化交给Web Bean管理器处理。例如:
@ApplicationScoped
public class Generator {
private Random random = new Random( System.currentTimeMillis() );
@Produces @Random int next() {
return random.nextInt(100);
}
}
一个生产者方法的输出或者返回值将被注入到注入点中,就像其他任何的Web Bean一样。
@Random int randomNumber
方法返回的类型和它直接或者间接继承或者实现的所有接口都是这个生产者方法的API类型。如果返回的类型一个类,其所有的父类也是API类型。
一些生产者方法返回的对象需要显式地销毁
@Produces @RequestScoped Connection connect(User user) {
return createConnection( user.getId(), user.getPassword() );
}
这些生产者方法可以定义对应的清除方法:
void close(@Disposes Connection connection) {
connection.close();
}
这个清除方法将在请求结束的时候自动被Web Bean管理器调用。
我们将在第 6 章 生产者方法一章中详述生产者方法。
最后,一个JMS的队列或者主题能够成为一个Web Bean。为了向队列或者主题发送消息,开发者不得不处理所有不同JMS对象的生命周期。Web Bean将开发者从这些冗长乏味的工作中解放出来。我们将在第 13.4 节 “JMS端点”一章中讨论JMS端点。
让我们通过一个完整的例子来演示这些想法。我们将是使用JSF来实现一个应用的用户登录/退出功能。首先,我们定一个Web Bean来持有登录过程中用户输入的用户名和密码:
@Named @RequestScoped
public class Credentials {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
这个Web Bean和下面的JSF表单的login绑定:
<h:form>
<h:panelGrid columns="2" rendered="#{!login.loggedIn}">
<h:outputLabel for="username"
>Username:</h:outputLabel>
<h:inputText id="username" value="#{credentials.username}"/>
<h:outputLabel for="password"
>Password:</h:outputLabel>
<h:inputText id="password" value="#{credentials.password}"/>
</h:panelGrid>
<h:commandButton value="Login" action="#{login.login}" rendered="#{!login.loggedIn}"/>
<h:commandButton value="Logout" acion="#{login.logout}" rendered="#{login.loggedIn}"/>
</h:form
>
实际的工作由一个会话范围的Web Bean完成。这个会话范围的Web Bean维护当前登录用户的信息,并且将User
实体暴露给其他Web Bean:
@SessionScoped @Named
public class Login {
@Current Credentials credentials;
@PersistenceContext EntityManager userDatabase;
private User user;
public void login() {
List<User
> results = userDatabase.createQuery(
"select u from User u where u.username=:username and u.password=:password")
.setParameter("username", credentials.getUsername())
.setParameter("password", credentials.getPassword())
.getResultList();
if ( !results.isEmpty() ) {
user = results.get(0);
}
}
public void logout() {
user = null;
}
public boolean isLoggedIn() {
return user!=null;
}
@Produces @LoggedIn User getCurrentUser() {
return user;
}
}
当然, @LoggedIn
是一个绑定注释:
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD})
@BindingType
public @interface LoggedIn {}
现在,任何其他的Web Bean可以轻松地将当前用户注入:
public class DocumentEditor {
@Current Document document;
@LoggedIn User currentUser;
@PersistenceContext EntityManager docDatabase;
public void save() {
document.setCreatedBy(currentUser);
docDatabase.persist(document);
}
}
希望这个例子能够让你尝试了Web Bean的编程模型。在下一章中,我们将更加深入的研究Web Bean的依赖注入。
Web Bean参考实现由Seam项目开发。你可以从the downloads page下载最新的开发者版本。
Web Beans RI自带了两个例子:webbeans-numberguess
(一个仅包含一个简单Bean的WAR应用例子)和webbeans-translator
(一个包含企业Bean的EAR应用例子)。为了运行例子,你需要:
最新的Web Bean参考实现版本,
JBoss AS 5.0.0.GA, 和
Apache Tomcat 6.0.X,和
Ant 1.7.0.
当前,Web Beans参考实现只能运行在JBoss AS 5之上。你需要从jboss.org下载JBoss AS 5.0.0.GA, 然后解压。例如:
$ cd /Applications $ unzip ~/jboss-5.0.0.GA.zip
然后从seamframework.org下载Web Beans的参考实现,然后解压。例如:
$ cd ~/ $ unzip ~/webbeans-1.0.0.ALPHA1.zip
然后,我们需要告诉Web Beans JBoss的位置。编辑jboss-as/build.properties
,设置jboss.home
属性。例如:
jboss.home=/Applications/jboss-5.0.0.GA
为了安装更新,你需要安装Ant 1.7.0,设置ANT_HOME
环境变量。例如:
$ unzip apache-ant-1.7.0.zip $ export ANT_HOME=~/apache-ant-1.7.0
然后,你需要安装更新,更新脚本使用Maven来自动下载Web Beans和EJB3。
$ cd webbeans-1.0.0.ALPHA1/jboss-as $ ant update
现在,你可以部署你的第一个例子了!
例子的构建脚本包含多个目标:
ant restart
- 以exploded形式部署例子
ant explode
- 无需重新部署,更新一个exploded形式部署的例子
ant deploy
- 以压缩jar包形式部署例子
ant undeploy
- 将例子从服务器中移除
ant clean
- 清除例子
部署猜数字(numberguess)例子:
$ cd examples/numberguess ant deploy
启动 JBoss AS:
jboss.home=/Applications/jboss-5.0.0.GA
如果你使用Windows操作系统,则使用run.bat
脚本。
等待应用部署完毕,好好体验一下http://localhost:8080/webbeans-numberguess!
Web Bean参考实现的第二个简单例子能够将你的文本翻译为拉丁文。猜数字例子是一个WAR应用,仅仅使用了一个简单Beans;翻译器例子是一个EAR应用,包含了打包在EJB模块中的企业Beans。试一下:
$ cd examples/traslator ant deploy
等待应用部署,试一下http://localhost:8080/webbeans-translator!
然后从seamframework.org下载Web Beans的参考实现,然后解压。例如:
$ cd /Applications $ unzip ~/jboss-5.0.0.GA.zip
然后从seamframework.org下载Web Beans的参考实现,然后解压。例如:
$ cd ~/ $ unzip ~/webbeans-1.0.0.ALPHA1.zip
然后,我们需要告诉Web Beans JBoss的位置。编辑jboss-as/build.properties
,设置jboss.home
属性。例如:
jboss.home=/Applications/jboss-5.0.0.GA
例子的构建脚本包含多个目标:
ant restart
- 以exploded形式部署例子
ant explode
- 无需重新部署,更新一个exploded形式部署的例子
ant deploy
- 以压缩jar包形式部署例子
ant undeploy
- 将例子从服务器中移除
ant clean
- 清除例子
部署猜数字(numberguess)例子:
$ cd examples/traslator ant deploy
启动Tomcat:
jboss.home=/Applications/jboss-5.0.0.GA
如果你使用Windows操作系统,则使用run.bat
脚本。
等待应用部署完毕,好好体验一下http://localhost:8080/webbeans-numberguess!
在猜数字应用中,你有十次机会来猜一个1到100之间的数字。每次猜测之后,应用都会告诉你你猜的数字是高了还是低了。
猜数字应用由Web Beans,配置文件,Facelete JSF页面组成,打包为一个WAR。我们先看一下配置文件。
猜数字应用的所有的配置文件位于WEB-INF/
,这个目录位于源码树的WebContent
中。首先,我们在faces-config.xml文件中告诉JSF使用Faceletes:
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="1.2"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<application>
<view-handler
>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config
>
这有一个空的web-beans.xml
文件,标识这个应用是一个Web Beans应用。
最后,这有一个 web.xml
:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name >Web Beans Numbergues example</display-name> <!-- JSF --><servlet> <servlet-name >Faces Servlet</servlet-name> <servlet-class >javax.faces.webapp.FacesServlet</servlet-class> <load-on
-startup >1</load-on-startup> </servlet> <servlet-mapping> <servlet
-name >Faces Servlet</servlet-name> <url-pattern >*.jsf</url-pattern> </servlet-mapping>
<context-param> <param-name >javax.faces.DEFAULT_SUFFIX</param-name> <param-v
alue >.xhtml</param-value> </context-param> <session-config> <session-timeout >10</session-timeout> </session-config> </web-app >
![]() | Enable and load the JSF servlet |
![]() | Configure requests to |
![]() | Tell JSF that we will be giving our source files (facelets) an extension of |
![]() | Configure a session timeout of 10 minutes |
Whilst this demo is a JSF demo, you can use Web Beans with any Servlet based web framework.
Let's take a look at the Facelet view:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:s="http://jboss.com/products/seam/taglib"> <ui:composition template="template.xhtml"> <ui:define name="content"> <h1 >Guess a number...</h1> <h:form
id="NumberGuessMain"> <div style="color: red"> <h:messages id="messages" globalOnly="false"/> <h:outputText id="Higher" value="Higher!" rendered="#{game.number gt game.guess and game.guess ne 0}"/> <h:outputText id="Lower" value="Lower!" rendered="#{game.number lt game.guess and game.guess ne 0}"/> </div> <div
> I'm thinking of a number between #{game.smallest} and #{game.biggest}. You have #{game.remainingGuesses} guesses. </div> <div> Y
our guess: <h:inputText id="inputGuess" value="#{game.guess}" required="true" size="3"
disabled="#{game.number eq game.guess}"> <f:validateLongRange maximum="#{game.biggest}" minimum="#{game.smallest}"/> <
/h:inputText> <h:commandButton id="GuessButton" value="Guess" action="#{game.check}" disabled="#{game.number eq game.guess}"/> </div> <div> <h:commandButton id="RestartButton" value="Reset" action="#{game.reset}" immediate="true" /> </div> </h:form> </ui:define> </ui:composition> </html >
![]() | Facelets is a templating language for JSF, here we are wrapping our page in a template which defines the header. |
![]() | There are a number of messages which can be sent to the user, "Higher!", "Lower!" and "Correct!" |
![]() | As the user guesses, the range of numbers they can guess gets smaller - this sentance changes to make sure they know what range to guess in. |
![]() | This input field is bound to a Web Bean, using the value expression. |
![]() | A range validator is used to make sure the user doesn't accidentally input a number outside of the range in which they can guess - if the validator wasn't here, the user might use up a guess on an out of range number. |
![]() | And, of course, there must be a way for the user to send their guess to the server. Here we bind to an action method on the Web Bean. |
这个例子包括4个类,前面两个是绑定类型。首先,这有一个@Random
绑定类型,用来注入一个随机数:
@Target( { TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@BindingType
public @interface Random {}
这还有一个@MaxNumber
绑定类型,用来注入一个最大值:
@Target( { TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@BindingType
public @interface MaxNumber {}
Generator
类通过一个生产者(producer)方法创建一个随机数。它也通过一个生产者方法暴露可能的最大值:
@ApplicationScoped
public class Generator {
private java.util.Random random = new java.util.Random( System.currentTimeMillis() );
private int maxNumber = 100;
java.util.Random getRandom()
{
return random;
}
@Produces @Random int next() {
return getRandom().nextInt(maxNumber);
}
@Produces @MaxNumber int getMaxNumber()
{
return maxNumber;
}
}
你会注意到Generator
是应用范围的;因此我们不会每次都得到一个不同的随机对象。
最终的应用中的Web Bean是会话范围的 Game
。
你也许注意到我们使用了 @Named
注释,以便我们能够通过EL(表达式语言)在JSF页面中使用Bean。最后,我们通过构造器注入来初始化猜数字游戏并给它设一个随机数。当然,在玩家猜对数字后,我们需要告诉玩家他赢了,所以我们通过FacesMessage
反馈给玩家一条信息。
package org.jboss.webbeans.examples.numberguess;
import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.webbeans.AnnotationLiteral;
import javax.webbeans.Current;
import javax.webbeans.Initializer;
import javax.webbeans.Named;
import javax.webbeans.SessionScoped;
import javax.webbeans.manager.Manager;
@Named
@SessionScoped
public class Game
{
private int number;
private int guess;
private int smallest;
private int biggest;
private int remainingGuesses;
@Current Manager manager;
public Game()
{
}
@Initializer
Game(@MaxNumber int maxNumber)
{
this.biggest = maxNumber;
}
public int getNumber()
{
return number;
}
public int getGuess()
{
return guess;
}
public void setGuess(int guess)
{
this.guess = guess;
}
public int getSmallest()
{
return smallest;
}
public int getBiggest()
{
return biggest;
}
public int getRemainingGuesses()
{
return remainingGuesses;
}
public String check()
{
if (guess
>number)
{
biggest = guess - 1;
}
if (guess<number)
{
smallest = guess + 1;
}
if (guess == number)
{
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Correct!"));
}
remainingGuesses--;
return null;
}
@PostConstruct
public void reset()
{
this.smallest = 0;
this.guess = 0;
this.remainingGuesses = 10;
this.number = manager.getInstanceByType(Integer.class, new AnnotationLiteral<Random
>(){});
}
}
在Tomcat中部署猜数字应用有很多地方不同。首先,WebBean应该作为一个Web应用库文件,部署在WEB-INF/lib
目录中。为方便起见,我们提供了一个单一的jar库 webbeans-tomcat.jar
,能够让我们在Tomcat中运行Web Bean。
当然,你必须也要包含JSF和EL, 以及通用的注释(jsr250-api.jar
),这些都是一个Java EE服务器默认应该包含的。
第二,我们需要在web.xml
中显式地指定Tomcat的Servlet监听器(用来启动Web Bean,并且控制其与请求的交互):
<listener> <listener-class >org.jboss.webbeans.environment.tomcat.Listener</listener-class> </listener >
翻译器例子能够将你输入的句子翻译为拉丁文。
翻译器例子是一个EAR应用,包含EJBs和企业Beans。因此,它的结构比猜数字例子复杂。
EJB3.1和Java EE 6允许你在WAR包中打包EJBs, 这将让这个结构更加简单!
首先,让我们看一下EAR聚合器,它位于webbeans-translator-ear
模块下。Maven将为我们自动生成application.xml
和jboss-app.xml
文件:
<plugin>
<groupId
>org.apache.maven.plugins</groupId>
<artifactId
>maven-ear-plugin</artifactId>
<configuration>
<modules>
<webModule>
<groupId
>org.jboss.webbeans.examples.translator</groupId>
<artifactId
>webbeans-translator-war</artifactId>
<contextRoot
>/webbeans-translator</contextRoot>
</webModule>
</modules>
<jboss>
<loader-repository
>webbeans.jboss.org:loader=webbeans-translator</loader-repository>
</jboss>
</configuration>
</plugin
>
我们需要在这里做些事情-首先我们需要设置上下文路径为一个不错的URL(http://localhost:8080/webbeans-translator),我们还需要将JBoss AS的类加载器隔离配置激活。
如果你不使用Maven来生成这些文件,你将需要META-INF/jboss-app.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application_5.xsd"
version="5">
<display-name
>webbeans-translator-ear</display-name>
<description
>Ear Example for the reference implementation of JSR 299: Web Beans</description>
<module>
<web>
<web-uri
>webbeans-translator.war</web-uri>
<context-root
>/webbeans-translator</context-root>
</web>
</module>
<module>
<ejb
>webbeans-translator.jar</ejb>
</module>
</application
>
然后,我们看一下WAR包。在猜数字例子中,我们需要faces-config.xml
(配置Facelets)和一个位于WebContent/WEB-INF
下的web.xml
(配置JSF并将Web Beans服务加入Servlet容器中)。
更有意思的是用来翻译文本的facelet。在猜数字应用中我们有一个模板,这个模板套着表单(省略了表单):
<h:form id="NumberGuessMain">
<table>
<tr align="center" style="font-weight: bold" >
<td>
Your text
</td>
<td>
Translation
</td>
</tr>
<tr>
<td>
<h:inputTextarea id="text" value="#{translator.text}" required="true" rows="5" cols="80" />
</td>
<td>
<h:outputText value="#{translator.translatedText}" />
</td>
</tr>
</table>
<div>
<h:commandButton id="button" value="Translate" action="#{translator.translate}"/>
</div>
</h:form
>
用户可以在左手边的文本区输入一些文本,点击翻译按钮查看右边的翻译结果。
最后,我们看一下EJB模块,webbeans-translator-ejb
。在src/main/resources/META-INF
下有两个配置文件,一个是空的web-beans.xml
,用来标识这个档案包包含Web Beans,一个是ejb-jar.xml
。Web Beans为所有的EJB提供注入和初始化服务,使用ejb-jar.xml
文件来配置。你将在使用Web Beans的EJB项目中需要这些配置:
我们将最有意思的部分放在最后,那就是代码!这个例子有两个简单Beans, SentanceParser
和TextTranslator
,还有两个企业Beans,TanslatorControllerBean
和SentenceTranslator
。现在你应该对Web Beans有点熟悉了,我们在这里着重最有意思的部分。
SentanceParser
和TextTranslator
是相互依赖的Beans,TextTranslator
使用构造器初始化:
public class TextTranslator {
private SentenceParser sentenceParser;
private Translator sentenceTranslator;
@Initializer
TextTranslator(SentenceParser sentenceParser, Translator sentenceTranslator)
{
this.sentenceParser = sentenceParser;
this.sentenceTranslator = sentenceTranslator;
TextTranslator
是一个无状态Bean(拥有一个本地业务接口),这里是魔术展现的地方-当然,我们不会开发一个完整的翻译器,但我们可以开发一个不错的小玩意!
最后,这里又要一个面向UI的控制器,从用户输入处搜集文本,转发给翻译器。这个控制器是请求范围的,具名的,有状态的会话Bean,它可以将翻译器注入进来。
@Stateful
@RequestScoped
@Named("translator")
public class TranslatorControllerBean implements TranslatorController
{
@Current TextTranslator translator;
这个Bean也拥有页面上所有域的getter和setter方法。
这个Bean是有状态会话Bean,我们需要有一个remove方法:
@Remove
public void remove()
{
}
Web Beans管理器在这个bean销毁的时候调用remove方法;在这个例子中是请求结束的时候。
Web Beans参考实现的例子到此结束。想要获得关于参考实现更多的知识或者帮助,请访问http://www.seamframework.org/WebBeans/Development。
我们在各个方面都需要帮助-bug修复,新特性开发,例子开发和参考指南的翻译等等。
Web Beans支持三种主要的依赖注入机制:
构造器参数注入:
public class Checkout {
private final ShoppingCart cart;
@Initializer
public Checkout(ShoppingCart cart) {
this.cart = cart;
}
}
初始化 方法参数注入:
public class Checkout {
private ShoppingCart cart;
@Initializer
void setShoppingCart(ShoppingCart cart) {
this.cart = cart;
}
}
和直接的域注入:
public class Checkout {
private @Current ShoppingCart cart;
}
当Web Bean实例被首次初始化时,依赖注入总是随之发生。
首先,Web Bean管理器调用Web Bean构造器来获得一个Web Bean的实例。
接下来,Web Bean管理器初始化这个Web bean的所有注入域的值。
然后,Web Bean管理器调用这个Web Bean的初始化方法。
最后, 如果有 @PostConstruct
方法的话,调用这个方法。
EJB Beans不支持构造器参数注入,因为EJB是由EJB容器负责实例化,而不是Web Bean管理器负责。
当应用默认绑定类型@Current
时,构造器的参数和初始化方法需要显式地注释。然而,即使应用了默认的绑定类型,注入域 也必须 指定一个绑定类型。如果一个域没有指定绑定类型,这个域将不会有任何注入发生。
生产者方法也支持参数注入:
@Produces Checkout createCheckout(ShoppingCart cart) {
return new Checkout(cart);
}
最后,观察者方法(我们将在 第 9 章 事件一章中讨论),清除(disposal)方法和解构(destructor)方法都只支持参数注入。
Web Beans规范定义了一个称为 类型安全解析算法的过程,当在注入点识别所注入的Web Bean时,Web Bean管理器会遵循这个过程。这个算法初看起来非常复杂,然而你一旦理解了它,它就相当直观。类型安全解析在系统初始化的时候执行,这意味着如果Web Bean的依赖无法被满足的时候,管理器将立刻通知用户,抛出一个 UnsatisfiedDependencyException
异常或者 AmbiguousDependencyException
异常。
这个算法的目的是允许多个Web Beans实现相同的API类型,并且:
允许客户通过 绑定注释 选择它需要的具体实现,
允许应用部署者激活或者关闭 部署类型 ,从而实现在特定的部署环境下选择适当的具体实现,而无需修改客户,或者
使用部署类型优先级 来允许一个API的实现在部署时重载相同API的另一个实现,而无需修改客户。
让我们看看Web Bean管理器如何决定注入哪个Web Bean。
如果我们拥有实现特定API类型的多个Web Bean,我们可以使用一个绑定注释来指定在注入点注入哪个Web Bean。例如,我们可能有两个 PaymentProcessor
的实现:
@PayByCheque
public class ChequePaymentProcessor implements PaymentProcessor {
public void process(Payment payment) { ... }
}
@PayByCreditCard
public class CreditCardPaymentProcessor implements PaymentProcessor {
public void process(Payment payment) { ... }
}
其中 @PayByCheque
和 @PayByCreditCard
是绑定注释:
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
@BindingType
public @interface PayByCheque {}
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
@BindingType
public @interface PayByCreditCard {}
一个客户Web Bean开发者使用绑定注释来指定到底哪个Web Bean应该被注入。
使用域注入:
@PayByCheque PaymentProcessor chequePaymentProcessor;
@PayByCreditCard PaymentProcessor creditCardPaymentProcessor;
使用初始化方法注入:
@Initializer
public void setPaymentProcessors(@PayByCheque PaymentProcessor chequePaymentProcessor,
@PayByCreditCard PaymentProcessor creditCardPaymentProcessor) {
this.chequePaymentProcessor = chequePaymentProcessor;
this.creditCardPaymentProcessor = creditCardPaymentProcessor;
}
或者使用构造器注入:
@Initializer
public Checkout(@PayByCheque PaymentProcessor chequePaymentProcessor,
@PayByCreditCard PaymentProcessor creditCardPaymentProcessor) {
this.chequePaymentProcessor = chequePaymentProcessor;
this.creditCardPaymentProcessor = creditCardPaymentProcessor;
}
绑定注释可以拥有成员:
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
@BindingType
public @interface PayBy {
PaymentType value();
}
在这个例子,成员值是有意义的:
@PayBy(CHEQUE) PaymentProcessor chequePaymentProcessor;
@PayBy(CREDIT_CARD) PaymentProcessor creditCardPaymentProcessor;
你可以告诉Web Bean管理器忽略一个绑定注释的成员,只需在这个成员上使用 @NonBinding
注释。
一个注入点甚至可以指定多个绑定注释:
@Asynchronous @PayByCheque PaymentProcessor paymentProcessor
在这个情况下,只有拥有两个绑定注释的Web Bean才有资格被注入。
甚至生产者方法可以指定绑定注释:
@Produces
@Asynchronous @PayByCheque
PaymentProcessor createAsyncPaymentProcessor(@PayByCheque PaymentProcessor processor) {
return new AsynchronousPaymentProcessor(processor);
}
所有的Web Beans都有一个 部署类型 。每个部署类型标识一套Web Beans,这套Web Beans会有条件地在某些系统部署下面被安装。
例如,我们可以定义一套为名为 @Mock
的部署类型,这种部署类型用来标识只应该在整合测试环境下才在系统中安装的Web Beans:
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@DeploymentType
public @interface Mock {}
假定我们有一些和外部系统交互的Web Beans来处理付费:
public class ExternalPaymentProcessor {
public void process(Payment p) {
...
}
}
因为这个Web Bean并不显式地指定一个部署类型,因此它有一个默认的 @Production
部署类型。
在整合测试或者单元测试期间,外部系统可能会很慢或者无法获得,所以我们可能会创建一个模拟对象:
@Mock
public class MockPaymentProcessor implements PaymentProcessor {
@Override
public void process(Payment p) {
p.setSuccessful(true);
}
}
但是Web Bean管理器如何决定在一个特定部署中使用哪一个实现?
Web Beans定义了两个内置的部署类型: @Production
和 @Standard
。默认情况下,当系统被部署时,只有使用内置部署类型的Web Beans才被激活。我们可以在 web-beans.xml
文件中列出其他的部署类型以便在某个特定的部署中激活它们。
回到我们的例子中,当我们部署我们的整合测试时,我们希望我们标识的所有的 @Mock
对象被安装:
<WebBeans>
<Deploy>
<Standard/>
<Production/>
<test:Mock/>
</Deploy>
</WebBeans
>
现在,Web Bean管理器可以识别并且在部署期间安装所有拥有 @Production
, @Standard
和 @Mock
注释的Web Beans。
@Standard
只是Web Beans规范中为特定的Web Bean使用的部署类型。我们无法在我们自己的Web Bean中使用它,并且我们不能关闭它。
@Production
是没有显式声明部署类型的Web Beans的默认部署类型,它可以被关闭。
如果你注意的话,你可能想知道Web Bean如何决定使用哪个实现 ExternalPaymentProcessor
还是 MockPaymentProcessor
。思考一下当管理器遇到这个注入点时会发生什么:
@Current PaymentProcessor paymentProcessor
现在有两个Web Bean满足 PaymentProcessor
合约。当然,我们无法使用一个绑定注释来消除这个歧义,因为绑定注释在注入点被硬编码到了源代码中,并且我们希望管理器在部署时能够决定注入哪一个Web Bean!
这个问题的解决方案是每个不同的部署类型都拥有不同的 优先级 。部署类型的优先级由它们在 web-beans.xml
中出现的顺序决定。在我们这个例子中, @Mock
比 @Production
出现的晚,所以它拥有更高的优先级。
无论何时管理器发现有多个Web Bean能够满足一个注入点指定的合约(API类型加上绑定注释),它都会考虑Web Beans的相对优先级。它将注入拥有更高优先级的Web Bean。在我们这个例子中,当系统运行在整合测试环境中(这是我们想要的),Web Bean管理器将注入 MockPaymentProcessor
对象。
将其和当今流行的管理器体系比较是很有意思的。各种 "轻量级"的容器也许也可以支持条件化部署在类路径中的类,但是这些需要部署的类必须显式地,逐个地列在配置代码或某个XML配置文件中。Web Bean当然支持通过XML来定义和配置Web Bean,但是多数情况下,只要不需要复杂的配置,部署类型可以使用一行XML就能配置一整套Web Beans。同时,浏览代码的开发者可以很容易识别这些Web Bean应该部署在哪些场景中。
考虑到所有实现一个注入点API类型的Web Bean的绑定注释和部署类型类型,如果解析算法失败,那么Web Bean管理器无法识别究竟应该注入哪个Web Bean。
通常我们很容易修正一个UnsatisfiedDependencyException
或者 AmbiguousDependencyException
。
要修正一个 UnsatisfiedDependencyException
,只须简单的提供一个实现API类型的Web Bean并且拥有注入点的绑定类型 或者激活一个已经实现API类型并且拥有绑定类型的Web Bean的部署类型。
要修正一个 AmbiguousDependencyException
,我们需要引入一个绑定类型来区分API类型的两个不同的实现,或者改变其中一个实现的部署类型以便Web Bean管理器可以使用部署类型优先级来决定究竟部署哪一个实现。只有两个Web Bean共享一个绑定类型并且拥有相同部署类型的时候才会抛出 AmbiguousDependencyException
。
使用Web Bean依赖注入的时候还需要注意一个问题。
注入的Web Bean的客户通常不会直接拥有这个Web bean实例的引用。
想象一下如果一个应用范围的Web Bean能够拥有一个请求范围的Web Bean的直接引用。应用范围的Web Bean是被很多不同的请求共享的。但是不同的请求应该看到不同的请求范围的Web Bean实例!
现在再想象一个会话范围的Web Bean拥有一个应用范围的Web Bean的直接引用,会话上下文常常被序列化到硬盘以便更高效的使用内存。但是,应用范围的Web Bean实例不能和会话范围的Web Bean一起被序列化!
因此,除非Web Bean使用默认的 @Dependent
的范围,Web Bean管理器必须通过一个代理对象来间接地拥有所有注入的Web Bean引用。这个 客户代理 负责确保收到方法调用的Web Bean实例就是当前上下文相关联的实例。客户代理也允许诸如绑定到会话上下文的Web Bean可以序列化到硬盘中,而无需递归地序列化注入到这个Web Bean中的其他的Web Bean。
不幸的是,由于Java语言的限制,一些Java类型无法被Web Bean管理器代理。因此,如果注入点的类型无法被代理的话,Web Bean管理器会抛出一个 UnproxyableDependencyException
异常。
下面的Java类型无法被Web Bean管理器代理:
声明为 final
的类或者拥有 final
方法的类,
没有无参非私有构造器的类,以及
数组和原始类型。
修正 UnproxyableDependencyException
很容易。只需简单的想注入类添加一个无参构造器,引入一个接口或者将注入的Web Bean的范围 @Dependent
即可。
应用可以通过注入获得一个 Manager
接口实例:
@Current Manager manager;
Manager
对象提供一套通过编程获得一个Web Bean实例的方法。
PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class);
绑定注释可以通过编写一个帮助类 AnnotationLiteral
的子类来指定,否则很难在Java中实例化一个注释类型。
PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class,
new AnnotationLiteral<CreditCard
>(){});
如果绑定类型拥有一个注释成员,我们无法使用 AnnotationLiteral
的匿名子类 相反,我们需要创建一个具名子类:
abstract class CreditCardBinding
extends AnnotationLiteral<CreditCard
>
implements CreditCard {}
PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class,
new CreditCardBinding() {
public void value() { return paymentType; }
} );
企业级Web Beans支持所有EJB规范定义的生命周期回调方法:@PostConstruct
, @PreDestroy
, @PrePassivate
和 @PostActivate
。
简单的Web Bean只支持 @PostConstruct
和 @PreDestroy
回调。
企业级和简单的Web Bean都支持使用f @Resource
, @EJB
和 @PersistenceContext
来分别注入Java EE资源,EJB和JPA持久化上下文。简单的Web Bean不支持使用 @PersistenceContext(type=EXTENDED)
。
@PostConstruct
调用总是在所有依赖注入之后发生。
我们有一些依赖对象 @Dependent
范围的Web Bean 需要知道它们所注入的对象或者注入点的信息,以便能够实现其功能。例如:
Logger
的日志分类取决于拥有它的类。
一个HTTP参数和报头值的注入取决于注入点指定的参数或者报头名称。
表达式运算结果的注入取决于在注入点指定的表达式。
一个 @Dependent
范围的Web Bean可以注入一个 InjectionPoint
实例并且访问这个注入点相关的元数据。
我们看一个例子。下面的代码很冗长脆弱,有重构问题:
Logger log = Logger.getLogger(MyClass.class.getName());
这个生产者方法允许你注入一个JDK的 Logger
,没有显式的指定一个日志分类:
class LogFactory {
@Produces Logger createLogger(InjectionPoint injectionPoint) {
return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
}
}
我们现在可以编写:
@Current Logger log;
没被说服?我们还有第二个例子。要注入HTTP参数,我们需要定一个绑定类型:
@BindingType
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface HttpParam {
@NonBinding public String value();
}
我们可以在注入点使用这个绑定类型:
@HttpParam("username") String username;
@HttpParam("password") String password;
下面的生产方法能够完成这个工作:
class HttpParams
@Produces @HttpParam("")
String getParamValue(ServletRequest request, InjectionPoint ip) {
return request.getParameter(ip.getAnnotation(HttpParam.class).value());
}
}
(注意 HttpParam
注释的成员 value()
将被Web Bean管理器忽略,因为它拥有 @NonBinding.
注释)
Web Bean管理器提供一个内置的实现 InjectionPoint
接口的Web Bean:
public interface InjectionPoint {
public Object getInstance();
public Bean<?> getBean();
public Member getMember():
public <T extends Annotation
> T getAnnotation(Class<T
> annotation);
public Set<T extends Annotation
> getAnnotations();
}
目前为止,我们已经看到了几个 范围类型注释的例子。Web Bean的范围决定了Web Bean实例的生命周期。范围还决定了哪个客户端引用了哪个Web Bean实例。根据Web Beans规范,一个范围决定:
该范围的Web Bean的一个实例何时被创建
该范围的Web Bean实例何时被销毁
注入的引用指向该范围的Web Bean的哪个实例
例如,如果我们有一个会话范围的Web Bean:CurrentUser
。那么在同一个HttpSession
的上下文中调用的所有的Web Bean都将看到同一个CurrentUser
实例。这个实例在会话第一次需要CurrentUser
时被自动创建,在会话结束时被自动销毁。
Web Bean有一个特性是可扩展的上下文模型。我们可以创建一个新的范围类型注释来定一个新的范围:
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@ScopeType
public @interface ClusterScoped {}
当然,这是这项工作最简单的部分。为了让这个范围类型可以使用,我们还需要定义一个Context(上下文)
对象来实现这个范围!实现上下文通常是一个非常具备挑战的技术任务,这常常只能由开发编程框架的专家完成。
我们可以在Web Bean实现类中应用范围类型注释来指定Web Bean的范围:
@ClusterScoped
public class SecondLevelCache { ... }
通常,你将会使用一个Web Bean内置的范围。
Web Beans定义了四个内置范围:
@RequestScoped
@SessionScoped
@ApplicationScoped
@ConversationScoped
对于使用Web Beans的Web应用:
任何Servlet请求可以访问激活的请求,会话和应用范围,并且
任何JSF请求可以访问一个激活的对话范围
在下列情况下请求和应用范围是激活的:
在EJB远程方法调用期间,
在EJB超时期间,
在消息发送给消息驱动Bean的期间,
在Web Service调用期间。
如果应用试图调用一个Web Bean,而对应的范围上下文没有处于激活状态时,Web Bean管理器在运行时将抛出一个ContextNotActiveException
异常。
这四个内置范围的其中三个对于每个Java EE程序员来说都非常熟悉,所以让我们别浪费时间来讨论他们。不过有一个范围是新的。
Web Beans的对话(Conversation)范围有点类似与传统的会话范围(Session),传统的会话范围常常用来存储和系统用户相关的状态,并且能够跨越多个请求。然而,对话范围还有很多地方和会话范围不一样:
它通过应用显式地声明,并且
在一个JSF应用中持有与一个特定的Web浏览标签页关联的状态。
从用户角度出发,一个对话代表一个任务,或者一个工作单元。用户当前工作相关的状态由对话上下文维护。如果用户同时处理多个事情,就会有多个对话与之对应。
一个对话上下文在任何JSF请求中都是激活的。但是,大部分对话都在请求结束的时候被销毁了。如果一个对话需要跨越多个请求来维护状态的话,它必须显式地升级为长时对话。
Web Beans提供了一个内置的Web Bean来在控制一个JSF应用中对话的生命周期。这个Web Bean可以通过注入来获得:
@Current Conversation conversation;
将当前请求关联的对话升级为长时对话的方法是从应用代码中调用 begin()
方法。将当前长时对话上下文在当前请求结束时销毁的方法是调用end()
方法。
在下面的例子中,一个对话范围的Web Bean控制和它关联的对话:
@ConversationScoped @Stateful
public class OrderBuilder {
private Order order;
private @Current Conversation conversation;
private @PersistenceContext(type=EXTENDED) EntityManager em;
@Produces public Order getOrder() {
return order;
}
public Order createOrder() {
order = new Order();
conversation.begin();
return order;
}
public void addLineItem(Product product, int quantity) {
order.add( new LineItem(product, quantity) );
}
public void saveOrder(Order order) {
em.persist(order);
conversation.end();
}
@Remove
public void destroy() {}
}
Web Bean可以通过Conversation
接口控制自己的生命周期。但是其他一些Web Bean的生命周期完全依赖与其他对象。
对话上下文在任何JSF faces请求中自动传播(JSF表单提交)。在非faces请求中,对话上下文将不会自动传播,例如通过一个链接来导航。
我们可以强迫在非faces请求中传播对话,方法是在请求参数中包含一个对话的唯一标识符即可。Web Beans规范为此保留了一个请求参数关键字cid
。对话的唯一标识符可以通过Conversation
对象获得,它拥有Web Beans的名字conversation
。
因此,下面的链接能够传播对话:
<a href="/addProduct.jsp?cid=#{conversation.id}" >Add Product</a >
Web Bean管理器也需要能够跨越任何重定向来传播对话,甚至这个对话没有被升级为长时对话。这样我们就能很容易实现常用的POST-then-redirect模式,而不需要构建一个脆弱的 "闪存"对象。在这个例子中,Web Bean管理器自动向重定向URL中添加一个请求参数。
除了内置的四个范围,Web Beans还提供了一个依赖的伪范围。这个范围是没有显式设置范围类型的Web Bean的默认范围。
例如,这个Web Bean有一个范围类型@Dependent
:
public class Calculator { ... }
当一个注入点被解析为一个依赖的Web Bean之后,每当第一个Web Bean被初始化时,都会创建一个依赖的Web Bean实例。不同的Web Beans或者不同的注入点的依赖的Web Beans的实例都不会被共享。它们是其它Web Bean实例的依赖的对象。
依赖的Web Bean实例在它们所依赖对象实例销毁的时候被销毁。
Web Bean能够让我们轻松获得一个Java类或者EJB Bean的依赖实例,甚至这个类或者EJB Bean已经被声明为一个其他范围的Web Bean也没问题。
内置的@New
绑定注释允许在注入点隐式地定义一个依赖的Web Bean。假设我们需要声明下面的注入域:
@New Calculator calculator;
这个Web Bean被隐式地定义为范围为@Dependent
,绑定类型为@New
,API类型为Calculator
,实现了Calculator
类,部署类型为@Standard
。
甚至在Calculator
已经通过不同的范围类型声明过的情况下也是如此。例如:
@ConversationScoped
public class Calculator { ... }
所以下面注入的属性,每个都获得一个不同的 Calculator
实例:
public class PaymentCalc {
@Current Calculator calculator;
@New Calculator newCalculator;
}
calculator
域有一个对话范围的Calculator
实例注入。newCalculator
域有一个新的Calculator
实例注入,这个实例的生命周期绑定在其拥有者PaymentCalc
类上。
这个特性对于生产者方法来说特别有用,我们将在下一章看到。
生产者方法能够让我们克服使用Web Bean管理器代替应用来负责实例化对象所带来的特定的限制。生产者方法也是将非Web Beans的对象整合到Web Beans环境中的最简单的途径。(我们将在第 12 章 使用XML定义Web Bean讨论第二种方法)
根据规范:
A Web Beans producer method acts as a source of objects to be injected, where:
the objects to be injected are not required to be instances of Web Beans,
the concrete type of the objects to be injected may vary at runtime or
the objects require some custom initialization that is not performed by the Web Bean constructor
For example, producer methods let us:
expose a JPA entity as a Web Bean,
expose any JDK class as a Web Bean,
define multiple Web Beans, with different scopes or initialization, for the same implementation class, or
vary the implementation of an API type at runtime.
In particular, producer methods let us use runtime polymorphism with Web Beans. As we've seen, deployment types are a powerful solution to the problem of deployment-time polymorphism. But once the system is deployed, the Web Bean implementation is fixed. A producer method has no such limitation:
@SessionScoped
public class Preferences {
private PaymentStrategyType paymentStrategy;
...
@Produces @Preferred
public PaymentStrategy getPaymentStrategy() {
switch (paymentStrategy) {
case CREDIT_CARD: return new CreditCardPaymentStrategy();
case CHEQUE: return new ChequePaymentStrategy();
case PAYPAL: return new PayPalPaymentStrategy();
default: return null;
}
}
}
Consider an injection point:
@Preferred PaymentStrategy paymentStrat;
This injection point has the same type and binding annotations as the producer method, so it resolves to the producer method using the usual Web Beans injection rules. The producer method will be called by the Web Bean manager to obtain an instance to service this injection point.
.默认的生产者方法的范围是 @Dependent
,所以Web Bean管理器每次注入这个域或者注入任何其他对应改生产者方法的域的时候,这个生产者方法都会被调用。这样,对于每个用户会话,我们有可能有多个PaymentStrategy
实例。
要改变这种行为,我们可以给这个方法添加一个 @SessionScoped
注释。
@Produces @Preferred @SessionScoped
public PaymentStrategy getPaymentStrategy() {
...
}
现在,当这个生产者方法被调用时,它返回的 PaymentStrategy
对象将被绑定到会话上下文中。这个生产者方法在同一个会话范围中不会被再次调用。
上面的代码有一个潜在的问题。 CreditCardPaymentStrategy
的实现使用Java的 new
操作符来实例化。应用直接实例化的对象无法获得依赖注入的好处,也无法获得拦截器。
如果这不是我们想要的,我们可以对生产者方法使用依赖注入来获得Web Bean实例:
@Produces @Preferred @SessionScoped
public PaymentStrategy getPaymentStrategy(CreditCardPaymentStrategy ccps,
ChequePaymentStrategy cps,
PayPalPaymentStrategy ppps) {
switch (paymentStrategy) {
case CREDIT_CARD: return ccps;
case CHEQUE: return cps;
case PAYPAL: return ppps;
default: return null;
}
}
等一下,如果 CreditCardPaymentStrategy
是一个请求范围的Web Bean,那会怎样?生产者方法将会把当前请求范围的实例"提升"到会话范围。这近乎是一个Bug!请求范围的对象会在会话结束前被Web Bean管理器销毁,但是这个对象的引用将被 "悬挂"在会话范围。Web Bean管理器不会检测到这种错误,因此使用生产者方法返回Web bean实例的时候一定要多加小心!
我们至少有三种方式来解决这个Bug。我们可以改变 CreditCardPaymentStrategy
实现的范围,但是这种方法将影响到其他使用这个Web Bean的客户。一个更好的选择是将生产者方法的范围更改到 @Dependent
或者 @RequestScoped
。
不过更加常用的方法是使用特殊的 @New
绑定注释。
考虑到下面的生产者方法:
@Produces @Preferred @SessionScoped
public PaymentStrategy getPaymentStrategy(@New CreditCardPaymentStrategy ccps,
@New ChequePaymentStrategy cps,
@New PayPalPaymentStrategy ppps) {
switch (paymentStrategy) {
case CREDIT_CARD: return ccps;
case CHEQUE: return cps;
case PAYPAL: return ppps;
default: return null;
}
}
一个新的依赖的 CreditCardPaymentStrategy
实例将被创建,然后传递给生产者方法,然后生产者方法将其返回,最终这个实例将被绑定到会话上下文。这个依赖对象不会被提前销毁,而是在会话结束时跟随 Preferences
一起被销毁。
Web Bean的首要宗旨就是松耦合。我们已经看到三种实现松耦合的方法:
部署类型能够实现部署时多态性
生产者方法能够实现运行时多态性,并且
具备上下文的生命周期管理将Web Bean的生命周期解耦。
这些技术都是为了将客户端和服务器端解耦。客户端不在和一个API的具体实现紧密绑定,也不需要管理服务器端对象的生命周期。这种方法能够让有状态的对象像服务一样交互。
松耦合能够让系统更加动态。系统可以以定义良好的方式来相应变化。在过去,多数框架总是通过牺牲类型安全来提供上述功能。实现此种高度的松耦合,同时又能够保证类型安全,Web Beans是第一个实现这个目标的技术。
Web Bean提供三种额外的功能来实现松耦合的目标:
拦截器能够将技术关注点从业务逻辑中解耦。
装饰器能够将一些业务关注点解耦,并且
事件通知机制能将事件消费者和事件生产者解耦。
让我们首先研究拦截器。
Web Beans重用了EJB3.0的基本的拦截器体系,并且在两个方向上扩展了其功能:
任何Web Bean都可以拥有拦截器,而不仅仅是会话Bean。
Web Beans拥有一个更复杂的基于注释的方法将拦截器绑定到Web Bean上。
EJB规范定义了两种拦截点:
业务方法拦截,和
生命周期回调方法拦截。
一个 业务方法拦截 是Web Bean的客户应用在Web Bean的方法调用上:
public class TransactionInterceptor {
@AroundInvoke public Object manageTransaction(InvocationContext ctx) { ... }
}
一个 生命周期回调拦截器 是容器应用在生命周期回调方法的调用上:
public class DependencyInjectionInterceptor {
@PostConstruct public void injectDependencies(InvocationContext ctx) { ... }
}
一个拦截器类既可以拦截生命周期回调方法,也可以拦截业务方法。
假定我们想声明我们的某些Web Beans是事务性的。我们要做的第一个事情就是需要一个 拦截器绑定注释 以便指定哪些Web Beans是事务性的:
@InterceptorBindingType
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Transactional {}
现在我们可以很容易地指定我们的 ShoppingCart
是一个事务性的对象:
@Transactional
public class ShoppingCart { ... }
或者,如果我们愿意的话,我们可以仅仅指定一个方法是事务性的:
public class ShoppingCart {
@Transactional public void checkout() { ... }
}
很好,但是在代码中有些地方需要我们实现提供事务管理的拦截器。我们需要做的事情就是创建一个标准的EJB拦截器,使用 @Interceptor
和 @Transactional
注释它。
@Transactional @Interceptor
public class TransactionInterceptor {
@AroundInvoke public Object manageTransaction(InvocationContext ctx) { ... }
}
所有的Web Beans拦截器都是简单的Web Beans,因此可以使用依赖注入和具有上下文的生命周期的管理。
@ApplicationScoped @Transactional @Interceptor
public class TransactionInterceptor {
@Resource Transaction transaction;
@AroundInvoke public Object manageTransaction(InvocationContext ctx) { ... }
}
多个拦截器可以使用相同的拦截器绑定类型
最后,我们需要在 web-beans.xml
配置文件中 激活 我们的拦截器。
<Interceptors>
<tx:TransactionInterceptor/>
</Interceptors
>
哇塞!为何使用尖括号?
好吧,XML声明解决了两个问题:
它可以让我们在我们的系统中指定所有的拦截器顺序,确保这些行为具有确定性,并且
它能够让我们在部署期间激活或者关闭拦截器类。
例如,我们可以指定在 TransactionInterceptor
之前运行我们的安全拦截器。
<Interceptors>
<sx:SecurityInterceptor/>
<tx:TransactionInterceptor/>
</Interceptors
>
或者我们可以在测试环境中将它们都关闭!
假定我们想要往 @Transactional
注释中添加一些额外信息:
@InterceptorBindingType
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Transactional {
boolean requiresNew() default false;
}
Web Beans将使用 requiresNew
的值来决定在两种不同的拦截器 TransactionInterceptor
和 RequiresNewTransactionInterceptor
中选择哪一个。
@Transactional(requiresNew=true) @Interceptor
public class RequiresNewTransactionInterceptor {
@AroundInvoke public Object manageTransaction(InvocationContext ctx) { ... }
}
现在我们可以像这样使用 RequiresNewTransactionInterceptor
:
@Transactional(requiresNew=true)
public class ShoppingCart { ... }
但是如果我们只有一个拦截器,并且我们希望管理器在绑定拦截器的时候忽略 requiresNew
的值,那该怎么办?我们可以使用 @NonBinding
注释:
@InterceptorBindingType
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Secure {
@NonBinding String[] rolesAllowed() default {};
}
通常我们可以组合拦截器绑定类型将多个拦截器绑定到一个Web Bean上。例如,下面的声明可以将 TransactionInterceptor
和 SecurityInterceptor
绑定到相同的Web Bean上:
@Secure(rolesAllowed="admin") @Transactional
public class ShoppingCart { ... }
然而,在非常复杂的情况下,一个拦截器本身可以指定一些拦截器绑定类型的组合:
@Transactional @Secure @Interceptor
public class TransactionalSecureInterceptor { ... }
然后,这个拦截器可以使用下面的任何一个组合来绑定到 checkout()
方法上:
public class ShoppingCart {
@Transactional @Secure public void checkout() { ... }
}
@Secure
public class ShoppingCart {
@Transactional public void checkout() { ... }
}
@Transactionl
public class ShoppingCart {
@Secure public void checkout() { ... }
}
@Transactional @Secure
public class ShoppingCart {
public void checkout() { ... }
}
Java语言对注释的支持有一个限制,那就是缺少注释的继承机制。实际上,注释应该可以重用内置,应该支持下列工作:
public @interface Action extends Transactional, Secure { ... }
不过幸运的是,Web Beans能够解决这个Java缺失的特性。我们可以向一个拦截器绑定类型注释另一个拦截器绑定类型。拦截器绑定是具有传递性的 拥有第一个拦截器绑定的任何Web Bean都继承被声明为元注释的拦截器绑定。
@Transactional @Secure
@InterceptorBindingType
@Target(TYPE)
@Retention(RUNTIME)
public @interface Action { ... }
任何使用 @Action
注释的Web Bean都被绑定到 TransactionInterceptor
和 SecurityInterceptor
。(如果存在 TransactionalSecureInterceptor
的话,甚至可以绑定到这个拦截器)
企业级和简单的Web Bean都支持EJB规范指定的 @Interceptors
注释,例如:
@Interceptors({TransactionInterceptor.class, SecurityInterceptor.class})
public class ShoppingCart {
public void checkout() { ... }
}
然而,这种方法具有下列缺陷:
拦截器实现被硬编码到在业务代码中,
拦截器可能无法轻松地在部署期间关闭,并且
拦截器的顺序是非全局性的 它由在类级别上列出的拦截器的顺序决定。
因此,我们建议使用Web Bean风格的拦截器绑定。
拦截器能够以一种强大的方式来捕获和分离关注点,拦截器和类型系统的关系是正交的。任何拦截器都能够拦截任何Java类型的调用。这种特性能够让拦截器很好的处理技术关注点,例如事务管理和安全。不过,拦截器无法领会其所拦截时间的真实语义。因此,拦截器并不是分离业务相关的关注点的好工具。
对装饰器来说,反之亦然。一个装饰器只拦截特定Java接口的调用,因此它能够领会这个接口关联的语义。装饰器的特性使其成为某些业务关注点的理想的建模工具。这也意味着装饰器并不拥有一个拦截器的普遍性。装饰器无法解决跨越多个不同类型的技术关注点。
假定我们有一个表现帐户的接口:
public interface Account {
public BigDecimal getBalance();
public User getOwner();
public void withdraw(BigDecimal amount);
public void deposit(BigDecimal amount);
}
我们系统实现中可以有多个不同的Web Beans实现 Account
接口。然而我们有一个通用的法律要求对于任何帐户,大的交易必须由系统在一个特定的日志中记录。装饰器非常适合处理这种工作。
装饰器是一个简单的Web Bean,能够实现其装饰的类型。装饰器使用 @Decorator
注释。
@Decorator
public abstract class LargeTransactionDecorator
implements Account {
@Decorates Account account;
@PersistenceContext EntityManager em;
public void withdraw(BigDecimal amount) {
account.withdraw(amount);
if ( amount.compareTo(LARGE_AMOUNT)
>0 ) {
em.persist( new LoggedWithdrawl(amount) );
}
}
public void deposit(BigDecimal amount);
account.deposit(amount);
if ( amount.compareTo(LARGE_AMOUNT)
>0 ) {
em.persist( new LoggedDeposit(amount) );
}
}
}
和其他简单的Web Beans不同,一个装饰器可以是一个抽象类。如果被装饰的接口中一个特殊方法对装饰器没有用处,那么装饰器可以不实现这个方法。
所有装饰器都有一个 委托属性 。委托属性的类型和绑定类型决定了装饰器绑定到哪个Web Bean上。委托属性类型必须实现或者继承装饰器实现的所有接口。
下面的委托属性指定了装饰器绑定到所有实现 Account
接口的Web Beans:
@Decorates Account account;
一个委托属性能够指定一个绑定注释。装饰器只能绑定具有相同绑定的Web Beans。
@Decorates @Foreign Account account;
一个装饰器可以绑定到任何符合下面条件的Web Bean上:
有一个作为API类型的委托属性类型,以及
具备委托属性声明的说有绑定类型。
装饰器可以调用委托属性,这和从一个拦截器中调用 InvocationContext.proceed()
具有相同效果。
Web Beans事件通知机制能够让Web Beans以完全解耦的方式交互。事件产生者(producers)触发事件,然后由Web Bean管理器发送给事件 观察者(observers) 。基本的模式和我们常见的观察者设计模式类似,但也有一些曲解:
不仅是事件产生者和观察者解耦;观察者和产生者也完全解耦,
观察者可以指定一个"选择器"组合来限定接受的事件通知,并且
观察者可以即刻被通知,也可以指定只有当前事务结束后再发送事件通知。
一个 观察者方法是一个在Web Bean中的方法,该方法有一个用@Observes
注释的参数。
public void onAnyDocumentEvent(@Observes Document document) { ... }
这个使用注释的参数被称为事件参数。事件参数的类型是被观察的事件类型。观察者方法也可以指定"选择器",选择器其实就是Web Beans绑定类型的实例。当一个绑定类型作为事件选择器时,它也被称为一个事件绑定类型。
@BindingType
@Target({PARAMETER, FIELD})
@Retention(RUNTIME)
public @interface Updated { ... }
我们通过注释事件参数来指定观察者方法的事件绑定。
public void afterDocumentUpdate(@Observes @Updated Document document) { ... }
一个观察者方法无须指定一个事件绑定 在这种情况下,它对一个特定类型的 所有 事件都感兴趣(观察)。如果它没有指定事件绑定,那么它只对那些有事件绑定的事件感兴趣(观察)。
观察者方法可以有其他的参数,它们可以按照普通的Web Beans方法参数注入语法注入:
public void afterDocumentUpdate(@Observes @Updated Document document, User user) { ... }
事件生产者可以通过注入获得一个 事件通知者对象:
@Observable Event<Document
> documentEvent
@Observable
注释隐式指定了一个 @Dependent
范围的,部署类型为@Standard
的Web Bean。其实现由Web Bean管理器提供。
一个生产者通过调用 Event
接口的 fire()
方法来触发事件,传递一个 事件对象 :
documentEvent.fire(document);
一个事件对象可以是没有任何类型变量或者通配类型参数的任何Java类的实例。事件可以通过符合下面条件的观察者方法传送:
有一个事件参数可以赋给事件对象,并且
没有指定事件绑定
Web Bean管理器简单地调用所有观察者方法,以事件参数值来传递事件对象。如果任何观察者方法抛出一个异常,Web Bean管理器将停止调用观察者方法,fire()
方法再次抛出这个异常。
要指定"选择器",事件生产者可以将一个事件绑定类型的实例传递给 fire()
方法:
documentEvent.fire( document, new AnnotationLiteral<Updated
>(){} );
AnnotationLiteral
帮助类能够让我们内部实例化绑定类型,这在Java里很难这么做。
事件将被发送给每个符合下面条件的观察者方法:
有一个事件参数可以赋给事件对象,并且
除了传递给fire()
方法的事件绑定之外,没有指定任何事件绑定。
或者,通过注释事件通知者注入点来指定事件绑定:
@Observable @Updated Event<Document
> documentUpdatedEvent
然后,每个通过 Event
实例触发的事件都有注释的事件绑定。事件将被发送给每个符合下面条件的观察者方法:
有一个事件参数可以赋给事件对象,并且
除了传递给 fire()
方法或者是事件通知者注入点的事件绑定。
动态地注册一个事件观察者相当有用。应用可以实现 Observer
接口,通过调用 observe()
方法注册一个事件通知者的观察者实例。
documentEvent.observe( new Observer<Document
>() { public void notify(Document doc) { ... } } );
事件绑定类型可以通过事件通知者注入点指定,或者将一个事件绑定类型实例传递给 observe()
方法:
documentEvent.observe( new Observer<Document
>() { public void notify(Document doc) { ... } },
new AnnotationLiteral<Updated
>(){} );
一个事件绑定类型可以有注释成员:
@BindingType
@Target({PARAMETER, FIELD})
@Retention(RUNTIME)
public @interface Role {
RoleType value();
}
成员值用来限制发送给观察者的消息:
public void adminLoggedIn(@Observes @Role(ADMIN) LoggedIn event) { ... }
事件绑定类型成员可以通过事件产生者静态地指定,通过在事件通知者注入点使用注释:
@Observable @Role(ADMIN) Event<LoggedIn
> LoggedInEvent;}}
或者事件绑定类型成员的值也可以由事件产生者动态地决定。我们先写一个 AnnotationLiteral
的抽象子类:
abstract class RoleBinding
extends AnnotationLiteral<Role
>
implements Role {}
事件产生者将一个这个类的实例传递给 fire()
:
documentEvent.fire( document, new RoleBinding() { public void value() { return user.getRole(); } } );
事件绑定类型可以组合,例如:
@Observable @Blog Event<Document
> blogEvent;
...
if (document.isBlog()) blogEvent.fire(document, new AnnotationLiteral<Updated
>(){});
当这个事件发生时,下列所有观察者方法都会被通知:
public void afterBlogUpdate(@Observes @Updated @Blog Document document) { ... }
public void afterDocumentUpdate(@Observes @Updated Document document) { ... }
public void onAnyBlogEvent(@Observes @Blog Document document) { ... }
public void onAnyDocumentEvent(@Observes Document document) { ... }}}
事务性的观察者在事件触发的事务完成阶段之前或者之后接收到事件通知。例如下面的观察者方法需要刷新在应用上下文中缓存的查询结果集,但只有在事务成果地更新了 Category
树之后才能发生:
public void refreshCategoryTree(@AfterTransactionSuccess @Observes CategoryUpdateEvent event) { ... }
我们有三种事务性观察者:
@AfterTransactionSuccess
观察者仅仅在事务成功完成之后被调用。
@AfterTransactionFailure
观察者仅仅在事务失败之后被调用。
@AfterTransactionCompletion
观察者在事务完成阶段之后被调用。
@BeforeTransactionCompletion
观察者在事务完成阶段之前被调用。
事务性的观察者在诸如Web Beans这类具有状态的对象模型中非常重要,因为状态通常比一个单一的原子事务所保留的时间更长。
设想一下我们已经在应用范围内缓存了一个JPA的查询结果集:
@ApplicationScoped @Singleton
public class Catalog {
@PersistenceContext EntityManager em;
List<Product
> products;
@Produces @Catalog
List<Product
> getCatalog() {
if (products==null) {
products = em.createQuery("select p from Product p where p.deleted = false")
.getResultList();
}
return products;
}
}
一个 Product
时常会被创建或者删除。当这个动作发生后,我们需要刷新 Product
分类。但是我们需要在事务完全成功之后才能处理刷新。
创建或者删除 Product
的Web Bean应该触发事件,例如:
@Stateless
public class ProductManager {
@PersistenceContext EntityManager em;
@Observable Event<Product
> productEvent;
public void delete(Product product) {
em.delete(product);
productEvent.fire(product, new AnnotationLiteral<Deleted
>(){});
}
public void persist(Product product) {
em.persist(product);
productEvent.fire(product, new AnnotationLiteral<Created
>(){});
}
...
}
现在 Catalog
能够观察到事务成功完成之后触发的事件:
@ApplicationScoped @Singleton
public class Catalog {
...
void addProduct(@AfterTransactionSuccess @Observes @Created Product product) {
products.add(product);
}
void addProduct(@AfterTransactionSuccess @Observes @Deleted Product product) {
products.remove(product);
}
}
Web Bean的第二个宗旨是强类型。Web Bean有关依赖,拦截器和装饰器的信息以及事件生产者对应的事件消费者的信息都包含在类型安全的Java构件中,可以被编译器验证。
在Web Bean代码中,你不会看到基于字符串的标识符。这不是因为这个框架使用智能默认规则 也叫"按惯例配置" 将它们隐藏起来,这是因为从开始就没有任何字符串!
这种方法的显而易见的好处是任何IDE都可以提供自动完成,验证和重构而无需任何特殊工具。除此之外,还有第二个,不是那么明显的好处。它能够让你考虑识别对象,事件或者拦截器的时候使用注释,而不是名字,这样你将可以提升你的代码的语义水平。
Web Beans能够鼓励你为概念建模,开发注释。例如:
@Asynchronous
,
@Mock
,
@Secure
or
@Updated
,
而不是使用复合名字,像:
asyncPaymentProcessor
,
mockPaymentProcessor
,
SecurityInterceptor
or
DocumentUpdatedEvent
.
注释可以重用。它们能够描述系统不同部分的通用特征。它们能够帮助我们分类和理解我们的代码。它们能够以通用的方式为我们处理共同的关注点。它们能够让我们的代码具有更高的可读性,更加容易被理解。
Web Beans的模板是这种思想的延伸。一个模板为我们应用体系中的一个通用的角色建模。它将一个角色的各种不同的属性,包括范围,拦截器绑定,部署类型,等等封装到一个单一的可重用的包中。
甚至Web Bean的XML元数据也是强类型的!对于XML没有编译器,Web Bean充分利用了XML模式去验证XML中出现的Java类型和属性。这种方式让XML更加具有可读性,就像注释让我们的Java代码更加具有可读性一样。
我们现在可以接触Web Bean中更加高级的特性了。要记住这些特性能够让我们的代码不仅更容易验证,也更容易理解。大部分时间,你根本不需要这些特性,但是如果你能聪明地运用它们的话,你将充分体会到它们的强大。
根据Web Bean规范:
在很多系统中,体系模式的使用会产生一套重复发生的Web Bean角色。一个模板允许一个框架开发者来识别这样的一个角色,并且在一个中心地方为拥有这个角色的Web Bean声明一些通用的元数据。
一个模板封装了下面的任何组合:
一个默认的部署类型,
一个默认的范围类型,
一个对于Web Bean范围的限制
一个Web Bean实现或者继承一个特定类型的需求,以及
一套拦截器绑定注释。
一个模板也可以指定所有使用这个模板的Web Bean拥有的默认的Web Bean名称。
一个Web Bean可以声明零个,一个或者多个模板。
一个模板其实就是一个Java注释类型。这个模板在一些MVC框架中识别动作类:
@Retention(RUNTIME)
@Target(TYPE)
@Stereotype
public @interface Action {}
我们注释来让Web Bean应用模板
@Action
public class LoginAction { ... }
一个模板可以为使用该模板的Web Bean指定一个默认的范围或者默认的部署类型。例如,如果@WebTier
部署类型识别那些只应该在系统作为一个Web应用运行时才应该部署的Web Bean,我们可以为动作类指定下面的默认值:
@Retention(RUNTIME)
@Target(TYPE)
@RequestScoped
@WebTier
@Stereotype
public @interface Action {}
当然,如果必要的话,一个特殊的动作类仍旧可以重载这些默认值:
@Dependent @Mock @Action
public class MockLoginAction { ... }
如果我们想要敬爱那个所有动作类强制在某个特殊的范围,我们也可以这样做。
假定我们希望那个通过特定的范围声明来阻止一个动作。Web Bean可以让我们显式地为使用特定模板的Web Bean指定一套允许的范围。例如:
@Retention(RUNTIME)
@Target(TYPE)
@RequestScoped
@WebTier
@Stereotype(supportedScopes=RequestScoped.class)
public @interface Action {}
如果一个特殊的动作类试图指定超越Web Bean请求范围的范围,Web Bean管理器在初始化时就会抛出一个异常。
我们也可以强制所有的使用特定模板的Web Bean实现一个接口或者继承一个类:
@Retention(RUNTIME)
@Target(TYPE)
@RequestScoped
@WebTier
@Stereotype(requiredTypes=AbstractAction.class)
public @interface Action {}
如果一个特殊的动作类没有继承 AbstractAction
,Web Bean管理器会在初始化时抛出一个异常。
一个模板指定一套拦截器绑定让使用该模板的Web Bean继承。
@Retention(RUNTIME)
@Target(TYPE)
@RequestScoped
@Transactional(requiresNew=true)
@Secure
@WebTier
@Stereotype
public @interface Action {}
这将能够帮助我们从业务代码中剥离技术关注点!
最后,我们可以为所有使用模板的Web Bean指定一个Web Bean名称,Web Bean管理器将其设为默认名称。JSP页面中常常引用动作类,所以它们是这个特性的最好的例子。我们所需做的就是添加一个空的 @Named
注释:
@Retention(RUNTIME)
@Target(TYPE)
@RequestScoped
@Transactional(requiresNew=true)
@Secure
@Named
@WebTier
@Stereotype
public @interface Action {}
现在, LoginAction
将拥有一个名为 loginAction
Web Bean名称.
我们已经看到Web Bean的依赖注入模型如何让我们在部署时期 重载 一个API实现。例如,下面的企业级Web Bean在生产环境下使提供一个PaymentProcessor
接口的实现:
@CreditCard @Stateless
public class CreditCardPaymentProcessor
implements PaymentProcessor {
...
}
但在我们的阶段开发环境中,我们可以使用一个不同的Web Bean来重载这个PaymentProcessor
实现:
@CreditCard @Stateless @Staging
public class StagingCreditCardPaymentProcessor
implements PaymentProcessor {
...
}
我们试图做的是在特定的系统部署环境中使用StagingCreditCardPaymentProcessor
来完全替代AsyncPaymentProcessor
。在这个部署中,部署类型为@Staging
的将有比默认部署类型@Production
更高的优先级,因此下面注入点的客户:
@CreditCard PaymentProcessor ccpp
将会收到一个 StagingCreditCardPaymentProcessor
的实例。
不幸的是,我们很容易掉入几个陷阱:
更高优先级的Web Bean可能没有实现其要重载的所有的API类型,
更高优先级的Web Bean可能没有声明其要重载的所有绑定类型,
更高优先级的Web Bean可能没有和其要重载的Web Bean拥有相同的名字,或者
其要重载的Web Bean有可能声明了一个生产者方法,清除方法或者观察者方法。
在上面的任何一种情况,我们试图重载的Web Bean仍旧可以在运行时被调用。因此重载对于开发者来说是很容易出错的。
Web Bean提供了一个特殊的特性,称之为特化,能够帮助开发者避免这些陷阱。特化初看起来有些神秘,但是在实际应用中非常容易使用,你很快就会感激它提供的特殊的安全性。
特化是为简单和企业级Web Bean提供的特性。要使用特化,更高优先级的Web Bean必须:
是其重载的Web Bean的直接子类,并且
如果其重载的是简单的Web Bean, 它也要是一个简单的Web Bean;如果其重载的是企业级的Web Bean, 它也要是一个企业级的Web Bean,并且
需要使用 @Specializes
注释。
@Stateless @Staging @Specializes
public class StagingCreditCardPaymentProcessor
extends CreditCardPaymentProcessor {
...
}
我们称更高优先级的Web Bean特化了它的超类。
当使用特化时:
超类的绑定类型自动被使用@Specializes
注释的Web Bean继承,并且
超类的Web Bean名称自动被使用@Specializes
注释的Web Bean继承,并且
超类声明的生产者方法,清除方法和观察者方法将在使用@Specializes
注释的Web Bean实例上调用。
在我们这个例子中,CreditCardPaymentProcessor
的绑定类型 @CreditCard
被StagingCreditCardPaymentProcessor
继承。
进一步,Web Bean管理器将验证:
超类的所有API类型是使用 @Specializes
注释的Web Bean的API类型(所有企业级Bean超类的本地接口是子类的本地接口)
使用 @Specializes
注释的Web Bean的部署类型比超类的部署类型拥有更高的优先级,并且
没有其他特化该超类的Web Bean被激活。
如果任何一条有冲突的话,Web Bean管理器将在初始化时抛出一个异常。
因此,我们可以确定当使用@Specializes
注释的Web Bean被部署和激活时,超类在系统的任何部署中都不会被调用。
目前为止,我们看了大量使用注释来声明Web Bean的例子。然而,我们有时候并不使用注释来定义Web Bean,如下列情况:
当实现类是从先前已经存在的类库中产生的时候,或者
多个Web Bean对应同一个实现类的时候。
在上述任何一种情况下,Web Bean给我们两种选择:
写一个生产者方法,或者
使用XML来声明Web Bean。
很多框架使用XML来提供Java类相关的元数据。然而,Web Bean使用了和大多其他框架不同的方法来指定Java类的名字,域或者方法。Web Bean让你使用类或者成员名作为XML元素名,而不是将类和成员名作为XML元素的字符串值来声明Web Bean。
这种方法的好处是你可以使用XML模式来验证XML, 阻止XML文档中的拼写错误。它甚至可以让一个工具从编译好的Java代码中自动生成XML模式。或者一个整合开发环境(IDE)可以直接进行验证,无需使用显式的中间生成步骤。
对于每个Java包,Web Bean定义了一个对应的XML名域空间。这个名域由 urn:java:
前缀加上Java包名组成。对于 com.mydomain.myapp
包来说,对应的XML名域是 urn:java:com.mydomain.myapp
。
属于一个包的Java类型指的是在对应这个包的名域中使用一个XML元素。元素的名字就是Java类型的名字。类型的域和方法通过相同的名域下的子元素定义。如果类型是一个注释的话,其成员通过这个元素的属性指定。
例如, 在下面的XML片段中的元素<util:Date/>
指的是 java.util.Date
类:
<WebBeans xmlns="urn:java:javax.webbeans"
xmlns:util="urn:java:java.util">
<util:Date/>
</WebBeans
>
这个是将Date
声明为一个简单web Bean所需的所有代码!现在任何一个 Date
实例都可以被任何其他Web Bean注入了:
@Current Date date
我们可以直接通过Web Bean声明的子元素来声明范围,部署类型和拦截器绑定类型:
<myapp:ShoppingCart>
<SessionScoped/>
<myfwk:Transactional requiresNew="true"/>
<myfwk:Secure/>
</myapp:ShoppingCart
>
我们可以使用相同方法来指定名字和绑定类型:
<util:Date>
<Named
>currentTime</Named>
</util:Date>
<util:Date>
<SessionScoped/>
<myapp:Login/>
<Named
>loginTime</Named>
</util:Date>
<util:Date>
<ApplicationScoped/>
<myapp:SystemStart/>
<Named
>systemStartTime</Named>
</util:Date
>
@Login
和 @SystemStart
是绑定注释类型。
@Current Date currentTime;
@Login Date loginTime;
@SystemStart Date systemStartTime;
通常,一个Web Bean可以支持多个绑定类型:
<myapp:AsynchronousChequePaymentProcessor>
<myapp:PayByCheque/>
<myapp:Asynchronous/>
</myapp:AsynchronousChequePaymentProcessor
>
拦截器和装饰器只不过是简单的Web Beans,所以它们可以像其他简单Web Bean一样被声明:
<myfwk:TransactionInterceptor>
<Interceptor/>
<myfwk:Transactional/>
</myfwk:TransactionInterceptor
>
Web Beans让我们能够在一个注入点定义一个Web Bean。例如:
<myapp:System>
<ApplicationScoped/>
<myapp:admin>
<myapp:Name>
<myapp:firstname
>Gavin</myapp:firstname>
<myapp:lastname
>King</myapp:lastname>
<myapp:email
>gavin@hibernate.org</myapp:email>
</myapp:Name>
</myapp:admin>
</myapp:System
>
<Name>
元素声明了一个范围为 @Dependent
,类为 Name
的一个简单的Web Bean,并且设置了一套初始的域值。这个Web Bean有一个特殊的,容器生成的绑定,因此只能在它声明的特定注入点被注入。
这个简单但是很强大的特性能够让我们使用Web Bean XML配置格式来指定整个Java类的图。这并不是完整的数据绑定方案,但是它很接近了!
如果我们希望我们的XML文档格式由非Java开发者或者没有权限访问我们代码的人来制定,我们需要提供一个模式。在Web Beans中使用模式没有什么特殊的地方。
<WebBeans xmlns="urn:java:javax.webbeans"
xmlns:myapp="urn:java:com.mydomain.myapp"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:java:javax.webbeans http://java.sun.com/jee/web-beans-1.0.xsd
urn:java:com.mydomain.myapp http://mydomain.com/xsd/myapp-1.2.xsd">
<myapp:System>
...
</myapp:System>
</WebBeans
>
编写一个XML模式相当繁琐。因此,Web Bean的参考实现项目提供了一个工具,可以从编译好的Java代码中自动化生成XML模式。
Web Bean的第三个宗旨是整合。Web Bean设计思想就是能够和其他的技术相互合作,能够让开发者更容易地将其他技术结合使用。Web Bean是一个开发的技术。它形成了Java EE生态系统的一部分,而它本身也是一个新的生态系统的基石,这个新的生态系统能够为已有的框架和技术提供一个更便携的扩展和整合机制。
我们已经看到了Web Bean如何帮助整合EJB和JSF, 允许我们将EJB直接绑定到JSF页面上。而这只是开始,Web Bean为其他技术提供了同样的潜在能力,例如业务流程管理引擎,其他的Web框架,第三方组件模型。Java EE平台永远不可能将Java应用开发世界中所有有意思的技术都标准化。但是Web Bean能够提供一个便捷的方式将这些尚未成为平台一部分的技术更加容易并且无缝地集成到Java EE环境中。
我们已经看到了如何在应用中使用Web Bean来充分利用Java EE平台。我们也粗略地看到了一些能够支持Web Bean的便携性扩展点的SPI。你也许永远不回直接使用这些SPI,但是,知道在需要的时候你可以使用它们很有好处。最重要的是,每次你使用一个第三方扩展的时候,都有可能间接地利用了它们。
Web Beans被完全地整合进了Java EE环境里。Web Beans 能够访问Java EE资源和JPA持久化上下文。它们可以在JSF和JSP页面中的统一表达式语言使用。它们甚至可以被注入到一些对象中,例如Servlet和消息驱动Bean,而这些对象都不是Web Bean。
所有简单的和企业的Web Beans都可以通过@Resource
, @EJB
和@PersistenceContext
来使用Java EE的依赖注入。我们已经看过很多例子了,虽然我们没有在这上面耗费精力:
@Transactional @Interceptor
public class TransactionInterceptor {
@Resource Transaction transaction;
@AroundInvoke public Object manageTransaction(InvocationContext ctx) { ... }
}
@SessionScoped
public class Login {
@Current Credentials credentials;
@PersistenceContext EntityManager userDatabase;
...
}
所有的简单的和企业的Web Beans都支持Java EE的@PostConstruct
和@PreDestroy
回调。@PostConstruct
方法在所有的 注入完成后被调用。
这里需要注意的一个限制是:简单的Web Beans不支持 @PersistenceContext(type=EXTENDED)
在Java EE 6中,从一个Servlet中调用一个Web Bean非常容易,只须使用Web Beans域注入一个Web Bean或者将初始方法注入即可。
public class Login extends HttpServlet {
@Current Credentials credentials;
@Current Login login;
@Override
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
credentials.setUsername( request.getAttribute("username") ):
credentials.setPassword( request.getAttribute("password") ):
login.login();
if ( login.isLoggedIn() ) {
response.sendRedirect("/home.jsp");
}
else {
response.sendRedirect("/loginError.jsp");
}
}
}
Web Beans客户端代理将Servlet中的方法调用路由到当前请求和HTTP会话中相应的Credentials
和 Login
实例。
Web Beans注入可以应用于所有的EJB,甚至这些EJB不在Web Bean管理器的控制之下也可以(如果它们可以直接从JNDI查找或者使用@EJB
注入)。消息驱动Bean不能成为Web Beans,因为你无法注入它们,但你仍然可以将Web Beans注入到消息驱动Bean中。
你甚至可以将Web Beans拦截器绑定到消息驱动Bean上。
@Transactional @MessageDriven
public class ProcessOrder implements MessageListener {
@Current Inventory inventory;
@PersistenceContext EntityManager em;
public void onMessage(Message message) {
...
}
}
这样,在一个Web Beans环境中接受消息就相当简单了。但是需要注意的是,当一个消息发送到一个消息驱动Bean的时候,此时没有会话,我们无法获得对话上下文。我们只能获得 @RequestScoped
和 @ApplicationScoped
注释的Web Beans。
使用Web Beans发送消息也相当简单。
使用JMS发送消息有点复杂,因为你需要处理很多不同的对象。对于队列(Queue),我们有Queue
, QueueConnectionFactory
, QueueConnection
, QueueSession
和 QueueSender
。对于主题(Topic)有Topic
, TopicConnectionFactory
, TopicConnection
, TopicSession
和 TopicPublisher
。这些对象每个都有自己的生命周期和线程模型,我们都需要考虑这些问题。
Web Beans为我们处理所有这些问题。我们只须在 web-beans.xml
声明队列或者主题,指定一个关联的绑定类型和连接工厂即可。
<Queue>
<destination
>java:comp/env/jms/OrderQueue</destination>
<connectionFactory
>java:comp/env/jms/QueueConnectionFactory</connectionFactory>
<myapp:OrderProcessor/>
</Queue
>
<Topic>
<destination
>java:comp/env/jms/StockPrices</destination>
<connectionFactory
>java:comp/env/jms/TopicConnectionFactory</connectionFactory>
<myapp:StockPrices/>
</Topic
>
现在,对于队列,我们只须注入 Queue
, QueueConnection
, QueueSession
或者 QueueSender
;对于主题,我们只须注入Topic
, TopicConnection
, TopicSession
或者 TopicPublisher
。
@OrderProcessor QueueSender orderSender;
@OrderProcessor QueueSession orderSession;
public void sendMessage() {
MapMessage msg = orderSession.createMapMessage();
...
orderSender.send(msg);
}
@StockPrices TopicPublisher pricePublisher;
@StockPrices TopicSession priceSession;
public void sendMessage(String price) {
pricePublisher.send( priceSession.createTextMessage(price) );
}
被注入的JMS对象的生命周期完全由Web Bean管理器控制。
Web Beans意图成为框架的平台,扩展的平台,和能够与其他技术整合的平台。因此,Web Bean将一套SPI暴露给开发者,从而能够让他们便于扩展Web Bean。例如,下面是Web Bean设计者设想的扩展类型:
整合业务流程管理引擎,
整合第三方框架,例如Spring, Seam, GWT或者Wicket,和
基于Web Bean编程模型的新技术。
扩展Web Bean的神经中枢是 Manager
对象。
Manager
接口能够让我们通过编程来注册和获得Web Bean,拦截器,装饰器,观察者和上下文。
public interface Manager
{
public <T
> Set<Bean<T
>
> resolveByType(Class<T
> type, Annotation... bindings);
public <T
> Set<Bean<T
>
> resolveByType(TypeLiteral<T
> apiType,
Annotation... bindings);
public <T
> T getInstanceByType(Class<T
> type, Annotation... bindings);
public <T
> T getInstanceByType(TypeLiteral<T
> type,
Annotation... bindings);
public Set<Bean<?>
> resolveByName(String name);
public Object getInstanceByName(String name);
public <T
> T getInstance(Bean<T
> bean);
public void fireEvent(Object event, Annotation... bindings);
public Context getContext(Class<? extends Annotation
> scopeType);
public Manager addContext(Context context);
public Manager addBean(Bean<?> bean);
public Manager addInterceptor(Interceptor interceptor);
public Manager addDecorator(Decorator decorator);
public <T
> Manager addObserver(Observer<T
> observer, Class<T
> eventType,
Annotation... bindings);
public <T
> Manager addObserver(Observer<T
> observer, TypeLiteral<T
> eventType,
Annotation... bindings);
public <T
> Manager removeObserver(Observer<T
> observer, Class<T
> eventType,
Annotation... bindings);
public <T
> Manager removeObserver(Observer<T
> observer,
TypeLiteral<T
> eventType, Annotation... bindings);
public <T
> Set<Observer<T
>
> resolveObservers(T event, Annotation... bindings);
public List<Interceptor
> resolveInterceptors(InterceptionType type,
Annotation... interceptorBindings);
public List<Decorator
> resolveDecorators(Set<Class<?>
> types,
Annotation... bindings);
}
我们可以通过注入来获得一个 Manager
实例:
@Current Manager manager
抽象类 Bean
的实例代表Web Bean。应用中的每个Web Bean都有一个在 Manager
注册的 Bean
实例。
public abstract class Bean<T> {
private final Manager manager;
protected Bean(Manager manager) {
this.manager=manager;
}
protected Manager getManager() {
return manager;
}
public abstract Set<Class> getTypes();
public abstract Set<Annotation> getBindingTypes();
public abstract Class<? extends Annotation> getScopeType();
public abstract Class<? extends Annotation> getDeploymentType();
public abstract String getName();
public abstract boolean isSerializable();
public abstract boolean isNullable();
public abstract T create();
public abstract void destroy(T instance);
}
我们可以继承 Bean
类,通过调用 Manager.addBean()
方法注册实例,从而提供超出Web Bean规范之外(简单Web Bean, 企业WebBean, 生产者方法和JMS端点)的新的Web Bean类型。例如,我们可以使用 Bean
类来允许其他框架管理的对象能够注入到Web Bean中。
Web Bean规范定义了两种 Bean
的子类:拦截器
和 装饰器
。
因为Web Bean是新生事物,所以网上可以获得的有关信息很少。
当然,Web Bean规范是有关Web Bean信息的最佳来源。这个规范大约有100页,只是这个文章的两倍长,并且可读性很好。它覆盖了我们这里跳过的很多细节。这个规范可以从http://jcp.org/en/jsr/detail?id=299
下载。
Web Bean参考实现正在http://seamframework.org/WebBeans
项目中开发。参考实现的开发团队和Web Bean规范领导的博客位于http://in.relation.to
。本篇文章的撰写完全基于先前发表在那里的一系列博客文章。
Web Beans is the reference implementation of JSR-299, and is used by JBoss AS and Glassfish to provide JSR-299 services for Java Enterprise Edition applications. Web Beans also goes beyond the environments and APIs defined by the JSR-299 specification and provides support for a number of other environments (such as a servlet container such as Tomcat, or Java SE) and additional APIs and modules (such as logging, XSD generation for the JSR-299 XML deployment descriptors).
If you want to get started quickly using Web Beans with JBoss AS or Tomcat and experiment with one of the examples, take a look at 第 3 章 使用Web Beans的参考实现. Otherwise read on for a exhaustive discussion of using Web Beans in all the environments and application servers it supports, as well the Web Beans extensions.
No special configuration of your application, beyond adding either META-INF/beans.xml
or WEB-INF/beans.xml
is needed.
If you are using JBoss AS 5.0.1.GA then you'll need to install Web Beans as an extra. First we need to tell Web Beans where JBoss is located. Edit jboss-as/build.properties
and set the jboss.home
property. For example:
jboss.home=/Applications/jboss-5.0.1.GA
Now we can install Web Beans:
$ cd webbeans-$VERSION/jboss-as $ ant update
A new deployer, webbeans.deployer
is added to JBoss AS. This adds supports for JSR-299 deployments to JBoss AS, and allows Web Beans to query the EJB3 container and discover which EJBs are installed in your application.
Web Beans is built into all releases of JBoss AS from 5.1 onwards.
Web Beans can be used in Tomcat 6.0.
Web Beans doesn't support deploying session beans, injection using @EJB
, or @PersistenceContext
or using transactional events on Tomcat.
Web Beans should be used as a web application library in Tomcat. You should place webbeans-tomcat.jar
in WEB-INF/lib
. webbeans-tomcat.jar
is an "uber-jar" provided for your convenience. Instead, you could use its component jars:
jsr299-api.jar
webbeans-api.jar
webbeans-spi.jar
webbeans-core.jar
webbeans-logging.jar
webbeans-tomcat-int.jar
javassist.jar
dom4j.jar
You also need to explicitly specify the Tomcat servlet listener (used to boot Web Beans, and control its interaction with requests) in web.xml
:
<listener> <listener-class >org.jboss.webbeans.environment.servlet.Listener</listener-class> </listener >
Tomcat has a read-only JNDI, so Web Beans can't automatically bind the Manager. To bind the Manager into JNDI, you should add the following to your META-INF/context.xml
:
<Resource name="app/Manager" auth="Container" type="javax.inject.manager.Manager" factory="org.jboss.webbeans.resources.ManagerObjectFactory"/>
and make it available to your deployment by adding this to web.xml
:
<resource-env-ref> <resource-env-ref-name> app/Manager </resource-env-ref-name> <resource-env-ref-type> javax.inject.manager.Manager </resource-env-ref-type> </resource-env-ref>
Tomcat only allows you to bind entries to java:comp/env
, so the Manager will be available at java:comp/env/app/Manager
Web Beans also supports Servlet injection in Tomcat. To enable this, place the webbeans-tomcat-support.jar
in $TOMCAT_HOME/lib
, and add the following to your META-INF/context.xml
:
<Listener className="org.jboss.webbeans.environment.tomcat.WebBeansLifecycleListener" />
Apart from improved integration of the Enterprise Java stack, Web Beans also provides a state of the art typesafe, stateful dependency injection framework. This is useful in a wide range of application types, enterprise or otherwise. To facilitate this, Web Beans provides a simple means for executing in the Java Standard Edition environment independently of any Enterprise Edition features.
When executing in the SE environment the following features of Web Beans are available:
Simple Web Beans (POJOs)
Typesafe Dependency Injection
Application and Dependent Contexts
Binding Types
Stereotypes
Decorators
(TODO: Interceptors ?)
Typesafe Event Model
To make life easy for developers Web Beans provides a special module with a main method which will boot the Web Beans manager, automatically registering all simple Web Beans found on the classpath. This eliminates the need for application developers to write any bootstrapping code. The entry point for a Web Beans SE applications is a simple Web Bean which observes the standard @Deployed Manager
event. The command line paramters can be injected using either of the following:
@Parameters List<String
> params;
@Parameters String[] paramsArray; // useful for compatability with existing classes
Here's an example of a simple Web Beans SE application:
@ApplicationScoped
public class HelloWorld
{
@Parameters List<String
> parameters;
public void printHello( @Observes @Deployed Manager manager )
{
System.out.println( "Hello " + parameters.get(0) );
}
}
Web Beans SE applications are started by running the following main method.
java org.jboss.webbeans.environments.se.StartMain <args
>
If you need to do any custom initialization of the Web Beans manager, for example registering custom contexts or initializing resources for your beans you can do so in response to the @Initialized Manager
event. The following example registers a custom context:
public class PerformSetup
{
public void setup( @Observes @Initialized Manager manager )
{
manager.addContext( ThreadContext.INSTANCE );
}
}
The command line parameters do not become available for injection until the @Deployed Manager
event is fired. If you need access to the parameters during initialization you can do so via the public static String getParameters()
method in StartMain
.
These modules are usable on any JSR-299 implementation, not just Web Beans!
目前,Web Bean的参考实现只能运行在JBoss AS5中;将参考实现整合到其他EE环境中(例如像Glassfish的其他的应用服务器)以及一个Servlet容器(像Tomcat)中或者一个内嵌的EJB3.1实现中相当容易。在附录中我们将简要的讨论所需的步骤。
Web Bean可以在SE环境中运行,但是你需要做更多的工作,添加你自己的上下文和生命周期。Web Bean参考实现目前没有暴露生命周期扩展点,所以你不得不直接编写Web Bean参考实现的类。
The Web Beans SPI is located in webbeans-spi
module, and packaged as webbeans-spi.jar
. Some SPIs are optional, if you need to override the default behavior, others are required.
所有的SPI接口都支持装饰器模式,并且提供一个 Forwarding
类。
public interface WebBeanDiscovery {
/**
* Gets list of all classes in classpath archives with web-beans.xml files
*
* @return An iterable over the classes
*/
public Iterable<Class<?>
> discoverWebBeanClasses();
/**
* Gets a list of all web-beans.xml files in the app classpath
*
* @return An iterable over the web-beans.xml files
*/
public Iterable<URL
> discoverWebBeansXml();
}
Web Bean类的发现和 web-bean.xml
配置文件的都是自解释性的(算法在JSR-299规范中已经描述了,这里就不再重复)
Web Bean参考实现也将EJB3 Bean的发现委托给容器,以便它不用再扫描EJB3注释和解析 ejb-jar.xml
文件。对于应用中的每个EJB都应发现一个EJBDescriptor:
public interface EjbServices
{
/**
* Gets a descriptor for each EJB in the application
*
* @return The bean class to descriptor map
*/
public Iterable<EjbDescriptor<?>> discoverEjbs();
public interface EjbDescriptor<T
> {
/**
* Gets the EJB type
*
* @return The EJB Bean class
*/
public Class<T
> getType();
/**
* Gets the local business interfaces of the EJB
*
* @return An iterator over the local business interfaces
*/
public Iterable<BusinessInterfaceDescriptor<?>
> getLocalBusinessInterfaces();
/**
* Gets the remote business interfaces of the EJB
*
* @return An iterator over the remote business interfaces
*/
public Iterable<BusinessInterfaceDescriptor<?>
> getRemoteBusinessInterfaces();
/**
* Get the remove methods of the EJB
*
* @return An iterator over the remove methods
*/
public Iterable<Method
> getRemoveMethods();
/**
* Indicates if the bean is stateless
*
* @return True if stateless, false otherwise
*/
public boolean isStateless();
/**
* Indicates if the bean is a EJB 3.1 Singleton
*
* @return True if the bean is a singleton, false otherwise
*/
public boolean isSingleton();
/**
* Indicates if the EJB is stateful
*
* @return True if the bean is stateful, false otherwise
*/
public boolean isStateful();
/**
* Indicates if the EJB is and MDB
*
* @return True if the bean is an MDB, false otherwise
*/
public boolean isMessageDriven();
/**
* Gets the EJB name
*
* @return The name
*/
public String getEjbName();
}
EjbDescriptor
具有相当的自解释性,应该返回EJB规范中定义的相关元数据。除了这两个接口,还有一个表示本地业务接口的 BusinessInterfaceDescriptor
(封装了接口类和用于查询EJB实例的jndi名字)
The resolution of @EJB
and @Resource
is delegated to the container. You must provide an implementation of org.jboss.webbeans.ejb.spi.EjbServices
which provides these operations. Web Beans passes in the javax.inject.manager.InjectionPoint
the resolution is for, as well as the NamingContext
in use for each resolution request.
Just as resolution of @EJB
is delegated to the container, so is resolution of @PersistenceContext
.
OPEN ISSUE: Web Beans also requires the container to provide a list of entities in the deployment, so that they aren't discovered as simple beans.
Web Bean RI必须将JTA活动委托给容器。SPI提供者提供一些钩子(hooks)结合TransactionServices
接口来轻松完成这个任务。
public interface TransactionServices
{
/**
* Possible status conditions for a transaction. This can be used by SPI
* providers to keep track for which status an observer is used.
*/
public static enum Status
{
ALL, SUCCESS, FAILURE
}
/**
* Registers a synchronization object with the currently executing
* transaction.
*
* @see javax.transaction.Synchronization
* @param synchronizedObserver
*/
public void registerSynchronization(Synchronization synchronizedObserver);
/**
* Queries the status of the current execution to see if a transaction is
* currently active.
*
* @return true if a transaction is active
*/
public boolean isTransactionActive();
}
枚举 Status
对实现者来说是一个很方便的工具,可以追踪在事务成功或者失败或者无论事务成功还是失败的情况下,是否应该将一个同步通知给观察者。
任何 javax.transaction.Synchronization
实现必须传递给 registerSynchronization()
方法,SPI实现应该立刻将同步注册到EJB使用的JTA事务管理器。
为了更容易的决定对于请求线程来说一个事务在当前是否是活动的,我们可以使用isTransactionActive()
方法。SPI实现应该查询EJB使用的同一个JTA事务管理器。
Web Bean期望应用服务器或者其他容器能够提供每个应用上下文的存储。org.jboss.webbeans.context.api.BeanStore
应该被实现以便提供一个应用范围的存储。你也许会发现 org.jboss.webbeans.context.api.helpers.ConcurrentHashMapBeanStore
非常有用。
org.jboss.webbeans.bootstrap.api.Bootstrap
接口定义了Web Bean的自举机制。为了启动Web Beans,你必须获得一个org.jboss.webbeans.bootstrap.WebBeansBootstrap
实例(它实现了Boostrap
) ,告诉它使用的SPI,然后请求容器启动。
The bootstrap is split into phases, bootstrap initialization and boot and shutdown. Initialization will create a manager, and add the standard (specification defined) contexts. Bootstrap will discover EJBs, classes and XML; add beans defined using annotations; add beans defined using XML; and validate all beans.
The bootstrap supports multiple environments. Different environments require different services to be present (for example servlet doesn't require transaction, EJB or JPA services). By default an EE environment is assumed, but you can adjust the environment by calling bootstrap.setEnvironment()
.
To initialize the bootstrap you call Bootstrap.initialize()
. Before calling initialize()
, you must register any services required by your environment. You can do this by calling bootstrap.getServices().add(JpaServices.class, new MyJpaServices())
. You must also provide the application context bean store.
调用initialize()
后,我们能够通过Bootstrap.getManager()
来获得管理器
。
To boot the container you call Bootstrap.boot()
.
要关闭容器,你需要调用 Bootstrap.shutdown()
。这将让容器执行一些必要的清洁工作。
Web Beans 参考实现实现了JNDI 的绑定和根据标准来查询,然而你可能想要改变这些绑定和查询(例如在一个没有JNDI的环境中)。你可以通过实现 org.jboss.webbeans.resources.spi.NamingContext
来完成这个工作:
public interface NamingContext extends Serializable {
/**
* Typed JNDI lookup
*
* @param <T
> The type
* @param name The JNDI name
* @param expectedType The expected type
* @return The object
*/
public <T
> T lookup(String name, Class<? extends T
> expectedType);
/**
* Binds an item to JNDI
*
* @param name The key to bind under
* @param value The item to bind
*/
public void bind(String name, Object value);
}
Web Beans参考实现需要在不同时间从类路径上加载类和资源。默认情况下,它们使用加载参考实现的类加载器加载,但是这在一些环境中可能是不适合的。如果出现这种情况的话,你可以实现org.jboss.webbeans.spi.ResourceLoader
:
public interface ResourceLoader {
/**
* Creates a class from a given FQCN
*
* @param name The name of the clsas
* @return The class
*/
public Class<?> classForName(String name);
/**
* Gets a resource as a URL by name
*
* @param name The name of the resource
* @return An URL to the resource
*/
public URL getResource(String name);
/**
* Gets resources as URLs by name
*
* @param name The name of the resource
* @return An iterable reference to the URLS
*/
public Iterable<URL
> getResources(String name);
}
为了接口实现之外的正确的功能,Web Beans参考实现对容器有大量的要求。
如果你将一个Web Bean参考实现整合到一个支持多应用部署的环境中,你需要以用户配置或者自动化方式为每个Web Bean应用激活类加载器隔离。
如果你将Web Bean整合到一个Servlet环境中,对每个使用Servlet的 Web Bean应用,你需要以用户配置或者自动化形式将 org.jboss.webbeans.servlet.WebBeansListener
注册为一个Servlet监听器。
如果你将Web Bean整合到一个JSF环境中,对每个使用JSF的 Web Bean应用,你需要以用户配置或者自动化形式将 org.jboss.webbeans.servlet.ConversationPropagationFilter
注册为一个Servlet监听器。这个过滤器可以安全地向所有Servlet部署注册。
如果你将Web Beans整合到EJB环境中,对每个使用企业级Bean的Web Bean应用,你需要以用户配置或者自动化形式将 org.jboss.webbeans.ejb.SessionBeanInterceptor
注册为应用中所有EJB的EJB拦截器。
你必须为所有的EJB在栈中将SessionBeanInterceptor
注册为一个最内部的拦截器。
webbeans-core.jar
If you are integrating the Web Beans into an environment that supports deployment of applications, you must insert the webbeans-core.jar
into the applications isolated classloader. It cannot be loaded from a shared classloader.