精通Spring MVC 4

978-7-115-44758-6
作者: 【美】Geoffroy Warin
译者: 张卫滨孙丽文
编辑: 陈冀康
分类: Spring

图书目录:

详情

本书介绍了使用Spring MVC4构建现代的Web应用,书中还涉及Spring Boot、Spring工具套件等。全书分为10章,分别介绍了快速建立Spring Web应用、精通MVC结构、URL映射、文件上传和错误处理、开发Restful应用、使应用程序安全、单元测试和验收测试、需求优化、将Web应用部署到云等。

图书摘要

版权信息

书名:精通Spring MVC 4

ISBN:978-7-115-44758-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Geoffroy Warin

  译    张卫滨 孙丽文

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright ©2015 Packt Publishing. First published in the English language under the title Mastering Spring MVC 4.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Spring MVC属于Spring Framework的衍生产品,已经融合在Spring Web Flow里面。Spring框架提供了构建Web应用程序的全功能MVC模块。Spring MVC 4是当前最新的版本,在众多特性上有了进一步的提升。

本书中从头开始构建了一个完整的Web应用。全书共10章,分别介绍了快速搭建Spring Web应用、精通MVC结构、处理表单和复杂的URL映射、文件上传与错误处理、创建RESTful应用、保护应用、单元测试与验收测试、优化请求、将Web应用部署到云等内容,循序渐进地讲解了Spring MVC 4的开发技巧。

本书最适合已经熟悉Spring编程基础知识并迫切希望扩展其Web技能的开发人员。通过阅读本书,读者将深度把握Spring MVC的各项特性及实用技巧。


Spring MVC是Spring社区项目的重要组成部分,目前用到了无数规模各异的项目之中。Spring以简化企业级应用开发为己任,十多年来的发展历史证明,他们确实做到了这一点。不论是Web应用开发、数据库访问还是应用集成、大数据处理,我们都能看到Spring的相关项目,借助IoC与AOP的核心理念,Spring让开发人员的工作更加轻松愉悦。

Spring MVC本身的发展也是如此,从最初的XML文件配置,到后来的注解和Java代码方式的配置,都是尽可能地让开发过程更加便利,同时,Spring MVC还在不断完善其测试功能,推动优秀编码实践能够更容易地落地。

Spring Boot项目的横空出世更是引起了空前的关注,这是一个改变游戏规则的项目,它能够极大地简化配置,高效地管理依赖,并且与当下流行的微服务架构模式契合良好,得到了广泛地应用。本书组合使用了Spring Boot和Spring MVC,从项目的搭建过程入手,涉及页面开发、文件上传、应用安全、RESTful API开发、测试等,涵盖了Web应用开发的方方面面,最后还介绍了如何将应用部署到云端,不管你关心哪个领域,希望通过本书都能获取到有用的知识。

从《Spring实战》(第三版)开始,我参与翻译了多本Spring相关的书籍,结识了许多志同道合的好友,这也是一种特殊的缘分。技术的发展日新月异,作为从业者的我们自然不能停下前进的脚步,唯有不断学习、不断思考、不断实践,才能保证不被时代所淘汰,希望与诸位读者共勉。

读者在阅读本书的过程中,如果发现错误或问题,请不吝指正,您可以通过levinzhang1981@126.com或微博@张卫滨1895联系到我。

希望本书能够为您的工作和学习带来帮助。

张卫滨

2017年2月于大连


作为Web开发人员,我愿意创建新的东西,将它们快速上线,然后将注意力转移到下一个新的想法上。

如今,所有的应用都互相连接在了一起,我们需要与社交媒体进行交互,促进产品和复杂系统的发展,为用户提供更大的价值。

直到现在,这些对于Java开发人员来说都非常遥远和复杂。随着Spring Boot的诞生和云平台的平民化,我们可以在有限的时间内创建精彩的应用并让所有的人来访问,而这个过程不需要花一分钱。

在本书中,我们将会从头开始构建一个有用的Web应用。这个应用有很多很棒的特性,如国际化、表单校验、分布式会话与缓存、社交登录、多线程编程等。

同时,我们还会对其进行完整的测试。

在本书结束之前,我们还会将这个小应用部署到云端,使它能够通过Web进行访问。

如果你觉得这挺有意思的话,那么就别浪费时间了,马上开始着手编码吧!

第1章“快速搭建Spring Web应用”能够让我们非常快速地开始使用Spring Boot。本章介绍了让我们更具生产效益的工具,如Spring Tool Suite和Git,本章还会帮助我们搭建应用的主体框架,并见识Spring Boot背后的魔力。

第2章“精通MVC架构”指导我们创建一个小的Twitter搜索引擎,同时,本章还涵盖了Spring MVC和Web架构的基础知识。

第3章“处理表单和复杂的URL映射”帮助你理解如何创建用户基本信息表单,本章介绍如何在服务端和客户端校验数据,并且让我们的应用支持多语言访问。

第4章“文件上传与错误处理”将会指导你为基本信息表单添加文件上传功能,它阐述了如何在Spring MVC中恰当地处理错误并展示自定义的错误页面。

第5章“创建RESTful应用”阐述了RESTful架构的理念,它还帮助我们创建了一个可以通过HTTP调用的用户管理API,这个过程中会看到帮助我们设计API的工具,并且会讨论如何很简便地实现文档化。

第6章“保护应用”将会指导我们如何保护应用,包括如何使用基本HTTP认证保护RESTful API,以及如何保护登录页之后的Web页面,它阐述了如何通过Twitter进行登录以及如何将会话保存在Redis中,从而允许我们的应用进行扩展。

第7章“单元测试与验收测试”帮助我们对应用进行测试。它讨论了测试与TDD,介绍了如何对控制器进行单元测试,如何使用现代的库设计端到端的测试。最后,介绍了Groovy如何提升测试的生产效率和可读性。

第8章“优化请求”对应用进行了优化。它包括缓存控制和Gzip,本章将教会我们如何把Twitter搜索结果缓存到内存和Redis中,以及如何对搜索实现多线程执行。除此之外,还会介绍如何实现Etag和使用WebSocket。

第9章“将Web应用部署到云中”会指导我们对应用进行部署,通过对比,阐述了不同PaaS解决方案的差异。然后,介绍了如何将应用部署到Cloud Foundry和Heroku中。

第10章“超越Spring Web”在整体上讨论了Spring生态系统,介绍了现代Web应用的组成部分以及后续的发展方向。

尽管我们将要构建的是一个很高级的应用,但是并不需要你安装很多的东西。

我们将要构建的应用需要Java 8。

我们并不强制你使用Git,不过你绝对应该使用Git来对自己的应用进行版本控制。如果你希望将应用部署到Heroku上,那么会需要用到它。另外,借助Git可以非常容易地回顾你的工作,通过查看代码的差异和历史来了解其演进过程。在第1章中包含了很多开始使用Git的资源。

我还推荐你使用一个好的IDE。我们会看到如何使用Spring Tool Suite(免费)和IntelliJ Idea(一个月的免费试用)实现快速起步。

