写点什么

书摘:Java 开发者的 Rails 之路

  • 2007-12-25
  • 本文字数:8450 字

    阅读完需:约 28 分钟

Ruby 和 Rails 为 web 开发带来了许多优秀思想。关于 Ruby 和 Rails 的书已经不少,但 _ Rails for Java Developers _ 却别具一格。不同于“一切从零开始”的传统方式,我们在整本书中都是基于 Java 开发者的已有知识来展开讲述。

将本书建构在 Java 知识之上可以使我们的讲述以比较的方式进行。贯穿全书始终,我们都将示例的 Rails 版本和 Java web 框架版本相比较,以便您能够迅速的了解其中的相似性和差异性。

举例来说,让我们来考察一个控制器方法,它负责保存业务逻辑中的某一模型对象。在这方面,大多数 Java 框架和 Rails 最大的差别在于:大多数 Java 框架中,每一个控制器都会显式的暴露大多数框架提供的编程模型(比如 request、response)。而与之相反,Rails 中对于绝大部分框架编程模型的使用是以隐式的方式进行。在 Rails 中,除非你确实需要 xx,您才应该在程序中看到它。

书摘 1:控制器的保存方法

这里给出一个在 Struts 中保存或更新 person 模型实例的 action:

code / appfuse_people / src/ web/ com/ relevancellc/ people/ webapp/ action/ PersonAction.java

public ActionForward save(ActionMapping mapping, ActionForm form,<br></br> HttpServletRequest request,<br></br> HttpServletResponse response)<br></br> throws Exception {<br></br> ActionMessages messages =new ActionMessages();<br></br> PersonForm personForm = (PersonForm) form;<br></br> boolean isNew = ("".equals(personForm.getId()));<br></br> PersonManager mgr = (PersonManager) getBean("personManager");<br></br> Person person = (Person) convert(personForm);<br></br> mgr.savePerson(person);<br></br> if(isNew) {<br></br> messages.add(ActionMessages.GLOBAL_MESSAGE,<br></br> new ActionMessage("person.added"));<br></br> saveMessages(request.getSession(), messages);<br></br> return mapping.findForward("mainMenu");<br></br> } else {<br></br> messages.add(ActionMessages.GLOBAL_MESSAGE,<br></br> new ActionMessage("person.updated"));<br></br> saveMessages(request, messages);<br></br> return mapping.findForward("viewPeople");<br></br> }<br></br> }让我们从用户编辑成功这个令人愉快的场景开始思考。这段代码的大多数内容与前面的例子相似;新引入的部分是一条状态信息。在第 5 行我们创建了一个ActionMessages实例来保存状态信息,而在第 12-14 行和 17-19 行我们将ActionMessages保存到了 request 中,这样它们就可以在视图中被渲染。

下面给出 Rails 版本的update

code/people/app/controllers/people_controller.rb

def update<br></br> @person = Person.find(params[:id])<br></br> if @person.update_attributes(params[:person])<br></br> flash[:notice] = 'Person was successfully updated.' <br></br> redirect_to :action =>'show', :id => @person<br></br> else<br></br> render :action =>'edit'<br></br> end<br></br> end实际的更新发生在第 3 行。ActiveRecord 类的update_attributes方法可以一次同时修改多个属性。和createsave方法类似,update_attributes会自动进行验证。由于params[:person]这个哈希表包含了所有输入表单中的名 / 值对,update_attributes的一次调用完成了更新@person实例所需要的所有工作。

与 Struts 的更新类似,Rails 版本的更新也设置了一条状态消息。在第 4 行,消息"Person was successfully updated."被加入到名为flash的特定对象中。通常,更新操作在结束时会重定向到其它 action。那么如何在重定向过程中保证状态消息不会丢失呢?如果将状态消息保存到成员变量中,会导致这一消息在重定向后丢失。而使用 session 来作为保存机制虽然可行,但开发人员必须在随后执行清理 session 这一很容易被遗漏的操作。因此,Rails 提供了 flash 作为解决方案。使用 flash 时,消息首先被保存到 session 中,以便本次重定向可以使用。而在下一次重定向后,Rails 会自动在 session 中清理该消息。从而有效地解决了更新操作的状态信息在重定向时的保存问题。

