首先,本文的目标读者是正在从事技术工作的架构师。为了避免浪费大家的才智,我会避免讲述一些陈腐的最佳实践,例如"日常构建(build daily)"、"测试一切(test everything)"和"经常集成( integrate often)。 任何具有称职架构师的项目都有分工明确的、定义良好的团队结构。他们还为进行编码检查、构建代码(每日或在需要时)、进行测试(单元、集成和系统的)、部署和配置/释放管理而具备已记录的过程。
其次,我将跳过通常吹捧的最佳实践,例如"基于接口的设计"、"使用著名的设计模型"以及"使用面向服务的架构"等。相反,我将集中讲述我曾学过并且使用了若干年的6(不是很多)个方面的in-the-trench课程。最后,本文的目的是让您思考一下自己的架构,提供工作代码示例或者解决方案超出了本文的范围。下面就让我介绍一下这6课:
第1课:切勿绕过服务器端验证
作为一位软件顾问,我曾有机会不但设计并实现了Web应用程序,而且还评估/审核了许多Web应用程序。在复杂的、并且用JavaScript客户端封装的应用程序内,我经常遇到对用户输入信息执行大量检查的Web页面。即使HTML元素具有数据有效性的属性也如此,例如MAXLENGTH。只有在成功验证所有输入信息后,才能提交HTML表单。结果,一旦服务器端收到通知表单(请求),便恰当地执行业务逻辑。
在此,您发现问题了么?开发人员已经做了许多重要的假设。例如,他们假设所有的Web应用程序用户都同样诚实。开发人员还假设所有用户将总是使用他们测试过的浏览器访问Web应用程序。还有很多其他的假设。这些开发人员忘记了利用可以免费得到的工具,通过命令行很容易地模拟类似浏览器的行为。事实上,通过在浏览器窗口中键入适当的URL,您可以发送任何"posted"表单,尽管如此,通过禁用这些页面的GET请求,您很容易地阻止这样的"表单发送"。但是,您不能阻止人们模拟甚至创建他们自己的浏览器来入侵您的系统。
根本的问题在于开发人员不能确定客户端验证与服务器端验证的主要差别。两者的主要差别不在于验证究竟发生在哪里,例如在客户端或者在服务器端。主要的差别在于验证背后的目的不同。
客户端验证仅仅是方便。执行它可为用户提供快速反馈??使应用程序似乎做出响应,给人一种运行桌面应用程序的错觉。
另一方面,服务器端验证是构建安全Web应用程序必需的。不管在客户端一侧输入的是什么,它可以确保客户端送往服务器的所有数据都是有效的。
因而,只有服务器端验证才可以提供真正应用程序级的安全。许多开发人员陷入了错误感觉的圈套:只有在客户端进行所有数据的验证才能确保安全。下面是说明此观点的一个常见的示例:
一个典型的登录页面拥有一个用来输入用户名的文本框和一个输入密码的文本框。在服务器端,某人在接收servlet中可能遇到一些代码,这些代码构成了下面形式的SQL查询:
"SELECT * FROM SecurityTable WHERE username = '" + form.getParameter("username") + "' AND password = '" + form.getParameter("password") + "';",并执行这些代码。如果查询在结果集的某一行返回,则用户登录成功,否则用户登录失败。
第一个问题是构造SQL的方式,但现在让我们暂时忽略它。如果用户在用户名中输入"Alice'--"会怎样呢?假设名为"Alice"的用户已经在SecurityTable中,这时此用户(更恰当的说法是黑客)成功地登录。我将把找出为什么会出现这种情况的原因做为留给您的一道习题。
许多创造性的客户端验证可以阻止一般的用户从浏览器中这样登录。但对于已经禁用了JavaScript的客户端,或者那些能够使用其他类似浏览器程序直接发送命令(HTTP POST和GET命令)的高级用户(或者说黑客)来说,我们又有什么办法呢?服务器端验证是防止这种漏洞类型所必须的。这时,SSL、防火墙等都派不上用场了。
第2课:安全并非是附加物
如第1课所述,我曾有幸研究过许多Web应用程序。我发现所有的JavaServer Page(JSP)都有一个共同的主题,那就是具有类似下面伪代码的布局:
HTML, JavaScript, and JSP
code to display data and
allow user interaction -->
如果项目使用诸如Struts这样的MVC框架,所有的Action Bean都会具有类似的代码。尽管最后这些代码可能运行得很好,但如果您发现一个bug,或者您必须添加一个新的角色(例如,"guest"或者"admin"),这就会代表一场维护恶梦。
此外,所有的开发人员,不管您多年轻,都需要熟悉这种编码模式。当然,您可以用一些JSP标签来整理JSP代码,可以创建一个清除派生Action Bean的基本Action Bean。尽管如此,由于与安全相关的代码会分布到多个地方,所以维护时的恶梦仍旧存在。由于Web应用程序的安全是强迫建立在应用程序代码的级别上(由多个开发人员),而不是建立在架构级别上,所以Web应用程序还是很可能存在弱点。
很可能,根本的问题是在项目接近完成时才处理安全性问题。最近作为一名架构师,我曾在一年多的时间里亲历了某一要实现项目的6个版本,而直到第四版时我们才提到了安全性??即使该项目会将高度敏感的个人数据暴露于Web上,我们也没有注意到安全性。为了更改发布计划,我们卷入了与项目资助人及其管理人员的争斗中,以便在第一版中包含所有与安全相关的功能,并将一些"业务"功能放在后续的版本中。最终,我们赢得了胜利。而且由于应用程序的安全性相当高,能够保护客户的私有数据,这一点我们引以为荣,我们的客户也非常高兴。
遗憾的是,在大多数应用程序中,安全性看起来并未增加任何实际的商业价值,所以直到最后才解决。发生这种情况时,人们才匆忙开发与安全相关的代码,而丝毫没有考虑解决方案的长期可维护性或者健壮性。忽视该安全性的另一个征兆是缺乏全面的服务器端验证,如我在第1课中所述,这一点是安全Web应用程序的一个重要组成部分。
记住:J2EE Web应用程序的安全性并非仅仅是在Web.xml 和ejb-jar.xml文件中使用合适的声明,也不是使用J2EE技术,如Java 认证和授权服务(Java Authentication and Authorization Service,JAAS)。而是经过深思熟虑后的设计,且实现一个支持它的架构。
第3课:国际化(I18N)不再是纸上谈兵
当今世界的事实是许多英语非母语的人们将访问您的公共Web应用程序。随着电子政务的实行,由于它允许人们(某个国家的居民)在线与政府机构交互,所以这一点特别真实。这样的例子包括换发驾照或者车辆登记证。许多第一语言不是英语的人们很可能将访问这样的应用程序。国际化(即:"i18n",因为在"internationalization"这个单词中,字母i和字母n之间一共有18个字母)使得您的应用程序能够支持多种语言。
显然,如果您的JSP 页面中有硬编码的文本,或者您的Java代码返回硬编码的错误消息,那么您要花费很多时间开发此Web应用程序的西班牙语版本。然而,在Web应用程序中,为了支持多种语言,文本不是惟一必须"具体化"的部分。因为许多图像中嵌有文字,所以图形和图像也应该是可配置的。在极端的情况下,图像(或者颜色)在不同的文化背景中可能有完全不同的意思。类似地,任何格式化数字和日期的Java代码也必须本地化。但问题是:您的页面布局可能也需要更改。
例如,如果您使用HTML表格来格式化和显示菜单选项、应用程序题头或注脚,则您可能必须为每一种支持的语言更改每一栏的最小宽度和表格其他可能的方面。为了适应不同的字体和颜色,您可能必须为每一种语言使用单独的样式表。
显然,现在创建一个国际化的Web应用程序面临的是架构挑战而不是应用程序方面的挑战。一个架构良好的Web应用程序意味着您的JSP页面和所有与业务相关的(应用程序特有的)Java代码都不知不觉地选择了本地化。要记住的教训是:不要因为Java、J2EE支持国际化而不考虑国际化。您必须从第一天起就记住设计具有国际化的解决方案。
第4课:在MVC表示中避免共同的错误
J2EE开发已经足够成熟,在表示层,大多数项目使用MVC架构的某些形式,例如Struts。在这样的项目中,我常见到的现象是对MVC模式的误用。下面是几个示例。
常见的误用是在模型层(例如,在Struts的Action Bean中)实现了所有的业务逻辑。不要忘了,表示层的模型层仍然是表示层的一部分。使用该模型层的正确方法是调用适当的业务层服务(或对象)并将结果发送到视图层(view layer)。用设计模式术语来说,MVC表示层的模型应该作为业务层的外观(Fa?ade)来实现。更好的方法是,使用核心J2EE模式(Core J2EE Patterns)中论述到的Business Delegate模式。这段自书中摘录的内容精彩地概述了将您的模型作为Business Delegate来实现的要点和优点:
Business Delegate起到客户端业务抽象化的作用。它抽象化,进而隐藏业务服务的实现。使用Business Delegate,可以降低表示层客户端和系统的业务服务.之间的耦合程度。根据实现策略不同,Business Delegate可以在业务服务API的实现中,保护客户端不受可能的变动性影响。这样,在业务服务API或其底层实现变化时,可以潜在地减少必须修改表示层客户端代码的次数。
另一个常见的错误是在模型层中放置许多表示类型的逻辑。例如,如果JSP页面需要以指定方式格式化的日期或者以指定方式排序的数据,某些人可能将该逻辑放置在模型层,对该逻辑来说,这是错误的地方。实际上,它应该在JSP页面使用的一组helper类中。当业务层返回数据时,Action Bean应该将数据转发给视图层。这样,无需创建模型和视图之间多余的耦合,就能够灵活支持多个视图层(JSP、Velocity、XML等)。也使视图能够确定向用户显示数据的最佳方式。
最后,我见过的大多数MVC应用程序都有未充分应用的控制器。例如,绝大多数的Struts应用程序将创建一个基本的Action类,并完成所有与安全相关的功能。其他所有的Action Bean都是此基类的派生类。这种功能应该是控制器的一部分,因为如果没有满足安全条件,则首先调用不应该到达Action Bean(即:模型)。记住,一个设计良好的MVC架构的最强大功能之一是存在一个健壮的、可扩展的控制器。您应该利用该能力以加强自己的优势。
第5课:不要被JOPO束缚住手脚
我曾目睹许多项目为了使用Enterprise JavaBean而使用Enterprise JavaBean。因为EJB似乎给项目带来优越感和妄自尊大的表现,所以有时它是显酷的要素(coolness factor)。而其他时候,它会使J2EE和EJB引起混淆。记住,J2EE和EJB不是同意词。EJB只是J2EE 的一部分,J2EE 是包含JSP、servlet、Java 消息服务(JMS)、Java数据库连接(JDBC)、JAAS、 Java管理扩展(JMX)和EJB在内的一系列技术,同样也是有关如何共同使用这些技术建立解决方案的一组指导原则和模式。
如果在不需要使用EJB的情况下使用EJB,它们可能会影响程序的性能。与老的Web服务器相比,EJB一般对应用服务器有更多的需求。EJB提供的所有增值服务一般需要消耗更大的内存和更多的CPU时间。许多应用程序不需要这些服务,因此应用服务器要与应用程序争夺资源。
在某些情况下,不必要地使用EJB可能使应用程序崩溃。例如,最近我遇到了一个在开源应用服务器上开发的应用程序。业务逻辑封装在一系列有状态会话bean(EJB)中。开发人员为了在应用服务器中完全禁用这些bean的"钝化"费了很大的劲。客户端要求应用程序部署在某一商用应用服务器上,而该服务器是客户端技术栈的一部分。该应用服务器却不允许关闭"钝化"功能。事实上,客户端不想改变与其合作的应用服务器的设任何置。结果,开发商碰到了很大的麻烦。(似乎)有趣的事情是开发商自己都不能给出为什么将代码用EJB(而且还是有状态会话bean)实现的好理由。不仅仅是开发商会遇到性能问题,他们的程序在客户那里也无法工作。
在Web应用程序中,无格式普通Java 对象(POJO)是EJB强有力的竞争者。POJO是轻量级的,不像EJB那样负担额外的负担。在我看来,对许多EJB的优点,例如对象入池,估计过高。POJO是您的朋友,不要被它束缚住手脚。
第6课:数据访问并不能托管O/R映射
我曾参与过的所有Web应用程序都向用户提供从其他地方存取的数据,并且因此需要一个数据访问层。这并不是说所有的项目都需要标识并建立这样一个层,这仅仅说明这样层的存在不是隐含的就是明确的。如果是隐含的数据层,数据层是业务对象(即:业务服务)层的一部分。这适用于小型应用程序,但通常与大一些项目所接受的架构指导原则相抵触。
总之,数据访问层必须满足或超出以下四个标准:
具有透明性
业务对象在不知道数据源实现的具体细节情况下,可以使用数据源。由于实现细节隐藏在数据访问层的内部,所以访问是透明的。
易于迁移
数据访问层使应用程序很容易迁移到其他数据库实现。业务对象不了解底层的数据实现,所以迁移仅仅涉及到修改数据访问层。进一步地说,如果您正在部署某种工厂策略,您可以为每个底层的存储实现提供具体的工厂实现。如果是那样的话,迁移到不同的存储实现意味着为应用程序提供一个新的工厂实现。
尽量减少业务对象中代码复杂性
因为数据访问层管理着所有的数据访问复杂性,所以它可以简化业务对象和使用数据访问层的其他数据客户端的代码。数据访问层,而不是业务对象,含有许多与实现相关的代码(例如SQL语句)。这样给开发人员带来了更高的效率、更好的可维护性、提高了代码的可读性等一系列好处。
把所有的数据访问集中在单独的层上
由于所有的数据访问操作现在都委托给数据访问层,所以您可以将这个单独的数据访问层看做能够将应用程序的其他部分与数据访问实现相互隔离的层。这种集中化可以使应用程序易于维护和管理。
注意:这些标准都不能明确地调出对O/R(对象到关系)映射层的需求。O/R映射层一般用O/R映射工具创建,它提供对象对关系数据结构的查看和感知(look-and-feel)。在我看来,在项目中使用O/R映射与使用EJB类似。在大多数情况下,并不要求它。对于包含中等规模的联合以及多对多关系的关系型数据库来说,O/R映射会变得相当复杂。由于增加O/R 映射解决方案本身的内在复杂性,例如延迟加载(lazy loading)、高速缓冲等,您将为您的项目带来更大的复杂性(和风险)。
为了进一步支持我的观点,我将指出按照Sun Microsystem所普及的实体Bean(O/R映射的一种实现)的许多失败的尝试,这是自1.0版以来一直折磨人的难题。在SUN的防卫措施中,一些早期的问题是有关EJB规范的开发商实现的。这依次证明了实体Bean规范自身的复杂性。结果,大多数J2EE架构师一般认为从实体Bean中脱离出来是一个好主意。
大多数应用程序在处理他们的数据时,只能进行有限次数的查询。在这样的应用程序中,访问数据的一种有效方法是实现一个数据访问层,该层实现执行这些查询的一系列服务(或对象、或API)。如上所述,在这种情况下,不需要O/R映射。当您要求查询灵活性时,O/R映射正合适,但要记住:这种附加的灵活性并不是没有代价的。
就像我承诺的那样,在本文中,我尽量避免陈腐的最佳实践。相反,关于J2EE项目中每一位架构师必须做出的最重要的决定,我集中讲解了我的观点。最后,您应该记住:J2EE并非某种具体的技术,也不是强行加入到解决方案中的一些首字母缩写。相反,您应该在适当的时机,恰当的地方,使用合适的技术,并遵循J2EE的指导原则和J2EE中所包含的比技术本身重要得多的实践。