如果你使用Mac的话,应该了解一下Homebrew(http://brew.sh)。通过使用这个包管理器,你可以安装本书中提到的所有工具。

本书最适合已经熟悉Spring编程基础知识并迫切希望扩展其Web技能的开发人员。建议你事先掌握一些Spring框架的知识。

在本书中,你会看到多种样式的文本,它们用来区分不同类型的信息。如下是这些风格的样例以及对它们的描述。

文本中的代码、数据库表名、文件夹的名称、文件名、文件扩展名、路径名称、伪URL(dummy URL)、用户输入以及Twitter如下所示。

“你会在目录build/libs中找到JAR文件”。

代码片段如下所示:

public class ProfileForm {
    private String twitterHandle; 
    private String email; 
    private LocalDate birthDate; 
    private List<String> tastes = new ArrayList<>();
    // getters and setters
}

如果希望你关注特定的代码片段,那么相关的行或条目会粗体显示:

public class ProfileForm {
    private String twitterHandle; 
    private String email; 
private LocalDate birthDate;
    private List tastes = new ArrayList<>();
    // getters and setters
}

命令行的输入和输出会按照如下所示进行编写:

$ curl https://start.spring.io

新术语关键字会粗体显示。在屏幕、菜单或对话框中看到的内容将会按照如下的样例显示:“转到新建项目菜单并选择Spring Initializr项目类型”。

 

警告或关键的提示按照这种方式显示。

 

小技巧或窍门按照这种方式显示。

我们欢迎读者的反馈。请告诉我们你觉得这本书怎么样——你喜欢什么,不喜欢什么。读者的反馈是非常重要的,这将有助于我们开发出读者得到真正想要的图书。

如果是一般的反馈,只需发送邮件到feedback@packtpub.com即可,并在邮件的主题中包含本书的名称。

如果对于某个话题,你有专业的知识,并且愿意编写一本书或者为某本书做出贡献,那么可以参考我们的读者指南:www.packtpub.com/authors 。

如果你购买了Packt图书,那么我们会从很多方面为你提供帮助,尽可能最大化你的购买所带来的价值。

你可以通过账号,在http://www.packtpub.com下载所有你所购买的Packt图书的示例代码文件。如果你是在其他途径购买的本书,那么可以访问http://www.packtpub.com/support并进行注册 ,这些文件会直接邮件发送给你。

对于本书的样例代码,你也可以通过https://github.com/Mastering-Spring-MVC-4/mastering- spring-mvc4进行下载。

在互联网上,版权材料的盗版问题在任何的媒体形式中都普遍存在。在Packt出版社,我们非常重视版权和许可证问题,如果你在互联网上遇到关于我们图书的任何形式的非法拷贝版本,请立即告诉我们相应的网址或Web站点名称。

如果遇到涉嫌盗版的内容,请将它的链接发送至copyright@packtpub.com。

我们非常感谢你对作者们的保护,这也是对我们提供有价值内容能力的保护。

如果你对本书有任何的问题,可以通过questions@packtpub.com联系我们,我们将尽全力为你解答。


Geoffroy Warin从10岁就开始编程了,是软件匠艺(Software Craftsmanship)运动的坚定信奉者和开源的倡导者,他跟随自己的内心选择成为一名开发人员并对其坚定不移。在职业生涯中,他一直致力于使用Java和JavaScript语言开发企业级的Web应用。

Geoffroy在前端和后端方面都游刃有余,他非常关注代码整洁和可测试性。他深信开发人员应该尽其所能编写出可读性更强的代码,这样的代码能够持续地为用户交付价值。

在推动测试驱动开发和创建伟大的软件设计方面,他的核心工具就是结对编程和导师制。

他还讲授Java Web技术栈的课程,是Groovy和Spring的狂热支持者。

最近,他还担任《Learning Spring Boot》和《Spring Boot Cookbook》等书的审阅者,这两本书都是由Packt出版社所出版,涵盖了Spring生态系统中新增的主要内容。

如果想学习最新的Spring和JavaScript编程技巧,请查阅Geoffroy的博客(http://geowarin. github.io)和他的Twitter账号(https://twitter.com/geowarin)。


Raymundo Armendariz是有超过10年经验的软件开发人员,他之前主要致力于为Java和.NET平台构建软件,但是现在他投身于JavaScript。

他还是一本JavaScript微框架图书的作者。

在他的职业生涯中,大部分的工作都是与汽车行业相关,他曾经就职的公司包括Autozone、Alldata、TRW和1A Auto。

他是《Getting Started with Backbone Marionette》(Packt出版社)一书的作者,这本书可以在https://www.packtpub.com/web-development/getting-started-backbone-marionette找到。

我要感谢朋友们对我的支持和帮助。

——Raymundo

Abu Kamruzzaman是纽约城市大学的Web程序员和数据库分析师。在过去的10多年以来,他所开发和维护的Web应用作为班级教学和注册课程,用于高校的教学。从2014年11月开始,他担任CUNY总部的PeopleSoft开发专家,目前,他所从事的项目是与商业智能团队(Business Intelligence)一起使用OBIEE为CUNY构建数据仓库。在加入总部之前,自2001年以来,他曾经在CUNY的各个校区工作过,还讲授研究生和本科生的IT课程,指导学生们使用他所开发的应用。从2001年开始,他所讲授的课程包括J2EE、DBMS、数据仓库、面向对象编程、Web设计以及Web编程。他是柏鲁克学院(Baruch College)杰克林商学院(Zicklin School of Business)计算机信息系统系的教员之一。他非常热心于教育事业,对开源项目也有着极大的热情,如Hadoop、Hive、Pig、NoSQL数据库、Java、云计算以及移动应用开发。他从布鲁克林学院(Brooklyn College)/CUNY获得了硕士学位,从宾厄姆顿大学(Binghamton University)/SUNY计算机科学专业获得了学士学位。他的Web站点地址:http://faculty.baruch.cuny.edu/akamruzzaman/

我要感谢美丽的妻子Nicole Woods,感谢她对我所从事事业的耐心、支持以及鼓励。感谢我的父母,感谢他们的祝福和祈祷。感谢本书的作者和Packt出版团队能够给我参与本书的机会。

——Abu

Jean-Pol Landrain是软件工程专业的学士,从1998年开始就主要从事面向网络、实时和分布式计算的工作。他逐渐成为一名软件架构师,具有超过17年的面向对象编程经验,尤其擅长C++、Java/JEE、各种应用服务器、操作系统(Windows和Linux)以及相关的技术。

他目前就职于Agile Partner,这是一家位于卢森堡的IT咨询公司,从2006年以来一直致力于敏捷方法论的推广、教育和应用。在过去的5年中,他参与了针对欧洲议会(European Parliament)开发团队的工具与技术方案的选择与验证。

他与Packt出版社协作,审校了《HornetQ Messaging Developer’s Guide》,与Manning出版社合作,审校了《Docker in Action》《Git in Practice》《ActiveMq in Action》以及《Spring in Action》(第一版)。

首先,我要感谢我的妻子Marie Smets以及9岁的女儿Phoebe,感谢他们理解我对技术的热爱以及在这上面所花费的时间。我还要感谢在Agile Partner的朋友和同事,如果一个人单枪匹马地投身于技术会非常乏味的,感谢他们所增添的乐趣。

很不幸的是,在这本书制作期间,我失去了我的祖父André Landrain和祖母Hélène Guffens,我非常悲痛。因为这些私人的事件导致了一些延迟,非常感谢Packt出版社编辑团队的耐心,尤其要感谢该项目的协调者Nidhi Joshi和本书的作者GeoffroyWarin。他们做得非常棒,我完全信任本书的品质。在Spring MVC方面,这是市面上很好的一本书。

——Jean-Pol

Wayne Lund是Pivotal的PaaS和现场工程师。在企业级软件开发和分布式环境方面具有超过25年的经验,主要的方向是Spring、企业级Java、Groovy和Grails,并将这些技术扩展至使用Smalltalk和C++的系统,他非常热衷于个性化和新兴的技术。他的目标是继续享受下一代的技术,这就是PaaS以及新的Spring工具集,这包括构建在Spring Boot、Spring Cloud和Spring XD技术上的原生云应用,这样的技术方案能够启用快数据(Fast Data)、大数据、社交以及移动等特性。

目前,他就职于Pivotal,关注云、数据以及敏捷的结合,之前曾经在一家财富500强的医疗保健公司和一家全球性的大型咨询公司工作多年。

他还参与出版了Packt出版社的《Learning Spring Application Development》一书。


在本章中,我们将会直接接触代码并搭建一个Web应用,本书的其他章节将会基于该应用进行讲解。

在这里,我们将会使用Spring Boot的自动配置功能来构建应用,这样的话,就能完全避免使用样板式的配置文件。

本书将会从整体上介绍Spring Boot是如何运行的以及该如何对其进行配置,共有4种方式来开启一个Spring项目:

本书中将会使用Gradle和Java 8,但是也不必为此感到担心。如果你还在使用Maven和更早版本的Java的话,相信你会发现这些技术也是很易于使用的。

很多官方的Spring教程同时提供了Gradle构建和Maven构建,因此,如果你决定继续使用Maven的话,也能很容易地找到样例。Spring 4完全兼容Java 8,如果你不采用Lambda表达式来简化代码库的话,那真的是很遗憾的事情。

本书同时还会为你展示一些Git命令。笔者认为,跟踪工作进展并在稳定的状态进行提交是一件好事。另外,这样还能很容易地将你的工作成果与本书提供的源码进行对比。

本书第9章将借助Heroku部署我们的应用,建议从一开始就使用Git对代码进行版本管理。在本章中,关于如何开始使用Git,我会给出一些建议。

如果要开始学习Spring并使用Spring社区所提供的指南和Starter项目的话,那么最好的起步方式之一就是下载Spring Tool Suite(STS)。STS是一个自定义版本的Eclipse,它被用来与各种Spring项目进行协作,它同时还包括Groovy和Gradle功能。即便如此,你可能像我一样,还会使用其他的IDE,但是,我强烈建议你给STS一个机会,因为它通过“Getting Started”项目,能够让你快速地了解Spring广阔的生态系统。

所以,你可以访问https://Spring.io/tools/sts/all,并下载STS的最新发布版。在生成第一个Spring Boot项目之前,首先需要安装Gradle对STS的支持。在Dashboard中,可以看到“Manage IDE Extensions”按钮,然后,需要在“Language and framework tooling”区域中选择下载“Gradle Support”。

还推荐你下载“Groovy Eclipse”以及“Groovy 2.4 compiler”,如图1-1所示,在本书的后文中,介绍使用geb构建验收测试时会用到它们。

图1-1

现在,在起步阶段,提供两种可选方案。

 下载示例代码

通过你的账号,可以在http://www.packtpub.com站点下载购买的所有Packt书籍的示例代码文件。如果你通过其他途径购买本书的话,那么可以访问http://www.packtpub.com/support并进行注册,这些文件就能通过Email直接发送给你了。也可以直接通过https://github.com/ Mastering-Spring-MVC-4/mastering-spring-mvc4下载本书的示例代码。

第一个方案是使用“File | New | Spring Starter Project”导航菜单,如图1-2的截屏所示。这里的可选项是与http://start.Spring.io相同的,只不过嵌入到了IDE中。

图1-2

通过使用顶部菜单中的“File | New | Import Getting Started Content”,我们可以看到http://spring.io上所有的可配置项,这里可以选择使用Gradle或Maven,如图1-3所示。

 

可以下载Starter代码,并按步骤学习本书中的内容,也可以直接下载完整的代码。

图1-3

在“Getting Started Content”中有很多有意思的内容,建议读者自行对其进行一下探索。它阐述了如何将Spring与各种读者可能感兴趣的技术进行集成。

此时,将会生成一个Web项目,如图1-3所示,这是一个Gradle应用,会生成JAR文件并使用Java 8。

表1-1是我们想要使用的配置。

表1-1

属性

Name

masterSpringMvc

Type

Gradle project

Packaging

Jar

Java version

1.8

Language

Java

Group

masterSpringMvc

Artifact

masterSpringMvc

Version

0.0.1-SNAPSHOT

Description

Be creative!

Package

masterSpringMvc

在第2个界面中,将会询问你想要使用的Spring Boot版本以及想要添加进工程的依赖。

在编写本书的时候,Spring Boot的最新版本是1.2.5,请确保你始终选择最新的版本。

当你阅读本书的时候,可以使用最新的快照版本。如果到那时Spring Boot1.3还没有发布的话,那么你可以试一下快照版本。你可以参考https://spring.io/blog/2015/06/17/devtools-in-spring-boot-1-3来了解更多细节。

在配置窗口的底部会有一些复选框,代表各种Spring Boot starter库。它们是可以添加到构建文件中的依赖项,针对各种Spring项目,它们提供了自动配置功能。

现在只关心Spring MVC,所以只选中Web这个复选框。

 

为Web应用生成一个JAR文件?将Web应用打包为JAR文件,这一点你们可能会觉得有些诡异。尽管仍然可以将其打包为WAR,但这并不是推荐的实践。在默认情况下,Spring Boot将会创建一个胖JAR包(fat JAR),这个JAR包中包含了应用所有的依赖,提供了通过“java-jar”命令便捷启动Web应用的方法。

我们的应用将会打包为JAR文件,如果你想创建WAR文件的话,可以参考http://spring.io/ guides/gs/convert-jar-to-war/

你点击了“Finish”按钮了吗?如果已经点击了的话,将会得到如图1-4所示的项目结构。

图1-4

可以看到主类MasterSpringMvcApplication及其测试类MasterSpringMvc ApplicationTests,还可以看到两个空的文件夹——static和templates,它们分别用来存放静态Web资源(图片、样式文件等)和模板(jsp、freemarker或Thymeleaf)。最后一个文件是空的application.properties,它是Spring Boot默认的配置文件。这是一个很便利的文件,在本章中,将会看到Spring Boot如何使用它。

对于构建文件build.gradle,稍后将会详细介绍。

如果你觉得已经准备就绪,那么运行应用的主方法,这样就能会启动一个Web服务器。

要做到这一点,切换至应用的主方法,然后右键点击该类,并在工具栏中导航至“Run as | Spring Application”,或者点击工具栏上绿色的Play按钮。

遵循上面的步骤,并导航至http://localhost:8080,此时会产生一个错误,不必担心,请继续往下阅读。

接下来将为读者展示如何不使用STS来生成相同的项目,然后再回过头来看这些文件。

IntelliJ IDEA是在Java开发人员中非常流行的一个工具。在过去的几年中,因为这个很棒的编辑器,我非常心甘情愿地为Jetbrains支付了年费。

IntelliJ也有快速创建Spring Boot项目的方法。

如图1-5所示,进入新建项目菜单,默认的择“Spring Initializr”项目。

图1-5

这将会出现与STS相同的配置选项,所以请参照之前的小节来了解详细的配置。

 

你可能需要将Gradle项目导入到IntelliJ之中。推荐你首先生成Gradle包装器(参考1.5.1节)。

如果需要的话,通过再次打开项目的build.gradle文件,可以重新导入该项目。

请导航至http://start.Spring.io站点来开始使用start.Spring.io,对于这个类似于Bootstrap的站点,你可能会感到很熟悉。如果进入上述的链接,那么看到的内容会如图1-6的截屏所示。

图1-6

在这里所看到的配置选项与STS中是相同的,点击“Generate Project”按钮后将会下载一个ZIP文件,这个文件中会包含我们的Stater项目。

对于钟情于控制台的读者来说,可以采用“curl http://start.Spring.io”的方式。采用这种方式的话,将会需要一些指令,帮助我们组织curl请求。

例如,要生成与之前相同的项目,那么可以输入如下的命令:

$ curl http://start.Spring.io/starter.tgz \
-d name=masterSpringMvc \
-d dependencies=web \
-d language=java \
-d JavaVersion=1.8 \
-d type=gradle-project \
-d packageName=masterSpringMvc \
-d packaging=jar \
-d baseDir=app | tar -xzvf -
% Total % Received % Xferd Average Speed Time Time Time
Current
Dload Upload Total Spent Left Speed
100 1255 100 1119 100 136 1014 123 0:00:01 0:00:01 --:--:-
- 1015
x app/
x app/src/
x app/src/main/
x app/src/main/Java/
x app/src/main/Java/com/
x app/src/main/Java/com/geowarin/
x app/src/main/resources/
x app/src/main/resources/static/
x app/src/main/resources/templates/
x app/src/test/
x app/src/test/Java/
x app/src/test/Java/com/
x app/src/test/Java/com/geowarin/
x app/build.Gradle
x app/src/main/Java/com/geowarin/AppApplication.Java
x app/src/main/resources/application.properties
x app/src/test/Java/com/geowarin/AppApplicationTests.Java

现在,我们不离开控制台就能开始使用Spring了,美梦变成了现实。

 

可以考虑为上述的命令创建一个别名(alias),这样的话,就能快速地创建Spring项目的原型了。

现在Web应用已经准备就绪,先看一下它是如何编写的。在进一步学习之前,我们可以将工作的成果保存到Git上。

如果你还不了解Git的话,我推荐下面的两个教程:

 安装Git

在Windows下,需要安装Git bash,这可以在https://msysgit.github.io找到。在Mac下,如果你使用homebrew的话,很可能已经安装过Git了,否则的话,使用brew install git命令来进行安装。如果有疑问的话,请查阅https://git-scm.com/book/en/v2/Getting-Started-Installing-Git上的文档。

如果要使用Git版本化我们的工作内容,那么可以在控制台中输入如下的命令:

$ cd app
$ git init

使用IntelliJ的话,要忽略自动生成的文件,即“.idea”和“*.iml”。使用Eclipse的话,应该将“.classpath”文件和“.settings”文件夹提交上去。不管是哪种情况,都要忽略“.gradle”文件夹和build文件夹。

创建一个包含如下文本内容的“.gitignore“文件:

# IntelliJ project files
.idea
*.iml

# gradle
.gradle
build

现在,我们可以将其他文件添加到Git中:

$ git add .
$ git commit -m "Generated with curl start.Spring.io"
[master (root-commit) eded363] Generated with curl start.Spring.io
4 files changed, 75 insertions(+)
create mode 100644 build.Gradle
create mode 100644 src/main/Java/com/geowarin/AppApplication.Java
create mode 100644 src/main/resources/application.properties
create mode 100644 src/test/Java/com/geowarin/AppApplicationTests.Java

如果你还不熟悉Gradle的话,那么可以将其视为Maven的继任者,它是一个现代化的构建工具。与Maven类似,它会使用约定,例如如何组织Java应用的结构。我们的源码依然会放在“src/main/java”之中,Web应用的代码放到“src/main/webapp”之中,诸如此类。与Maven类似,我们可以使用Gradle插件来处理各种构建任务。但是,Gradle真正的闪光点在于,它允许我们使用Groovy DSL编写自己的构建任务。默认库使得管理文件、声明任务之间的依赖以及增量执行job都变得非常容易。

 安装Gradle

如果你使用OS X的话,那么可以通过brew install gradle命令,借助brew来安装Gradle。在任意的*NIX的系统下(包括Mac),都可以使用gvm(http://gvmtool.net/)来进行安装。另外,也可以在https://Gradle.org/downloads下获取二进制分发包。

使用Gradle创建应用的第一个最佳实践就是生成Gradle包装器(wrapper)。Gradle包装器是一个小的脚本,它能够在你的代码中进行共享,从而确保会使用相同版本的Gradle来构建你的应用。

生成包装器的命令是gradle wrapper:

$ gradle wrapper
:wrapper

BUILD SUCCESSFUL

Total time: 6.699 secs

如果我们看一下新创建的文件,可以看到有两个脚本和两个目录:

$ git status -s
?? .gradle/
?? gradle/
?? gradlew
?? gradlew.bat

在“.gradle”目录中包含了Gradle二进制文件,我们不希望将其添加到版本控制之中。

前面已经忽略了这个文件和构建目录,所以可以安全地对其他内容执行git add操作:

$ git add .
$ git commit -m "Added Gradle wrapper"

gradle 目录包含了如何得到二进制文件的信息。另外两个文件是脚本:用于 Windows的批处理脚本(gradlew.bat)以及用于其他系统的shell脚本。

我们可以使用Gradle运行应用,替换借助IDE来执行应用的方式:

$ ./gradlew bootrun

执行上面的命令将会运行一个嵌入式的Tomcat,应用会位于它里面!

如图1-7所示,日志提示服务器运行在8080端口上,我们检查一下。

图1-7

可以想象到你内心的失望,因为应用还没有为完全公开做好准备。

换句话说,在工程中,这两个文件所完成的工作内容还是很让人振奋的。我们来看一下。

首先是Gradle构建文件,也就是build.gradle:

buildscript {
    ext {
        springBootVersion = '1.2.5.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-
plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-
plugin:0.5.1.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'

jar {
    baseName = 'masterSpringMvc'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}
eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.
eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

在这里,都看到了什么呢?

Spring Boot插件将会产生一个胖(fat)JAR文件,其中包含了项目的所有依赖。要构建的话,只需输入:

./gradlew build

我们将会在“build/libs”目录下找到这个JAR文件。该目录下会包含两个文件,其中一个为胖JAR包,名为masterSpringMvc-0.0.1-SNAPSHOT.jar,另外一个是普通的JAR文件,名为masterSpringMvc-0.0.1-SNAPSHOT. jar.original,这个文件不包含任何的依赖。

 可运行的JAR

Spring Boot主要的一个优势在于将应用所需的所有内容都放到一个易于重发布的JAR文件中,其中包含了Web服务器。如果你运行java jar masterSpringMvc-0.0.1- SNAPSHOT.jar的话,Tomcat将会在8080端口上启动,就像在开发期一样。如果要将其部署到生产环境或云中,这都是相当便利的。

在这里,主要的依赖是spring-boot-starter-web,Spring Boot提供了很多的Starter,它们会对应用的很多方面进行自动化配置,这是通过提供典型的依赖和Spring配置来实现的。

例如,spring-boot-starter-web将会包含对tomcat-embedded和Spring MVC的依赖。它会自动运行Spring MVC最为常用的配置并提供一个分发器(dispatcher),使其监听“/”根路径,还会提供错误处理页面,就像之前所看到的404页面那样。除此之外,还有一个典型的视图解析器(view resolver)配置。

稍后,我们将会看到更多的内容,首先从下一节开始吧!

这里将会展现运行应用的所有代码,它是一个经典的main函数,这种方式有很大的优势,因为我们可以在IDE中像运行其他程序那样运行这个应用。我们可以对其进行调试,并且不需要插件就能实现一些类的重新加载。

在开发模式下,当我们在Eclipse中保存文件或者在IntelliJ中点击“Make Project”就会触发重新加载的过程。只有JVM支持切换至新编译版本的类文件时,它才是可行的,如果修改静态变量或配置文件的话,我们必须要重新加载应用。

主类如下所示:

package masterSpringMvc;

import org.Springframework.boot.SpringApplication;
import org.Springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppApplication {

    public static void main(String[] args) {
        SpringApplication.run(AppApplication.class, args);
    }
}

需要注意的是@SpringBootApplication注解,如果看一下这个注解的代码的话,就会发现它实际上组合了3个其他的注解,也就是@Configuration、@EnableAutoConfiguration和@ComponentScan:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {

  /**
  * Exclude specific auto-configuration classes such that they will
never be applied.
  */
  Class<?>[] exclude() default {};
}

如果你之前使用Java代码配置过Spring应用的话,那么你应该对@Configuration非常熟悉。它表明我们的这个类将会处理Spring的常规配置,如bean的声明。

@ComponentScan也是一个比较经典的注解,它会告诉Spring去哪里查找Spring组件(服务、控制器等)。在默认情况下,这个注解将会扫描当前包以及该包下面的所有子包。

在这里,比较新颖的是@EnableAutoConfiguration注解,它会指导Spring Boot发挥其魔力。如果你将其移除掉的话,就无法从Spring Boot的自动配置中收益了。

使用Spring Boot来编写MVC应用的第一步通常是在代码中添加控制器。将控制器放到controller子包中,这样它就能够被@ComponentScan注解所发现:

package masterSpringMvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

    @RequestMapping("/")
    @ResponseBody
    public String hello() {
        return "Hello, world!";
    }
}

现在,如果打开浏览器并访问http://localhost:8080的话,就能看到我们钟爱的“Hello, world!”被输出了出来(见图1-8)。

图1-8

如果你之前搭建过Spring MVC应用,那么可能已经习惯于编写相关的XML文件或Java注解配置类。

一般来讲,初始的步骤如下所示:

1.初始化Spring MVC的DispatcherServlet;

2.搭建转码过滤器,保证客户端请求进行正确地转码;

3.搭建视图解析器(view resolver),告诉Spring去哪里查找视图,以及它们是使用哪种方言编写的(JSP、Thymeleaf模板等);

4.配置静态资源的位置(CSS、JS);

5.配置所支持的地域以及资源bundle;

6.配置multipart解析器,保证文件上传能够正常工作;

7.将Tomcat或Jetty包含进来,从而能够在Web服务器上运行我们的应用;

8.建立错误页面(如404)。

不过,Spring Boot为我们处理了所有的事情。因为这些配置一般是与应用相关的,所以你可以无限制地将它们进行组合。

在一定程度上来讲,Spring Boot是带有一定倾向性的Spring项目配置器。它基于约定,并且默认会在你的项目中使用这些约定。

接下来,让我们看一下在幕后到底发生了什么。

我们使用默认生成的Spring Boot配置文件,并将其设置为debug模式。在src/main/resources/ application.properties中添加下面这一行:

debug=true

现在,如果重新启动应用的话,就能看到Spring Boot的自动配置报告。它分为两部分:一部分是匹配上的(positive matches),列出了应用中,所有的自动配置,另一部分是没有匹配上的(negative matches),这部分是应用在启动的时候,需求没有满足的Spring Boot自动配置:

=========================
AUTO-CONFIGURATION REPORT
=========================



Positive matches:
-----------------


  DispatcherServletAutoConfiguration
      - @ConditionalOnClass classes found: org.Springframework.web.
servlet.DispatcherServlet (OnClassCondition)
      - found web application StandardServletEnvironment
(OnWebApplicationCondition)


  EmbeddedServletContainerAutoConfiguration
      - found web application StandardServletEnvironment
(OnWebApplicationCondition)


  ErrorMvcAutoConfiguration
      - @ConditionalOnClass classes found: javax.servlet.Servlet,org.
springframework.web.servlet.DispatcherServlet (OnClassCondition)
      - found web application StandardServletEnvironment
(OnWebApplicationCondition) 


  HttpEncodingAutoConfiguration
      - @ConditionalOnClass classes found: org.springframework.web.
filter.CharacterEncodingFilter (OnClassCondition)
      - matched (OnPropertyCondition)



<Input trimmed>

仔细看一下DispatcherServletAutoConfiguration:

/**
* {@link EnableAutoConfiguration Auto-configuration} for the Spring
* {@link DispatcherServlet}. Should work for a standalone application
where an embedded
* servlet container is already present and also for a deployable
application using
* {@link SpringBootServletInitializer}.
*
* @author Phillip Webb
* @author Dave Syer
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

    /*
    * The bean name for a DispatcherServlet that will be mapped to the
root URL "/"
    */
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME =
"dispatcherServlet";

    /*
    * The bean name for a ServletRegistrationBean for the
DispatcherServlet "/"
    */
    public static final String DEFAULT_DISPATCHER_SERVLET_
REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
    @Configuration
    @Conditional(DefaultDispatcherServletCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    protected static class DispatcherServletConfiguration {

        @Autowired
        private ServerProperties server;

        @Autowired(required = false)
        private MultipartConfigElement multipartConfig;

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServlet dispatcherServlet() {
            return new DispatcherServlet();
        }

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_
NAME)
        public ServletRegistrationBean dispatcherServletRegistration()
{
            ServletRegistrationBean registration = new
ServletRegistrationBean(
                    dispatcherServlet(), this.server.
getServletMapping());
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_
NAME);
            if (this.multipartConfig != null) {
                registration.setMultipartConfig(this.multipartConfig);
            }
            return registration;
        }

        @Bean
        @ConditionalOnBean(MultipartResolver.class)
        @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_
RESOLVER_BEAN_NAME)
        public MultipartResolver multipartResolver(MultipartResolver
resolver) {
            // Detect if the user has created a MultipartResolver but
named it incorrectly
            return resolver;
        }
    }

    @Order(Ordered.LOWEST_PRECEDENCE - 10)
    private static class DefaultDispatcherServletCondition extends
SpringBootCondition {

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext
context,
                AnnotatedTypeMetadata metadata) {
            ConfigurableListableBeanFactory beanFactory = context.
getBeanFactory();
            ConditionOutcome outcome = checkServlets(beanFactory);
            if (!outcome.isMatch()) {
                return outcome;
            }
            return checkServletRegistrations(beanFactory);
        }

    }
}