flash的设计思想很巧妙。然而不幸的是,通常放入到 flash 中的数据则一点也不灵活。在 Rails 暂时还不支持国际化的情况下,状态消息却被直接以字符串(通常是英文)的形式存储。与之不同,在 Structs 应用中,状态消息被存储为一些诸如"person.added."这样的键值,其后视图可以用这些键值查找相应的本地化字符串。对国际化支持不足是 Rails 的一个重要缺陷。如果您的应用需要国际化支持,您必须自力更生或者使用第三方库。

在成功更新后,控制器应该重定向到执行“读操作”的 URL。这降低了用户因为误收藏一个更新操作的 URL 而使更新操作重复执行的可能性。一些可选的“读操作”包括显示被更新过的对象信息,显示同类对象的列表,或显示顶层视图。在 Structs 版本中,重定向通过调用findForward实现:

return mapping.findForward("mainMenu");为了确认这条跳转语句确实执行了重定向,您可以查看配置文件struts.xml。看起来一切如我们所愿:

<global-forwards><br></br> <forward name="mainMenu" path="/mainMenu.html" redirect="true"/><br></br> <!-- etc. --><br></br> </global-forwards>与 Struts 中findForward兼具渲染和重定向功能不同,Rails 使用了两个单独的方法。在保存后,控制性显式的进行了重定向:

redirect_to :action => 'show', :id => @person值得注意的是,此处重定向的定义包含了 action 和参数。Rails 通过其后台的路由表,将 action 和参数转换为 URL。当使用默认的路由规则时,所得到的 URL 为 /people/show/(some_int)。

在展示了一个更新成功的场景后,我们来看一个更新失败的例子。Struts 和 Rails 都提供了验证用户输入的机制。在 Struts 中,Validator 对象基于 XML 配置文件中的声明自动验证 form bean。验证规则是与表单相关联的。如果要指明某人的名字为必填项,您可以在 XML 中这样写:

code/appfuse_people/snippets/person_form.xml

<form name="personForm"><br></br> <field property="firstName" depends="required"><br></br> <arg0 key="personForm.firstName"/><br></br> </field><br></br> <!-- other fields --><br></br></form>采用将验证规则分别写入到多个表单对应的配置文件中这种做法,其初衷在于分离关注点。不过有时将相关的关注点聚集在一起是一种更便捷的办法。在这里,我们使用在Person模型类中添加 XDoclet annotations 的方法来生成验证:

code/appfuse_people/src/dao/com/relevancellc/people/model/Person.java

/**<br></br> * @hibernate.property column="first_name" length="50"<br></br> * @struts.validator type="required"<br></br> */<br></br> public String getFirstName() {<br></br> return firstName;<br></br> }在 Ant 的构建过程中,这条struts.validator annotation 将会在validation.xml文件中生成相应的代码。(在 Java 5 以及之后的版本中,annotation 提供了更为简单和集成的注解机制)

Rails 没有单独的 form bean,而是直接在Person模型类中进行验证声明。您已经在第 4.5 节验证数据值中看到了这种做法。

code/people/app/models/person.rb

class Person < ActiveRecord::Base<br></br> validates_presence_of :first_name, :last_name<br></br>endStruts 和 Rails 采用了同样的方式来处理验证错误:重新渲染相应的页面,并将需要改正的表单输入域用错误信息标识出来。在 Struts 中,该页面转向由 Validator 负责。诸如PersonForm这样的 Form bean 继承了 Struts 类org.apache.struts.validator.ValidatorFormValidatorForm类提供了一个validate方法。Struct 框架自动的调用validate,并在验证失败时重新渲染表单页面。

