阿里巴巴的Service框架指南

makesuper

贡献于2011-02-23

字数:39035 关键词: JEE框架

Service框架指南 1. 前言 目前阿里巴巴的Java框架,全部是基于Service框架的。本文主要从使用者的角度来介绍我们的Service框架。 不过在开始介绍之前,我想先介绍一下Service框架的历史。 1.1. 历史简介 Service框架是用来创建可重用“组件”的通用框架。用现在的眼光来看,似乎这是很平常的一种设计。现在流行的每一种Web框架,在底层几乎都会有一个通用组件框架来支持它。例如, ² Tapestry是建立在HiveMind通用框架之上的; ² Webwork是建立在Xwork通用框架之上的; ² Spring MVC是建立在Spring通用框架基础之上的。 HiveMind、Xwork、Spring都是功能类似于我们的Service框架的通用组件框架。除了这些常见的以外,还有很多。但有一些项目由于用的人少,或者是不够好,就慢慢淡出我们的视线了,例如Avalon。这很正常 —— 技术总是在进步的。 我们的Service框架的设计,来源于几年前流行的Jakarta Turbine框架(http://jakarta.apache.org/turbine/)。和Turbine同时流行的,还有Struts框架(http://jakarta.apache.org/struts/)。虽然Struts的知名度比较高,但是从设计的角度而言,Turbine在当时是非常领先的。虽然Turbine项目最后没有成功,但它的思想却被现在的许多框架借用了。Turbine有很多重要的创举,例如: ² Turbine开创了在Web应用中的Convention over Configuration的先河。在Turbine中,被称为Page-driven。相对于当时Struts的Configuration-driven,方便了许多。 ² Turbine的screen/navigation/layout的页面布局模型,今天用起来还是够方便。 ² Turbine的很多子项目都获得了局部或全部的成功,例如:Maven、Torque。 ² Turbine鼓励使用轻量级的模板,例如Velocity代替JSP,打破了JSP一统天下的局面。而且Turbine建立了良好的Template框架,允许插入任意类型的模板引擎。 ² Turbine的Pull Model,非常像现在常说的“注入”机制,只不过是注入到页面。 ² Turbine 3.0(一直没有完成)第一次引入了开创性的pipeline机制。 ² Turbine上述所有的功能,全部建立于一套Service框架基础之上。Turbine创建了很多很有用的Service。 正由于Turbine建立在Service的基础之上,才使得Turbine框架具有极大的弹性。Turbine正如它的名字所示(涡轮),它可以把很多系统整合到它里面来。相对而言,在当时,想在Struts中加点什么新功能就没这么容易。也许是Turbine的雄心太大,什么都想做,什么都想整合进来,导致它看起来异常庞大。就像现在的Spring,整合了很多内容。人们普遍认为Turbine是一个很Heavy的系统,因而害怕去接近它。此外,Turbine框架中也包含了很多不好的设计(如过度使用Singleton,分层不够清晰,框架的侵入性太大等),影响了它的扩展性和易用性,最终导致整个项目的失败。 但不管怎么说,Turbine的Service框架是整个Turbine的精髓。阿里巴巴最初直接使用Turbine和它的Service框架。后来有感于Turbine中很多的设计缺陷,却等不及Turbine极为缓慢的发展速度,因而取其所长、克其所短地自建了一套框架。这就是我们将要介绍的Service框架。我们在这套Service框架之上建立了Webx框架及业务层框架。 随后,Turbine也将它的Service框架分离了出来,改名叫Fulcrum。再后来,Turbine将其合并到Avalon框架中。Avalon框架是最早使用“依赖注入”的框架之一。很可惜,先行者成了后来人的垫脚石,Avalon被历史无情地淘汰了 —— 真的很可惜。 1.2. 怎样看待Service框架 任何技术都是一步一步前进的。先进的东西很快会变成落后的。Service框架也不例外。 从设计理念上,Service框架继承了Turbine Service框架所声称的IoC(Inversion of Control)的观念。然而,从今天的眼光来看Service框架不算是IoC的,因为现在的IoC往往意味着DI(Dependency Injection,参见Martin Fowler著名文章)。但Service框架很好地符合了DIP的原则,即Dependency Inversion Principle —— 通俗地讲,就是利用接口,反转依赖关系,从而解除紧密的耦合关系,实现良好的系统架构。DIP也被称为Hollywood Principle,即:Don't call us, we'll call you if we needed。另人迷惑的是,从现在的眼光看,所有的这些名词:IoC、DI、DIP、Hollywood Principle都代表一个意思,就是“注入”。 再去争论这些名词变得没有意义。我们从实用的角度来看,Service框架确实缺失了“注入”、“轻量”等现在我们觉得一个组件框架所必须具备的特性。因为Service框架是在这些新玩意成熟之前的产物。 那我们应该怎样看待Service框架呢?我曾听见有人质问我:我们为什么要使用没有“注入”机制的Service框架?为什么不直接使用Spring这样的优秀框架呢?下面是我的回答: 首先,要客观地评价Service框架的技术。事实上,只要你的框架很好地吻合了DIP的原则,就已经达到了所有框架为之奋斗的基本目标 —— “解耦”的目的。Service框架解决了原Turbine框架在滥用Singleton带来的种种弊端。因此,在松散耦合、易测试性等方面,还是做得不错的。至少,我们现在的Webx框架、业务层框架都建立在此之上,并且非常稳定。  其次,Service框架中有一些创新特性,是现在的Spring等框架中所没有的。例如:资源装载、多实例等。这些特性在我们目前的系统中工作得非常好。多实例的特性是Webx组件的基础。 最后,我们已经对Service框架进行了适应性的改进。例如,我们整合了Spring框架。这样,所有应用都可以使用Spring带来的一切便利。更进一步,使用我们所整合的Spring框架,能够无缝衔接独特的资源装载器,简化我们的应用和测试代码。 总之,既然Service框架目前工作得既稳定又好,我们就没有理由去轻易地替换掉它。本文将要介绍Service框架使用的方法和技巧。 1.3. Service框架的发展 但没有“注入”功能的组件框架毕竟不流行了。依赖注入给我们带来的好处是显而易见的。因此,我们的Service框架必须朝那个方向去发展。 我们能不能通过改造Spring框架,加入一些它无我有的特性,来形成自己的新框架呢?当然可以的。但是我们想做更多。如果你不能走在前面,就算Spring这样的先进框架,也很快会变得落后。当我审视Spring,我发现,它还是遗漏了一些事情,而这些事情正是我们所需要的。 因此我想,开发一个新的框架,吸收现有框架的优越性,融入自己的创新,也许是更好的方法。新框架的代号为:Citrus。 有关新框架的开发思路、理念,我将在另外的文档中慢慢展现。 现在,让我们先用好手上兵器。 2. Service框架基础 本章通过由浅入深的方式,介绍Service框架在各种环境下的使用方法。 2.1. 预备环境 首先,我们将要创建一些代码范例。为了统一起见,建议你先创建一个项目,以便顺利地测试所有的代码。不要怕,用antx创建一个项目非常简单。 1. 首先,请创建一个目录,来存放新的项目。例如:C:\testservice 2. 进入该目录,在命令行上运行:antx gen,生成项目的基本目录结构。 C:\testservice │ project.jelly │ project.xml │ └─src └─java 3. 修改project.xml,改成下面的样子: 4. 在testservice目录下,执行antx eclipse,生成eclipse项目文件。 5. 打开Eclipse集成环境,导入该项目。 现在,你可以在eclipse下工作了,所有依赖的jar包都已经设置好了。 2.2. 启动Service框架 Service框架是一个Service的容器。这个容器可以通过ServiceManager来启动。 下面是一个极简单的程序,启动了ServiceManager,并从中取得一个Service。 package test.service; import com.alibaba.service.DefaultServiceManager; import com.alibaba.service.ServiceManager; import com.alibaba.service.resource.ResourceLoaderService; import com.alibaba.service.resource.ResourceNotFoundException; public class Main { public static void main(String[] args) throws ResourceNotFoundException { ServiceManager manager = getServiceManager(); ResourceLoaderService resourceLoaderService = (ResourceLoaderService) manager .getService("ResourceLoaderService"); System.out.println(resourceLoaderService.getResource("classpath/java/lang/String.class")); } private static ServiceManager getServiceManager() { ServiceManager manager = new DefaultServiceManager(); manager.init(); return manager; } } 从程序中可以看见,创建一个Service容器可以只用一行代码: ServiceManager manager = new DefaultServiceManager(); 随后,需要对manager进行初始化: manager.init(); ServiceManager初始化完成以后,就可以从容器中取得Service了。当然,上面的代码只不过初始化了一个空的ServiceManager,并没有告诉ServiceManager应该初始化哪些Service。在这种情形下,只有一个Service可用,就是:ResourceLoaderService。下面的代码取得了ResourceLoaderService: ResourceLoaderService resourceLoaderService = (ResourceLoaderService) manager.getService("ResourceLoaderService"); 在ServiceManager中,Service是用一个字符串的key来索引的。在绝大多数的情况下,这个key和Service的类名相同。因此我们也可以用常量来引用ResourceLoaderService: ResourceLoaderService resourceLoaderService = (ResourceLoaderService) manager .getService(ResourceLoaderService.SERVICE_NAME); 当然,在后面的例子中,我们可以看到,你完全可以用不同的key来创建同一个Service。 请注意,在这个取得ResourceLoaderService的例子中,ResourceLoaderService是一个接口。 使用Service框架,你的应用不需要关心Service是怎样实现的,而是通过接口来访问一个Service。这样就实现了DIP原则(Dependency Inversion Principle)。这样做有什么好处呢?很明显,你的程序从某个具体的Service实现中解脱出来。你的Service可以被扩展、被替换 —— 只要接口不改变,你的程序就不需要改变。有人争辩说,应该用“注入”的方式才能实现真正的解耦。而事实是:这两种方式达到了同样的目的,区别只是程序和Service对于框架的依赖性。在将来的改进中,我们会尽可能减少Service对于框架的依赖性。 2.3. 配置Service框架(原理) 怎样让ServiceManager来初始化一些Service呢?答案是通过配置。 ServiceManager使用了Jakarta commons-configuration(http://jakarta.apache.org/commons/configuration/)来配置Service。Commons-configuration是一个通用的配置框架。 这使得你可以使用任何格式来定义你的配置文件,无论是传统的properties格式,或是用XML的格式,甚至从JNDI、Database数据库中获取配置文件,只要最终表现成一个统一的接口,应用程序就照单全收。 让我们来看一个具体的例子,仍然以ResourceLoaderService为例。虽然刚才我们在未指定的时候,也可以取得ResourceLoaderService,不过这只不过是一个BootstrapResourceLoaderService —— 它除了能取得classpath下的资源以外,什么也不能做。现在我们来配置一个真正的ResourceLoaderService。 首先,你需要在project.xml中添加ResourceLoaderService的依赖: 然后,请修改前例中的Main.java如下: public class Main { …… private static ServiceManager getServiceManager() { ServiceManager manager = new DefaultServiceManager(); manager.setConfiguration(getConfiguration()); manager.init(); return manager; } private static Configuration getConfiguration() { Configuration config = new BaseConfiguration(); config.setProperty("services.ResourceLoaderService.class", "com.alibaba.service.resource.DefaultResourceLoaderService"); config.setProperty("services.ResourceLoaderService.resource.descriptors", "/classpath/resources.xml"); return config; } } 和前例的区别在于,本例中增加了一个额外的步骤: manager.setConfiguration(getConfiguration()); 在getConfiguration方法中,我们创建了一个简单的BaseConfiguration: Configuration config = new BaseConfiguration(); 我们指定了ResourceLoaderService的实现类: config.setProperty("services.ResourceLoaderService.class", "com.alibaba.service.resource.DefaultResourceLoaderService"); 我们还指定了ResourceLoaderService的配置文件: config.setProperty("services.ResourceLoaderService.resource.descriptors", "/classpath/resources.xml"); 显然,你还需要在src/java目录下建立一个resources.xml配置文件,该文件将可以从classpath中找到(即:"/classpath/resources.xml")。本文的目的不是讲解ResourceLoaderService,因此不给出详细的资源配置文件了。请参见《ResourceLoader服务指南》。 2.4. 配置Service框架(实际) 既然commons-configuration对产生Configuration对象的机制没有特别要求,那么我们就可以定义出更容易掌握的配置文件格式。在实际应用中,我们不大可能通过前面例子所示的方法,用纯代码来初始化Service框架。配置Service框架最好的方法,是通过XML文件。这个XML配置文件的格式是我们自己定义的,符合DTD:http://toolkit.alibaba-inc.com/dtd/toolkit/service/services.dtd。最终,这个配置文件将被解释并转换成和前面例子中相似的Configuration对象。 现在,让我们用这个XML配置文件来初始化Service框架。和前例相同,我们仍然以ResourceLoaderService为例。 首先,请确信你的project.xml项目定义文件中,包含了toolkit/service/resource的引用。(详见前例) 其次,请在src/java目录下建立一个services.xml文件。你可以用"/classpath/services.xml"来引用它。文件的内容如下: classpath/resources.xml 最后,请创建一个Java类来读取这个配置文件: package test.service; import com.alibaba.service.DefaultServiceManager; import com.alibaba.service.ServiceInitializationException; import com.alibaba.service.ServiceManager; import com.alibaba.service.resource.ResourceLoaderService; public class Main2 { private static String applicationRoot = null; private static String serviceConfig = "classpath/services.xml"; private static boolean initAll = false; public static void main(String[] args) throws Exception { ServiceManager manager = getServiceManager(); ResourceLoaderService resourceLoaderService = (ResourceLoaderService) manager .getService(ResourceLoaderService.SERVICE_NAME); System.out.println(resourceLoaderService.getClass()); System.out.println(resourceLoaderService.getResource("classpath/java/lang/String.class")); } private static ServiceManager getServiceManager() throws ServiceInitializationException { return new DefaultServiceManager(applicationRoot, serviceConfig, initAll); } } 你会发现和前例相比,程序被简化了: 1. 创建ServiceManager的方法有所改变,使用了三个参数: a) applicationRoot – BootstrapResourceLoaderService的根目录。可以为null。 b) serviceConfig – Service框架的配置文件。 c) initAll – 是否初始化所有Service。 2. 不需要调用manager.init()方法了。 值得注意的是:service配置文件是从哪里装载的?答案是从BootstrapResourceLoaderService装载的。由于在本例中,我们没有为BootstrapResourceLoaderService提供一个applicationRoot(即:applicationRoot=null),所以我们只能从classpath中装载该配置文件(classpath/services.xml)。 然而在很多情况下,指定一个applicationRoot,并将所有配置文件和其它资源放在这个目录及它的子目录下,是很方便的。例如,在单元测试时,我们可以将src/conf.test目录指定为applicationRoot。这样,我们就可以把包括services.xml在内的所有配置文件都放在src/conf.test目录。引用这些文件也变得非常自然 —— 不需要在前面冠以“classpath/”前缀了。请参见另一篇文章:《业务层、数据访问层单元测试指南》,其中包含了利用这种方案进行单元测试的例子。 如果第三个参数:initAll=true,那么所有的Service都会被立即被初始化。这样一来,一旦哪个Service初始化失败,就立刻可以晓得。否则,只有当第一次使用到Service时,才会开始初始化该Service。在生产环境中,指定initAll=true是非常有用的。因为我们不希望等到用户使用到这个Service时,才报错。我们希望在布署系统的时候就知道所有Service是否初始化成功。 2.5. 创建一个Service 创建一个Service是相对容易的事情。现在让我们来尝试建立一个读取properties的Service。所谓properties是指下列形式的(key, value)对: xxx.yyy = 111 xxx.yyy.zzz = 222 …… 首先,让我们创建一个Service接口:PropertyService。 package test.service; import com.alibaba.service.Service; public interface PropertyService extends Service { String getProperty(String key); } 注意,所有的Service必须从com.alibaba.service.Service接口派生。在这个接口中,包含了管理Service生命期的一些方法: public interface Service { void init(ServiceConfig serviceConfig) throws ServiceInitializationException; void destroy(); Logger getLogger(); ServiceConfig getServiceConfig(); Service getParentService(); boolean isInitialized(); } 说老实话,这个接口确实设计得不太好。它使Service和Service框架紧密地结合在一起了。在将来的改进中,我们将完全取消这种耦合,实现更轻量的Service。好在这种设计也没给我们带来严重的问题。我们推荐从com.alibaba.service.GenericService类派生出Service的实现。GenericService中包含了Service接口的默认实现,从而简化了Service的实现。 让我们创建一个PropertyService的实现:PropertyServiceImpl。 package test.service; import com.alibaba.service.GenericService; public class PropertyServiceImpl extends GenericService implements PropertyService { } 当然这个类肯定不能通过编译,因为我们还没有实现PropertyService接口中的方法。让我们来实现它: public class PropertyServiceImpl extends GenericService implements PropertyService { private Properties properties; public String getProperty(String key) { return properties.getProperty(key); } } 然而,properties中内容从何而来呢?幸运的是,commons-configuration的Configuration对象本身就是一种类似properties的数据结构。因此,我们可以直接从services.xml中获取这些properties。为此,我们需要覆盖GenericService.init()方法。 public class PropertyServiceImpl extends GenericService implements PropertyService { …… public void init() throws ServiceInitializationException { super.init(); Configuration config = getConfiguration(); properties = new Properties(); for (Iterator i = config.getKeys(); i.hasNext();) { String key = (String) i.next(); String value = config.getString(key); properties.setProperty(key, value); } } } 实现init()方法时,需要注意几点: 1. 如果初始化失败,请抛出ServiceInitializationException。 2. 务必调用super.init()方法。(这也是一个不好的设计L) 3. 通过getConfiguration()方法可以取得Service的配置信息。该方法将会返回一个commons-configuration的Configuration接口,接下来,你就可以参照该接口所提供的方法来配置你的Service。 在本例中,我们简单地把config中的(key, value)对设置到我们的properties对象中。 对于这个例子,我们还可以做一些改进。 1. 创建日志 适当的时候输出一些日志是一个好习惯,它会有助于你发现问题。 public void init() throws ServiceInitializationException { …… for (……) { …… getLogger().debug("Created property: " + key + " => " + value); } } 其中,getLogger()方法是GenericService提供的一个简单的取得Logger的方式。你只需要直接调用它就可以了,不必亲自创建它。 2. 调用其它的Service 调用另一个Service是很常见的功能。很遗憾,Service框架目前不支持“注入”的功能,因此你不能指望写一个setter方法就取得另一个Service的引用。好在取得另一个Service也不是太复杂。下面的代码取得ResourceLoaderService。 public class PropertyServiceImpl extends GenericService implements PropertyService { private ResourceLoaderService resourceLoaderService; …… public void init() throws ServiceInitializationException { …… resourceLoaderService = (ResourceLoaderService) getService(ResourceLoaderService.SERVICE_NAME); …… } } 请注意,我们取得的是另一个Service的接口 —— 我们既不知道这个service的实现是什么,也不用关心这个service的配置是什么。因此,这两个Service依然是解耦合的 —— 你可以替换任何一个Service,或者更改它的配置,只要保持它的名称和接口不变就可以了。 3. 创建析构方法 有时,一个Service不仅需要init(),也需要destroy(),以便释放所占用的资源。创建destroy()的方法如下: public class PropertyServiceImpl extends GenericService implements PropertyService { …… public void destroy() { super.destroy(); …… } } 注意,super.destroy()仍然是必须调用的L。 最后,我们需要略微修改一下services.xml,以便将刚刚创建的PropertyService放到Service容器中: …… 111 222 在这个配置中,我故意把事情“做复杂”了,目的是为了演示配置文件的各种写法。事实上,上面的配置和下面的配置是完全等效的: …… 其中,“xxx.yyy.aaa”有两个值,这是完全可以的。在代码中,你可以用config.getStringArray("xxx.yyy.aaa")来取得所有的值。有时使用前一种格式更易读,有时使用后一种更好,这个完全由你来决定。不管哪种形式,它们最终都被转变成标准的Configuration对象。 现在,我们可以修改一下Main类,测试一下PropertyService: public class Main2 { …… public static void main(String[] args) throws Exception { ServiceManager manager = getServiceManager(); …… PropertyService propService = (PropertyService) manager.getService("PropertyService"); System.out.println(propService.getProperty("xxx.yyy.zzz")); System.out.println(propService.getProperty("xxx.yyy.aaa")); System.out.println(propService.getProperty("hello")); } …… } 如果我们在PropertyService中创建一个常量: public interface PropertyService extends Service { String SERVICE_NAME = "PropertyService"; …… } 那么,取得PropertyService的代码可以稍稍改进如下: PropertyService propService = (PropertyService) manager.getService(PropertyService.SERVICE_NAME); 3. 在Webx应用中使用Service 3.1. 在Screen/Action/Control中使用Service Webx框架是建立在Service框架基础之上的。因此,在Webx模块中,你可以和前述Main例子一样简单地使用任何Service,。下面这段代码演示了在Screen/Action/Control等Webx模块中,如何引用Service: public class LoginAction extends TemplateAction { private FormService getFormService() { return (FormService) getWebxComponent().getService(FormService.SERVICE_NAME); } public void doLogin(RunData rundata, TemplateContext context) throws WebxException { Form form = getFormService().getForm(rundata); if (form.isValid()) { …… } } …… } 在Webx中,你不能直接使用ServiceManager,而应该使用WebxComponent对象,但它们的接口是完全类似的。上面的代码取得FormService,并用它来验证表单。 然而,还有一种更流行更简单的使用方案 —— 注入。请看下面的代码,它实现了和上面代码完全相同的功能: public class LoginAction extends TemplateAction { private FormService formService; public void setFormService(FormService formService) { this.formService = formService; } private FormService getFormService() { return formService; } public void doLogin(RunData rundata, TemplateContext context) throws WebxException { Form form = getFormService().getForm(rundata); if (form.isValid()) { …… } } …… } 事实上,还可以更简单!请看下面的代码: public abstract class LoginAction extends TemplateAction { protected abstract FormService getFormService(); public void doLogin(RunData rundata, TemplateContext context) throws WebxException { Form form = getFormService().getForm(rundata); if (form.isValid()) { …… } } …… } 有关Webx模块的注入机制,请详见另一篇文章:《Webx Modules(screen/action/control)的装载,以及依赖注入》。 3.2. 在Webx中配置Service Webx是通过一个Servlet或Listener来启动的。在它启动的时候,会自动初始化Service框架,以及其中部分或全部的Service。然而,需要特别注意的是,Webx将一个war应用分割成若干个car组件(详见《Java应用框架总览》): WAR应用 CAR组件 Service CAR组件 Service 每个car都有一个自己的/WEB-INF/webx.xml配置文件 —— 这就是Service的配置文件了。它的格式和我们前面的例子中的services.xml没有任何差别。当screen/action/control等模块调用其getWebxComponent().getService()方法时,取得的是模块所在car中的Service。 任何两个car之间的Services都是独立的、互不干涉的。即便是同一个Service,例如:ResourceLoaderService,也会在两个car中各自创建一个Service实例,享有各自的可能和对方不同的配置。这种设计的主要目的之一,是让car可以独立配置、独立发布,而不会受到其它car的牵连。 当多个car被组合成一个war的时候,car将被转换成一个子目录,相应的,webx.xml也会被转移到WEB-INF的子目录下。例如: WAR └─ WEB-INF ├─ car1 │ webx.xml └─ car2 webx.xml 除了每个car有一个webx.xml以外,在WAR的WEB-INF目录下,也可以放一个webx.xml: WAR └─ WEB-INF │ webx.xml │ ├─ car1 └─ car2 这个webx.xml是干什么的呢? 原来,有一些Service是和car无关的。最典型的例子是RunDataService。RunDataService的功能是包装request、response,并提供特殊的服务,例如:解析参数、buffering、设置locale/charset等。所有这些功能都是相对于一个request而言的,把它放在任何一个car中都不合适。所以需要把这一类的Service配置在war的/WEB-INF/webx.xml中才合适。 另外,还有一些Service需要被多个car共享。例如:Spring的BeanFactoryService。我们后面会特别提到Spring和Service框架的整合。类似这样的Service有时也需要被配置在war的/WEB-INF/webx.xml中。 3.3. 在Webx中初始化Service Webx有两种启动方式,一种是通过Servlet,另一种是通过Listener。两者效果完全相同。前者比较简单;后者是为了给Webx Controller Servlet之外的组件提供Service的支持,例如:Filter和其它Listener。初始化Webx的过程就是初始化Service的过程。 和ServiceManager类似,Webx启动Service时,也可以打开initAll开关,以便在启动的时候就捕获所有初始化失败的异常。 下面是一个用Servlet初始化Webx的配置文件:/WEB-INF/web.xml …… WebxController com.alibaba.webx.WebxControllerServlet initAllServices true …… 1 …… 下面是一个用Listener初始化Webx的配置文件:/WEB-INF/web.xml initAllServices true …… com.alibaba.webx.WebxControllerListener …… 如果采用Listener来初始化Webx的话,就可以在其它的filter或listener的初始化过程中,取得Service容器。例如下面的代码创建了一个filter,在filter初始化的时候取得由listener创建的ServiceManager: public class MyFilter implements Filter { private ServiceManager manager; public void init(FilterConfig filterConfig) throws ServletException { ServletContext context = filterConfig.getServletContext(); WebxController controller = WebxUtil.getWebxController(context); manager = controller.getServiceManager(); } public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { …… } } 3.4. 配置默认的Webx Service 我们知道,整个Webx框架都是建立在Service框架之上的,因此Webx本身就依赖了很多Service。这些Service被定义在两个内部文件中 : 1. classpath/com/alibaba/webx/scheme/webx-default.xml 2. classpath/com/alibaba/turbine/scheme/webx-turbine-default.xml 这两个文件中分别定义了下列Service以及它们的默认配置: FormService 读取并验证用户submit的form表单的service。 JSPService JSP模板引擎,用来渲染jsp写成的页面模板。需要TemplateService的支持。 MappingService 将一个名字映射到另一个名字的service。 ModuleLoaderService 装载turbine module的service,例如:screen、action、control等。 PipelineService 用来创建pipeline的service。 PoolService 缓存以减少对象创建的开销。 PullService Pull model service,自动创建一定作用域的对象,而不需要应用程序干预(push)。 ResourceLoaderService 用来装载资源的Resource loader service。 RunDataService Webx核心service之一,用来保存每一个HTTP请求的状态。需要PoolService和UploadService的支持。 TemplateService 用来管理template engine,渲染页面的service。 ThreadContextService 线程空间的singleton服务,方便service存取request scope的对象。 URIBrokerService 自动生成URI的service。 UploadService 用来上传图片和文件的service。 VelocityService Velocity模板引擎,用来渲染velocity写成的页面模板。需要TemplateService的支持。 这些Services是Webx正常运行所必须的。为了方便起见,我们为这些默认Service提供了更简单的配置方法: 1. Webx.xml中可以省略上述Service,除非你希望改变某Service的默认配置,或者默认实现类。 2. Webx.xml中可以省略class="…Service",除非你希望改变Service的默认实现类。 3. Webx.xml中的配置将覆盖同名Service的同名配置项。 4. Webx.xml中的配置将与同名Service的所有配置项合并。因此,你可以只写出被添加、修改的部分配置项,而不需要把所有配置项都写全。 下面是petstore范例中的webx.xml的内容: /webroot/WEB-INF/resources.xml com.alibaba.sample.petstore.web.user.module /WEB-INF/form.xml /petstore/uri.xml /WEB-INF/access.xml /petstore/biz/bean/biz-delegate.xml 在这个配置中, ² 前面表格中提到的大部分Service都被省略不写了。 ² 配置文件中最后两个Service并非默认Service,因此必须指定class="…Service"。 ² 配置文件中其余的Service都是默认的Service,因此都没有指定class实现。其配置覆盖了默认的配置。 ² 在PullService的配置中,在原有默认的tools中,添加了一些新的tool。 3.5. 定义自己的默认配置 除了Webx提供的默认配置以外,我们还可以定义自己的默认配置文件。假如,我的所有car中都包含了某个Service,并且它们的实现完全相同、配置完全相同、或略有不同,那么我们就可以为此定义一个默认的Service,从而简化car中webx.xml的配置。 怎么做呢?只需在war的/WEB-INF目录下放一个webx-default.xml文件即可。文件的格式如下: 简言之,Part 1中的service配置将合并到/WEB-INF/webx.xml中;Part 2中的service配置将合并到/WEB-INF/*/webx.xml中。还是举一个petstore范例中的例子吧。你可以在petstore/bundle/war/src/webroot/WEB-INF目录下创建webx-default.xml文件,该文件最终将被打包到war的WEB-INF目录中。文件内容如下: /webroot/WEB-INF/resources.xml Part 2中的ResourceLoaderService将被合并到每个car的/WEB-INF/webx.xml中,所以你可以把每个car的webx.xml中配置相同的ResourceLoaderService删除掉了。Part 1中的Service将被合并到war的/WEB-INF/webx.xml中,所以你可以删除掉war的/WEB-INF/webx.xml文件了。 需要记住的是,webx-default.xml文件中所指定的默认配置,只在系统启动时被合并到各个webx.xml中。尽管每个默认Service只写了一遍,但是效果和你将同样的配置直接写在每个webx.xml中完全一样 —— 各个car仍然是完全独立的,它们各自有一套自己的service实例。 4. Service和Spring整合 Spring是目前最好的Service容器之一。我们的Service框架可以和Spring进行无缝地衔接。和Service框架整合以后的Spring不仅功能毫发无损,更可以取得对应用透明的使用ResourceLoaderService装载资源的便利,以及一些扩展的功能。有关ResourceLoaderService和Spring整合的内容,可以参见文档:《ResourceLoader服务指南》。 4.1. 配置Spring Service 对于Service框架而言,Spring是一个普通的Service。在services.xml或car的/WEB-INF/webx.xml中添加如下内容,便可以将Spring引入Service框架: /beans-1.xml /beans-2.xml …… 其中,bean.descriptors参数指明了一个或多个Spring的配置文件。系统将通过ResourceLoaderService来取得这些配置文件。这样,Spring就配置好了。 在Webx模块中,可以通过WebxComponent取得BeanFactoryService。由于BeanFactoryService同时实现了BeanFactory接口,因此你完全可以把BeanFactoryService看成BeanFactory的一个实现。下面的Action代码将取得Spring中的名为“commandDispatcher”的bean: public class LoginAction extends TemplateAction { …… private CommandDispatcher getCommandDispatcher() { BeanFactory beanFactory = (BeanFactory) getWebxComponent() .getService(BeanFactoryService.SERVICE_NAME); return (CommandDispatcher) beanFactory.getBean("commandDispatcher", CommandDispatcher.class); } public void doLogin(RunData rundata, TemplateContext context) throws WebxException { …… getCommandDispatcher()…… …… } …… } 对于非Webx环境,唯一的区别是,你需要用ServiceManager来取得BeanFactoryService: BeanFactory beanFactory = (BeanFactory) getServiceManager().getService(BeanFactoryService.SERVICE_NAME); 此外,在Webx环境中,只需稍加配置,就可以把Spring中的beans注入到screen/action/control中。为此,可以将上述代码可以简化成: public abstract class LoginAction extends TemplateAction { …… protected abstract CommandDispatcher getCommandDispatcher(); public void doLogin(RunData rundata, TemplateContext context) throws WebxException { …… getCommandDispatcher()…… …… } …… } 有关Webx模块的注入机制,请详见文章:《Webx Modules(screen/action/control)的装载,以及依赖注入》。 4.2. 配置级联的Spring容器 我们说过,每个car里的service都是一份独立的实例。同样,作为一个普通的Service,每个car中都会创建一份自己的Spring容器。假如我有10个car,就有10个各不相干的Spring容器。 WAR应用 CAR组件 Spring Service(独立容器) Beans CAR组件 Spring Service(独立容器) Beans 但是,有一些Beans是不能够这样做的,否则会占用不必要的系统资源。例如,和cache有关的beans。怎么解决这个问题呢?有办法!我们可以把Spring容器级联起来,使父容器被所有的car共享。改进方案如图: WAR应用 /WEB-INF/car1/webx.xml Spring Service(子容器) Beans /WEB-INF/webx.xml Spring Service(共享父容器) Beans /WEB-INF/car2/webx.xml Spring Service(子容器) Beans 要配置这种级联的Spring容器非常简单: WAR └─ WEB-INF │ webx.xml - 在此配置共享的父容器 │ ├─ car1 │ webx.xml - 在此配置派生的子容器 └─ car2 webx.xml - 在此配置派生的子容器 配置文件的写法完全同前例。这样,当查找一个bean时,Spring会首先在子容器中查找,如果找不到,再到父容器里查找。你可以自由地决定,哪些beans需要被共享,哪些beans需要被某个car独享。 4.3. 在Spring beans中注入Service 有时候,你需要在Spring beans中,注入Service。这很简单,以ResourceLoaderService为例,你可以在Spring配置文件中这样写: …… 上述配置文件就可以在myBean中注入ResourceLoaderService。 5. 深入理解Service框架 5.1. Service容器的结构 对于绝大数人,我们只需要知道一个简单的概念:Service框架是一个Service的容器。 然而,当你深入使用我们的框架时,有时会遇到一些颇令人困惑的接口,例如:ServiceContext、ServiceInstanceContext、ServiceManager、WebxComponent等。它们之间是什么关系呢?为什么需要这些接口呢? 原来,Webx CAR要求每个Service支持多个实例,互不干扰。为了支持这个功能,我们把Service容器设计成“二维”的,以便支持多实例的Service。容器结构如图所示: Instances ServiceInstanceContext 1 (Service 1, Instance 1) (Service 2, Instance 1) (Service 3, Instance 1) Services ServiceContext ServiceInstanceContext 2 (Service 1, Instance 2) (Service 2, Instance 2) (Service 3, Instance 2) ServiceInstanceContext 3 (Service 1, Instance 3) (Service 2, Instance 3) (Service 3, Instance 3) 5.1.1. ServiceContext接口 ServiceContext接口代表整个二维的容器: 1. 每一行代表一个Service。每个Service都有一个唯一的名称,和一个或多个instance实例。 2. 每一列代表一个Instance,每个Instance也有一个唯一的名称。Instance名称可以为null,代表默认的instance或称为main instance。 3. 每个行和列的交点,代表某个Service的某个Instance。 下面的语句,可以取得ResourceLoaderService的一个instance,instance名称为"instance1": serviceContext.getService("ResourceLoaderService", "instance1"); 下面的语句,可以取得ResourceLoaderService的默认instance,也叫main instance: serviceContext.getService("ResourceLoaderService", null); 这句话也可以简写为: serviceContext.getService("ResourceLoaderService"); 5.1.2. ServiceInstanceContext接口 ServiceInstanceContext接口代表二维容器的一列。 我们可以从ServiceContext中取得其中的一列,instance名称为"instance1": ServiceInstanceContext serviceInstanceContext = serviceContext.getServiceInstanceContext("instance1"); 下面的语句,将取得默认的列: ServiceInstanceContext serviceInstanceContext = serviceContext.getServiceInstanceContext(null); 一旦取得了ServiceInstanceContext,我们就可以把它看作一个一维的容器,取得其中的任意Service: serviceInstanceContext.getService("ResourceLoaderService"); 上面语句的效果等同于下列对serviceContext调用的语句: serviceContext.getService("ResourceLoaderService", instanceName); 然而serviceInstanceContext.getService()的好处在于,你不需要关心instance的名称 —— 你可以把容器看成是一维的。 5.1.3. 其它接口和类 1. ServiceManager接口继承了ServiceContext接口。 2. DefaultServiceManager实现了ServiceManager接口,从而实现了ServiceContext接口。 3. WebxComponent接口继承了ServiceInstanceContext接口。 4. Webx框架扩展了DefaultServiceManager,并在内部实现WebxComponent接口。 5.1.4. 选择适合你的使用方式 对于简单的独立应用,我们可以直接创建DefaultServiceManager —— 就像本文开始所举的例子一样。此时ServiceContext中只包含一个默认instance。因此,ServiceContext退化成等同于ServiceInstanceContext —— 你可以直接从ServiceContext中取得任何Service,而不需要指明instance的名称(因为只有一个instance)。例如: getServiceManager().getService("ResourceLoaderService"); 对于多CAR结构的Webx应用,Webx框架会为每一个webx.xml创建一个WebxComponent。 WAR └─ WEB-INF │ webx.xml - 默认WebxComponent,instanceName = null │ ├─ car1 │ webx.xml - WebxComponent,instanceName = "car1" └─ car2 webx.xml - WebxComponent,instanceName = "car2" 在每个CAR的模块中,你不需要关心自己的instance名称,你只需要用下列方法取得任意Service: getWebxComponent().getService("ResourceLoaderService"); 5.2. Service相关的接口和类 1. 所有Service必须要实现Service接口。 2. Service接口包含一个init方法。Service容器会在需要时调用Service.init方法,同时传给它一个ServiceConfig对象。 3. ServiceConfig中包含了当前Servie instance的配置信息。 4. 除此之外,ServiceConfig还实现了ServiceInstanceContext接口。事实上,ServiceConfig只是代理了ServiceInstanceContext接口。这样,Service就可以通过ServiceConfig取得同一instance(列)中的其它Service实例。例如: public class MyService implements Service { private ResourceLoaderService resourceLoaderService; public void init(ServiceConfig serviceConfig) throws ServiceInitializationException { resourceLoaderService = (ResourceLoaderService) serviceConfig.getService(ResourceLoaderService.SERVICE_NAME); } …… } 5. 多数Service可以从GenericService派生,以简化Service的实现。 6. GenericService不仅实现了Service接口的所有方法,也顺便实现了ServiceConfig接口。事实上,GenericService只是代理了ServiceConfig接口。这样,Service内部取得其它Service实例就更简单了: public class MyService extends GenericService { private ResourceLoaderService resourceLoaderService; public void init() throws ServiceInitializationException { super.init(); resourceLoaderService = (ResourceLoaderService) getService(ResourceLoaderService.SERVICE_NAME); } …… } 6. 启动Service容器的其它方法 前面,我们已经详细讨论了通过DefaultServiceManager类以及Webx框架来启动Service容器的方法。除此之外,为了使开发更方便,我们还提供了另外几种启动Service容器的方法。 6.1. Web应用(非Webx)启动Service容器 假如你的Web应用没有使用Webx框架,可不可以使用Service框架呢?当然可以。 第一种方法,我们前面已经提到过了 —— 你可以使用WebxControllerListener来初始化Service。 第二种方法,更简单 —— 不启动Webx,只启动一个Service容器。你只需要在web.xml中增加一个Servlet就可以了: …… serviceManager com.alibaba.service.context.WebappServiceManagerLoaderServlet serviceConfig /WEB-INF/services.xml 1 …… 其中,serviceConfig参数代表service配置文件的位置。这个配置文件是用BootstrapResourceLoaderService装载的,因此你也可以从classpath中装载该配置文件。如果serviceConfig参数被省略,则使用默认值:/WEB-INF/services.xml。 程序怎么取得ServiceManager呢?很简单: public class MyServlet extends HttpServlet { private ServiceManager manager; public void init() throws ServletException { manager = WebappServiceManagerLoader.getInstance(getServletConfig()); // 方法1,或者 // manager = WebappServiceManagerLoader.getInstance(getServletContext()); // 方法2 } } 6.2. 用Singleton启动Service容器 有时候,你既没有机会直接创建DefaultServiceManager,也取不到Web应用的ServletContext。此时,前述所有方法都不能使用了。例如:你希望在EJB中使用Service。由于EJB没有像Servlet这样的load-on-startup的机制,所以这种情况下,唯一的创建Service容器的方法就是使用Singleton。 你可以在EJB中需要使用Service的地方,用如下方法取得ServiceManager。 ServiceManager manager = SingletonServiceManagerLoader.getInstance("classpath/conf/services.xml"); 其中,getInstance()的参数指出了service配置文件的位置,它是用BootstrapResourceLoaderService装载的。 假设,两个EJB在不同的时刻,以同一个配置文件参数,采用上述方法来取得ServiceManager,那么它们将取得同一个ServiceManager的实例。当第一次调用该方法时,ServiceManager被创建并初始化。 6.3. 用Spring启动Singleton Service容器 在开发WEB应用时,为了分离WEB层和业务层的对象,我们常常将WEB层的Service容器和业务层的Service容器彻底分开。这样,WEB层的Spring容器也随之和业务层的Spring容器彻底分开了。如图所示: 假如我们有2个Webx CAR,如前所述,我们将会有两个WebxComponent,包含各自独立的Spring容器(BeanFactoryService)。为了防止WEB层程序误用,我们不应该在这两个Spring容器中放置业务层的beans,例如AO、DAO等。我们可以在这两个Spring容器中配置CommandDispatcher,如下所示: …… /biz/services.xml …… 这个commandDispatcher在初始化时,会调用SingletonServiceManagerLoader来启动一个独立于WEB层的业务层Service容器,再从中取得业务层的Spring容器。所有业务层的对象,都配置在这个Spring容器中。请记住,这是一个Singleton,意味着业务层的Spring容器只会被初始化一遍。 现在还有一个小问题:业务层的Service容器只会在第一次调用CommandDispatcher时被初始化。在生产环境中,我们希望这些Service在系统启动时就初始化好,好让我们及早知道初始化是否成功。为了达到这个目的,你只要在WEB层的Spring配置文件中增加下列bean的配置就可以了: …… 需要注意几点: 1. serviceConfig指出了service配置文件的位置,它必须和CommandDispatcher中的参数值完全相同,才能保证它们装载了同一份Singleton的Service容器。 2. serviceConfig是从当前的ResourceLoaderService中装载的,所以你可以把这个文件存在任何WEB层ResourceLoaderService可以访问得到的地方,例如:/WEB-INF目录、classpath等。 3. 请确保serviceConfig配置文件中包含了BeanFactoryService的定义。 有关方案的详情,请参见:《业务层框架 —— Command Pattern指南》。 7. Service框架和解耦 7.1. Service框架和Spring框架解耦的比较 虽然我们的Service框架还没有实现“依赖注入(DI)”的功能,但是我们确实达到了对Service解耦的目的。假如,有两个Service:ServiceA和ServiceB。在ServiceA中用到了ServiceB。由于ServiceA和ServiceB都是接口,因此它们的实现类是绝对不会相互耦合的。 这符合了Dependency Inversion Principle(DIP): 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 高层的模块(类)不应该依赖低层的模块(类)。它们都应该依赖抽象(类或接口)。 2. Abstractions should not depend upon details. Details should depend upon abstractions. 抽象(类或接口)不应该依赖它们的具体实现。具体实现应该依赖它们的抽象。 在这个例子中,ServiceA和ServiceB不依赖它们的实现,反之,它们的实现依赖各自的抽象接口。另一方面,ServiceAImpl(高层的类)不依赖ServiceBImpl(低层的类),它们都依赖ServiceB抽象接口。 那么,Service框架和现在流行的依赖注入(DI)框架(如Spring、HiveMind、PicoContainer)的本质区别在哪里呢?区别在于组装对象的方法。仍以ServiceA和ServiceB为例: 1. 在Service框架中,ServiceAImpl必须通过ServiceContext(或ServiceInstanceContext)取得ServiceB的实现; 而在Spring等DI框架中,ServiceB的实现被注入到ServiceAImpl中。 2. 在Service框架中,ServiceAImpl必须依赖Service框架的API才能取得ServiceB; 而在Spring等DI框架中,ServiceAImpl不需要依赖Spring框架。 从Service对框架的依赖度来看,Service框架显得比Spring更“重”一些。那么,是不是Spring下的Service一定不需要依赖容器呢?不一定。 首先,有很多时候,在设计期间无法确定向Service中注入哪些内容。这样的情况在Spring的外围类库中比比皆是。例如:Spring MVC框架中,所有的Controller、View、ViewResolver、HandlerMapping等核心类,无一不实现了ApplicationContextAware接口,从而依赖于Spring容器的引用。随后,这些类通过Spring容器,主动地取得其它相关的对象、资源。 其次,有很多时候,不是Service本身需要依赖容器,而是Service所直接、间接创建的某些对象需要依赖其它的Service。为了不把内部实现细节完全暴露给最终开发者,我们不希望把所有细枝末节的对象都交给容器来管理。假如,这些对象所依赖的内容,同样无法在设计期间完全获知的话,我们能做的就只有把容器本身作为参数传递给这个对象了。 因此,在上述情况下,我们的Service框架和Spring并无本质的差别。 7.2. Service依赖容器时需要注意的事项 我们知道,在Service框架中,所有的Service都依赖于容器。在Service初始化的时候,容器会传递给Service一个ServiceConfig对象。ServiceConfig是从ServiceInstanceContext接口扩展的 —— 代表了容器的引用。Service可以通过ServiceConfig来取得其它Service。例如: public class ServiceA implements Service { private ResourceLoaderService resourceLoaderService; public void init(ServiceConfig serviceConfig) throws ServiceInitializationException { resourceLoaderService = (ResourceLoaderService) serviceConfig.getService(ResourceLoaderService.SERVICE_NAME); } …… } 对于Spring框架来说,在前面所述的情形中,Service(Spring称为bean)有时也不得不依赖容器。Spring bean依赖容器的方法是:实现ApplicationContext接口。ApplicationContext接口是从BeanFactory接口派生的 —— 代表了Spring容器的引用。例如: public class ServiceA implements ApplicationContextAware { private ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void someMethod() { applicationContext.getBean("serviceB"); } } 无论是Service框架还是Spring框架,当Service依赖容器时,应当特别注意一个问题。请看下面的代码: class SingletonServiceManager { private static ServiceManager manager = createServiceManager(); public static ServiceManager getInstance() { return manager; } …… } interface ServiceA { void serve(); } interface ServiceB { void serve(); } class ServiceAImpl extends GenericService implements ServiceA { public void serve() { ServiceB serviceB = (ServiceB) SingletonServiceManager.getInstance().getService("ServiceB"); serviceB.serve(); } } class ServiceBImpl extends GenericService implements ServiceB { public void serve() { } } 这段代码实现了两个Service:ServiceA和ServiceB。此外,SingletonServiceManager负责创建ServiceManager的实例。当ServiceAImpl需要用到ServiceB的时候,它调用SingletonServiceManager.getInstance()来取得ServiceManager的实例,然后从中取得ServiceB的实例: class ServiceAImpl extends …… { public void serve() { ServiceB serviceB = (ServiceB) SingletonServiceManager.getInstance().getService("ServiceB"); …… } } 这样做有问题吗?是的。 ² 假如,我想对ServiceA进行单元测试,在测试时,希望用Mock对象来代替ServiceManager和ServiceB。但这是不可能的。因为ServiceA只能从SingletonServiceManager中取得ServiceManager和ServiceB。 ² 假如,我想把ServiceA和ServiceB用于WEB环境(Webx也好,其它框架也好)。但这也是不可能的。因为ServiceA无法取得Web环境中的Service容器 —— 它只能从SingletonServiceManager中取得ServiceManager和ServiceB。 ² 假如,我想在Webx多实例的环境中使用ServiceA,每个ServiceA的实例引用不同的ServiceB的实例。但这也是不可能的。因为ServiceA只能从一处取得ServiceB。 问题的原因是什么呢?下面的类图表现了各类和接口之间的派生、依赖关系: 从图中可以清楚看到:ServiceAImpl依赖了SingletonServiceManager类。而SingletonServiceManager类是一个具体实现的类(concrete class),而不是抽象的类(abstract class)。所以这个程序违反了DIP原则:具体实现应该依赖抽象。由于具体类是不可替换的,因此ServiceA一旦依赖了这个具体的类,就意味着它和这个ServiceManager的实现紧密耦合了。除非修改ServiceA的代码,我们就无法改变ServiceManager的实现。因此ServiceA也是不可移植的。 有时候,不是Service自己,而是Service所创建的对象需要依赖容器。请看下面的程序代码和依赖关系类图: class FactoryServiceImpl extends GenericService implements FactoryService { public Object create() { return new MyObject(); } } class MyObject { public void someMethod() { ServiceB serviceB = (ServiceB) SingletonServiceManager.getInstance().getService("ServiceB"); serviceB.serve(); } } MyObject依赖SingletonServiceManager同样带来了强耦合的危害性。怎么去除这种耦合呢?方法是:永远使用被动的方式来取得容器引用,而不要主动去取得容器的引用。 对于Spring管理的Service,实现ApplicationContextAware接口,就可以被动获得容器的引用。对于Service框架,Service接口中有一个init(ServiceConfig serviceConfig)的方法,可以用来被动接收容器的引用,并初始化Service。如果你从GenericService派生,那么事情变得更简单了。我们可以将ServiceAImpl中的: ServiceB serviceB = (ServiceB) SingletonServiceManager.getInstance().getService("ServiceB"); 改进成如下样式: ServiceB serviceB = (ServiceB) getServiceConfig().getService("ServiceB"); 甚至还可以再简单一点: ServiceB serviceB = (ServiceB) getService("ServiceB"); 由于ServiceConfig是被动设置进来的,所以经过修改以后的Service和ServiceManager的具体实现解耦了。这样的Service既可以被单元测试,也可以在不同的Service容器的实现中使用,还可以在多实例的环境中使用。 我们再来改进一下FactoryServiceImpl和MyObject: class FactoryServiceImpl extends GenericService implements FactoryService { public Object getObject() { return new MyObject(getServiceConfig()); } } class MyObject { private ServiceInstanceContext context; public MyObject(ServiceInstanceContext context) { this.context = context; } public void someMethod() { ServiceB serviceB = (ServiceB) context.getService("ServiceB"); serviceB.serve(); } } FactoryServiceImpl在创建MyObject时,将自己的ServiceConfig传递给MyObject。由于ServiceConfig继承了ServiceInstanceContext接口,因而代表了Service容器。MyObject被动接收容器以后,就可以通过该容器来取得其它的Service。这样,MyObject也和ServiceManager的具体实现解除了耦合,因而可被测试和重用。 7.3. 进一步改进的方案 在这个例子中,我们可以看出,如果没有依赖注入的机制,我们必须在整个依赖链中,由上到下地传递容器的引用。MyObject必须从它的上层模块取得Service容器。如果MyObject还要创建其它对象,那么它还要将Service容器继续向下传递,这是十分麻烦的,有时候甚至是不可能的。 然而Spring的注入机制有一个问题:在Spring容器中的所有beans都是对最终开发者公开的,并由最终开发者来管理的。因此,有时候,为了避免向最终开发者暴露实现的细节 —— 例如,我不希望向最终开发者暴露 MyObject类,我也不希望最终开发者来管理这个类 —— 我们不得不避免使用注入的机制。 所以我想,最终的改进方案应该是这样的:建立分级的注入机制,将Service的提供者和使用者的权责分开。这样就能够既让Service充分享有注入的好处,又不会对最终开发者(即Service使用者)暴露他们所不需要关心,也不应该关心的实现细节。 8. 总结 Service框架是我们目前所有基于Java的Web层、业务层、数据访问层框架的基础。 Service框架能够确保Service之间的耦合是松散的。然而,由于没有提供“依赖注入(DI)”的机制,因此它看起来比Spring等框架要“重”一些。但是Spring亦有不足之处。 Service框架还会发展。下一个版本的Service框架,代号为:Citrus。集各家之所长,结合我们自己的经验,Citrus最终将成为一个高效、轻量、易用、分层的通用框架。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享文档获得金币 ]
4 人已下载

下载文档

相关文档