这是一个典型的Spring Boot配置类。

这个文件中还包含了Spring MVC分发器Servlet和multipart解析器的典型配置。整个Spring MVC配置被拆分到了多个文件之中。

另外,值得一提的是,这些bean会遵循特定的规则,以此来检查是否处于激活状态。在@Conditional(DefaultDispatcherServletCondition.class)条件满足的情况下,ServletRegistrationBean函数才会启用,这有些复杂,但是能够检查在你的配置中,是否已经注册了分发器Servlet。

只有在满足@ConditionalOnMissingBean(name=DispatcherServlet.MULTIPART_RESOLVER_ BEAN_NAME)条件的情况下,MultipartResolver函数才会处于激活状态,例如,当我们自己还没有注册的时候。

这意味着Spring Boot仅仅是基于常见的使用场景,帮助我们对应用进行配置。不过,可以在任意的地方覆盖这些默认值,并声明自己的配置。

因此,通过查看DispatcherServletAutoConfiguration,就了解了为什么我们已经拥有了分发器Servlet和multipart解析器。

另外一个密切相关的配置是WebMvcAutoConfiguration,它声明了视图解析器、地域解析器(localeresolver)以及静态资源的位置。视图解析器如下所示:

@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class,
ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends
WebMvcConfigurerAdapter {

  @Value("${spring.view.prefix:}")
  private String prefix = "";

  @Value("${spring.view.suffix:}")
  private String suffix = "";

  @Bean
  @ConditionalOnMissingBean(InternalResourceViewResolver.class)
  public InternalResourceViewResolver defaultViewResolver() {
    InternalResourceViewResolver resolver = new
InternalResourceViewResolver();
    resolver.setPrefix(this.prefix);
    resolver.setSuffix(this.suffix);
    return resolver;
  }
}