Rails 所采用的方法则更为显式。当您调用 ActiveRecord 模型的saveupdate_attributes方法时,如果验证失败将得到一个 false 的返回值。此时,您可以使用render来重新渲染用于编辑的表单页面:

code/people/snippets/update_fails.rb

if @person.update_attributes(params[:person])<br></br> # ...success case elided...<br></br> else<br></br> render :action => 'edit'<br></br>end验证错误被存放在 **@person对象的属性errors** 中,所以您不需要作其他任何工作来将错误信息传递给表单视图。第 6.5 节创建 HTML 表单,介绍了如何在视图中渲染验证结果。

通过比较以上章节中 Ruby 代码和 Java 代码的相似部分,开发人员可以迅速通过示例掌握 Ruby 的用法。这种做法很有效,但同时也很危险,因为 Ruby 是一种与 Java 很不同的语言(如果您曾经读过用 C 语言风格编写的 Java 代码,您肯定能切实体会到这种危险!)有效使用 Rails 的重要条件之一是对 Ruby 语言拥有通彻的了解。因此,我们在本书中用两个章节单独介绍 Ruby 语言。书摘#2 介绍 Ruby 语言最重要和强大的特性之一:核心类可修改。

书摘 2:扩展核心类

开发人员经常有向某一语言内建类中添加方法的需求。在这种情况下,继承这个类通常是行不通的,因为所添加的方法需要在基类的实例中可用。例如,Java 和 Ruby 都没有判定一个String是否空白的方法,即为 null、为空或只包含空格。由于许多应用都希望用同样的方式来处理各种空白字符串,因此这一判定方法是有用的。Java 和 Ruby 的开源社区都提供了判定空白字符串的方法。下面给出 Apache Commons Lang 中isBlank()的 Java 实现:

code/Language/IsBlank.java

public class StringUtils { <br></br> public static boolean isBlank(String str) {<br></br> int strLen;<br></br> if (str == null || (strLen = str.length()) == 0) {<br></br> return true;<br></br> }<br></br> for (int i = 0; i < strLen; i++) {<br></br> if ((Character.isWhitespace(str.charAt(i)) == false)) {<br></br> return false;<br></br> }<br></br> }<br></br> return true;<br></br> }<br></br>}因为无法向 Java 核心类添加方法,Apache Commons Lang 使用了标准的 Java 惯用法,即将扩展方法以静态方法的形式置入另一个类中。isBlank()的实现被置入StringUtils类中。

这样,isBlank()的使用者在每一次调用中都得加入辅助类StringUtils的类名作为前缀:

code/java_xt/src/TestStringUtils.java

import junit.framework.TestCase;<br></br> import org.apache.commons.lang.StringUtils;<p> public class TestStringUtils extends TestCase {</p><br></br> public void testIsBlank() {<br></br> assertTrue(StringUtils.isBlank(" "));<br></br> assertTrue(StringUtils.isBlank(""));<br></br> assertTrue(StringUtils.isBlank(null));<br></br> assertFalse(StringUtils.isBlank("x"));<br></br> }<br></br> }Ruby 类则是开放的——您可以在任何时候修改它们。所以,在 Ruby 中的相应做法是像 Rails 那样,将blank?加入到String中:

code/rails/activesupport/lib/active_support/core_ext/blank.rb

class String<br></br> def blank?<br></br> empty? || strip.empty?<br></br> end<br></br> end下面是对于blank?的一些调用示例:

code/rails_xt/test/examples/blank_test.rb

require File.dirname(__FILE__) + '/../test_helper'<p> class BlankTest < Test::Unit::TestCase</p><br></br> def test_blank<br></br> assert "".blank?<br></br> assert " ".blank?<br></br> assert nil.blank?<br></br> assert !"x".blank?<br></br> end<br></br>end### null 的处理

