通过 CDI 和 EJB 3.1 按需使用接口

作者:Adam Bien

Java EE 6 不再要求接口,因此您可以更有意识地使用接口来实现业务逻辑。

2012 年 1 月发布

简介

从 Java Platform, Enterprise Edition 6 (Java EE 6) 开始,容器不再要求接口即可实现常见用例。普通类无需任何接口也可以提供事务、安全、自定义方面、并发和监视。Java EE 6 使接口重新变得有意义。由于平台不再要求接口,您可以更有意识地使用接口来实现业务逻辑。接口成为封装或抽象的载体,正如其初衷所在:

接口用于对各种类型的类所共享的相似之处进行编码,但与这些类之间不必构成类关系。例如,鹦鹉都能吹口哨;但没有道理将 Human Parrot 表示为 Whistler 类的子类。反而它们最有可能是 Animal 类的子类(可能带有中间类),但两者均可实现 Whistler 接口。”[http://en.wikipedia.org/wiki/Java_interface]

遗憾的是,在 Java EE 上下文中,接口并不用作对相似之处进行编码的工具;而是用于确保将来可能但未必实现的增强。对接口的广泛使用出自这样的信念:它们在将来可能有用。

不成熟的可扩展性埋下祸根

理论上来讲,可以通过接口让所有一切具有可扩展性。如果您对所有一切均使用接口,就可以更换实现而不用重新编译客户端。但是,这种可扩展性是有代价的。构件的数量会加倍,而且必须引入配置工具。

配置的目的是管理接口与其实现之间的关联。在配置中,至少必须维护该接口的独有名称以及接口实现的名称,这会导致重复。接口中存储的信息与类中的信息完全相同。每次对类或接口的完全限定名称的更改必须以不可分割的方式来执行。必须同时更改源文件和配置。

当您无法确定名称时

当引入接口的唯一动机不是抽象已有实现而是为了将来的扩展时,就会遇到接口与其实现之间的命名冲突。如此,您将不能按照接口及其实现的职责来对其进行命名。对于此问题,通常的解决方法是为接口添加“I”前缀,或者为实现添加 Impl 后缀。例如,您可以看到类似以下的代码:Communicator communicator = new CommunicatorImpl();。

这样的命名根本无助于您识别职责。它强调的无非是已知的事实:类是在实现关键字之后声明的接口的实现。

但情况比这更糟。尽管这种命名机制背后的动机是要引入将来的扩展点,但它无法引入具有合理命名的其他实现。(CommunicatorImpl2 不是一个合理的名称。)因此,如果您不能对实现或接口正确命名,就不要引入接口。

回到自然约定

类的约定包括其全部公有方法。公有方法旨在用于其客户端。下面是 EJB 3.1 规范 (JSR 318) 第 3.4.4 章中对 Enterprise JavaBeans 3.1 bean 的无接口视图的确切定义:

“……会话 Bean 的无接口视图是提供该 bean 类的公有方法而不另行使用业务接口的本地视图的变体……

所有私有方法都是隐藏的。具有程序包私有和受保护可见性的方法只对同一程序包内的类可见,对它们的访问通常仅限于测试目的。JUnit 测试类驻留在与“受测类”(CUT) 相同的程序包内并模拟不方便的引用,通常直接访问程序包私有或受保护的字段。

尽可能简单……

通过表示状态传输 (REST) 提供现有 EJB 3.1 bean 也无需接口(参见清单 1)。MessageResource 是作为用于 RESTful Web 服务的 Java API (JAX-RS) 端点直接提供的 EJB 3.1 bean。

     @Path("messages")
     @Stateless
     @Produces(MediaType.APPLICATION_JSON)
     public class MessagesResource {
         @Inject          
         Communicator communicator; 
         @GET
         public List<Message> allMessages(){
             return communicator.getRecentMessages();
         }
     } 
清单 1:示例 REST 端点

 

您可以使用 JAX-RS 对接口进行批注并以此方式隐藏某些 JAX-RS 批注。不过,这种隔离只是在理论上不错。在实践中,更复杂的应用程序可能会访问 JAX-RS 特定类(如 javax.ws.rs.core.UriInfo、javax.ws.rs.core.UriBuilder 或 javax.ws.rs.core.Response),不管怎样,它将依赖于 JAX-RS API。实际上,在所有重要的应用程序中,将协议相关部分(SOAP、JAX-RS、CORBA 等)与纯边界部分拆分成两个独立类是一个好主意。

尽管类 Communicator 是直接注入的,但仍然可以截取它。Java EE 6 拦截器(参见清单 2)对接口和普通 Java 类同样有效。

     public class CommunicationSniffer {
         @AroundInvoke
         public Object sniff(InvocationContext ic) throws Exception{
             Object result = ic.proceed();
             System.out.printf("Method %s returned %s",ic.getMethod(),result);
             return result;
         }
      }

清单 2:使用 CommunicationSniffer 实现无接口方法调用截取

您只需在批注或 XML 描述文件中声明一个拦截器,即可对类应用横切关注点。您也可以实现一个接口并注入它,但不是必须如此。

     @Interceptors(CommunicationSniffer.class)
     public class Communicator {
         public List<Message> getRecentMessages(){
             return new ArrayList<Message>(){{
                 add(new Message("first"));
                 add(new Message("second"));
             }};
         }
     }

清单 3:截取的 POJO

对 URI [WAR_NAME]/资源/消息的 GET 请求由方法 allMessages 处理,它委托给注入的 Communicator#getRecentMessages() 实例。该调用被 CommunicationSniffer 拦截器截取,该拦截器向标准输出流写入以下字符串:

Method public java.util.List 
com.abien.patterns.nointerfaces.control.Communicator.getRecentMessages() 
returned [Message{content=first}, Message{content=second}].

对于用于 XML 绑定的 Java 架构 (JAXB)-POJO 消息的序列化也无需接口(参见清单 4)。为 Message 实体引入接口实际上会相当令人困惑。没有方法要在约定中指定。

	@XmlRootElement
	@XmlAccessorType(XmlAccessType.FIELD)
	public class Message {
    	     private String content;
    	     public Message(String content) {
        	this.content = content;
    	     }
    	     public Message() {}
	}
    

清单 4:带 JAXB 批注的 Message 实体

……但并非更简单

现在 MessageResource 直接依赖于 Communicator 类的实现。由于直接依赖于实现,不再可以轻松换掉 Communicator。您必须更改 MessageResource 代码,使用其他实现替换 Communicator。

我们引入 Communicator 类的另一种变体,名为 ConfidentialCommunicator。该变体返回不同的消息,且不被 CommunicationSniffer 拦截器截取(参见清单 5)。

	public class ConfidentialCommunicator {
    	     public List<Message> getRecentMessages(){
        	return new ArrayList<Message>(){{
            	     add(new Message("top secret"));
        	}};
    	     }
	}
    

清单 5:另一种 Communicator

我们可以同时注入 ConfidentialCommunicator 和 Communicator,并在 MessageResource 内部决定使用哪个实例。这种解决方案将会不必要的复杂、难以理解,因此也难以维护。

第一个 Communicator 版本被 CommunicationSniffer 截取,它不大关心隐私。因此将其重命名为 PublicCommunicator。这样,我们有了 ConfidentialCommunicator 和 PublicCommunicator,两者分别有不同的职责,但下一步自然是对它们进行相同的签名以引入接口,从而隐藏实现细节并定义一个公用的约定。

MessageResource 不关心具体的实现。它只希望将 List<Message> 序列化成 JavaScript 对象表示法 (JSON) 字符串。该类的名称正好也是一个很好的接口名称:就是 Communicator。

有趣的是,在引入接口之后 MessageResource 类保持不变:

	public class MessagesResource {
    	     @Inject     	              Communicator communicator; 
        //…
	}
    

实际上,MessagesResources 不应关心实现细节。它只对一个方法感兴趣:getRecentMessages。此方法可由一个公有类或一个隐藏不同实现的接口来实现。从 MessageResources 的角度来看,这两种情况完全一样。

灵活的代价

现在,我们有一个直接注入到 MessageResource EJB 3.1 bean 中的 Communicator 接口。接口的存在暗示着多种实现:ConfidentialCommunicator 和 PublicCommunicator。

现在,我们必须在这些实现之间做出选择。没有配置,直接注入 Communicator 接口将不再成功,并会导致以下错误:

 org.jboss.weld.exceptions.DeploymentException: WELD-001409 Ambiguous dependencies 
for type [Communicator] with qualifiers [@Default] at injection point 
[[field] @Inject com.abien.nointerfaces.boundary.MessagesResource.communicator]. 
Possible dependencies [[Managed Bean [class 
com.abien.nointerfaces.control.PublicCommunicator] with qualifiers 
[@Any @Default], Managed Bean [class 
com.abien.nointerfaces.control.ConfidentialCommunicator] with qualifiers 
[@Any @Default]]].

我们可以通过停用其中一个托管 bean 来消除依赖的歧义性。使用 javax.enterprise.inject.Alternative 批注对一个托管 bean 进行批注可以将其停用:

	@Alternative
	public class PublicCommunicator implements Communicator{}

现在是一对一关系。Communicator 接口可以直接注入 MessagesResource。调用 URL 将产生以下输出(由 ConfidentialCommunicator 实现产生):

	{"message":{"content":"top secret”}}

要重新激活 PublicCommunicator,您必须将批注从 PublicCommunicator 移动到 ConfidentialCommunicator,您还须重新编译整个应用程序以在这两个实现之间进行切换。

您还可以将这两者同时停用并激活您在 beans.xml 中选择的实现(清单 6)。

	<beans>
		<alternatives>
			<class>com.abien.nointerfaces.control.ConfidentialCommunicator</class>
		</alternatives>
	</beans>

清单 6:在 beans.xml 中对完全限定托管 Bean 的 Alternative 激活

现在,必须同时在 XML 文件中和源代码中维护该类的完全限定名称。对层次结构的更改甚至是简单的重命名都必须同时在两个位置执行。上下文和依赖注入 (CDI) 构造型为数据复制问题提供了极好的解决方案。它不是重复完全限定类名,而是只指定一个标记接口(参见清单 7)。

	<beans>
		<alternatives>
			<stereotype>com.abien.nointerfaces.control.Confidential</stereotype>
 		</alternatives>
	</beans>

清单 7:激活使用构造型批注的类

构造型是一个使用 @Stereotype 元批注进行自我批注的批注(参见清单 8)。

	@Alternative
	@Stereotype
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.TYPE)
	public @interface Confidential {}
Listing 8: An @Alternative @Stereotype

现在只需使用构造型即可停用托管 bean,而不需要用原始的 @Alternative 批注(参见清单 9)。

	@Confidential
	public class ConfidentialCommunicator implements Communicator {}

清单 9:使用 @Stereotype 来停用

所有使用 @Confidential 构造型批注的类均可通过指定构造型的完全限定名称来立即激活,而无需通过配置托管 bean 的完全限定类名。将构造型移动到另一个程序包或对其重命名将产生同样的麻烦:必须同时维护 Java 源代码和 beans.xml 文件的内容。但构造型重构的可能性小得多。

您还可以使用一个构造型批注多个类并同时激活它们。构造型与托管 bean 之间的 1:n 关系使得基于构造型的解决方案更易于维护。您不必在 beans.xml 中维护托管 bean 的所有完全限定名称。

让使用者来决定

提供者和使用者均可以配置注入。我们可以在注入点决定选择哪种实现,而不是使用 beans.xml、批注或两者来固定地激活和停用托管 bean。

为此,必须使用自定义限定符批注来解决歧义。@Qualifier 批注与 @Stereotype 批注类似,但用于使用具有相同元素的相同批注标记这两个部分来解决引起歧义的依赖性。

@Stereotype 是标记接口和宏的混合物。您可以使用多个批注对构造型进行批注,并使类声明更简明。构造型批注中使用的所有批注均在类级别扩展。在上面的示例中,我们使用了 @Alternative 批注作为元批注。

我们在这里不使用 @Confidential 构造型,而是使用 @Confidentiality 限定符标记每个实现以及注入点(参见清单 10)。

 	@Qualifier
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ElementType.FIELD, ElementType.TYPE})
	public @interface Confidentiality {
		Level value();
		enum Level{
			STRONG, WEAK
		}
	}