视图解析器的配置并没有什么特殊之处,这里真正有意思的是使用了配置属性,从而允许用户对其进行自定义。

它的意思就是说“将会在用户的application.properties文件中查找两个变量,这两个变量的名字是spring.view.prefix和spring.view.suffix”。在配置中只需两行代码就能将视图解析器搭建起来了,这是非常便利的。

为了下一章内容的讲解,你需要牢记这一点,不过,我们现在会继续浏览Spring Boot的代码。

关于静态资源,配置中包含了如下的内容:

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
    "classpath:/META-INF/resources/", "classpath:/resources/",
    "classpath:/static/", "classpath:/public/" };

private static final String[] RESOURCE_LOCATIONS;
static {
  RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
      + SERVLET_RESOURCE_LOCATIONS.length];
  System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
0,
      SERVLET_RESOURCE_LOCATIONS.length);
  System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_
LOCATIONS,
      SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.
length);
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  if (!this.resourceProperties.isAddMappings()) {
    logger.debug("Default resource handling disabled");
    return;
  }

  Integer cachePeriod = this.resourceProperties.getCachePeriod();
  if (!registry.hasMappingForPattern("/webjars/**")) {
    registry.addResourceHandler("/webjars/**")
        .addResourceLocations("classpath:/META-INF/resources/
webjars/")
        .setCachePeriod(cachePeriod);
}
if (!registry.hasMappingForPattern("/**")) {
  registry.addResourceHandler("/**")
        .addResourceLocations(RESOURCE_LOCATIONS)
        .setCachePeriod(cachePeriod);
  }
}