Java 版本的isBlank()使用 helper 类StringUtils还有第二个原因。即使在 Java 中可以将isBlank()方法加入String类,我们仍然不应该这么做。因为当调用一个nullStringisBlank()方法时,期望得到返回值为false(译注:原文这里为 false,但似乎应为 true)。然而在 Java 中,企图调用一个null对象的方法将会引发一个NullPointerException异常。通过在StringUtils的静态方法中判断第一个参数,您可以避免在编写String方法的代码时必须加入诸如判断 this 是否为 null 这种无意义的代码。但问题是为什么 Ruby 的版本可以良好地进行工作呢?

Ruby 中 nil 亦为对象

Rudy 语言中的nil是 Java 中null的等价物。然而与之不同的是,nil是作为一个对象存在的。因此您可以像调用其它对象一样来调用nil对象的方法。对于编写isBlank()方法,更有意义的是,您可以像给其他对象添加方法一样,为nil添加方法:下面这段代码使nil.blank? 返回true

code/rails/activesupport/lib/active_support/core_ext/blank.rb

class NilClass #:nodoc:<br></br> def blank?<br></br> true<br></br> end<br></br> endRails 同时为许多其他对象提供了合理的blank?定义:truefalse****空数组及哈希表,数值类型,甚至是Object类本身。

目前存在众多语言和 web 框架。让 Java 脱颖而出的是其所在的整个“生态系统”。当我们启动一个新的 Java 项目时,我们知道任何我们遇到的问题都有很大的几率已经被 Java 世界中的开源项目解决了。对于任何像 Rails 这样的新兴框架,了解其周围的“生态系统”也是非常重要的。是否有优秀的 IDE 可用?是否可以方便的进行自动测试以及持续集成?对于常用的编程任务是否有标准库提供支持?

Java 拥有无与伦比的强大“生态系统”,因此当您从 Java 迁移到其他语言时很可能遭遇一些挫折。但是我们发现 Ruby 的“生态系统”比我们想象的要丰富得多。在书摘#3 中,我们将会介绍 Ruby 对于自动测试的支持,以及 Rails 如何扩展该支持来测试 web 控制器。

书摘 3:Rails 中的 Test::Unit 扩展

学习 Rails 中的 Test::Unit 扩展时,最简单的入门方式是从 Rails 脚手架所生成的测试文件开始:

$ script/generate scaffold Person我们已经在第 1.2 节 Rails 应用 15 分钟入门中了解过脚手架所生成的大部分内容。这里我们将着重介绍用于功能测试的生成文件:test/functional/people_controller_test.rb
下面我们逐行讲解PeopleControllerTest这个类。首先,该测试包含了一些夹具(fixture):

code/rails_xt/test/functional/people_controller_test.rb

fixtures :people, :users脚手架在生成代码时会猜测PeopleController用于处理 people 模型类,因此它引入了相应的夹具。在很多应用中,一个控制器可能同时与多个模型类进行交互。在这种情况下,只需将其他模型加入到夹具声明中。例如:

fixtures :people, :widgets, :thingamabobs, :sheep下面是 setup 方法:

def setup<br></br> @controller = PeopleController.new<br></br> @request = ActionController::TestRequest.new<br></br> @response = ActionController::TestResponse.new<br></br> end几乎所有的功能测试都会模拟一次或多次的 web 请求 / 响应过程。因此,在每个测试中都需要 **@request@response** 的实例。举一个实际的例子。脚手架生成一个显示模型内容列表的 index 页面。 该页面所对应的测试如下所示:

举一个实际的例子。脚手架生成一个显示模型内容列表的 index 页面。 该页面所对应的测试如下所示:

def test_index<br></br> get :index<br></br> assert_response :success<br></br> assert_template 'list'<br></br> end首先,get()方法模拟了一个发送给控制器的 HTTP GET 请求。这个方法有接收不同参数的多个版本,此处所使用的版本的参数为被请求 Rails action 的名称。其后的assert_response :success用于断言响应成功,即 HTTP 状态码为 200。而assert_template 'list’则用于断言返回页面由list模板所渲染。

作为 Java 程序员,我们很可能要问,“为什么没有用到那些在 setup 方法中生成的对象?”。或许test_index()更应该像下面的代码一样显式的使用这些对象:

# hypothetical, with explicit objects<br></br> @controller.get :index<br></br> assert_equal :success, @response.status<br></br> assert_equal 'list', @response.template前面的这两个例子在功能上是能价的,只是风格有所不同。在 Java 中,我们趋向于显式的使用对象。而在 Ruby 尤其是 Rails 中,我们更喜欢尽可能的将那些显而易见的事情隐式化。请再比较一下上面两个版本的代码,我相信这次您可以更好的理解其中的差异。

接下来是用于测试list action 的代码:

def test_list<br></br> get :list<br></br> assert_response :success<br></br> assert_template 'list'<br></br> assert_not_nil assigns(:people)<br></br> end这段代码与test_index()大体相似,新加入的部分是下面这句:

assert_not_nil assigns(:people)assigns是一个特殊的变量。如果您在控制器中创建了一个实例变量,那么这个变量就可以直接在视图模板中使用。这一机制背后的原理其实很简单:Rails 首先通过反射将控制器中的变量拷贝到一个容器中,其后又将该容器中的变量拷贝回视图实例。而这个容器其实就是上面提到的assigns,所以前面的断言可以被理解为“控制器应该创建一个名为people的非空变量”。

下面到了show action 的测试代码:

def test_show<br></br> get :show, :id => 1<br></br> assert_response :success<br></br> assert_template 'show'<br></br> assert_not_nil assigns(:person)<br></br> assert assigns(:person).valid?<br></br> end这个测试看起来和前面几个有些不同,因为show方法需要指明所要显示的 person 实例。Rails 的默认机制是通过在 URL 中加入id来标识特定的模型实例。因此,get()接收某一 person 实例的 id 作为第二个参数:

get :show, :id => 1get()方法的一般形式可以用于处理各种场景下的请求:

get(action=nil, parameters=nil, session=nil, flash=nil)但我们如何保证 ID 为 1 的 person 实例确实存在?让我们看一下相应的夹具文件

code/rails_xt/test/fixtures/people.yml

first:<br></br> id: 1<br></br> first_name: Stuart<br></br> last_name: Hallowaytest_show()的另一个变化是valid?()

assert assigns(:person).valid?这正是我们在第 4.5 节 Validating Data Values 所讨论过的,ActiveRecord 对于验证的标准支持。随着您向Person类中加入验证方法,valid?()会自动的根据这些方法进行验证。

test_new()中没有什么新内容,所以我们这里就略过不讲,直接进入test_create()

code/rails_xt/test/functional/people_controller_test.rb

def test_create<br></br> num_people = Person.count<br></br> post :create, :person => {}<br></br> assert_response :redirect<br></br> assert_redirected_to :action => 'list'<br></br> assert_equal num_people + 1, Person.count<br></br> end这里有几个值得注意的新内容。与前面讨论的shownew等众多方法不同,create方法会改变数据库内容。这一变化也对test_create的设计产生了一些影响。首先,由于create并不是一个幂等操作 [1],测试中用post()代替了get()。其次,我们现在需要测试数据库的变化是否正确。下面的这行代码:

num_people = Person.count获取了create()被调用前的 person 实例的数目。而这一句:

assert_equal num_people + 1, Person.count则验证有且仅有一个 person 实例被创建。(如果您愿意的话,您还可以进行更严格的测试来保证新创建的 person 实例与创建时所传入的参数相匹配)

create()这样的非幂等操作所造成的第三个影响是我们不能再将:success作为预期响应。现在,创建成功后将重定向到show action。下面的代码:

assert_response :redirect<br></br> assert_redirected_to :action => 'list'用于验证create()正确的进行了重定向。

剩下的脚手架测试方法(test_edit()test_update()test_destroy())也就没什么新内容了,不过阅读一下它们的代码还是有助于加强您对于 Rails 脚手架的理解。