清单 10:用于依赖性解析的自定义限定符

Confidentiality 限定符还自带嵌入式枚举 Level。Level 枚举的值也用作匹配条件。因为未指定默认值,您将必须通过应用批注来选择值。

ConfidentialCommunicator 使用 STRONG Level 值进行批注:

	@Confidentiality(Confidentiality.Level.STRONG)
	public class ConfidentialCommunicator implements Communicator {}

PublicCommunicator 使用相对的 WEAK Level 值进行批注:

	@Confidentiality(Confidentiality.Level.WEAK)
	public class PublicCommunicator implements Communicator{}

Communicator 使用者现在可以通过设置 Level 枚举值来决定使用哪个实现(参见清单 11)。

	public class MessagesResource {
		@Inject @Confidentiality(Confidentiality.Level.WEAK)
		Communicator communicator;
	//..
	}

清单 11:注入点的限定符

使用者与特定实现分离,并通过在注入点指定限定符来决定使用哪个实现。相反,使用构造型,限定符是最具侵入性的方法。使用者的代码必须通过限定符批注来扩展并在每次更改时重新编译。另一方面,自定义限定符易于维护,因为编译器可防止任何误拼。

运行时选择

您甚至可以在运行时探查接口的所有实现并决定使用哪个实现。注入的 javax.enterprise.inject.Instance 提供了最大的灵活性。您可以查询注入的 Instance 来确定依赖关系是否有歧义或未得到满足并遍历所有实现。