资源位置的声明有点复杂,但是通过它,我们可以了解到以下两点:

 

WebJars是JAR包格式的客户端JavaScript库,可以通过Maven中央仓库来获取。它们包含了Maven项目文件,这个文件允许定义传递性依赖,能够用于所有基于JVM的应用之中。WebJars是JavaScript包管理器的替代方案,如bower或npm。对于只需要较少JavaScript库的应用来说,这种方案是很棒的。你可以在www.webjars.org站点上看到所有可用的WebJars列表。

在这个文件中,还专门有一部分用来声明地域管理:

@Bean
@ConditionalOnMissingBean(LocaleResolver.class)
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
  return new FixedLocaleResolver(
      StringUtils.parseLocaleString(this.mvcProperties.getLocale()));
}

默认的地域解析器只会处理一个地域,并且允许我们通过spring.mvc.locale配置属性来进行定义。

还记得在没有添加控制器的时候,第一次启动应用吗?当时看到了一个有意思的“Whitelabel Error Page”输出。

错误处理要比看上去更麻烦一些,尤其是在没有web.xml配置文件并且希望应用能够跨Web服务器部署时更是如此。好消息是Spring Boot将会处理这些事情!让我们看一下ErrorMvcAutoConfiguration:

ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication
// Ensure this loads before the main WebMvcAutoConfiguration so that
the error View is
// available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@Configuration
public class ErrorMvcAutoConfiguration implements
EmbeddedServletContainerCustomizer,
        Ordered {

    @Value("${error.path:/error}")
    private String errorPath = "/error";

    @Autowired
    private ServerProperties properties;

    @Override
    public int getOrder() {
        return 0;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search =
SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search =
SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes
errorAttributes) {
        return new BasicErrorController(errorAttributes);
    }
    @Override
    public void customize(ConfigurableEmbeddedServletContainer
container) {
        container.addErrorPages(new ErrorPage(this.properties.
getServletPrefix()
                + this.errorPath));
    }

    @Configuration
    @ConditionalOnProperty(prefix = "error.whitelabel", name =
"enabled", matchIfMissing = true)
    @Conditional(ErrorTemplateMissingCondition.class)
    protected static class WhitelabelErrorViewConfiguration {

        private final SpelView defaultErrorView = new SpelView(
                "<html><body><h1>Whitelabel Error Page</h1>"
                        + "<p>This application has no explicit mapping
for /error, so you are seeing this as a fallback.</p>"
                        + "<div id='created'>${timestamp}</div>"
                        + "<div>There was an unexpected error
(type=${error}, status=${status}).</div>"
                        + "<div>${message}</div></body></html>");

        @Bean(name = "error")
        @ConditionalOnMissingBean(name = "error")
        public View defaultErrorView() {
            return this.defaultErrorView;
        }

        // If the user adds @EnableWebMvc then the bean name view
resolver from
        // WebMvcAutoConfiguration disappears, so add it back in to
avoid disappointment.
        @Bean
        @ConditionalOnMissingBean(BeanNameViewResolver.class)
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver resolver = new
BeanNameViewResolver();
            resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
            return resolver;
        }
    }
}

这段配置都做了些什么呢?

在本书后面的内容中,我们将会看到如何恰当地处理错误。

至于转码的问题,非常简单的HttpEncodingAutoConfiguration将会负责处理相关的事宜,这是通过提供Spring的CharacterEncodingFilter类来实现的。通过spring.http.encoding.charset配置,我们可以覆盖默认的编码(“UTF-8”),也可以通过spring.http.encoding.enabled禁用这项配置。

默认情况下,Spring Boot在打包和运行应用时,会使用Tomcat嵌入式API(Tomcat embedded API)。

我们来看一下EmbeddedServletContainerAutoConfiguration:

@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(EmbeddedServletContainerCustomizerBeanPostProcessorRegistrar.
class)
public class EmbeddedServletContainerAutoConfiguration {

  /**
  * Nested configuration for if Tomcat is being used.
  */
  @Configuration
  @ConditionalOnClass({ Servlet.class, Tomcat.class })
  @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.
class, search = SearchStrategy.CURRENT)
  public static class EmbeddedTomcat {

    @Bean
    public TomcatEmbeddedServletContainerFactory
tomcatEmbeddedServletContainerFactory() {
      return new TomcatEmbeddedServletContainerFactory();
    }
  }
  /**
  * Nested configuration if Jetty is being used.
  */
  @Configuration
  @ConditionalOnClass({ Servlet.class, Server.class, Loader.class })
  @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.
class, search = SearchStrategy.CURRENT)
  public static class EmbeddedJetty {

    @Bean
    public JettyEmbeddedServletContainerFactory
jettyEmbeddedServletContainerFactory() {
      return new JettyEmbeddedServletContainerFactory();
    }
  }
  /**
  * Nested configuration if Undertow is being used.
  */
  @Configuration
  @ConditionalOnClass({ Servlet.class, Undertow.class,
SslClientAuthMode.class })
  @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.
class, search = SearchStrategy.CURRENT)
  public static class EmbeddedUndertow {

    @Bean
    public UndertowEmbeddedServletContainerFactory
undertowEmbeddedServletContainerFactory() {
      return new UndertowEmbeddedServletContainerFactory();
    }
  }
}

上面的代码非常简单直接,这个代码包含了3个不同的配置,哪一个会处于激活状态要取决于类路径下哪些内容是可用的。

可以将Spring Boot与Tomcat、tc-server、Jetty或者Undertow结合使用。服务器可以很容易地进行替换,只需将spring-boot-starter-tomcat JAR依赖移除掉,并将其替换为Jetty或Undertow对应的依赖即可。如果你想这样做的话,请参考相关的文档。

对Servlet容器(Tomcat)的所有配置都会在TomcatEmbeddedServletContainerFactory中进行。尽管你应该读一下这个类,它为嵌入式Tomcat提供一个非常高级的配置(为其查找文档会非常困难),但是在这里我们不会直接查看这个类。

我会为读者介绍配置Servlet容器时不同的选项。

通过在application.properties文件中定义server.port属性或者定义名为SERVER_PORT的环境变量,我们可以修改默认的HTTP端口。

通过将该变量设置为−1,可以禁用HTTP,或者将其配置为0,这样的话,就会在随机的端口上启动应用。对于测试,这是很便利的。

配置SSL是一项很麻烦的事情,但是Spring Boot有一项很简单的解决方案。我们只需一点属性就能保护服务器了:

server.port = 8443
server.ssl.key-store = classpath:keystore.jks
server.ssl.key-store-password = secret
server.ssl.key-password = another-secret

不过,为了使上面的例子运行起来,我们需要生成一个keystore文件。

我们将会在第 6 章中,深入介绍安全的可选方案。当然,我们还可以通过添加自己EmbeddedServletContainerFactory来进一步自定义TomcatEmbeddedServletContainerFactory的功能。如果你希望添加多个连接器的话,这会是非常便利的,可以参考http://docs.spring.io/ spring-boot/docs/current/reference/html/howto-embedded-servlet-containers.html# howto-configure-ssl来获取更多信息。

在配置中,我们可以通过简单地声明@Bean 元素来添加典型的 Java Web 元素,如Servlet、Filter和ServletContextListener。

除此之外,Spring Boot还为我们内置了3项内容:

我们将会在第5章中,更详细地了解Jackson的配置。关于JMX配置,我们可以在本地通过jconsole连接应用之后进行尝试,如图1-9所示。

图1-9

通过将org.springframework.boot:spring-boot-starter-actuator添加到类路径下,我们可以添加更多有意思的MBean。我们甚至可以定义自己的MBean,并通过Jolokia将其暴露为HTTP。另一方面,我们也可以禁用这些端点,只需在配置中添加spring.jmx.enabled=false即可。

 

参考http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-jmx.html了解更多细节。

我们现在已经有了一个非常简陋的Spring Web应用,虽然我们没有对其进行什么配置,但是它可以输出RESTful JSON的“Hello world”。我们已经看到过Spring Boot做了什么、是如何实现的,也了解到如何重写默认的自动配置。

单是详细介绍Spring Boot如何运行就够写一本书了。如果你想更深入探究的话,我推荐一本很棒的书,这就是由Greg Turnquist编写的《Learning Spring Boot》,它和本书是同一个系列的。

