这篇文章将讨论怎样组合几个著名的框架去做到松耦合的目的,怎样建立你的构架,怎样让你的各个应用层保持一致。富于挑战的是:组合这些框架使得每一层都以一种松耦合的方式彼此沟通,而与底层的技术无关。这篇文章将使用3种流行的开源框架来讨论组合框架的策略
其实,就算用java建造一个不是很烦琐的web应用程序,也不是件轻松的事情。当为一个应用程序建造一个构架时有许多事情需要考虑。从高层来说,开发者需要考虑:怎样建立用户接口?在哪里处理业务逻辑?和怎样持久化应用数据。这三层每一层都有它们各自的问题需要回答。 各个层次应该使用什么技术?怎样才能把应用程序设计得松耦合和能灵活改变?构架允许层的替换不会影响到其它层吗?应用程序怎样处理容器级的服务,比如事务处理?
当为你的web应用程序创建一个构架时,需要涉及到相当多的问题。幸运的是,已经有不少开发者已经遇到过这类重复发生的问题,并且建立了处理这类问题的框架。一个好框架具备以下几点: 减轻开发者处理复杂的问题的负担(“不重复发明轮子”);内部定义为可扩展的;有一个强大的用户群支持。框架通常能够很好的解决一方面的问题。然而,你的应用程序有几个层可能都需要它们各自的框架。就如解决你的用户接口(ui)问题时你就不应该把事务逻辑和持久化逻辑掺杂进来。例如,你不应该在控制器里面写jdbc代码,使它包含有业务逻辑,这不是控制器应该提供的功能。它应该是轻量级的,代理来自用户接口(ui)外的调用请求给其它服务于这些请求的应用层。好的框架自然的形成代码如何分布的指导。更重要的是,框架减轻开发者从头开始写像持久层这样的代码的痛苦,使他们专注于对客户来说很重要的应用逻辑。
这篇文章将讨论怎样组合几个著名的框架去做到松耦合的目的,怎样建立你的构架,怎样让你的各个应用层保持一致。富于挑战的是:组合这些框架使得每一层都以一种松耦合的方式彼此沟通,而与底层的技术无关。这篇文章将使用3种流行的开源框架来讨论组合框架的策略。表现层我们将使用struts;业务层我们将使用spring;持久层使用hibrenate.你也可以在你的应用程序中替换这些框架中的任何一种而得到同样的效果。图1展示了当这些框架组合在一起时从高层看是什么样子。
图1用struts, spring, 和 hibernate框架构建的概览
应用程序的分层
大多数不复杂的web应用都能被分成至少4个各负其责的层次。这些层次是:表现层、持久层、业务层、领域模型层。每层在应用程序中都有明确的责任,不应该和其它层混淆功能。每一应用层应该彼此独立但要给他们之间放一个通讯接口。让我们从审视各个层开始,讨论这些层应该提供什么和不应该提供什么。
表现层
在一个典型的web应用的一端是表现层。很多java开发者也理解struts所提供的。然而,太常见的是,他们把像业务逻辑之类的耦合的代码放进了一个org.apache.struts.action。所以,让我们在像struts这样一个框架应该提供什么上取得一致意见。这儿是struts负责的:
为用户管理请求和响应;
提供一个控制器代理调用业务逻辑和其它上层处理;
处理从其它层掷出给一个struts action的异常;
为显示提供一个模型;
执行用户接口验证。
这儿是一些经常用struts编写的但是却不应该和struts表现层相伴的项目:
直接和数据库通讯,比如jdbc调用;
业务逻辑和与你的应用程序相关的验证;
事务管理;
在表现层中引入这种代码将导致典型耦合和讨厌的维护。
持久层
在典型web应用的另一端是持久层。这通常是使事情迅速失控的地方。开发者低估了构建他们自己的持久层框架的挑战性。一般来说,机构内部自己写的持久层不仅需要大量的开发时间,而且还经常缺少功能和变得难以控制。有几个开源的“对象-关系映射”框架非常解决问题。尤其是,hibernate框架为java提供了"对象-关系持久化"机制和查询服务。hibernate对那些已经熟悉了sql和jdbc api的java开发者有一个适中的学习曲线。hibernate持久对象是基于简单旧式java对象和java集合。此外,使用hibernate并不妨碍你正在使用的ide。下面的列表包含了你该写在一个持久层框架里的代码类型:
查询相关的信息成为对象。hibernate通过一种叫作hql的面向对象的查询语言或者使用条件表达式api来做这个事情。 hql非常类似于sql-- 只是把sql里的table和columns用object和它的fields代替。有一些新的专用的hql语言成分要学;不过,它们容易理解而且文档做得好。hql是一种使用来查询对象的自然语言,花很小的代价就能学习它。
保存、更新、删除储存在数据库中的信息。
像hibernate这样的高级“对象-关系”映射框架提供对大多数主流sql数据库的支持,它们支持“父/子”关系、事务处理、继承和多态。
这儿是一些应该在持久层里被避免的项目:
业务逻辑应该在你的应用的一个高一些的层次里。持久层里仅仅允许数据存取操作。
你不应该把持久层逻辑和你的表现层逻辑搅在一起。避免像jsps或基于servlet的类这些表现层组件里的逻辑和数据存取直接通讯。通过把持久层逻辑隔离进它自己的层,应用程序变得易于修改而不会影响在其它层的代码。例如:hebernate能够被其它持久层框架或者api代替而不会修改在其它任何层的代码。
业务层
在一个典型的web应用程序的中间的组件是业务层或服务层。从编码的视角来看,这个服务层是最容易被忽视的一层。不难在用户接口层或者持久层里找到散布在其中的这种类型的代码。这不是正确的地方,因为这导致了应用程序的紧耦合,这样一来,随着时间推移代码将很难维护。幸好,针对这一问题有好几种frameworks存在。在这个领域两个最流行的框架是spring和picocontainer,它们叫作微容器,你可以不费力不费神的把你的对象连在一起。所有这些框架都工作在一个简单的叫作“依赖注入”(也通称“控制反转”)的概念上。这篇文章将着眼于spring的为指定的配置参数通过bean属性的setter注入的使用。spring也提供了一个构建器注入的复杂形式作为setter注入的一个替代。对象们被一个简单的xml文件连在一起,这个xml文件含有到像事务管理器、对象工厂、包含业务逻辑的服务对象、和数据存取对象这些对象的引用。
这篇文章的后面将用例子来把spring使用这些概念的方法说得更清楚一些。业务层应该负责下面这些事情:
处理应用程序的业务逻辑和业务验证;
管理事务;
预留和其它层交互的接口;
管理业务层对象之间的依赖;
增加在表现层和持久层之间的灵活性,使它们互不直接通讯;
从表现层中提供一个上下文给业务层获得业务服务;
管理从业务逻辑到持久层的实现。
领域模型层
最后,因为我们讨论的是一个不是很复杂的、基于web的应用程序,我们需要一组能在不同的层之间移动的对象。领域对象层由那些代表现实世界中的业务对象的对象们组成,比如:一份订单、订单项、产品等等。这个层让开发者停止建立和维护不必要的数据传输对象(或者叫作dtos),来匹配他们的领域对象。例如,hibernate允许你把数据库信息读进领域对象的一个对象图,这样你可以在连接断开的情况下把这些数据显示到ui层。那些对象也能被更新和送回到持久层并在数据库里更新。而且,你不必把对象转化成dtos,因为dtos在不同的应用层间移动,可能在转换中丢失。这个模型使得java开发者自然地以一种面向对象的风格和对象打交道,没有附加的编码。
结合一个简单的例子
既然我们已经从一个高的层次上理解了这些组件, 现在就让我们开始实践吧。在这个例子中,我们还是将合并struts、spring、hibernate框架。每一个这些框架在一篇文章中都有太多的细节覆盖到。这篇文章将用一个简单的例子代码展示怎样把它们结合在一起,而不是进入每个框架的许多细节。示例应用程序将示范一个请求怎样跨越每一层被服务的。这个示例应用程序的一个用户能保存一个订单到数据库中和查看一个在数据库中存在的订单。进一步的增强可以使用户更新或删除一个存在的订单。
因为领域对象将和每一层交互,我们将首先创建它们。这些对象将使我们定义什么应该被持久化,什么业务逻辑应该被提供,和哪种表现接口应该被设计。然后,我们将配置持久层和用hibernate为我们的领域对象定义“对象-关系”映射。然后,我们将定义和配置我们的业务对象。在有了这些组件后,我们就能讨论用spring把这些层连在一起。最后,我们将提供一个表现层,它知道怎样和业务服务层交流和知道怎样处理从其它层产生的异常。
领域对象层
因为这些对象将和所有层交互,这也许是一个开始编码的好地方。这个简单的领域模型将包括一个代表一份订单的对象和一个代表一个订单项的对象。订单对象将和一组订单项对象有一对多的关系。例子代码在领域层有两个简单的对象:
com.meagle.bo.order.java: 包括一份订单的概要信息;
com.meagle.bo.orderlineitem.java: 包括一份订单的详细信息;
考虑一下为你的对象选择包名,它将反映你的应用程序是怎样分层的。例如:简单应用的领域对象可以放进com.meagle.bo包。更多专门的领域对象将放入在com.meagle.bo下面的子包里。业务逻辑在com.meagle.service包里开始打包,dao对象放进com.meagle.service.dao.hibernate包。对于forms和actions的表现类分别放入com.meagle.action 和 com.meagle.forms包。准确的包命名为你的类提供的功能提供一个清楚的区分,使当故障维护时更易于维护,和当给应用程序增加新的类或包时提供一致性。
持久层配置
用hibernate设置持久层涉及到几个步骤。第一步是进行配置持久化我们的领域业务对象。因为我们用于领域对象持久化的hibernate和pojos一起工作,因此,订单和订单项对象包括的所有的字段的都需要提供getter和setter方法。订单对象将包括像id、用户名、合计、和订单项这样一些字段的标准的javabean格式的setter和getter方法。订单项对象将同样的用javabean的格式为它的字段设置setter和getter方法。
hibernate在xml文件里映射领域对象到关系数据库。订单和订单项对象将有两个映射文件来表达这种映射。有像xdoclet这样的工具来帮助这种映射。hibernate将映射领域对象到这些文件:
order.hbm.xml
orderlineitem.hbm.xml
你可以在webcontent/web-inf/classes/com/meagle/bo目录里找到这些生成的文件。配置hibernate sessionfactory使它知道是在和哪个数据库通信,使用哪个数据源或连接池,加载哪些持久对象。sessionfactory提供的session对象是java对象和像选取、保存、更新、删除对象这样一些持久化功能间的翻译接口。我们将在后面的部分讨论hibernate操作session对象需要的sessionfactory配置。
业务层配置
既然我们已经有了领域对象,我们需要有业务服务对象来执行应用逻辑、执行向持久层的调用、获得从用户接口层的请求、处理事务、处理异常。为了将所有这些连接起来并且易于管理,我们将使用spring框架的bean管理方面。spring使用“控制反转”,或者“setter依赖注入”来把这些对象连好,这些对象在一个外部的xml文件中被引用。“控制反转”是一个简单的概念,它允许对象接受其它的在一个高一些的层次被创建的对象。使用这种方法,你的对象从必须创建其它对象中解放出来并降低对象耦合。
这儿是个不使用ioc的对象创建它的从属对象的例子,这导致紧的对象耦合:
图2:没有使用ioc的对象组织。对象a创建对象b和c。
这儿是一个使用ioc的例子,它允许对象在一个高一些层次被创建和传进另外的对象,所以另外的对象能直接使用现成的对象?[译者注:另外的对象不必再亲自创建这些要使用的对象]:
图3:对象使用ioc组织。对象a包含setter方法,它们接受到对象b和c的接口。这也可以用对象a里的接受对象b和c的构建器完成。
建立我们的业务服务对象
我们将在我们的业务对象中使用的setter方法接受的是接口,这些接口允许对象的松散定义的实现,这些对象将被设置或者注入。在我们这个例子里我们将使我们的业务服务对象接受一个dao去控制我们的领域对象的持久化。当我们在这篇文章的例子中使用hibernate,我们可以容易的转换到一个不同的持久框架的实现,通知spring使用新的实现的dao对象。你能明白编程到接口和使用“依赖注入”模式是怎样宽松耦合你的业务逻辑和你的持久化机制的。
这儿是业务服务对象的接口,它是一个dao对象依赖的桩。
public interface iorderservice { public abstract order saveneworder(order order) throws orderexception, orderminimumamountexception; public abstract list findorderbyuser( string user) throws orderexception; public abstract order findorderbyid(int id) throws orderexception; public abstract void setorderdao( iorderdao orderdao); } |
注意上面的代码有一个为dao对象准备的setter方法。这儿没有一个getorderdao方法因为它不是必要的,因为不太有从外面访问连着的orderdao对象的需要。dao对象将被用来和我们的持久层沟通。我们将用spring把业务服务对象和dao对象连在一起。因为我们编码到接口,我们不会紧耦合实现。
下一步是写我们的dao实现对象。因为spring有内建的对hibernate的支持,这个例子dao将继承hibernatedaosupport类,这使得我们容易取得一个到hibernatetemplate类的引用,hibernatetemplate是一个帮助类,它能简化hibernate session的编码和处理hibernateexceptions。这儿是dao的接口:
public interface iorderdao { public abstract order findorderbyid( final int id); public abstract list findordersplacebyuser( final string placedby); public abstract order saveorder( final order order); } |
我们还有两个对象要和我们的业务层连在一起。这包括hibernatesessionfactory和一个transactionmanager对象。这在spring配置文件里直接完成。spring提供一个hibernatetransactionmanager,它将从工厂绑定一个hibernate session到一个线程来支持事务。这儿是hibernatesessionfactory和hibernatetransactionmanager的spring配置。
每一个对象能被spring配置里的一个 既然我们已经配置了我们的容器服务beans和把它们连在了一起,我们需要把我们的业务服务对象和我们的dao对象连在一起。然后,我们需要把这些对象连接到事务管理器。 这是在spring配置文件里的样子: 图4:这是spring怎样将在这个配置的基础上装配beans。 这个例子使用一个transactionproxyfactorybean,它有一个为我们已经定义了的事务管理者准备的setter方法。这是一个有用的对象,它知道怎样处理声明的事务操作和你的服务对象。你可以通过transactionattributes属性定义事务怎样被处理,transactionattributes属性为方法名定义模式和它们怎样参与进一个事务。 transactionproxyfactorybean类也有一个为一个target准备的setter,target将是一个到我们的叫作ordertarget的业务服务对象的引用。 ordertarget bean定义使用哪个业务服务对象并有一个指向setorderdao()的属性。orderdao bean将居于这个属性中,orderdao bean是我们的和持久层交流的dao对象。 还有一个关于spring和bean要注意的是bean能以两种模式工作。这两种模式被定义为singleton和prototype。一个bean默认的模式是singleton,意味着一个共享的bean的实例将被管理。这是用于无状态操作--像一个无状态会话bean将提供的那样。当bean由spring提供时,prototype模式允许创建bean的新实例。你应当只有在每一个用户都需要他们自己的bean的拷贝时才使用prototype模式。 提供一个服务定位器 既然我们已经把我们的服务和我们的dao连起来了,我们需要把我们的服务暴露给其它层。通常是一个像使用struts或swing这样的用户接口层里的代码来使用这个服务。一个简单的处理方法是使用一个服务定位器模式的类从一个spring上下文中返回资源。这也可以靠引用bean id通过spring来直接完成。 这儿是一个在struts action中怎样配置一个服务定位器的例子: 用户接口层配置 示例应用的用户接口层使用struts框架。这儿我们将讨论当为一个应用分层时和struts相关的部分。让我们从在struts-config.xml文件里检查一个action配置开始。 saveneworder action被用来持久化一个用户从用户接口层提交的订单。这是一个典型的struts action;然而,注意这个action的异常配置。这些exceptions为我们的业务服务对象也在spring 配置文件中配置了。当这些异常被从业务层掷出我们能在我们的用户接口里恰当的处理它们。第一个异常,orderexception,当在持久层里保存订单对象失败时将被这个action使用。这将引起事务回滚和通过业务对象传递把异常传回给struts层。orderminimumamountexception,在业务对象逻辑里的一个事务因为提交的订单达不到最小订单数量而失败也将被处理。然后,事务将回滚和这个异常能被用户接口层恰当的处理。 最后一个连接步骤是使我们的表现层和我们的业务层交互。这已经通过使用前面讨论的服务定位器来完成了。服务层充当一个到我们的业务逻辑和持久层的接口。这儿是 struts中的saveneworder action可能怎样使用一个服务定位器调用一个业务方法: 结论 这篇文章按照技术和架构覆盖了许多话题。从中而取出的主要思想是怎样更好的给你的应用程序分层:用户接口层、持久逻辑层、和其它任何你需要的应用层。这样可以解耦你的代码,允许添加新的代码组件,使你的应用在将来更易维护。这里覆盖的技术能很好的解决这类的问题。不管怎样,使用这样的构架可以让你用其他技术代替现在的层。例如,你也许不想使用hibernate持久化。因为你在你的dao对象中编码到接口,你能怎样使用其它的技术或框架,比如 ibatis,作为一个替代是显而易见的。或者你可能用不同于struts的框架替代你的ui层。改变ui层的实现不会直接影响你的业务逻辑层或者你的持久层。替换你的持久层不会影响你的ui逻辑或业务服务层。集成一个web应用其实也不是一件烦琐的工作,靠解耦你的各应用层和用适当的框架组成它,它能变得更容易处理。
图4是我们已经连在一起的东西的一个概览。它展示了每个对象是怎样相关联的和怎样被spring设置进其它对象中。把这幅图和示例应用中的spring配置文件对比查看它们之间的关系。 public abstract class baseaction extends action {
private iorderservice orderservice;
public void setservlet(actionservlet
actionservlet) {
super.setservlet(actionservlet);
servletcontext servletcontext =
actionservlet.getservletcontext();
webapplicationcontext wac =
webapplicationcontextutils.
getrequiredwebapplicationcontext(
servletcontext);
this.orderservice = (iorderservice)
wac.getbean("orderservice");
}
protected iorderservice getorderservice() {
return orderservice;
}
}
name="orderform"
scope="request"
validate="true"
input="/neworder.jsp">
scope="request"
type="com.meagle.exception.orderexception"/>
scope="request"
type="com.
meagle.
exception.
orderminimumamountexception"/>
public actionforward execute(
actionmapping mapping,
actionform form,
javax.servlet.http.httpservletrequest request,
javax.servlet.http.httpservletresponse response)
throws java.lang.exception {
orderform oform = (orderform)form;
// use the form to build an order object that
// can be saved in the persistence layer.
// see the full source code in the sample app.
// obtain the wired business service object
// from the service locator configuration
// in baseaction.
// delegate the save to the service layer and
// further upstream to save the order object.
getorderservice().saveneworder(order);
oform.setorder(order);
actionmessages messages = new actionmessages();
messages.add(
actionmessages.global_message,
new actionmessage(
"message.order.saved.successfully"));
savemessages(request, messages);
return mapping.findforward("success");
}