使用 @Any 限定符,可发现 Communicator 接口的所有实现,无论它们是否用自定义限定符进行了批注(参见清单 12)。

	@Path("messages")
	@Stateless
	@Produces(MediaType.APPLICATION_JSON)
	public class MessagesResource {
		@Inject @Any
		Instance<Communicator> communicatorInstances;
		@GET
		public List<Message> allMessages(){
			System.out.println("--isAmbiguous: " + communicatorInstances.isAmbiguous());
			System.out.println("--isUnsatisfied: " + communicatorInstances.isUnsatisfied());
			List<Message> allMessages = new ArrayList<Message>();
			for (Communicator communicator : communicatorInstances) {
				allMessages.addAll(communicator.getRecentMessages());
			}
 			return allMessages;
			}
		}

清单 12:在运行时遍历所有实现

这是由清单 12 生成的 JSON 字符串:

{"message":[{"content":"first"},{"content":"second"},{"content":"top secret”}]}

按需接口

对于 JAX-RS、EJB、CDI 或 Java Persistence API (JPA) 组件的实现,Java EE 6 不要求您使用接口。您可以从直接注入类开始。无需任何牺牲。不使用接口,拦截器、事务、安全性、监视、线程处理和依赖注入同样都可以正常工作。

如果按照其业务职责对实现进行命名,则注入类的使用者将很难判断它是一个具体类、抽象类还是接口。由于这一原因,Impl 后缀之类的命名规范不利于提高工作效率。同时,这些命名规范绝对是多余的,因为它们所强调的是明显的事实。

有时,您需要引入另一个类,并使用形式约定(一种显式 Java 接口)对两种实现进行抽象。这只是一个小小的重构。您需要执行以下操作:

  1. 按照职责对原始实现进行重命名。(在前面的示例中,我们将 Communicator 重命名为 PublicCommunicator。)

  2. 用原始实现的名称 (Communicator) 引入一个接口。原始类须实现此接口。

  3. 按照业务职责对另一个类进行命名并实现该接口。

如果您需要更多灵活性,可以引入构造型或限定符或在运行时查询框架(使用 javax.enterprise.inject.Instance)。无论哪种情况,都只是一个小小的重构。

概率驱动的决策

使用接口对每个实现进行抽象,如果这样的方法明确合理,就没有任何问题,但当必须引入人工命名规范避免命名冲突时,接口就变得不确定了。使用“I”之类的前缀或者 IF 或 Impl 之类的后缀应被视为代码味道。

只有当作为已有类的约定、用于实现 Strategy 或 Bridge 模式,或者当您需要设计 API(例如 Java 数据库连接 (JDBC)、Java 消息服务 (JMS) 等)时,才应引入接口。在典型业务应用程序中,这种情况很少。

通常注入一个类就可以,无需任何接口。在罕见的最坏的情况下,需要在事后引入接口。如果为每个始终重要的类均实现一个接口,会使构件数量翻倍,与此相比,现在的部分重构方法在事后进行更为经济。

程序不兼容的更改对于企业应用程序也不是问题。在持续集成 (CI) 环境中,所有代码都将在每次提交时重新编译、重新测试、重新打包并重新部署。在 Java EE 6 中,以“它可能将来需要”或为每个可能的用例实现不成熟的扩展作为理由不再可行。只需小小重构即可在事后轻松扩展编写得非常好的、简明的代码。

另请参见

关于作者

顾问兼作家 Adam Bien 是 Java EE 6 和 7、EJB 3.X、JAX-RS、CDI 和 JPA 2.X JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并在几个大型项目中使用了 servlet/EJB 1.0,目前是 Java SE 和 Java EE 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是《Real World Java EE Patterns—Rethinking Best Practices》《Real World Java EE Night Hacks—Dissecting the Business Tier》两本书的作者。Adam 还是 Java Champion 和 JavaOne 2009 Rock Star。