现在,我们已经为下一章做好了准备,在下一章中这个应用将会推进到一个新的阶段,它会真正地提供Web页面,我们还会学习Spring MVC的更多哲学理念。


在本章中,我们将会讨论MVC架构理念以及Spring MVC是如何实现这些理念的。

本章将会继续使用上一章的应用,并且会构建一些有意思的功能。目标是设计一个简单的页面,在这个页面上用户可以根据特定的条件(criteria)查询Tweet,并将其展现出来。

为了实现该功能,我们将使用Spring Social Twitter项目,可以通过该地址http://projects. spring.io/spring-social-twitter/了解这个项目。

我们会让Spring MVC与一个现代化的模板引擎协作,这个引擎也就是Thymeleaf,并且还会试图理解这个框架的内部机制。引导用户在不同的视图间流转,最后,会借助WebJars和Materialize (http://materializecss.com)让应用在外观上看起来更棒。

大多数人对MVC这个缩写应该不会感到陌生。它代表的是模型(Model)、视图(View)和控制器(Controller),它会将数据和展现层进行解耦,被视为构建用户界面的一种很流行的方式。

自从在Smalltalk领域中提出这个理念,并在Ruby on Rails框架中采用之后,MVC就变得广受欢迎。

如图2-1所示,它的架构可以分为3层。

图2-1

MVC背后的理念是将视图与模型进行解耦,模型必须是自包含的并且与UI无关。这样的话,基本上就可以实现相同的数据跨多个视图重用。其实,这些视图就是以不同的方式来查看数据。通过钻取(Drill down)或使用不同的渲染器(HTML、PDF),可以很好地阐述这一原则。

控制器会作为用户和数据的中间协调者,它的角色就是控制终端用户的可用行为,并引导他们在应用的不同视图间跳转。

尽管MVC依然是当前设计UI的首选方案,但是随着它的流行,也有很多对它的批评。实际上,大多数的批评都指向了该模式的错误用法。

Eric Evans编写过一本很有影响力的书,名为《领域驱动设计》(Domain Driven Design,DDD)。在这本书中,定义了一组架构规则,能够指导我们更好地将业务领域集成到代码之中。

其中有一项核心的理念就是将面向对象的范式应用到领域对象之中。如果违背这一原则的话,就会被称之为贫血的领域模型(Anemic Domain Model)。Martin Fowler的博客(http://www. martinfowler.com/bliki/AnemicDomainModel.html)对这一问题进行了很好的定义。

贫血的领域模型通常来讲会具有如下的症状:

根据业务领域的复杂性不同,这可能是一种较差的实践方式。通常来讲,DDD实践需要付出额外的努力,将领域从应用逻辑中分离出来。

架构通常都是一种权衡,需要注意的是,设计Spring应用的典型方式往往会在这个过程中导致系统在可维护性上变得较为复杂。

避免领域贫血的途径如下:

DDD所涉及的内容远不止上述的规则:实体(Entity)、值类型(value type)、通用语言(Ubiquitous Language)、限界上下文(Bounded Context)、洋葱架构(Onion Architecture)以及防腐化层(anti corruption layer),我强烈建议你自行学习一下这些原则。就我们而言,在构建Web应用的过程中,会努力遵循上述的指导原则。随着本书的推进,你会对这些关注点更加熟悉的。

如果熟悉Spring的话,那么很可能你已经访问过Spring的Web站点,即http://spring.io。它全部是由Spring构建的,而且很棒的一点在于它是开源的。

这个项目的名称为sagan,它包含了很多有意思的特性:

这个项目的Github wiki页面非常详尽,能够帮助你非常容易地开始了解该项目。

 

如果你对这个实际应用的Spring架构感兴趣的话,可以访问如下的URL:

https://github.com/spring-io/sagan

在Spring MVC中,模型是由Spring MVC的Model或ModelAndView封装的简单Map。它可以来源于数据库、文件、外部服务等,这取决于你如何获取数据并将其放到模型中。与数据层进行交互的推荐方式是使用Spring Data库:Spring Data JPA、Spring Data MongoDB等。有10多个与Spring Data相关的项目,推荐你查看一下http://projects.spring.io/spring-data

Spring MVC的控制层是通过使用@Controller注解来进行处理的。在Web应用中,控制器的角色是响应HTTP请求。带有@Controller注解的类将会被Spring检索到,并且能够有机会处理传入的请求。

通过使用@RequestMapping注解,控制器能够声明它们会根据HTTP方法(如GET或POST方法)和URL来处理特定的请求。控制器就可以确定是在Web响应中直接写入内容,还是将应用路由一个视图并将属性注入到该视图中。

纯粹的RESTful应用将会选择第一种方式,并且会在HTTP响应中直接暴露模型的JSON或XML表述,这需要用到@ResponseBody注解。在Web应用中,这种类型的架构通常会与前端JavaScript框架关联,如Backbone.js、AngularJS或React。在这种场景中,Spring应用只需处理MVC中的模型层。我们将会在第4章中学习这种架构。

在第二种方式中,模型会传递到视图中,视图会由模板引擎进行渲染,并写入到响应之中。

视图通常会与某种模板方言关联,这种模板允许遍历模型中的内容,流行的模板方言包括JSP、FreeMarker或Thymeleaf。

混合式的方式则会在某些方面采用模板引擎与应用进行交互,并将视图层委托给前端框架。

Thymeleaf是一个模板引擎,在Spring社区中,它备受关注。

它的成功在很大程度上要归因于对用户友好的语法(它几乎就是HTML)以及扩展的便利性。

如表2-1所示,现在有各种可用的扩展,并且能够与Spring Boot进行集成。

表2-1

所支持的功能

依赖

布局

nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect

HTML 5 data-*属性

com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute

Internet Explorer的条件注释

org.thymeleaf.extras:thymeleaf-extras-conditionalcomments

对Spring Security的支持

org.thymeleaf.extras:thymeleaf-extras-springsecurity3

关于Thymeleaf与Spring集成有一个很好的入门指南,参见http://www.thymeleaf.org/doc/ tutorials/2.1/thymeleafspring.html

闲言少叙,现在我们将spring-boot-starter-thymeleaf依赖添加进来,这样就能启动Thymeleaf模板引擎了:

buildscript {
    ext {
        springBootVersion = '1.2.5.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-
plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-
plugin:0.5.1.RELEASE")
    }
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'

jar {
    baseName = 'masterSpringMvc'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
    testCompile 'org.springframework.boot:spring-boot-starter-test'
}

eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.
eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

现在,我们将第一个页面添加到应用之中,我们将其放到src/main/resources/templates中,并将其命名为resultPage.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>Hello thymeleaf</title>
</head>
<body>
    <span th:text="|Hello thymeleaf|">Hello html</span>
</body>
</html>

我们首先能够看到Thymeleaf与HTML结合得非常好,它的语法看上去也非常自然。

th:text的值放在两个竖线中间,这意味着文本中所有的值都会连接在一起。

看上去,这可能有点怪异,但实际上,我们很少在页面中进行硬编码,因此,Thymeleaf在这里采用了具有一定倾向性的设计。

对于Web设计人员来说,Thymeleaf有一项很大的优势,那就是在服务器没有运行的时候,模板中所有的动态内容都可以采用一个默认值。资源URL可以采用相对的路径来指定,每个标签都可以包含占位符。在前面的样例里面,如果是在应用的上下文中,那么文本“Hello html”将不会显示,但是如果直接在Web浏览器中打开这个文件的话,那么它就会显示了。

为了加快开发速度,在application.properties文件中添加该属性:

spring.thymeleaf.cache=false

这将会禁止启用视图缓存,每次访问的时候都会重新加载模板。

当然,在部署到生产环境时,该项配置需要禁用。在第8章时,我们会再进行设置。

 重新加载视图

如果禁用了缓存,在修改视图之后,只需在Eclipse中进行保存或者在IntelliJ中使用Build > Make Project操作就可以刷新视图。

最后,需要修改HelloController类,它必须要导航至我们新建的视图,而不是展现简单的文本。为了完成该功能,需要移除@ResponseBody注解。这样做完之后,如果再次返回字符串的话,就会告诉Spring MVC要将这个字符串映射为视图名,而不是在响应中直接展现特定的模型。

我们的控制器将会变为如下所示:

@Controller
public class HelloController {

    @RequestMapping("/")
    public String hello() {
        return "resultPage";
    }
}

在本例中,控制器会将用户转移到名为resultPage的视图中,ViewResolver接口会将这个名字与我们的视图进行关联。

再次启动应用并转至http://localhost:8080

你将会看到如图2-2所示的页面。

图2-2

让我们从这个新的“Hello World”页面后退一步,尝试去理解在这个Web应用中到底发生了什么。为了做到这一点,需要跟踪浏览器所发送的HTTP请求的行程以及它是如何从服务器端得到响应的。

每个Spring Web应用的入口都是DispatcherServlet。图2-3展现了DispatcherServlet的架构。

图2-3

这个一个典型的HttpServlet类,它会将HTTP请求分发给HandlerMapping。HandlerMapping会将资源(URL)与控制器关联起来。

控制器上对应的方法(也就是带有@RequestMapping注解的方法)将会被调用。在这个方法中,控制器会设置模型数据并将视图名称返回给分发器。

然后,DispatcherServlet将会查询ViewResolver接口,从而得到对应视图的实现。

在样例中,ThymeleafAutoConfiguration将会为我们搭建视图解析器。

通过查看ThymeleafProperties类,可以知道视图的默认前缀是“classpath:/templates/”,后缀是“.html”。

这就意味着,假设视图名为resultPage,那么视图解析器将会在类路径的templates目录下查找名为resultPage.html的文件。

在我们的应用中,ViewResolver接口是静态的,但是更为高级的实现能够根据请求的头信息或用户的地域信息,返回不同的结果。

视图最终将会被渲染,其结果会写入到响应之中。

第一个页面完全是静态的,其实并没有真正发挥出Spring MVC的威力。我们现在更进一步,如果“Hello World”这个字符串不是硬编码的,而是来源于服务器,那该怎么实现呢?

你可能会问,还是显示这个无聊的“hello world”吗?是的,不过这种方式会开启更多的可能性。现在,修改resultPage.html文件,让它展现来自模型中的信息:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>Hello thymeleaf</title>
</head>
<body>
    <span th:text="${message}">Hello html</span>
</body>
</html>

然后,我们修改控制器,将该信息保存到模型中:

@Controller
public class HelloController {

    @RequestMapping("/")
    public String hello(Model model) {
        model.addAttribute("message", "Hello from the controller");
        return "resultPage";
    }
}

我知道,这种悬疑的感觉会让你觉得很受折磨!那么,我们访问http://localhost:8080,看一下效果是什么样子的(见图2-4)。

图2-4

需要注意的第一件事情就是传递了一个新的参数到控制器的方法之中,DispatcherServlet会提供正确的对象。实际上,很多对象都可以注入到控制器的方法之中,例如HttpRequest、HttpResponse、Locale、TimeZone和Principal,其中Principal代表了一个认证过的用户。完整的对象列表可以在文档中查阅,参见http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-arguments

当使用“${}”语法时,我们实际上使用的是Spring表达式语言(Spring Expression Language,SpEL)。关于EL,有多个不同的变种,而SpEL是其中威力强大的一种。

表2-2是它主要特性的概览。

表2-2

特性

语法

描述

访问列表元素

list[0]

访问Map条目

map[key]

三元运算符

condition ? 'yes' :'no'

Elvis运算符

person ?: default

如果person的值为空的话,将会返回default

安全导航

person?.name

如果person为空或其name为空的话,返回null

模板

'Your name is #{person.name}'

将值注入到字符串中

投影

${persons.![name]}

抽取所有persons的name,并将其放到一个列表中

选择

persons.?[name =='Bob']'

返回列表中name为Bob的person

函数调用

person.sayHello()

 

 

如果要查看完整的指导文档,可以参考其手册:http://docs.spring.io/spring/docs/current/ spring-framework-reference/html/expressions.html

SpEL的用处并不仅限于视图之中,可以将它用在Spring框架的各种地方,例如,在通过@Value注解往bean中注入属性时,也可以使用SpEL。

我们已经能够在视图中展现来自服务端的数据,但是,如果想获取用户的输入该怎么办呢?根据HTTP协议,有很多方式可以实现这一点,其中最简单的就是传递查询参数到URL之中。

 查询参数

你肯定早就知道什么是查询参数了吧,它们会位于URL的“?”字符后面,是由名称和值所组成的列表,每一项会使用“&”符号进行分割,例如:page?var1= value1&var2=value2。

可以使用这项技术要求用户提供他们的名字,再次修改HelloController类,如下所示:

@Controller
public class HelloController {

    @RequestMapping("/")
    public String hello(@RequestParam("name") String userName, Model
model) {
        model.addAttribute("message", "Hello, " + userName);
        return "resultPage";
    }
}

如果此时导航至localhost:8080/?name=Geoffroy,将会看到如图2-5所示的结果。

图2-5

默认情况下,请求参数是强制要求存在的。这意味着,如果导航至localhost:8080,那么将会看到一个错误页面。

查阅一下@RequestParam的代码,可以看到除了value属性以外,它还有其他两个可用的属性:required和defaultValue。

因此,可以修改一下代码,为其指定一个默认值或者将其设置为非必填项:

@Controller
public class HelloController {

    @RequestMapping("/")
    public String hello(@RequestParam(defaultValue = "world") String
name, Model model) {
        model.addAttribute("message", "Hello, " + name);
        return "resultPage";
    }
}

 

在Java 8中,我们可以不指定value参数。如果这样的话,将会使用带有注解的方法参数名称。

好了,毕竟这本书的名字不是“精通Hello World”,我们结束这一话题。借助Spring,使用Twitter的API进行查询是非常容易的事情。

在开始之前,我们需要在Twitter的开发者控制台中注册应用。

访问https://apps.twitter.com,并创建一个新的应用。

根据你喜好为其设定一个名称,在Website和Callback URL区域中,输入http://127.0.0.1:8080(见图2-6)。这样的话,就能在本地机器上,测试开发阶段的应用。

图2-6

现在,导航至“Keys and Access Token”,并复制Consumer KeyConsumer Secret,稍后我们会用到它们,参见图2-7所示的截图。

图2-7

默认情况下,应用会具有只读的权限,对于该应用来说,这就足够了,但是如果你愿意的话,可以对其进行调整。

添加如下的依赖到build.gradle文件中:

compile 'org.springframework.boot:spring-boot-starter-social-twitter'

 

Spring Social是一组项目,提供了对各种社交网络公开API的访问功能。Spring Boot内置了对Twitter、Facebook和LinkedIn的支持。Spring Social一共包括大约30个项目,可以访问http://projects.spring.io/spring-social/来进一步了解它的情况。

添加如下的两行代码到application.properties中:

spring.social.twitter.appId= <Consumer Key>
spring.social.twitter.appSecret= <Consumer Secret>

这是与刚才所创建的应用相关的key。

我们将会在第6章中详细介绍OAuth。就现在而言,只是使用这些凭证信息发送请求到Twitter的API上,以满足我们应用的需要。

现在,就可以在控制器中使用Twitter了,将它的名字改为TweetController,从而能够以更好的方式反映其新功能:

@Controller
public class TweetController {

    @Autowired
    private Twitter twitter;

    @RequestMapping("/")
    public String hello(@RequestParam(defaultValue =
"masterSpringMVC4") String search, Model model) {
        SearchResults searchResults = twitter.searchOperations().
search(search);
        String text = searchResults.getTweets().get(0).getText();
        model.addAttribute("message", text);
        return "resultPage";
    }
}

我们可以看到,上面的代码会搜索匹配请求参数的Tweet。如果一切运行正常的话,结果中第一条记录的文本将会显示出来(见图2-8)

图2-8

当然,如果搜索没有得到任何结果的话,这段蹩脚的代码将会因为ArrayOutOfBoundException异常而导致失败。因此,可以抓紧发一条Tweet来解决这个问题!

如果想展现Tweet列表的话,那该怎么办呢?让我们修改一下resultPage.html文件:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>Hello twitter</title>
</head>
<body>
    <ul>
        <li th:each="tweet : ${tweets}" th:text="${tweet}">Some
tweet</li>
    </ul>
</body>
</html>

 

th:each是由Thymeleaf所定义的标签,它允许我们遍历一个集合并且能够在循环中将集合中的每个值赋给一个变量。

我们也需要修改控制器:

@Controller
public class TweetController {

    @Autowired
    private Twitter twitter;

    @RequestMapping("/")
    public String hello(@RequestParam(defaultValue =
"masterSpringMVC4") String search, Model model) {
        SearchResults searchResults = twitter.searchOperations().
search(search);
        List<String> tweets =
                searchResults.getTweets()
                        .stream()
                        .map(Tweet::getText)
                        .collect(Collectors.toList());
        model.addAttribute("tweets", tweets);
        return "resultPage";
    }
}

注意,我们在这里使用了Java 8的流来收集Tweet的信息。Tweet类包含了很多其他的属性,如发送者、转推的数量等。但是,现在我们尽可能地保持简单,如图2-9中的截图所示。

图2-9

可能你对lambda还不太了解,在Java 8中,每个集合都会有一个默认的方法stream(),它能够实现函数式风格的操作。

这些操作可以是中间操作(intermediate_operation),它会返回一个流,这样就能将其连接起来,也可以是终止操作(terminal operation),这样的话会返回一个值。

最著名的中间操作如下所示。

Lambda是函数表达式的便捷语法,它可以用到单个的抽象方法(Single Abstract Method)之中,也就是只包含一个函数的接口。

例如,我们可以按照如下的方式来实现Comparator接口:

Comparator<Integer> c = (e1, e2) -> e1 - e2;

在lambda之中,return关键字就是最后的表达式。

之前所使用的双冒号操作符是引用类函数的快捷方式:

Tweet::getText

之前的表达式等价于:

(Tweet t) -> t.getText()

collect方法允许我们调用一个终止操作。Collectors类是一组终止操作,它会将结果放到列表、集合或Map之中,允许进行分组(grouping)、连接(joining)等操作。

调用collect(Collectors.toList())方法将会产生一个列表,其中包含了流中的每一个元素,在我们的例子中,也就是Tweet的内容。

现在,我们的应用已经很棒了,但是在美学方面却差得很多。你可能听说过质感设计(material design),这是Google的扁平化设计。

如图2-10所示,我们将会使用Materialize(http://materializecss.com),这是一个非常漂亮的CSS和JavaScript库,与Bootstrap类似。

图2-10

在第1章中,我们曾经简单介绍过WebJars,现在要开始使用它们了。在依赖中,我们要添加jQuery和Materialize CSS:

compile 'org.webjars:materializecss:0.96.0'
compile 'org.webjars:jquery:2.1.4'

每个WebJar的结构都是标准的,每个库的JS和CSS文件都会位于/webjars/{lib}/ {version}/­*.js中。

例如,如果要添加jQuery到我们的页面中,那Web页面需要如下所示:

<script src="/webjars/jquery/2.1.4/jquery.js"></script>

接下来修改一下控制器,让它显示所有Tweet对象的列表,而不是只显示简单的文本:

package masterSpringMvc.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.twitter.api.SearchResults;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
public class TweetController {

    @Autowired
    private Twitter twitter;

    @RequestMapping("/")
    public String hello(@RequestParam(defaultValue =
"masterSpringMVC4") String search, Model model) {
        SearchResults searchResults = twitter.searchOperations().
search(search);
        List<Tweet> tweets = searchResults.getTweets();
        model.addAttribute("tweets", tweets);
        model.addAttribute("search", search);
        return "resultPage";
    }
}

在视图中,将Materialize CSS包含进来:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>Hello twitter</title>

    <link href="/webjars/materializecss/0.96.0/css/materialize.css"
type="text/css" rel="stylesheet" media="screen,projection"/>
</head>
<body>
<div class="row">

    <h2 class="indigo-text center" th:text="|Tweet results for
${search}|">Tweets</h2>

    <ul class="collection">
        <li class="collection-item avatar" th:each="tweet :
${tweets}">
            <img th:src="${tweet.user.profileImageUrl}" alt=""
class="circle"/>
            <span class="title" th:text="${tweet.user.
name}">Username</span>
            <p th:text="${tweet.text}">Tweet message</p>
        </li>
    </ul>

</div>

<script src="/webjars/jquery/2.1.4/jquery.js"></script>
<script src="/webjars/materializecss/0.96.0/js/materialize.js"></
script>
</body>
</html>

如图2-11所示结果看起来会好很多。

图2-11

我们最后想实现的就是将UI中可重用的代码块放到模板之中。为了实现该功能,我们需要使用thymeleaf-layout-dialect依赖,它已经包含在项目的spring-boot-starter-thymeleaf依赖里面。

我们会创建一个新的文件,名为default.html,并将其放在src/main/resources/templates/ layout之中,它包含了每个页面中都重复出现的代码:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta http-equiv="Content-Type" content="text/html;
charset=UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-
scale=1, maximum-scale=1.0, user-scalable=no"/>
    <title>Default title</title>

    <link href="/webjars/materializecss/0.96.0/css/materialize.css"
type="text/css" rel="stylesheet" media="screen,projection"/>
</head>
<body>
<section layout:fragment="content">
    <p>Page content goes here</p>
</section>

<script src="/webjars/jquery/2.1.4/jquery.js"></script>
<script src="/webjars/materializecss/0.96.0/js/materialize.js"></
script>
</body>
</html>

现在,我们要修改resultPage.html文件,让它使用布局,这会简化它的内容:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/default">
<head lang="en">
    <title>Hello twitter</title>
</head>
<body>
<div class="row" layout:fragment="content">

    <h2 class="indigo-text center" th:text="|Tweet results for
${search}|">Tweets</h2>

    <ul class="collection">
        <li class="collection-item avatar" th:each="tweet :
${tweets}">

            <img th:src="${tweet.user.profileImageUrl}" alt=""
class="circle"/>
            <span class="title" th:text="${tweet.user.
name}">Username</span>

            <p th:text="${tweet.text}">Tweet message</p>
        </li>
    </ul>
</div>
</body>
</html>

其中,layout:decorator="layout/default"能够表明去哪里查找布局。这样,我们可以将内容注入到布局的不同layout:fragment区域中。需要注意的是,每个模板都是合法的HTML文件,我们可以非常容易地重写它的标题。

我们现在已经有了一个很棒的用于展现Tweet的小应用,但是,我们的用户需要提供一个“search”请求参数,这该如何实现呢(见图2-12)?

图2-12

如果我们能够在应用上增加一个小表单的话,那就非常完美了。

我们接下来要做的事情如下所示。

首先,我们需要修改TweetController,为应用增加第二个视图。访问应用的根目录会展现出搜索页面,在search域中点击回车键会展现结果页面:

@Controller
public class TweetController {

    @Autowired
    private Twitter twitter;

    @RequestMapping("/")
    public String home() {
        return "searchPage";
    }

    @RequestMapping("/result")
    public String hello(@RequestParam(defaultValue =
"masterSpringMVC4") String search, Model model) {
        SearchResults searchResults = twitter.searchOperations().
search(search);
        List<Tweet> tweets = searchResults.getTweets();
        model.addAttribute("tweets", tweets);
        model.addAttribute("search", search);
        return "resultPage";
    }
}

我们会添加另外一个页面到templates文件夹下,并将其命名为searchPage.html文件。这个页面会包含一个简单的表单,它会通过get方法将要搜索的术语传递到结果页面:

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/default">
<head lang="en">
    <title>Search</title>
</head>
<body>

<div class="row" layout:fragment="content">

    <h4 class="indigo-text center">Please enter a search term</h4>

    <form action="/result" method="get" class="col s12">
        <div class="row center">
            <div class="input-field col s6 offset-s3">
                <i class="mdi-action-search prefix"></i>
                <input id="search" name="search" type="text"
class="validate"/>
                <label for="search">Search</label>
            </div>
        </div>
    </form>
</div>

</body>
</html>

这是一个很简单的HTML,但是已经可以正常运行了,你现在可以尝试一下。

如果不允许展现某些搜索结果的话,该怎么办呢?假设如果用户输入struts的话,我们想展现一个出错页面。

要实现该功能,最好的方式就是修改提交数据的表单。在控制器中,我们可以拦截提交的内容并实现该业务相关的规则。

首先,要修改searchPage中的表单,原来的内容如下所示:

<form action="/result" method="get" class="col s12">

现在,我们将表单改成如下的形式:

<form action="/postSearch" method="post" class="col s12">

我们还需要在服务器端处理该POST请求。在TweetController中添加如下的方法:

@RequestMapping(value = "/postSearch", method = RequestMethod.POST)
public String postSearch(HttpServletRequest request,
    RedirectAttributes redirectAttributes) {
        String search = request.getParameter("search");
        redirectAttributes.addAttribute("search", search);
        return "redirect:result";
}

在这里,有几项比较新鲜的内容:

RedirectAttributes是一个Spring的模型,专门用于redirect场景下传送值。

 

在Java Web应用中,Redirect/Forward是典型的可选方案。它们都会改变展现给用户的视图,其中的区别在于Redirect会发送一个302头信息,它会在浏览器内部触发导航,而Forward则不会导致URL的变化。在Spring MVC中,我们可以任意使用这两种方案,只需在方法返回的字符串上添加“redirect:”或“forward:”前缀即可。在这两种场景中,我们所返回的字符串都不会像前面看到的那样解析为视图,而是触发到特定URL的导航。

之前的样例有些牵强,在下一章中会进行更加巧妙的处理。如果你在postSearch方法上添加一个断点的话,就会发现它会在post表单之后进行调用。

那么,该怎样显示错误信息呢?

我们改一下postSearch方法:

@RequestMapping(value = "/postSearch", method = RequestMethod.POST)
public String postSearch(HttpServletRequest request,
    RedirectAttributes redirectAttributes) {
        String search = request.getParameter("search");
        if (search.toLowerCase().contains("struts")) {
                redirectAttributes.addFlashAttribute("error", "Try
using spring instead!");
                return "redirect:/";
        }
        redirectAttributes.addAttribute("search", search);
        return "redirect:result";
}

如果用户的搜索包含“struts”这个术语的话,我们会将其重定向到searchPage页面并且使用flash属性添加一点错误信息。

这个特殊的属性会在该请求的时间范围内一直存活,直到页面渲染完成才消失。当使用POST-REDIRECT-GET模式的时候,它是非常有用的,就像刚刚做的这样。

我们接下来需要在searchPage结果页面中展现这个错误信息:

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/default">
<head lang="en">
    <title>Search</title>
</head>
<body>

<div class="row" layout:fragment="content">

    <h4 class="indigo-text center">Please enter a search term</h4>

    <div class="col s6 offset-s3">
        <div id="errorMessage" class="card-panel red lighten-2"
th:if="${error}">
            <span class="card-title" th:text="${error}"></span>
        </div>
        <form action="/postSearch" method="post" class="col s12">
            <div class="row center">
                <div class="input-field">
                    <i class="mdi-action-search prefix"></i>
                    <input id="search" name="search" type="text"
class="validate"/>
                    <label for="search">Search</label>
                </div>
            </div>
        </form>
    </div>
</div>

</body>
</html>

现在,如果用户试图搜索包含“struts2”的Tweet,将会得到有用且合适的答案(见图2-13)。

图2-13

在本章结束的时候,你应该有了一个控制器,也就是TweetController,它会负责处理搜索,同时还有一个没有发生变化的配置类MasterSpringMvcApplication,它们位于src/main/java目录之中(见图2-14)。

图2-14

在src/main/resources目录中,会有一个默认的布局以及使用该布局的两个页面。

在application.properties文件中,我们添加了Twitter应用的凭证,还有一个告诉Spring禁用模板缓存的属性,从而便于开发的进行,该目录的结构如图2-15所示:

图2-15

在本章中,我们学习了如何实现良好的MVC架构,看到了Spring MVC内部的一些运行机制,并通过非常少量的配置使用了Spring Social Twitter的功能。通过使用WebJars,可以设计非常漂亮的Web应用。

在下一章中,我们会要求用户填写其基本信息(profile),这样就能自动获取他们可能感兴趣的Tweet。这也给了我们一个机会来更深入地学习表单、格式化、校验以及国际化的功能。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程