为什么脚手架在一次 POST 请求后做重定向 在一次 POST 请求后做重定向可以防止用户在某些情况下做内容完全相同的重复更新。(您可能已经在一些写得不太好的 web 应用中见识过这一重复更新问题。一个标志性的表现是浏览器给出“您试图重复提交一个包含 POST 数据的 URL。您确定吗?”)

Rails 应用通常不会被重复更新问题所困扰,因为脚手架中内置了一个良好的解决方案(重定向)。

Rails 构建于一门优秀语言(Ruby)和坚实的相关配套设施之上,是一个强大的 web 框架。当然许多其他的 web 框架也号称如此。那么为什么您应该将宝贵的时间用于学习和应用 Rails 呢?因为 Rails 程序员们正在又快又好的完成着各种任务。在开发人员生产率方面,Rails 程序员们提出(并证实)了众多令人拍案叫绝的主张。而且,他们乐在其中。

Java 程序员应该为 Ruby on Rails 的迅速蹿红而感动惊慌失措吗?当然不是。Java 程序员在吸收利用 Ruby on Rails 方面具有天然而独特的优势。Rails for Java Developers 会告诉您如何开始。


[1] 一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等操作对于代理和缓存来说具有“友好性”,因为幂等操作的额外执行不会对二者产生危害性后果(除了带宽浪费)。幂等操作使用 GET 作为其 HTTP 动词

查看英文原文: InfoQ Book Excerpt: Rails for Java Developers

2007-12-25 04:322415
用户头像

发布了 24 篇内容, 共 27821 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

区块链软件开发:创新、安全、智能的数字未来

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

在script标签写export为什么会抛错|type module import ES5 ES6 预处理 指令序言 JavaScript JS

Geek_ee6d52

前端 JavaScrip

DevData Talks | 金融大咖说:金融企业如何持续提升研发效能

思码逸研发效能

思码逸企业版 4.0 特性之二:支持 DevOps 全工具链数据分析

思码逸研发效能

电商“变法”,AI维新

脑极体

AI

扯淡的DevOps,我们开发根本不想做运维!

京东科技开发者

“数智乡村”数字人源码助力乡村振兴!

青否数字人

详解 JSON 文件的打开方法

Apifox

JavaScript json 程序员 前端 教程

以太坊 Dencun 升级与潜在机会

TechubNews

Ethereum 区块链、 #Web3

oracle和mysql语句有哪些异同点?

伤感汤姆布利柏

C# 12 中新增的八大功能你都知道吗?

EquatorCoco

.net C语言 开发语言

基于OpenTelemetry实现Java微服务调用链跟踪

华为云开发者联盟

Java 微服务 Spring Boot 华为云 华为云开发者联盟

JavaScript和Java:看似相似但实际上截然不同

伤感汤姆布利柏

AI数字人直播强势“出圈”24小时无限畅播!

青否数字人

数字人

面试官:如何实现10亿数据判重?

王磊

Java 面试题

IPQ9574/Breaking the speed boundary: exploring the innovative technologies of WiFi 7

wallysSK

基于Java开发的工作流管理系统,快速开发平台

金陵老街

大模型开发:从数据挖掘到智能应用

百度开发者中心

自然语言处理 大模型 人工智能、

想要一个龙年头像,在线等挺急的

阿里巴巴云原生

阿里云 云原生 函数计算 Stable Diffusion

ACK One:构建混合云同城容灾系统

阿里巴巴云原生

阿里云 Kubernetes 云原生

信息茧房的困境

老张

信息茧房 sora

思码逸企业版 4.0 特性之一:支持 DevOps 全工具链数据分析

思码逸研发效能

文档图像大模型在智能文档处理领域中的应用

百度开发者中心

人工智能 深度学习 大模型 智能文档

微信小程序制作步骤,开发成本低,轻松打造

天津汇柏科技有限公司

小程序开发 开发小程序

Python可视化工具集合来报道(下)

小齐写代码

书摘:Java开发者的Rails之路_Java_Stuart Halloway_InfoQ精选文章