Spring实战(第6版)

978-7-115-59869-1
作者: [美]克雷格·沃斯(Craig Walls)
译者: 张卫滨吴国浩
编辑: 郭泳泽
分类: Spring

图书目录:

详情

本书是一本经典而实用的Spring学习指南,介绍了Spring使用框架、Spring Boot,以及Spring系统中的其他组成部分。 本书分为4个部分,共18章。第1部分(第1章~第6章)涵盖了构建Spring应用的基础知识。第2部分(第7章~第10章)讨论了如何讲Spring应用与其他应用进行集成。第3部分(第11章~第14章)探讨了Spring对反应式编程提供的全新支持。第4部分(第15章~第18章)介绍了如何做好应用投入生产环境前的准备工作,以及如何进行部署。 本书适合刚刚开始学习Spring Boot和Spring框架的Java开发人员阅读,也适合想要超越基础知识并学习Spring新特性的经验丰富的Spring开发者参考。

图书摘要

版权信息

书名:Spring实战(第6版)

ISBN:978-7-115-59869-1

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

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

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

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

著    [美] 克雷格•沃斯(Craig Walls)

译    张卫滨 吴国浩

责任编辑 郭泳泽

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59869”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


Original English language edition published by Manning Publications, USA. Copyright © 2021 by Manning Publications.

Simplified Chinese-language edition copyright © 2022 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由Manning Publications授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制本书内容。

版权所有,侵权必究。


本书是一本经典而实用的Spring学习指南,介绍了Spring使用框架、Spring Boot,以及Spring系统中的其他组成部分。

本书分为4个部分,共18章。第1部分(第1章~第6章)涵盖了构建Spring应用的基础知识。第2部分(第7章~第10章)讨论了如何将Spring应用与其他应用进行集成。第3部分(第11章~第14章)探讨了Spring对反应式编程提供的全新支持。第4部分(第15章~第18章)介绍了如何做好应用投入生产环境前的准备工作,以及如何进行部署。

本书适合刚刚开始学习Spring Boot和Spring框架的Java开发人员阅读,也适合想要超越基础知识并学习Spring新特性的经验丰富的Spring开发者参考。


距离撰写《Spring实战(第5版)》的译者序差不多已经过去了3年的时间,在这段时间里,虽然Spring没有大版本的更新,但是整个Spring生态却有了长足的进步和发展。Spring Boot在不断优化,越来越适应云原生的开发环境;反应式编程已经度过了理念阶段,逐渐在实际项目中落地;Spring Data正在支持越来越多的数据库类型;Spring Cloud在借助Netflix相关的项目成功成为微服务开发的首选方案之后,正在慢慢摆脱Netflix相关项目的束缚,开始自立门户;Spring Native更是借助GraalVM的东风,以脱离JVM为噱头,成功吸引了一批流量……

更不用说,大家期待的Spring Framework 6和Spring Boot 3甚至都要以Java 17作为最基础的运行时环境了(这让众多依然在使用Java 8的开发者情何以堪)。随着Java语言版本升级换代的加快,再加上Kubernetes(K8s)、服务网格等云原生技术的发展,Java相关技术快速进步。作为技术爱好者,总有一种几天不关注技术社区就会落伍的紧迫感。

但不管技术如何快速演进,有一些内在的精髓是相对稳定的。就Spring本身而言,虽然基于Spring的社区项目越来越多,功能越来越丰富,但Spring内核的理念依然是没有变化的,我们依然要从依赖注入、面向切面编程和自动配置等特性入手,探索和掌握新技术的发展思路和实现脉络。所以,希望本书能够帮助读者理解和掌握Spring的基本原理,探究具体功能背后的技术考量。

记得第一次接触Spring是在2007年,当时还在学校读研的我从图书馆借到了第1版《Spring实战》,虽然当时忙于毕业的事情没有把这本书读完,但它依然给我留下了极深的印象。在那个时代,J2EE without EJB真的是一种超前的理念,当时很多人可能都没有想到Spring居然会在企业级Java开发领域活跃这么多年的时间。当然,那时的我更没有想到会与这个系列的书有如此深厚的缘分,参与翻译了这本书的4个版本!

从Spring诞生到现在,已经有接近20年的时间了,技术领域有了翻天覆地的变化,尤其是近年来,Spring也面临着不少的挑战:微服务领域,服务网格技术是Spring Cloud相关项目的有力竞争者;以Quarkus、Micronaut为代表的一些新生代开发框架在强力挑战Spring的主导地位……我们希望在不断的竞争中,有越来越多的新技术涌现出来。

在《Spring实战(第6版)》的翻译中,感谢同事吴国浩的协助。他负责翻译了本书的部分章节。

当然,还要再次感谢我的爱人和儿子,容忍我没日没夜守在笔记本电脑前的这几个月。

希望本书对读者有所帮助。如果您在阅读中遇到问题,可以通过levinzhang1981@126.com与我联系。祝阅读愉快。

张卫滨

2022年5月3日于大连


编写本书的目的是让你学会使用Spring框架、Spring Boot及Spring生态系统中的其他组成部分构建令人赞叹的应用程序。本书首先介绍如何使用Spring和Spring Boot开发基于Web、以数据库作为后端的Java应用,随后进行必要的扩展,展现了如何与其他应用进行集成和使用反应式类型进行编程,最后讨论如何准备应用的部署。

尽管Spring生态系统中的每个项目都提供了完善的文档,但是本书所做的是所有参考文档都无法做到的事情:提供了一个实用的、项目驱动的指南,将Spring的各种元素组合起来形成一个真正的应用。

本书适合刚刚开始学习Spring Boot和Spring框架的Java开发人员阅读,也适合想要超越基础知识并学习Spring新特性的经验丰富的Spring开发者参考。

本书分成4个部分,共计18章。

第1部分涵盖了构建Spring应用的基础知识。

第1章介绍Spring、Spring Boot,以及如何初始化Spring项目。我们在这章中迈出构建Spring应用的第一步,在本书后续章节中,我们会对这个应用进行扩展。

第2章讨论如何使用Spring MVC构建应用的Web层。我们会构建处理Web请求的控制器,并在浏览器中渲染信息的视图。

第3章深入探讨Spring应用的后端,在这里数据会持久化到关系型数据库中。

第4章会继续数据持久化的话题,学习如何将数据持久化到非关系型数据库Cassandra和MongoDB中。

第5章介绍如何使用Spring Security认证用户并防止未认证的用户访问应用。

第6章介绍如何使用Spring Boot的配置属性功能来配置Spring应用。我们还会在这章学习如何使用profile选择性地应用配置。

第2部分讨论了如何将Spring应用与其他应用进行集成。

第7章延续第2章对Spring MVC的讨论,我们会学习如何在Spring中编写和消费REST API。

第8章展示如何使用Spring Security和OAuth 2保护我们在第7章创建的API。

第9章讨论如何使用异步通信技术让Spring应用发送和接收消息,这里会用到Java Message Service、RabbitMQ或Kafka。

第10章讨论如何使用Spring Integration进行声明式地应用集成。

第3部分探讨了Spring对反应式编程提供的全新支持。

第11章介绍Reactor项目,这是一个反应式编程库,支撑Spring 5的反应式特性。

第12章重新探讨REST API开发,介绍全新的Web框架Spring WebFlex。该框架借用了很多Spring MVC的理念,为Web开发提供了新的反应式模型。

第13章介绍如何使用Spring Data编写反应式数据持久化,我们会尝试读取和写入Cassandra与Mongo数据库。

第14章介绍RSocket协议。这是一个新的通信协议,在创建API方面,它提供了HTTP协议的反应式替代方案。

第4部分介绍了如何做好应用投入生产环境前的准备工作,以及如何进行部署。

第15章介绍Spring Boot Actuator。这是Spring Boot的一个扩展,它通过REST端点的形式暴露Spring应用内部的运行状况。

第16章介绍如何使用Spring Boot Admin。它是构建在Actuator之上的一个对用户友好的基于浏览器的管理应用。

第17章讨论如何将Spring bean暴露为JMX MBean,以及如何消费它们。

最后,第18章介绍如何将Spring应用部署到各种生产环境中,包括Kubernetes。

一般来讲,刚刚接触Spring的开发人员应该从第1章开始,按顺序阅读每一章。经验丰富的Spring开发人员可能更愿意从任何其感兴趣的章节开始阅读。每一章都是建立在前一章的基础上的,所以如果从中间开始阅读,可能会漏掉一些前文信息。

本书包含许多源代码的样例,有的是带有编号的程序清单,有的是在普通文本中嵌入的源码。

在许多情况下,原始源代码会重新排版。我们添加了换行符,并重新缩进,以适应书中可用的页面空间。在极少数情况下,这样做依然是不够的,在这种情况下,程序清单会包括换行符(➥)。此外,当在文中描述代码的时候,源代码中的注释通常会被移除。许多程序清单会带有代码注释,用来突出强调重要的概念。

本书中的样例源码可以从异步社区的本书页面下载。

还需要其他帮助吗?

Spring的官方网站有很多有用的起步指南(其中一部分就是由本书的作者编写的)。

StackOverflow论坛上的Spring标签页和Spring Boot标签页是询问有关Spring的问题和帮助别人的好地方。帮助解决别人的Spring问题是学习Spring的好办法。 -


克雷格•沃斯(Craig Walls)是Pivotal的高级工程师。他是Spring框架的热心推动者,经常在本地用户组和会议上发言,撰写关于Spring的文章。在不琢磨代码的时候,他往往在计划去迪士尼世界或迪士尼乐园的下一次旅行。他希望尽可能多地陪伴他的妻子、两个女儿,以及宠物。


本书的封面人物是“Le Caraco”,也就是约旦西南部卡拉克(Karak)省的居民。该省的首府是Al-Karak,那里的山顶有座城堡,对死海和周边的平原有着极佳的视野。这幅图出自法国1796年出版的旅游图书Encyclopédie des Voyages,该书由J. G. St. Sauveur编写。在那时,为了娱乐而去旅游还是相对新鲜的做法,而像这样的旅游指南是很流行的,它能够让旅行家和足不出户的人们了解法国其他地区和法国以外的居民。

Encyclopédie des Voyages中多种多样的图画生动描绘了200年前世界上各个城镇和地区的独特魅力。在那时,相隔几十千米的两个地区着装就不相同,可以通过着装判断人们究竟属于哪个地区。这本旅行指南展现了那个时代和其他历史时代的隔离感和距离感,这与我们这个人口高速流动的时代是截然不同的。

从那以后,服装风格发生了改变,富有地方特色的多样性开始淡化。现在,有时很难说一个大洲的居民和其他大洲的居民在着装上有什么不同。从积极的方面来看,我们可能用原来文化和视觉上的多样性换来了个人风格的多样性,或者说是更为多样、有趣,且科学技术更发达的智能化生活。这本旅行指南体现出两个世纪前地区间生活的丰富多样性。Manning将其中的图片作为书籍的封面,以此来体现计算机领域的创造性、积极性和趣味性。


Spring进入开发领域已经超过了18年,它的基本使命是使Java应用的开发更容易。最初,这意味着它会提供一个轻量级的EJB 2.x替代方案。但这只是Spring的序幕。多年来,Spring将其简化开发的使命扩展到了解决我们面临的各种挑战上,包括持久化、安全性、集成、云计算等。

尽管Spring在实现和简化企业级Java开发方面已走过了近20年,但它丝毫没有显示出发展速度放缓的迹象。Spring在继续解决Java开发的挑战,无论是创建部署在传统应用服务器上的应用,还是创建部署在云端Kubernetes集群上的容器化应用程序。随着Spring Boot开始提供自动配置、构建依赖辅助和运行时监控等功能,现在是成为Spring开发者的理想时机。

本书是Spring和Spring Boot指南,在第5版基础上进行了升级更新,以反映这两项技术所提供的新内容。即便是Spring新手,在第1章结束之前,也可以启动并运行第一个Spring应用。跟随本书,你会学习创建Web应用、处理数据、保证应用安全,以及管理应用配置等内容。接下来,你会探索将Spring应用与其他应用程序集成的方法,以及如何让Spring应用从反应式编程中获益,包括使用新的RSocket通信协议。在本书的末尾,你会看到如何为生产环境准备我们的应用程序,并学习各种部署方案。

无论你是第一次接触Spring,还是有多年的Spring开发经验,这本书都会带你开展一段精彩旅程。我为你感到兴奋,也很荣幸能为你编写这份指南。我期待你使用Spring创造出精彩的应用!


Spring和Spring Boot所做的最令人惊奇的事情之一就是自动为应用程序提供所有的基础功能,让开发人员关注于应用程序特有的逻辑。不幸的是,对于写书这件事来说,并没有这样的魔法。是这样的吗?

在Manning,有很多人在施展魔法,确保这本书是最好的。特别要感谢我的技术编辑Jenny Stout、生产负责人Deirdre Hiam、文字编辑Pamela Hunt、美术编辑Jennifer Houle,以及整个制作团队,感谢他们为实现本书所做的出色工作。

在此过程中,我们得到了几位同行评论的反馈,他们确保了这本书没有偏离目标,涵盖了正确的内容。为此,我要感谢Al Pezewski、Alessandro Campeis、Becky Huett、Christian Kreutzer-Beck、Conor Redmond、David Paccoud、David Torrubia Iñigo、David Witherspoon German Gonzalez-Morris、Iain Campbell、Jon Guenther、Kevin Liao、Mark Dechamps、Michael Bright、Philippe Vialatte、Pierre-Michel Ansel、Tony Sweets、William Fly和Zorodzayi Mukuya。

当然还要感谢Spring团队的杰出工作,你们创造了令人不可思议的成就。作为团队的一员,我很自豪。

非常感谢我的同行们在No Fluff/Just Stuff巡回演讲上的发言。我从你们每个人身上学到很多。也非常感谢那些参加过我在NFJS巡回演讲的人,虽然我是站在房间最前面演讲的人,但我经常从你们那里学到很多东西。

我要再次感谢腓尼基人,你们太棒了[1]

最后,我要感谢我美丽的妻子Raymie,她是我生命中的挚爱,是我最甜蜜的梦想。谢谢你的鼓励,也谢谢你为这本新书做的努力。致我可爱的女儿Maisy和Madi:我为你们感到骄傲,为你们即将成为了不起的年轻女士感到骄傲。我对你们的爱超出了你们的想象,也超出了我语言所能表达的程度。

[1] 腓尼基人被认为是字母系统的创建者,基于字母的所有现代语言都是由此衍生而来。在迪士尼世界的Epcot,有名为Spaceship Earth的时光穿梭体验项目,我们可以了解到人类交流的历史,甚至能够回到腓尼基人的时代,在这段旅程的旁白中这样说道:如果你觉得学习字母语言很容易,那感谢腓尼基人吧,是他们发明了它。这是作者的一种幽默说法。——译者注


本书由异步社区出品,社区(www.epubit.com)为您提供相关资源和后续服务。

微信扫码关注【异步社区】微信公众号,回复“e59869”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

本书提供样例源码。

请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ contribute即可)。

如果学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


书的第一部分会介绍如何开始编写Spring应用,并在这个过程中介绍Spring的基础知识。

在第1章中,我会简要介绍Spring和Spring Boot的核心知识,并且会在构建第一个Spring应用Taco Cloud的过程中,展示如何初始化Spring项目。在第2章中,我们会深入研究Spring MVC,了解如何在浏览器中显示模型数据,以及如何处理和验证表单输入。我们还会看到选择视图模板库的技巧。

在第3章中,我会介绍Spring的JDBC模板,以及如何使用预处理语句和key holder插入数据。随后,我们会学习使用Spring Data声明JDBC(Java Database Connectivity)和JPA(Java Persistence API)存储库。第4章会围绕Spring持久化的话题,介绍两个Spring Data模块,将数据分别持久化到Cassandra和MongoDB中。第5章介绍了Spring应用程序的安全性,包括自动配置Spring Security、声明自定义用户存储、自定义登录页面,以及防止跨站请求伪造(Gross-Site Request Forgery,CSRF)攻击。作为第1部分的结尾,我们会在第6章中学习配置属性。我们会了解如何细粒度地调整自动配置bean、让应用组件使用配置属性,以及如何使用Spring profile。


本章内容:

Spring和Spring Boot的必备知识;

初始化Spring项目;

Spring生态系统概览。

希腊哲学家赫拉克利特(Heraclitus)尽管并不以擅长软件开发而闻名,但似乎深谙此道。他的一句话经常被引用:“唯一不变的就是变化”,这句话抓住了软件开发的真谛。

我们现在开发应用的方式和1年前、5年前、10年前都是不同的,更别提20年前了,正是在20年前,Rod Johnson的图书Expert One-on-One J2EE Design and Development(Wrox,2002年)介绍了Spring框架的初始形态。

当时,最常见的应用形式是基于浏览器的Web应用,后端由关系型数据库作为支撑。尽管这种形式的开发依然有它的价值,Spring也为这种应用提供了良好的支持,但是我们现在感兴趣的还包括如何开发面向云的由微服务组成的应用,这些应用会将数据保存到各种类型的数据库中。另外一个崭新的关注点是反应式编程,它致力于通过非阻塞操作提供更好的扩展性并提升性能。

随着软件开发的发展,Spring框架也在不断变化,以解决现代应用开发中的问题,其中就包括微服务和反应式编程。Spring还通过引入Spring Boot简化了自己的开发模型。

不管你想要开发数据库作为支撑的简单Web应用,还是围绕微服务构建一个现代应用,Spring框架都能帮助你达成目标。本章是使用Spring进行现代应用开发的第一步。

我知道你现在可能迫不及待地想要开始编写Spring应用了。我向你保证,在本章结束之前,你肯定能够开发一个简单的Spring应用。但首先,我将使用Spring的一些基础概念为你搭建一个舞台,帮助你理解Spring是如何运转起来的。

任何实际的应用程序都是由很多组件组成的,每个组件负责整个应用功能的一部分,这些组件需要与其他的应用元素协调以完成自己的任务。当应用程序运行时,需要以某种方式创建并引入这些组件。

Spring的核心是提供了一个容器(container)。它们通常被称为Spring应用上下文(Spring application context),会创建和管理应用的组件。这些组件也可以称为bean,会在Spring应用上下文中装配在一起,从而形成一个完整的应用程序,这类似于砖块、砂浆、木材、管道和电线组合在一起,形成一栋房子。

将bean装配在一起的行为是通过一种基于依赖注入(Dependency Injection,DI)的模式实现的。此时,组件不会再去创建它所依赖的组件并管理它们的生命周期,使用依赖注入的应用依赖于单独的实体(容器)来创建和维护所有的组件,并将其注入到需要它们的bean中。通常,这是通过构造器参数和属性访问(property accessor)方法来实现的。

举例来说,假设在应用的众多组件中,有两个是我们需要处理的:库存服务(用来获取库存水平)和商品服务(用来提供基本的商品信息)。商品服务需要依赖于库存服务,这样它才能提供商品的完整信息。图1.1阐述了这些bean和Spring应用上下文之间的关系。

图1.1 应用组件通过Spring的应用上下文来进行管理并实现互相注入

在核心容器之上,Spring及其一系列的相关库提供了Web框架、各种持久化可选方案、安全框架、与其他系统集成、运行时监控、微服务支持、反应式编程模型,以及众多现代应用开发所需的其他特性。

在历史上,指导Spring应用上下文将bean装配在一起的方式是使用一个或多个XML文件,这些文件描述了各个组件以及它们与其他组件的关联关系。例如,如下的XML描述了两个bean —— InventoryService bean和ProductService bean,并且通过构造器参数将InventoryService装配到ProductService中:

<bean id = "inventoryService" class = "com.example.InventoryService" /><bean id = "productService" class = "com.example.ProductService" > <constructor-arg ref = "inventoryService" /></bean>

但是,在最近的Spring版本中,基于Java的配置更为常见。如下基于Java的配置类是与XML配置等价的:

@Configuration
public class ServiceConfiguration {
  @Bean
  public InventoryService inventoryService() {
    return new InventoryService();
  }

  @Bean
  public ProductService productService() {
    return new ProductService(inventoryService());
  }
}

@Configuration注解会告知Spring这是一个配置类,它会为Spring应用上下文提供bean。

这个配置类的方法上使用@Bean注解进行了标注,这表明这些方法所返回的对象会以bean的形式添加到Spring的应用上下文中(默认情况下,这些bean所对应的bean ID与定义它们的方法名称是相同的)。

相对于基于XML的配置方式,基于Java的配置会带来多项额外的收益,包括更强的类型安全性以及更好的重构能力。即便如此,不管是使用Java还是使用XML的显式配置,都只有在Spring不能自动配置组件的时候才具有必要性。

在Spring技术中,自动配置起源于所谓的自动装配(autowiring)和组件扫描(component scanning)。借助组件扫描技术,Spring能够自动发现应用类路径下的组件,并将它们创建成Spring应用上下文中的bean。借助自动装配技术,Spring能够自动为组件注入它们所依赖的其他bean。

最近,随着Spring Boot的引入,自动配置的能力已经远远超出了组件扫描和自动装配。Spring Boot是Spring框架的扩展,提供了很多生产效率方面的增强。最为大家所熟知的增强就是自动配置(autoconfiguration),Spring Boot能够基于类路径中的条目、环境变量和其他因素合理猜测需要配置的组件,并将它们装配在一起。

我非常愿意为你展现一些关于自动配置的示例代码,但是我做不到。自动配置就像风一样,你可以看到它的效果,但是我找不到代码指给你说,“看!这就是自动配置的样例!”事情发生了,组件启用了,功能也提供了,但是不用编写任何的代码。没有代码就是自动装配的本质,也是它如此美妙的原因所在。

Spring Boot的自动配置大幅度减少了构建应用所需的显式配置的数量(不管是XML配置还是Java配置)。实际上,当完成本章的样例时,我们会有一个可运行的Spring应用,该应用只有一行Spring配置代码。

Spring Boot极大地改善了Spring的开发,很难想象在没有它的情况下如何开发Spring应用。因此,本书会将Spring和Spring Boot当成一回事。我们会尽可能多地使用Spring Boot,只有在必要的时候才使用显式配置。因为Spring XML配置是一种过时的方式,所以我们主要关注Spring基于Java的配置。

闲言少叙,既然本书的名称中包含“实战”这个词,那么就开始动手吧!下面我们将会编写使用Spring的第一个应用。

在本书的课程中,我们将会创建一个名为Taco Cloud的在线应用,它能够订购人类所发明的一种美食——墨西哥煎玉米卷(taco)[1]。当然,在这个过程中,为了达成目标,我们将会用到Spring、Spring Boot及各种相关的库和框架。

有多种初始化Spring应用的可选方案。尽管我可以教你手动创建项目目录结构和定义构建规范的各个步骤,但这无疑是浪费时间,我们最好将时间花在编写应用代码上。因此,我们将会学习如何使用Spring Initializr初始化应用。

Spring Initializr是一个基于浏览器的Web应用,同时也是一个REST API,它能够生成一个Spring项目结构的骨架,我们还可以使用各种想要的功能来填充它。使用Spring Initializr的几种方式如下所示:

通过地址为https://start.spring.io/的Web应用;

在命令行中使用curl命令;

在命令行中使用Spring Boot命令行接口;

在Spring Tool Suite中创建新项目;

在IntelliJ IDEA中创建新项目;

在NetBeans中创建新项目;

在Apache NetBeans中创建新项目。

我将这些细节放到了附录中,这样就不用在这里花费很多页的篇幅介绍每种可选方案了。在本章和本书的其他章节中,我都会向你展示如何使用我最钟爱的方式创建新项目,也就是在Spring Tool Suite中使用Spring Initializr。

顾名思义,Spring Tool Suite是一个非常棒的Spring开发环境,能够以Eclipse、Visual Studio Code或Theia IDE扩展的方式来使用。我们也可以在Spring网站的Spring Tools页面下载直接可运行的Spring Tool Suite二进制文件。Spring Tool Suite提供了便利的Spring Boot Dashboard特性,让我们能够在IDE中很容易地启动、重启和停止Spring Boot应用。

如果你不是Spring Tool Suite用户,那也没有关系,我们依然可以做朋友。你可以跳转到附录中,查看最适合你的Initializr方案,以此来替换后面小节中的内容。但是,在本书中,我偶尔会提到Spring Tool Suite特有的特性,比如Spring Boot Dashboard。你如果不使用Spring Tool Suite,那么需要调整这些指令以适配你的IDE。

要在Spring Tool Suite中初始化一个新的Spring项目,我们首先要点击File菜单,选择New,接下来选择Spring Starter Project。图1.2展现了要查找的菜单结构。

图1.2 在Spring Tool Suite中使用Initializr初始化一个新项目

在选择Spring Starter Project之后,将会出现一个新的向导对话框(见图1.3)。向导的第一页会询问一些项目的通用信息,比如项目名称、描述和其他必要的信息。如果你熟悉Maven pom.xml文件的内容,那么就可以识别出大多数的输入域条目最终都会成为Maven的构建规范。对于Taco Cloud应用来说,我们可以按照图1.3的样子来填充对话框,然后选择Next。

向导的下一页会让我们选择要添加到项目中的依赖(见图1.4)。注意,在对话框的顶部,我们可以选择项目要基于哪个Spring Boot版本。它的默认值是最新的可用版本。一般情况下,最好使用默认值,除非你需要使用不同的版本。

至于依赖项本身,你可以打开各个区域并手动查找所需的依赖项,也可以在Available顶部的搜索框中对依赖进行搜索。对于Taco Cloud应用来说,我们最初的依赖项如图1.4所示。

图1.3 为Taco Cloud应用指定通用的项目信息

图1.4 选择Starter依赖

现在,你可以选择Finish来生成项目并将其添加到工作空间中。但是,如果你还想多体验一些功能,可以再次选择Next,看一下新Starter项目向导的最后一页,如图1.5所示。

图1.5 指定备用的Initializr地址

默认情况下,新项目的向导会调用Spring Initializr来生成项目。通常情况下,没有必要覆盖默认值,这也是我们可以在向导的第二页直接选择Finish的原因。但是,如果你基于某种原因托管了自己的Initializr克隆版本(可能是本地机器上的副本或者公司防火墙内部运行的自定义克隆版本),那么你可能需要在选择Finish之前修改Base Url输入域的值,使其指向自己的Initializr实例。

选择Finish之后,项目会从Initializr下载并加载到工作空间中。此时,要等待它加载和构建,然后你就可以开始开发应用的功能了。但首先,我们看一下Initializr都为我们提供了什么。

项目加载到IDE中之后,我们将其展开,看一下其中都包含什么内容。图1.6展现了Spring Tool Suite中已展开的Taco Cloud项目。

图1.6 Spring Tool Suite中所展现的初始Spring项目结构

你可能已经看出来了,这就是一个典型的Maven或Gradle项目结构,其中应用的源码放到了“src/main/java”中,测试代码放到了“src/test/java”中,而非Java的资源放到了“src/main/resources”中。在这个项目结构中,我们需要注意以下几点。

mvnw和mvnw.cmd:这是Maven包装器(wrapper)脚本。借助这些脚本,即便你的机器上没有安装Maven,也可以构建项目。

pom.xml:这是Maven构建规范,随后我们将会深入介绍该文件。

TacoCloudApplication.java:这是Spring Boot主类,它会启动该项目。随后,我们会详细介绍这个类。

application.properties:这个文件起初是空的,但是它为我们提供了指定配置属性的地方。在本章中,我们会稍微修改一下这个文件,但是我会将配置属性的详细阐述放到第6章。

static:在这个文件夹下,你可以存放任意为浏览器提供服务的静态内容(图片、样式表、JavaScript等),该文件夹初始为空。

templates:这个文件夹中存放用来渲染内容到浏览器的模板文件。这个文件夹初始是空的,不过我们很快就会往里面添加Thymeleaf模板。

TacoCloudApplicationTests.java:这是一个简单的测试类,它能确保Spring应用上下文成功加载。在开发应用的过程中,我们会将更多的测试添加进来。

随着Taco Cloud应用功能的增长,我们会不断使用Java代码、图片、样式表、测试以及其他附属内容来充实这个项目结构。不过,在此之前,我们先看一下Spring Initializr提供的几个条目。

探索构建规范

在填充Initializr表单的时候,我们声明项目要使用Maven来进行构建。因此,Spring Initializr所生成的pom.xml文件已经包含了我们所选择的依赖。程序清单1.1展示了Initializr为我们提供的完整pom.xml。

程序清单1.1 初始的Maven构建规范

<?xml version = "1.0" encoding = "UTF-8"?><project
     xmlns = "http://maven.apache.org/POM/4.0.0"
  xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
        https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>  ⇽---Spring Boot的版本
    <relativePath />
  </parent>
  <groupId>sia</groupId>
  <artifactId>taco-cloud</artifactId> 
  <version>0.0.1-SNAPSHOT</version>
  <name>taco-cloud</name>
  <description>Taco Cloud Example</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>  ⇽---Starter依赖
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

</dependencies>

<build>
  <plugins>
    <plugin>  ⇽---Spring Boot插件
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
  </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
  </pluginRepositories>

</project>

首先要注意的是<parent>元素,更具体来说是它的<version>子元素。这表明我们的项目要以spring-boot-starter-parent作为其父POM。除了其他的一些功能之外,这个父POM为Spring项目常用的一些库提供了依赖管理。对于这些父POM中所涵盖到的库,我们不需要指定它们的版本,因为它会通过父POM继承下来。这里的2.5.3表明要使用Spring Boot 2.5.3,所以就会根据这个版本的Spring Boot定义来继承依赖管理。除了指定其他的依赖之外,2.5.3版本的Spring Boot依赖管理会指定底层的核心Spring框架的版本为5.3.9。

既然我们谈到了依赖的话题,那么需要注意在<dependencies>元素下声明了4个依赖。你可能会对前三个感到更熟悉一些。它们直接对应我们在Spring Tool Suite新项目向导中,点击Finish之前所选择的Spring Web、Thymeleaf和Spring Boot DevTools依赖。第四个依赖提供了很多有用的测试功能。我们没有必要在专门的复选框中选择它,因为Spring Initializr会假定我们要编写测试(希望你能正确地开展这项工作)。

你可能也注意到了,除了DevTools依赖之外,其他的这些依赖的artifactId上都有starter这个单词。Spring Boot starter依赖的特别之处在于它们本身并不包含库代码,而是传递性地拉取其他的库。这种starter依赖主要有以下几个好处。

构建文件会显著减小并且更易于管理,因为这样不必为所需的每个依赖库都声明依赖。

我们能够根据它们所提供的功能来思考依赖,而不是根据库的名称。如果要开发Web应用,只需添加web starter依赖,而不必添加一堆单独的库。

我们不必再担心库版本的问题。你可以直接相信给定版本的Spring Boot,传递性引入的库的版本都是兼容的。现在,你只需要关心使用的是哪个版本的Spring Boot就可以了。

最后,构建规范还包含一个Spring Boot插件。这个插件提供了一些重要的功能,如下所示:

它提供了一个Maven goal,允许我们使用Maven来运行应用;

它会确保依赖的所有库都会包含在可执行JAR文件中,并且能够保证它们在运行时类路径下是可用的;

它会在JAR中生成一个manifest文件,将引导类(在我们的场景中,也就是TacoCloudApplication)声明为可执行JAR的主类。

谈到了引导类,我们打开它看一下。

引导应用

因为我们将会通过可执行JAR文件的形式来运行应用,所以很重要的一点就是要有一个主类,它将会在JAR运行的时候被执行。我们同时还需要一个最小化的Spring配置,用来引导该应用。这就是TacoCloudApplication类所做的事情,如程序清单1.2所示。

程序清单1.2 Taco Cloud的引导类

package tacos;

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

@SpringBootApplication  ⇽---Spring Boot应用
public class TacoCloudApplication {

  public static void main(String[] args) {
    SpringApplication.run(TacoCloudApplication.class, args);  ⇽---运行应用
  }

}

TacoCloudApplication尽管只有很少的代码,但是包含了很多的内容。其中,最强大的一行代码看上去很短:@SpringBootApplication注解明确表明这是一个Spring Boot应用。但是,@SpringBootApplication远比看上去更强大。

@SpringBootApplication是一个组合注解,组合了3个其他的注解。

@SpringBootConfiguration:将该类声明为配置类。尽管这个类目前还没有太多的配置,但是后续我们可以按需添加基于Java的Spring框架配置。这个注解实际上是@Configuration注解的特殊形式。

@EnableAutoConfiguration:启用Spring Boot的自动配置。我们随后会介绍自动配置的更多功能。就现在来说,我们只需要知道这个注解会告诉Spring Boot自动配置它认为我们会用到的组件。

@ComponentScan:启用组件扫描。这样我们能够通过像@Component、@Controller、@Service这样的注解声明其他类,Spring会自动发现它们并将它们注册为Spring应用上下文中的组件。

TacoCloudApplication另外一个很重要的地方是它的main()方法。这是JAR文件执行的时候要运行的方法。在大多数情况下,这个方法都是样板代码,我们编写的每个Spring Boot应用都会有一个类似或完全相同的方法(类名不同则另当别论)。

这个main()方法会调用SpringApplication中静态的run()方法,后者会真正执行应用的引导过程,也就是创建Spring的应用上下文。传递给run()的两个参数中,一个是配置类,另一个是命令行参数。尽管传递给run()的配置类不一定要和引导类相同,但这是最便利和最典型的做法。

你可能并不需要修改引导类中的任何内容。对于简单的应用程序来说,你可能会发现在引导类中配置一两个组件是非常方便的,但是对于大多数应用,最好还是要为没有实现自动配置的功能创建单独的配置类。在本书的整个过程中,我们将会创建多个配置类,所以请继续关注后续的细节。

测试应用

测试是软件开发的重要组成部分。我们始终可以通过在命令行中构建应用、运行测试,从而实现项目的手动测试:

$ ./mvnw package
...
$ java -jar target/taco-cloud-0.0.1-SNAPSHOT.jar

或者,鉴于我们在使用Spring Boot,Spring Boot的Maven插件会使这个过程更加简单:

$ ./mvnw spring-boot:run

但是,手动测试就意味着有人类的参与,因此有可能会出现人为的错误或者不一致的测试。自动测试会更加一致和可重复。

在这一点上,Spring Initializr为我们提供了一个测试类作为起步。程序清单1.3展现了这个测试类的概况。

程序清单1.3 应用测试类的概况

package tacos;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest  ⇽---Spring Boot测试
public class TacoCloudApplicationTests {

  @Test  ⇽---测试方法
  public void contextLoads() {
  }

}

TacoCloudApplicationTests类中的内容并不多:这个类中只有一个空的测试方法。即便如此,这个测试类还是会执行必要的检查,确保Spring应用上下文成功加载。如果你所做的变更导致Spring应用上下文无法创建,这个测试将会失败,这样你就可以做出反应来解决相关的问题。

@SpringBootTest会告诉JUnit在启动测试的时候要添加上Spring Boot的功能。像@SpringBootApplication一样,@SpringBootTest也是一个组合注解,它本身使用了ExtendWith(SpringExtension.class),从而能够将Spring的测试功能添加到JUnit 5中。就现在来讲,我们可以认为这个测试类与在main()方法中调用SpringApplication.run()是等价的。在这本书中,我们将会多次看到@SpringBootTest,并不断见识它的威力。

最后,就是测试方法本身了。尽管@SpringBootTest会为测试加载Spring应用上下文,但是如果没有任何测试方法,那么它其实什么事情都没有做。即便没有任何断言或代码,这个空的测试方法也会提示该注解完成了它的工作并成功加载Spring应用上下文。这个过程中出现任何问题,测试都会失败。

要在命令行运行这个测试类及任意其他的测试类,我们都可以使用如下的Maven指令:

$ ./mvnw test

至此,我们已经看完了Spring Initializr提供的代码。我们看到了一些用来开发Spring应用程序的基础样板,但是还没有编写任何的代码。现在是时候启动IDE、准备好键盘,向Taco Cloud应用程序添加一些自定义的代码了。

因为我们刚刚开始,所以首先为Taco Cloud应用做一些小的变更,但是这些变更会展现Spring的很多优点。在刚开始的时候,比较合适的做法是为Taco Cloud应用添加一个主页。在添加主页时,我们将会创建两个代码构件:

一个控制器类,用来处理主页相关的请求;

一个视图模板,用来定义主页看起来是什么样子。

测试是非常重要的,所以我们还会编写一个简单的测试类来测试主页。但是,要事优先,我们需要先编写控制器。

Spring自带了一个强大的Web框架,名为Spring MVC。Spring MVC的核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的HTML。

在第2章中,我们将会学习更多关于Spring MVC的知识。现在,我们会编写一个简单的控制器类以处理来自根路径(如“/”)的请求,并将这些请求转发至主页视图,在这个过程中不会填充任何的模型数据。程序清单1.4展示了这个简单的控制器类。

程序清单1.4 主页控制器

package tacos;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller  ⇽---控制器
public class HomeController {
  @GetMapping("/")  ⇽---处理对根路径“/ ”的请求
  public String home() {
    return "home";  ⇽---返回视图名
  }

}

可以看到,这个类带有@Controller注解。就其本身而言,@Controller并没有做太多的事情。它的主要目的是让组件扫描将这个类识别为一个组件。因为HomeController带有@Controller注解,所以Spring的组件扫描功能会自动发现它,并创建一个HomeController实例作为Spring应用上下文中的bean。

实际上,有一些其他的注解与@Controller有着类似的目的(包括@Component、@Service和@Repository)。你可以为HomeController添加上述的任意其他注解,其作用是完全相同的。但是,在这里选择使用@Controller更能描述这个组件在应用中的角色。

home()是一个简单的控制器方法。它带有@GetMapping注解,表明如果针对“/”发送HTTP GET请求,那么将会由这个方法来处理请求。该方法所做的只是返回String类型的home值。

这个值将会解析为视图的逻辑名。视图如何实现取决于多个因素,但是Thymeleaf位于类路径中,使得我们可以使用Thymeleaf来定义模板。

为何使用Thymeleaf?

你可能会想:为什么要选择Thymeleaf作为模板引擎?为何不使用JSP?为何不使用FreeMarker?为何不选择其他的几个可选方案呢?

简单来说,我必须要做出选择,我喜欢Thymeleaf,相对于其他的方案,我会优先使用它。即便JSP是更加显而易见的选择,但是组合使用JSP和Spring Boot需要克服一些挑战。我不想脱离第1章的内容定位,所以就此打住。在第2章中,我们会看到其他的模板方案,其中也包括JSP。

模板名称是由逻辑视图名派生而来的,再加上“/templates/”前缀和“.html”后缀。最终形成的模板路径将是“/templates/home.html”。所以,我们需要将模板放到项目的“/src/main/resources/templates/home.html”中。现在,就让我们来创建这个模板。

为了让主页尽可能简单,主页除了欢迎用户访问站点之外,不会做其他的任何事情。程序清单1.5展现了基本的Thymeleaf模板,定义了Taco Cloud的主页。

程序清单1.5 Taco Cloud主页模板

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:th = "http://www.thymeleaf.org">
  <head>
    <title>Taco Cloud</title>
  </head>

  <body>
    <h1>Welcome to...</h1>
    <img th:src = "@{/images/TacoCloud.png}"/>
  </body>
</html>

这个模板并没有太多需要讨论的。唯一需要注意的是用于展现Taco Cloud Logo的<img>标签。它使用了Thymeleaf的th:src属性和@{...}表达式,以便于引用相对于上下文路径的图片。除此之外,这个主页就是一个扮演“Hello World”角色的页面。

我们再讨论一下这个图片。我将定义Taco Cloud Logo的工作留给你,但是你需要将它放到应用的正确位置。

图片是使用相对于上下文的“/images/TacoCloud.png”路径来引用的。回忆一下我们的项目结构,像图片这样的静态资源位于“/src/main/resources/static”文件夹。这意味着,在项目中Taco Cloud Logo的图片路径必须为“/src/main/resources/static/images/ TacoCloud.png”。

现在,我们有了一个处理主页请求的控制器和渲染主页的模板,基本就可以启动应用来看一下它的效果了。但是,在此之前,我们先看一下如何为控制器编写测试。

在测试Web应用时,对HTML页面的内容进行断言是比较困难的。幸好,Spring对测试提供了强大的支持,这使得测试Web应用变得非常简单。

对于主页来说,我们所编写的测试在复杂性上与主页本身差不多。测试需要针对根路径“/”发送一个HTTP GET请求并期望得到成功结果,其中视图名称为home并且结果内容包含“Welcome to...”。程序清单1.6就能够完成该任务。

程序清单 1.6 针对主页控制器的测试

package tacos;

import static org.hamcrest.Matchers.containsString;
import static
     org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(HomeController.class)  ⇽---针对HomeController的Web测试
public class HomeControllerTest {

  @Autowired
  private MockMvc mockMvc;  ⇽---注入MockMvc

  @Test
  public void testHomePage() throws Exception {
    mockMvc.perform(get("/"))  ⇽---发起对“/”的GET请求
      .andExpect(status().isOk())  ⇽---期望得到HTTP 200
      .andExpect(view().name("home"))  ⇽---期望得到home视图
      .andExpect(content().string(  ⇽---期望包含“Welcome to...”
          containsString("Welcome to...")));
  }

}

对于这个测试,首先注意到的可能就是它使用了与TacoCloudApplicationTests类不同的注解。HomeControllerTest没有使用@SpringBootTest标记,而是添加了@WebMvcTest注解。这是Spring Boot提供的一个特殊测试注解,让这个测试在Spring MVC应用的上下文中执行。更具体来讲,在本例中,它会将HomeController注册到Spring MVC中,这样一来,我们就可以向它发送请求了。

@WebMvcTest同样会为测试Spring MVC应用提供了Spring环境的支持。尽管可以启动一个服务器来进行测试,但是对于我们的场景来说,仿造一下Spring MVC的运行机制就可以。测试类被注入了一个MockMvc,能够让测试实现mockup。

通过testHomePage()方法,我们定义了针对主页想要执行的测试。它首先使用MockMvc对象对“/”(根路径)发起HTTP GET请求。对于这个请求,我们设置了如下的预期:

响应应该具备HTTP 200 (OK)状态;

视图的逻辑名称应该是home;

渲染后的视图应该包含文本“Welcome to....”。

我们可以在所选的IDE中运行测试,也可以使用如下的Maven命令:

$ mvnw test

如果在MockMvc对象发送请求之后,上述预期没有全部满足,那么这个测试会失败。但是,我们的控制器和视图模板在编写时都满足了这些预期,所以测试应该能够通过,并且带有成功的图标——至少能够看到一些绿色的背景,表明测试通过了。

控制器已经编写好了,视图模板也已经创建完毕,而且我们还通过了测试。看上去,我们已经成功实现了主页。但是,尽管测试已经通过了,但是如果能够在浏览器中看到结果,会更有成就感。毕竟,这才是Taco Cloud的客户所能看到的效果。接下来,我们构建应用并运行它。

就像初始化Spring应用有多种方式一样,运行Spring应用也有多种方式。你如果愿意,可以翻到本书附录部分,了解运行Spring Boot应用的一些通用方式。

因为我们选择了使用Spring Tool Suite来初始化和管理项目,所以可以借助名为Spring Boot Dashboard的便捷功能来帮助我们在IDE中运行应用。Spring Boot Dashboard的表现形式是一个Tab,通常会位于IDE窗口的左下角附近。图1.7展现了一个带有标注的Spring Boot Dashboard截屏。

图1.7 Spring Boot Dashboard的重点功能

图1.7包含了一些有用的细节,但是我不想花太多时间介绍Spring Boot Dashboard支持的所有功能。对我们来说,现在最重要的事情是需要知道如何使用它来运行Taco Cloud应用。确保taco-cloud应用程序在项目列表中能够显示(这是图1.7中显示的唯一应用),然后单击启动按钮(最左边的按钮,也就是带有绿色三角形和红色正方形的按钮)。应用程序应该就能立即启动。

在应用启动的过程中,你会在控制台看到一些Spring ASCII码,随后会是描述应用启动各个步骤的日志条目。在控制台输出的最后,你将会看到一条日志显示Tomcat已经在port(s): 8080 (http)启动,这意味着此时可以打开Web浏览器并导航至主页,看到我们的劳动成果。

稍等一下!刚才说启动Tomcat?我们是什么时候将应用部署到Tomcat Web服务器的呢?

Spring Boot应用的习惯做法是将所有它所需要的东西都放到一起,没有必要将其部署到某种应用服务器中。在这个过程中,我们根本没有将应用部署到Tomcat中——Tomcat是我们应用的一部分!(在1.3.6小节,我会详细描述Tomcat是如何成为我们应用的一部分的。)

现在,应用已经启动起来了,打开Web浏览器并访问http://localhost:8080 (或者在Spring Boot Dashboard中点击地球样式的按钮),你将会看到如图1.8所示的界面。如果你设计了自己的Logo图片,显示效果可能会有所不同。但是,跟图1.8相比,应该不会有太大的差异。

图1.8 Taco Cloud主页

看上去,似乎并不太美观,但本书不是关于平面设计的,略显简陋的主页外观已经足够了。

到现在为止,我一直没有提及DevTools。在初始化项目的时候,我们将其作为一个依赖添加了进来。在最终生成的pom.xml文件中,它表现为一个依赖项。甚至Spring Boot Dashboard都显示项目启用了DevTools。那么,DevTools到底是什么,又能为我们做些什么呢?接下来,让我们快速浏览一下DevTool最有用的一些特性。

顾名思义,DevTools为Spring开发人员提供了一些便利的开发期工具和特性,其中包括:

代码变更后应用会自动重启;

当面向浏览器的资源(如模板、JavaScript、样式表)等发生变化时,会自动刷新浏览器;

自动禁用模板缓存;

如果使用H2数据库,则内置了H2控制台。

需要注意,DevTools并不是IDE插件,也不需要你使用特定的IDE。在Spring Tool Suite、IntelliJ IDEA和NetBeans中,它都能很好地运行。另外,因为它的用途仅仅是开发,所以它能够很智能地在生产环境中把自己禁用掉。我们将会在第18章讨论它是如何做到这一点的。现在,我们主要关注Spring Boot DevTools最有用的特性,那么先从应用的自动重启开始吧。

应用自动重启

如果将DevTools作为项目的一部分,那么你可以看到,当对项目中的Java代码和属性文件作出修改后,这些变更稍后就能发挥作用。DevTools会监控变更,在看到变化的时候自动重启应用。

更准确地说,当DevTools启用的时候,应用程序会加载到Java虚拟机(Java Virtual Machine,JVM)中的两个独立的类加载器中。其中一个类加载器会加载Java代码、属性文件,以及项目的“src/main/”路径下几乎所有的内容。这些条目很可能会经常发生变化。另外一个类加载器会加载依赖的库,这些库不太可能经常发生变化。

当探测到变更的时候,DevTools只会重新加载包含项目代码的类加载器,并重启Spring的应用上下文,在这个过程中,另外一个类加载器和JVM会原封不动。这个策略非常精细,但能减少应用启动的时间。

这种策略的一个不足之处就是自动重启无法反映依赖项的变化。这是因为包含依赖库的类加载器不会自动重新加载。这意味着每当在构建规范中添加、变更或移除依赖的时候,为了让变更生效,都要重新启动应用。

浏览器自动刷新和禁用模板缓存

默认情况下,像Thymeleaf和FreeMarker这样的模板方案在配置时,会缓存模板解析的结果,这样一来,在为每个请求提供服务的时候,模板就不用重新解析了。在生产环境中,这是一种很好的方式,因为它会带来一定的性能收益。

但是,在开发期,缓存模板就不太友好了。在应用运行的时候,如果缓存模板,刷新浏览器就无法看到模板变更的效果了。即便我们对模板做了修改,在应用重启之前,缓存的模板依然会有效。

DevTools通过禁用所有模板缓存解决了这个问题。你可以对模板进行任意数量的修改,只需刷新一下浏览器就能看到结果。

如果你像我一样,连浏览器的刷新按钮都懒得点,希望在对代码做出变更之后马上就能在浏览器中看到结果,那么很幸运,DevTools有一些特殊的功能可以供我们使用。

DevTools会和你的应用程序一起,自动启动一个LiveReload服务器。LiveReload服务器本身并没有太大的用处。但是,当它与LiveReload浏览器插件结合起来的时候,就能够在模板、图片、样式表、JavaScript等(实际上,几乎涵盖为浏览器提供服务的所有内容)发生变化的时候,自动刷新浏览器。

LiveReload有针对Google Chrome、Safari和Firefox的浏览器插件(这里要对Internet Explorer和Edge的支持者说声抱歉)。请访问LiveReload网站的Extensions页面了解如何为你的浏览器安装LiveReload。

内置的H2控制台

虽然我们的项目还没有使用数据库,但是这种情况在第3章中就会发生变化。如果你使用H2数据库进行开发,DevTools将会自动启用H2控制台,这样一来,我们可以通过Web浏览器进行访问。只需要让浏览器访问http://localhost:8080/h2-console,就能看到应用所使用的数据。

此时,我们已经编写了一个非常简单却很完整的Spring应用。在本书接下来的章节中,我们将会不断扩展它。但现在,要回过头来看一下我们都完成了哪些工作、Spring发挥了什么作用。

回想一下我们是怎样完成这一切的。简短来说,在构建基于Spring的Taco Cloud应用的过程中,我们执行了如下步骤:

使用Spring Initializr创建初始的项目结构;

编写控制器类处理针对主页的请求;

定义了一个视图模板来渲染主页;

编写了一个简单的测试类来验证工作符合预期。

这些步骤都非常简单直接,对吧?除了初始化应用的第一个步骤之外,我们所做的每一个操作都专注于生成主页的目标。

实际上,我们所编写的每行代码都致力于实现这个目标。除了Java import语句之外,我只能在控制器中找到两行Spring相关的代码,而在视图模板中,一行Spring相关的代码都没有。尽管测试类的大部分内容都使用了Spring对测试的支持,但是它在测试的运行环境中,似乎没有那么强的侵入性。

这是使用Spring进行开发的一个重要优势。你可以只关注满足应用需求的代码,无须考虑如何满足框架的需求。尽管我们偶尔还是需要编写一些框架特定的代码,但是它们通常只占整个代码库很小的一部分。正如我在前文所述,Spring(以及Spring Boot)可以视为感受不到框架的框架(frameworkless framework)。

但是,这一切到底是如何运行起来的呢?Spring在幕后做了些什么来保证应用的需求能够得到满足?要理解Spring到底做了些什么,我们首先来看一下构建规范。

在pom.xml文件中,我们声明了对Web和Thymeleaf starter的依赖。这两项依赖会传递引入大量其他的依赖,包括:

Spring的MVC框架;

嵌入式的Tomcat;

Thymeleaf和Thymeleaf布局方言。

它还引入了Spring Boot的自动配置库。当应用启动的时候,Spring Boot的自动配置将会探测到这些库,并自动完成如下功能:

在Spring应用上下文中配置bean以启用Spring MVC;

在Spring应用上下文中配置嵌入式的Tomcat服务器;

配置Thymeleaf视图解析器以便于使用Thymeleaf模板渲染Spring MVC视图。

简言之,自动配置功能完成了所有的脏活累活,让我们能够集中精力编写实现应用功能的代码。如果你问我的观点,我认为这是一个很好的安排!

我们的Spring之旅才刚刚开始。Taco Cloud应用程序只涉及了Spring所提供功能的一小部分。在开始下一步之前,我们先整体了解一下Spring,看看在我们的路途中都会有哪些地标。

要想了解Spring的整体状况,只需查看完整版本的Spring Initializr Web表单上的那些复选框列表。它列出了100多个可选的依赖项,所以我不会在这里列出所有选项,也不会提供截图,但我鼓励你去看一看。同时,在这里我会简单介绍一些重点的项目。

如你所料,Spring核心框架是Spring领域中一切的基础,提供了核心容器和依赖注入框架。另外,它还提供了一些其他重要的特性。

其中有一项就是Spring MVC,也就是Spring的Web框架。你已经看到了如何使用Spring MVC来编写控制器类以处理Web请求。但是,你还没看到的是,Spring MVC还能用来创建REST API,以生成非HTML的输出。在第2章,我会更深入地介绍Spring MVC。在第7章,我们会重新学习如何使用Spring MVC来创建REST API。

Spring核心框架还提供了一些对数据持久化的基础支持,尤其是基于模板的JDBC支持。在第3章,我们会看到如何使用JdbcTemplate。

Spring还添加了对反应式(reactive)风格编程的支持,其中包括名为Spring WebFlux的新反应式Web框架,这个框架大量借鉴了Spring MVC。在第3部分中,我们会学习Spring反应式编程模型,并在第12章专门学习Spring WebFlux。

我们已经看到了Spring Boot的很多优势,包括starter依赖和自动配置。在本书中,我们会尽可能多地使用Spring Boot,并避免任何形式的显式配置,除非显式配置是绝对必要的。除了starter依赖和自动配置,Spring Boot还提供了大量其他有用的特性:

Actuator能够洞察应用运行时的内部工作状况,包括指标、线程dump信息、应用的健康状况以及应用程序可用的环境属性;

灵活的环境属性规范;

在核心框架的测试辅助功能之上,提供了对测试的额外支持。

除此之外,Spring Boot还提供了一个基于Groovy脚本的编程模型,称为Spring Boot CLI[2]。使用Spring Boot CLI,我可以将整个应用程序编写为Groovy脚本的集合,并通过命令行运行它们。我不会花太多时间介绍Spring Boot CLI,但是当它匹配我们的需求时,会偶尔提及它。

Spring Boot已经成为Spring开发中不可或缺的一部分,很难想象如果没有它该如何开发Spring应用程序。因此,本书采用了以Spring Boot为核心的视角。当我介绍Spring Boot所做的事情的时候,你可能会发现我使用了Spring这个表述。

尽管Spring核心框架提供了基本的数据持久化支持,但Spring Data提供了非常令人惊叹的功能:将应用程序的数据存储库(repository)定义为简单的Java接口,在定义存储和检索数据的方法时使用一种特定的命名约定即可。

此外,Spring Data能够处理多种不同类型的数据库,包括关系型数据库(通过JDBC或JPA实现)、文档数据库(Mongo)、图数据库(Neo4j)等。在第3章,我们会使用Spring Data为Taco Cloud应用程序创建存储库。

应用程序的安全性一直是重要的话题,而且正在变得越来越重要。幸运的是,Spring有一个健壮的安全框架,名为Spring Security。

Spring Security解决了应用程序通用的安全性需求,包括身份验证、授权和API安全性。Spring Security的范围太大,在本书中无法得到充分的介绍,但是我们会在第5章和第12章中讨论一些最常见的使用场景。

一定程度上,大多数应用程序都需要与其他应用甚至本应用中的其他组件集成。在这方面,有一些应用程序集成的模式来解决这些需求。Spring Integration和Spring Batch为基于Spring的应用程序提供了这些模式的实现。

Spring Integration解决了实时集成问题,在实时集成中,数据在可用时马上就会得到处理。相反,Spring Batch解决的则是批处理集成的问题,在此过程中,数据可以收集一段时间,直到某个触发器(可能是一个时间触发器)发出信号表明是时候处理批量数据了,才会得到处理。我们会在第10章中研究Spring Integration。

应用程序开发领域正在进入新的时代,我们不再将应用程序作为一个部署单元的单体应用来开发,而是使用由微服务组成的多个独立部署单元来组合形成应用程序。

微服务是一个热门话题,解决了开发期和运行期的一些实际问题。然而,在这样做的过程中,它也面临着自己所带来的挑战。这些挑战将由Spring Cloud直面解决,Spring Cloud是使用Spring开发云原生应用程序的一组项目。

Spring Cloud覆盖了很多领域,这本书不可能面面俱到。关于对Spring Cloud的完整介绍,我推荐阅读Thomas Vitale的Cloud Native Spring in Action(Manning,2020年)。

Spring一个相对较新的进展是Spring Native项目。这个实验性的项目能够使用GraalVM原生镜像编译器将Spring Boot项目编译成原生可执行的文件,从而使镜像的启动速度显著加快,并且占用更小的空间。

关于Spring Native的更多信息,参见GitHub网站的spring-projects-experimental/spring- native代码库。

Spring旨在简化开发人员所面临的挑战,比如创建Web应用程序、处理数据库、保护应用程序,以及实现微服务。

Spring Boot构建在Spring之上,通过简化依赖管理、自动配置和运行时洞察,使Spring更加易用。

Spring应用程序可以使用Spring Initializr初始化,Spring Initializr是基于Web的应用,并且为大多数Java开发环境提供了原生支持。

在Spring应用上下文中,组件(通常称为bean)可以使用Java或XML显式声明,可以通过组件扫描发现,还可以使用Spring Boot自动配置功能实现自动化配置。

[1] 为了行文简洁,同时保持与示例应用中Web页面展现的一致性,后文不再将taco翻译为墨西哥煎玉米卷,而是直接使用taco这一称呼。——译者注

[2] 其中,CLI代表command-line interface,即命令行接口。——译者注

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59869”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


本章内容:

在浏览器中展现模型数据;

处理和校验表单输入;

选择视图模板库。

第一印象是非常重要的:外观足够有吸引力的房子更有可能被卖掉,即使购房者甚至没有进门;如果一辆车喷成了樱桃色,那么它的油漆会比它的发动机更引人注目;文学作品中充满了一见钟情的故事。内在固然非常重要,但是外在同样重要,因为外在往往是人们第一眼看到的。

我们使用Spring构建的应用会完成各种各样的事情,包括处理数据、从数据库中读取信息,以及与其他应用进行交互。但是,用户对应用程序的第一印象来源于用户界面。在很多应用中,用户界面(User Interface,UI)是以浏览器中的Web应用的形式来展现的。

在第1章中,我们创建了第一个Spring MVC控制器来展现应用的主页。但是,Spring MVC能做很多的事情,并不局限于展现静态内容。在本章中,我们将会开发Taco Cloud的第一个主要功能:定制taco。在这个过程中,我们将会深入研究Spring MVC,并看到它如何展现模型数据和处理表单输入。

从根本上来讲,Taco Cloud是一个可以在线订购taco的地方。但是,除此之外,Taco Cloud允许客户展现其创意,能够让他们通过丰富的配料(ingredient)设计自己的taco。

因此,Taco Cloud需要有一个页面为taco艺术家展现可以选择的配料。可选的配料可能随时会发生变化,所以不能将它们硬编码到HTML页面中。我们应该从数据库中获取可用的配料并将其传递给页面,进而展现给客户。

在Spring Web应用中,获取和处理数据是控制器的任务,而将数据渲染到HTML中并在浏览器中展现是视图的任务。为了支撑taco的创建页面,我们需要构建如下的组件:

用来定义taco配料属性的领域类;

用来获取配料信息并将其传递至视图的Spring MVC控制器类;

用来在用户的浏览器中渲染配料列表的视图模板。

这些组件之间的关系如图2.1所示。

图2.1 典型的Spring MVC请求流

因为本章主要关注Spring的Web框架,所以我们会将数据库相关的内容放到第3章中进行讲解。现在的控制器只负责向视图提供配料。在第3章中,我们会重新改造这个控制器,让它能够与存储库协作,从数据库中获取配料数据。

在编写控制器和视图之前,我们首先确定用来表示配料的领域类型,它会为开发Web组件奠定基础。

应用的领域指的是它所要解决的主题范围,也就是会影响应用理解的理念和概念[1]。在Taco Cloud应用中,领域对象包括taco设计、组成这些设计的配料、顾客以及顾客所下的taco订单。图2.2展示了这些实体以及它们是如何关联到一起的。

图2.2 Taco Cloud的领域类

作为开始,我们首先关注taco的配料。在我们的领域中,taco配料是非常简单的对象。每种配料都有一个名称和类型,以便于对其进行可视化的分类(蛋白质、奶酪、酱汁等)。每种配料还有一个ID,这样的话对它的引用就能非常容易和明确。程序清单2.1所示的Ingredient类定义了我们所需的领域对象。

程序清单2.1 定义taco配料

package tacos;

import lombok.Data;
@Data
public class Ingredient {

  private final String id;
  private final String name;
  private final Type type;

  public enum Type {
    WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
  }

}

我们可以看到,这是一个非常普通的Java领域类,它定义了描述配料所需的3个属性。在程序清单2.1中,Ingredient类最不寻常的一点就是它似乎缺少了常见的getter和setter方法,以及像equals()、 hashCode()、toString()等这些有用的方法。

在程序清单中没有这些方法,除了节省篇幅的目的外,还因为我们使用了名为Lombok的库。这是一个非常棒的库,它能够在编译期自动生成这些方法,这样一来,在运行期就能使用它们了。实际上,类级别的@Data注解就是由Lombok提供的,它会告诉Lombok生成所有缺失的方法,同时还会生成所有以final属性为参数的构造器。使用Lombok能够让Ingredient的代码简洁明了。

Lombok并不是Spring库,但是它非常有用,如果没有它,开发工作将很难开展。当我需要在书中将代码示例编写得短小简洁时,它简直成了我的救星。

要使用Lombok,首先要将其作为依赖添加到项目中。如果你使用Spring Tool Suite,只需要右键点击pom.xml,并从Spring上下文菜单选项中选择“Add Starters”。在第1章中看到的选择依赖的对话框将会再次出现(参见图1.4),这样,我们就有机会添加依赖或修改已选择的依赖。在Developer Tools下找到Lombok选项,并确保它处于已选中的状态,然后选择“OK”,Spring Tool Suite会自动将其添加到构建规范中。

另外,你也可以在pom.xml中通过如下的条目进行手动添加:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

如果想要手动添加Lombok到构建之中,还需要在pom.xml文件的<build>部分将其从Spring Boot Maven插件中排除:

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
         <excludes>
           <exclude>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
           </exclude>
        </excludes>
      </configuration>
    </plugin>
  </plugins>
</build>

Lombok的魔力是在编译期发挥作用的,所以在运行期没有必要用到它们。像这样将其排除出去,在最终形成的JAR或WAR文件中就不会包含它了。

Lombok依赖将会在开发阶段为你提供Lombok注解(例如@Data),并且会在编译期进行自动化的方法生成。但是,我们还需要将Lombok作为扩展添加到IDE上,否则IDE将会报错,提示缺少方法和final属性没有赋值。请访问Project Lombok网站以查阅如何在你所选择的IDE上安装Lombok。

为什么我的代码中有那么多的错误?

需要重申的是,在使用Lombok的时候,你必须在IDE中安装Lombok插件。否则,IDE将无从得知Lombok提供了getter、setter和其他方法,并且会因为缺失这些方法而报错。

许多流行的IDE都支持Lombok,包括Eclipse、Spring Tool Suite、IntelliJ IDEA和Visual Studio Code。请访问Project Lombok网站以了解如何在你的IDE中安装Lombok插件的更详细信息。

我相信你会发现Lombok非常有用,但你也需要知道,它是可选的。在开发Spring应用时,它并不是强制要使用的,所以你如果不想使用它,完全可以手动编写这些缺失的方法。你尽可以合上本书去这样做……我会在这里等你。

配料是taco的基本构成要素。为了解这些配料是如何组合在一起的,我们要定义Taco领域类,如程序清单2.2所示。

程序清单2.2 定义taco设计的领域对象

package tacos;
import java.util.List;
import lombok.Data;

@Data
public class Taco {

  private String name;

  private List<Ingredient> ingredients;

}

我们可以看到,Taco是一个很简单的Java领域对象,它包含两个属性。与Ingredient一样,Taco类使用了@Data注解,以便Lombok在编译期自动生成基本的JavaBean方法。

现在已经定义了Ingredient和Taco,我们还需要一个领域类来定义客户如何指定他们想要订购的taco并明确支付信息和投递信息(配送地址)。这就是TacoOrder类的职责了,如程序清单2.3所示。

程序清单2.3  taco订单的领域对象

package tacos;
import java.util.List;
import java.util.ArrayList;
import lombok.Data;

@Data
public class TacoOrder {

  private String deliveryName;
  private String deliveryStreet;
  private String deliveryCity;
  private String deliveryState;
  private String deliveryZip;
  private String ccNumber;
  private String ccExpiration;
  private String ccCVV;

  private List<Taco> tacos = new ArrayList<>();

  public void addTaco(Taco taco) {
    this.tacos.add(taco);
  }
}

除了比Ingredient或Taco具有更多的属性外,TacoOrder并没有什么特殊的新内容可以讨论。它是一个很简单的领域类,具有9个属性,其中5个是投递相关的信息,3个是支付相关的信息,还有一个是组成订单的Taco对象的列表。它有一个addTaco()方法,是为了方便向订单中添加taco而增加的。

现在领域类型已经定义完毕,我们可以让它们运行起来了。接下来,我们会在应用中添加一些控制器,让它们来处理应用的Web请求。

在Spring MVC框架中,控制器是重要的参与者。它们的主要职责是处理HTTP请求,要么将请求传递给视图以便于渲染HTML(浏览器展现),要么直接将数据写入响应体(RESTful)。在本章中,我们将会关注使用视图来为Web浏览器生成内容的控制器。在第7章,我们将会看到如何以REST API的形式编写控制器来处理请求。

对于Taco Cloud应用来说,我们需要一个简单的控制器,它要完成如下的功能:

处理路径为“/design”的HTTP GET请求;

构建配料的列表;

处理请求,并将配料数据传递给要渲染为HTML的视图模板,然后发送给发起请求的Web浏览器。

程序清单2.4中的DesignTacoController类解决了这些需求。

程序清单2.4 初始的Spring控制器类

package tacos.web;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;

@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("tacoOrder")
public class DesignTacoController {

@ModelAttribute
public void addIngredientsToModel(Model model) {
    List<Ingredient> ingredients = Arrays.asList(
      new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
      new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
      new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
      new Ingredient("CARN", "Carnitas", Type.PROTEIN),
      new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
      new Ingredient("LETC", "Lettuce", Type.VEGGIES),
      new Ingredient("CHED", "Cheddar", Type.CHEESE),
      new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
      new Ingredient("SLSA", "Salsa", Type.SAUCE),
      new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
    );

    Type[] types = Ingredient.Type.values();
    for (Type type : types) {
      model.addAttribute(type.toString().toLowerCase(),
      filterByType(ingredients, type));
  }
}

@ModelAttribute(name = "tacoOrder")
public TacoOrder order() {
  return new TacoOrder();
}

@ModelAttribute(name = "taco")
public Taco taco() {
  return new Taco();
}

@GetMapping
public String showDesignForm() {
  return "design";
}

private Iterable<Ingredient> filterByType(
    List<Ingredient> ingredients, Type type) {
  return ingredients
            .stream()
            .filter(x -> x.getType().equals(type))
            .collect(Collectors.toList());
  }

}

对于DesignTacoController,我们先要注意在类级别所应用的注解。首先是@Slf4j,这是Lombok所提供的注解,在编译期,它会在这个类中自动生成一个SLF4J Logger(SLF4J即simple logging facade for Java,请访问slf4j网站以了解更多)静态属性。这个简单的注解和在类中通过如下代码显式声明的效果是一样的:

private static final org.slf4j.Logger log =
    org.slf4j.LoggerFactory.getLogger(DesignTacoController.class);

随后,我们将会用到这个Logger。

DesignTacoController用到的下一个注解是@Controller。这个注解会将这个类识别为控制器,并且将其作为组件扫描的候选者,所以Spring会发现它并自动创建一个DesignTacoController实例,并将该实例作为Spring应用上下文中的bean。

DesignTacoController还带有@RequestMapping注解。当@RequestMapping注解用到类级别的时候,它能够指定该控制器所处理的请求类型。在本例中,它规定DesignTacoController将会处理路径以“/design”开头的请求。

最后,我们可以看到DesignTacoController还带有@SessionAttributes("tacoOrder")注解,这表明在这个类中稍后放到模型里面的TacoOrder对象应该在会话中一直保持。这一点非常重要,因为创建taco也是创建订单的第一步,而我们创建的订单需要在会话中保存,这样能够使其跨多个请求。

处理GET请求

修饰showDesignForm()方法的@GetMapping注解对类级别的@RequestMapping进行了细化。@GetMapping结合类级别的@RequestMapping,指明当接收到对“/design”的HTTP GET请求时,Spring MVC将会调用showDesignForm()来处理请求。

@GetMapping只是诸多请求映射注解中的一个。表2.1列出了Spring MVC中所有可用的请求映射注解。

表2.1  Spring MVC的请求映射注解

注解

描述

@RequestMapping

通用的请求处理

@GetMapping

处理HTTP GET请求

@PostMapping

处理HTTP POST请求

@PutMapping

处理HTTP PUT请求

@DeleteMapping

处理HTTP DELETE请求

@PatchMapping

处理HTTP PATCH请求

当showDesignForm()处理针对“/design”的GET请求时,其实并没有做太多的事情。它只不过返回了一个值为“design”的String,这是视图的逻辑名称,用来向浏览器渲染模型。

似乎针对“/design”的GET请求并没有做太多的事情,但事实恰恰相反,除了在showDesignForm()方法中看到的,它还有很多其他的事情做。你可能注意到,程序清单2.4中有一个名为addIngredientsToModel()的方法,它带有@ModelAttribute注解。这个方法也会在请求处理的时候被调用,构建一个包含Ingredient的配料列表并将其放到模型中。现在,这个列表是硬编码的。在第3章,我们会从数据库中获取可用的列表。

配料列表准备就绪之后,addIngredientsToModel()方法接下来的几行代码会根据配料类型过滤列表,这是通过名为filterByType()的辅助方法实现的。配料类型的列表会以属性的形式添加到Model对象上,并传递给showDesignForm()方法。Model对象负责在控制器和展现数据的视图之间传递数据。实际上,放到Model属性中的数据将会复制到Servlet Request的属性中,这样视图就能找到它们,并使用它们在用户的浏览器中渲染页面。

addIngredientsToModel()之后是另外两个带有@ModelAttribute注解的方法。这些方法要简单得多,只创建了一个新的TacoOrder和Taco对象来放置到模型中。TacoOrder对象在前面阐述@SessionAttributes注解的时候曾经提到过,当用户在多个请求之间创建taco时,它会持有正在建立的订单的状态。除此之外,Taco对象也被放置到了模型中,这样一来,为响应“/design”的GET请求而呈现的视图就能展示一个非空的对象了。

我们的DesignTacoController已经具备雏形了。如果现在运行应用并在浏览器上访问“/design”路径,DesignTacoController的showDesignForm()和addIngredientsToModel()方法将会被调用,它们在将请求传递给视图之前,会将配料和一个空的Taco放到模型中。但是,我们现在还没有定义视图,请求将会遇到很糟糕的问题,也就是HTTP 500 (Internal Server Error)错误。为了解决这个问题,我们将注意力切换到视图上,在这里数据将会使用HTML进行装饰,以便于在用户的Web浏览器中展现。

在控制器完成它的工作之后,现在就该视图登场了。Spring提供了多种定义视图的方式,包括JavaServer Pages(JSP)、Thymeleaf、FreeMarker、Mustache和基于Groovy的模板。就现在来讲,我们会使用Thymeleaf,这也是我们在第1章开启这个项目时的选择。我们会在2.5节考虑其他的可选方案。

在第1章,我们已经将Thymeleaf作为依赖添加了进来。在运行时,Spring Boot的自动配置功能会发现Thymeleaf在类路径中,因此会为Spring MVC自动创建支撑Thymeleaf视图的bean。

像Thymeleaf这样的视图库在设计时是与特定的Web框架解耦的。这样一来,它们无法感知Spring的模型抽象,因此,无法与控制器放到Model中的数据协同工作。但是,它们可以与Servlet的request属性协作。所以,在Spring将请求转移到视图之前,它会把模型数据复制到request属性中,Thymeleaf和其他的视图模板方案就能访问到它们了。

Thymeleaf模板就是增加一些额外元素属性的HTML,这些属性能够指导模板如何渲染request数据。举例来说,如果有个请求属性的key为“message”,我们想要使用Thymeleaf将其渲染到一个HTML <p>标签中,那么在Thymeleaf模板中,可以这样写:

<p th:text = "${message}">placeholder message</p>

模板渲染成HTML时,<p>元素体将会被替换为Servlet request中key为“message”的属性值。“th:text”是Thymeleaf命名空间中的属性,它会执行这个替换过程。${}操作符会告诉它要使用某个request属性(在本例中,也就是“message”)中的值。

Thymeleaf还提供了另外一个属性:th:each,它会迭代一个元素集合,为集合中的每个条目渲染HTML。在我们设计视图展现模型中的配料列表时,这就非常便利了。举例来说,如果只想渲染“wrap”配料的列表,可以使用如下的HTML片段:

<h3>Designate your wrap:</h3>
<div th:each = "ingredient : ${wrap}">
  <input th:field = "*{ingredients}" type = "checkbox"
         th:value = "${ingredient.id}"/>
  <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
</div>

在这里,我们在<div>标签中使用th:each属性,从而针对wrap request属性所对应集合中的每个元素重复渲染<div>标签。每次迭代时,配料元素都会绑定到一个名为ingredient的Thymeleaf变量上。

在<div>元素中,有一个<input>复选框元素,还有一个为复选框提供标签的<span>元素。复选框使用Thymeleaf的th:value来为渲染出的<input>元素设置value属性,这里会将其设置为所找到的ingredient的id属性。而th:field属性最终会用来设置<input>元素的name属性,用来记住复选框是否被选中。稍后添加校验功能时,这能够确保在出现校验错误的时候,复选框依然能够保持表单重新渲染前的状态。<span>元素使用th:text将“INGREDIENT”占位符文本替换为ingredient的name属性。

用实际的模型数据进行渲染时,其中一个<div>迭代的渲染结果可能会如下所示:

<div>
  <input name = "ingredients" type = "checkbox" value = "FLTO" />
  <span>Flour Tortilla</span><br/>
</div>

最终,上述的Thymeleaf片段会成为一大段HTML表单的一部分,我们的taco艺术家用户会通过这个表单来提交其美味的作品。完整的Thymeleaf模板会包括所有的配料类型,这个表单如程序清单2.5所示:

程序清单2.5 设计taco的完整页面

  <!DOCTYPE html>
  <html xmlns = "http://www.w3.org/1999/xhtml"
        xmlns:th = "http://www.thymeleaf.org">
    <head>
      <title>Taco Cloud</title>
      <link rel = "stylesheet" th:href = "@{/styles.css}" />
    </head>
    <body>
      <h1>Design your taco!</h1>
      <img th:src = "@{/images/TacoCloud.png}"/>

      <form method = "POST" th:object = "${taco}">
      <div class = "grid">
        <div class = "ingredient-group" id = "wraps">
        <h3>Designate your wrap:</h3>
        <div th:each = "ingredient : ${wrap}">
          <input th:field = "*{ingredients}" type = "checkbox"
                 th:value = "${ingredient.id}"/>
          <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
        </div>
        </div>

        <div class = "ingredient-group" id = "proteins">
        <h3>Pick your protein:</h3>
        <div th:each = "ingredient : ${protein}">
          <input th:field = "*{ingredients}" type = "checkbox"
                 th:value = "${ingredient.id}"/>
          <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
        </div>
        </div>

        <div class = "ingredient-group" id = "cheeses">
        <h3>Choose your cheese:</h3>
        <div th:each = "ingredient : ${cheese}">
          <input th:field = "*{ingredients}" type = "checkbox"
                 th:value = "${ingredient.id}"/>
          <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
        </div>
        </div>

        <div class = "ingredient-group" id = "veggies">
        <h3>Determine your veggies:</h3>
        <div th:each = "ingredient : ${veggies}">
          <input th:field = "*{ingredients}" type = "checkbox"
                 th:value = "${ingredient.id}"/>
          <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
        </div>
        </div>

      <div class = "ingredient-group" id = "sauces">
      <h3>Select your sauce:</h3>
      <div th:each = "ingredient : ${sauce}">
        <input th:field = "*{ingredients}" type = "checkbox"
               th:value = "${ingredient.id}"/>
        <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
      </div>
      </div>
      </div>

      <div>
      <h3>Name your taco creation:</h3>
      <input type = "text" th:field = "*{name}"/>
      <br/>

      <button>Submit Your Taco</button>
      </div>
    </form>
  </body>
</html>

可以看到,我们会为各种类型的配料重复定义<div>片段。另外,我们还包含了Submit按钮和用户用来定义其作品名称的输入域。

还值得注意的是,完整的模板包含了一个Taco Cloud的商标图片以及对样式表的<link>引用[2]。在这两个场景中,都使用了Thymeleaf的@{}操作符,用来生成一个相对于上下文的路径,以便于引用我们需要的静态制品(artifact)。正如我们在第1章中所学到的,在Spring Boot应用中,静态内容要放到根类路径的“/static”目录下。

我们的控制器和视图已经完成了,现在我们可以将应用启动起来,看一下我们的劳动成果。运行Spring Boot应用有很多种方式。在第1章中,我为你展示了如何通过在Spring Boot Dashboard中点击Start按钮来运行应用。不管采用哪种方式启动Taco Cloud应用,在启动之后,都可以通过http://localhost:8080/design来进行访问。你将会看到类似于图2.3的页面。

图2.3 渲染之后的taco设计页面

这看上去非常不错!访问你站点的taco艺术家可以看到一个包含了各种taco配料的表单,他们可以使用这些配料创建自己的杰作。但是当他们点击Submit your taco按钮的时候会发生什么呢?

我们的DesignTacoController还没有为接收创建taco的请求做好准备。此时提交设计表单会遇到一个错误(具体来讲,是一个HTTP 405错误:Request Method “POST” Not Supported)。接下来,我们通过编写一些处理表单提交的控制器代码来修正这个错误。

仔细看一下视图中的<form>标签,你将会发现它的method属性被设置成了POST。除此之外,<form>并没有声明action属性。这意味着当表单提交的时候,浏览器会收集表单中的所有数据,并以HTTP POST请求的形式将其发送至服务器端,发送路径与渲染表单的GET请求路径相同,也就是“/design”。

因此,在该POST请求的接收端,我们需要有一个控制器处理方法。在DesignTacoController中,我们会编写一个新的处理器方法来处理针对“/design”的POST请求。

在程序清单2.4中,我们曾经使用@GetMapping注解声明showDesignForm()方法要处理针对“/design”的HTTP GET请求。与@GetMapping处理GET请求类似,我们可以使用@PostMapping来处理POST请求。为了处理taco设计的表单提交,在DesignTacoController中添加如程序清单2.6所述的processTaco()方法。

程序清单2.6 使用@PostMapping来处理POST请求

@PostMapping
public String processTaco(Taco taco,
            @ModelAttribute TacoOrder tacoOrder) {
  tacoOrder.addTaco(taco);
  log.info("Processing taco: {}", taco);

  return "redirect:/orders/current";
}

如processTaco()方法所示,@PostMapping与类级别的@RequestMapping协作,指定processTaco()方法要处理针对“/design”的POST请求。我们所需要的正是以这种方式处理taco艺术家的表单提交。

表单提交时,表单中的输入域会绑定到Taco对象(这个类会在下面的程序清单中进行介绍)的属性中,该对象会以参数的形式传递给processTaco()。从这里开始,processTaco()就可以针对Taco对象采取任意想要的操作了。在本例中,它将Taco添加到了TacoOrder对象中(后者是以参数的形式传递到方法中来的),然后将taco以日志的形式打印出来。TacoOrder参数上所使用的@ModelAttribute表明它应该使用模型中的TacoOrder对象,这个对象是我们在前面的程序清单2.4中借助带有@ModelAttribute注解的order()方法放到模型中的。

回过头来再看一下程序清单2.5中的表单,你会发现其中包含多个checkbox元素,它们的名字都是ingredients,另外还有一个名为name的文本输入元素。表单中的这些输入域直接对应Taco类的ingredients和name属性。

表单中的name输入域只需要捕获一个简单的文本值。因此,Taco的name属性是String类型的。配料的复选框也有文本值,但是用户可能会选择零个或多个,所以它们所绑定的ingredients属性是一个List<Ingredient>,能够捕获选中的每种配料。

但是,稍等一下!如果配料的复选框是文本型(比如String)的值,而Taco对象以List<Ingredient>的形式表示一个配料的列表,那么这里是不是存在不匹配的情况呢?像["FLTO", "GRBF", "LETC"]这样的文本列表该如何绑定到一个Ingredient对象的列表上呢?要知道,Ingredient是一个更丰富的类型,不仅包括ID,还包括一个描述性的名字和配料类型。

这就是转换器(converter)的用武之地了。转换器是实现了Spring的Converter接口并实现了convert()方法的类,该方法会接收一个值并将其转换成另外一个值。要将String转换成Ingredient,我们要用到如程序清单2.7所示的IngredientByIdConverter。

程序清单2.7 将String转换为Ingredient

package tacos.web;

import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import tacos.Ingredient;
import tacos.Ingredient.Type;

@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {

  private Map<String, Ingredient> ingredientMap = new HashMap<>();

  public IngredientByIdConverter() {
    ingredientMap.put("FLTO",
        new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
    ingredientMap.put("COTO",
        new Ingredient("COTO", "Corn Tortilla", Type.WRAP));
    ingredientMap.put("GRBF",
        new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
    ingredientMap.put("CARN",
        new Ingredient("CARN", "Carnitas", Type.PROTEIN));
    ingredientMap.put("TMTO",
        new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES));
    ingredientMap.put("LETC",
        new Ingredient("LETC", "Lettuce", Type.VEGGIES));
    ingredientMap.put("CHED",
        new Ingredient("CHED", "Cheddar", Type.CHEESE));
    ingredientMap.put("JACK",
        new Ingredient("JACK", "Monterrey Jack", Type.CHEESE));
    ingredientMap.put("SLSA",
        new Ingredient("SLSA", "Salsa", Type.SAUCE));
    ingredientMap.put("SRCR",
        new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
  }

  @Override
  public Ingredient convert(String id) {
    return ingredientMap.get(id);
  }

}

因为我们现在还没有用来获取Ingredient对象的数据库,所以IngredientByIdConverter的构造器创建了一个Map,其中键(key)是String类型,代表了配料的ID,值则是Ingredient对象。在第3章,我们会调整这个转换器,让它从数据库中获取配料数据,而不是像这样硬编码。convert()方法只是简单地获取String类型的配料ID,然后使用它去Map中查找Ingredient。

注意,IngredientByIdConverter使用了@Component注解,使其能够被Spring识别为bean。Spring Boot的自动配置功能会发现它和其他Converter bean。它们会被自动注册到Spring MVC中,在请求参数与绑定属性需要转换时会用到。

现在,processTaco()方法没有对Taco对象进行任何处理。它其实什么都没做。目前,这样是可以的。在第3章,我们会添加一些持久化的逻辑,从而将提交的Taco保存到数据库中。

与showDesignForm()方法类似,processTaco()最后也返回了一个String类型的值。同样与showDesignForm()相似,返回的这个值代表了一个要展现给用户的视图。但是,区别在于processTaco()返回的值带有“redirect:”前缀,表明这是一个重定向视图。更具体地讲,它表明在processDesign()完成之后,用户的浏览器将会重定向到相对路径“/orders/current”。

这里的想法是:在创建完taco后,用户将会被重定向到一个订单表单页面,在这里,用户可以创建一个订单,将他们所创建的taco快递过去。但是,我们现在还没有处理“/orders/current”请求的控制器。

根据已经学到的关于@Controller、@RequestMapping和@GetMapping的知识,我们可以很容易地创建这样的控制器。它应该如程序清单2.8所示。

程序清单2.8 展现taco订单表单的控制器

package tacos.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import lombok.extern.slf4j.Slf4j;
import tacos.TacoOrder;

@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("tacoOrder")
public class OrderController {

  @GetMapping("/current")
  public String orderForm() {
    return "orderForm";
  }

}

在这里,我们再次使用Lombok @Slf4j注解在编译期创建一个SLF4J Logger对象。稍后,我们将会使用这个Logger记录所提交订单的详细信息。

类级别的@RequestMapping指明这个控制器的请求处理方法都会处理路径以“/orders”开头的请求。当与方法级别的@GetMapping注解结合之后,它就能够指定orderForm()方法会处理针对“/orders/current”的HTTP GET请求。

orderForm()方法本身非常简单,只返回了一个名为orderForm的逻辑视图名。在第3章学习完如何将所创建的taco保存到数据库之后,我们将会重新回到这个方法并对其进行修改,用一个Taco对象的列表来填充模型并将其放到订单中。

orderForm视图是由名为orderForm.html的Thymeleaf模板来提供的,如程序清单2.9所示。

程序清单2.9  taco订单的表单视图

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:th = "http://www.thymeleaf.org">
  <head>
    <title>Taco Cloud</title>
    <link rel = "stylesheet" th:href = "@{/styles.css}" />
  </head>

  <body>

    <form method = "POST" th:action = "@{/orders}" th:object = "${tacoOrder}">
      <h1>Order your taco creations!</h1>

      <img th:src = "@{/images/TacoCloud.png}"/>

      <h3>Your tacos in this order:</h3>
      <a th:href = "@{/design}" id = "another">Design another taco</a><br/>
      <ul>
        <li th:each = "taco : ${tacoOrder.tacos}">
          <span th:text = "${taco.name}">taco name</span></li>
      </ul>

      <h3>Deliver my taco masterpieces to...</h3>
      <label for = "deliveryName">Name: </label>
      <input type = "text" th:field = "*{deliveryName}"/>
      <br/>

      <label for = "deliveryStreet">Street address: </label>
      <input type = "text" th:field = "*{deliveryStreet}"/>
      <br/>

      <label for = "deliveryCity">City: </label>
      <input type = "text" th:field = "*{deliveryCity}"/>
      <br/>

      <label for = "deliveryState">State: </label>
      <input type = "text" th:field = "*{deliveryState}"/>
      <br/>

      <label for = "deliveryZip">Zip code: </label>
      <input type = "text" th:field = "*{deliveryZip}"/>
      <br/>

      <h3>Here's how I'll pay...</h3>
      <label for = "ccNumber">Credit Card #: </label>
      <input type = "text" th:field = "*{ccNumber}"/>
      <br/>

      <label for = "ccExpiration">Expiration: </label>
      <input type = "text" th:field = "*{ccExpiration}"/>
      <br/>

      <label for = "ccCVV">CVV: </label>
      <input type = "text" th:field = "*{ccCVV}"/>
      <br/>

      <input type = "submit" value = "Submit Order"/>
    </form>
  </body>
</html>

很大程度上,orderForm.html就是典型的HTML/Thymeleaf内容,不需要过多关注。它首先列出了添加到订单中的taco。这里,使用了Thymeleaf的th:each来遍历订单的tacos属性以创建列表。然后渲染了订单的表单。

但是,需要注意一点,那就是这里的<form>标签和程序清单2.5中的<form>标签不同,指定了一个表单的action。如果不指定action,表单将会以HTTP POST的形式提交到与展现该表单相同的URL上。在这里,我们明确指明表单要POST提交到“/orders”上(使用Thymeleaf的@{}操作符指定相对上下文的路径)。

因此,我们需要在OrderController中添加另外一个方法以便于处理针对“/orders”的POST请求。我们在第3章才会对订单进行持久化,在此之前,我们让它尽可能简单,如程序清单2.10所示。

程序清单2.10 处理taco订单的提交

@PostMapping
public String processOrder(TacoOrder order,
        SessionStatus sessionStatus) {
  log.info("Order submitted: {}", order);
  sessionStatus.setComplete();

  return "redirect:/";
}

调用processOrder()方法处理所提交的订单时,我们会得到一个Order对象,它的属性绑定了所提交的表单域。TacoOrder与Taco非常相似,是一个非常简单的类,其中包含了订单的信息。

在这个processOrder()方法中,我们只是以日志的方式记录了TacoOrder对象。在第3章,我们将会看到如何将其持久化到数据库中。但是,processOrder()方法在完成之前,还调用了SessionStatus对象的setComplete()方法,这个SessionStatus对象是以参数的形式传递进来的。当用户创建他们的第一个taco时,TacoOrder对象会被初始创建并放到会话中。通过调用setComplete(),我们能够确保会话被清理掉,从而为用户在下次创建taco时为新的订单做好准备。

现在,我们已经开发了OrderController和订单表单的视图,接下来可以尝试运行一下。打开浏览器并访问http://localhost:8080/design ,为taco选择一些配料,并点击Submit your taco按钮,从而看到如图2.4所示的表单。

图2.4 taco订单的表单

填充表单的一些输入域并点击Submit order按钮。在这个过程中,请关注应用的日志来查看你的订单信息。在我尝试运行的时候,日志条目如下所示(为了适应页面的宽度,重新进行了格式化):

Order submitted: TacoOrder(deliveryName = Craig Walls, deliveryStreet = 1234 7th
Street, deliveryCity = Somewhere, deliveryState = Who knows?,
deliveryZip = zipzap, ccNumber = Who can guess?, ccExpiration = Some day,
ccCVV = See-vee-vee, tacos = [Taco(name = Awesome Sauce, ingredients = [
Ingredient(id = FLTO, name = Flour Tortilla, type = WRAP), Ingredient(id = GRBF,
name = Ground Beef, type = PROTEIN), Ingredient(id = CHED, name = Cheddar,
type = CHEESE), Ingredient(id = TMTO, name = Diced Tomatoes, type = VEGGIES),
Ingredient(id = SLSA, name = Salsa, type = SAUCE), Ingredient(id = SRCR,
name = Sour Cream, type = SAUCE)]), Taco(name = Quesoriffic, ingredients = 
[Ingredient(id = FLTO, name = Flour Tortilla, type = WRAP), Ingredient(id = CHED,
name = Cheddar, type = CHEESE), Ingredient(id = JACK, name = Monterrey Jack,
type = CHEESE), Ingredient(id = TMTO, name = Diced Tomatoes, type = VEGGIES),
Ingredient(id = SRCR,name = Sour Cream, type = SAUCE)])])

似乎processOrder()完成了它的任务,通过日志记录订单详情来完成表单提交的处理。但是,如果仔细查看上述测试订单的日志,会发现它让一些“坏信息”混了进来。表单中的大多数输入域包含的可能都是不正确的数据。我们接下来添加一些校验,确保所提交的数据至少与所需的信息比较相似。

在设计新的taco作品的时候,如果用户没有选择配料或者没有为他们的作品指定名称,将会怎样?当提交表单的时候,如果没有填写所需的地址输入域,又将发生什么?或者,他们在信用卡域中输入了一个根本不合法的数字,又该怎么办?

就目前的情况来看,没有什么能够阻止用户在创建taco的时候不选择任何配料,或者输入空的快递地址,甚至将他们最喜欢的歌词作为信用卡号提交。这是因为我们还没有指明这些输入域该如何进行校验。

有种表单校验方法就是在 processTaco()和processOrder()方法中添加大量乱七八糟的if/then代码块,逐个检查每个输入域,以确保它们满足对应的校验规则。但是,这样操作会非常烦琐,并且会使代码难以阅读和调试。

比较幸运的是,Spring支持JavaBean校验API(JavaBean Validation API,也称为JSR-303),使我们能够更容易地声明检验规则,而不必在应用程序代码中显式编写声明逻辑。

要在Spring MVC中应用校验,我们需要:

在构建文件中添加Spring Validation starter;

在要被校验的类上声明校验规则,具体到我们的场景中,要被校验的类就是Taco类;

在需要进行校验的控制器方法中声明要进行校验,具体来讲,此处的控制器方法也就是DesignTacoController的processTaco()方法和OrderController的processOrder()方法;

修改表单视图以展现校验错误。

Validation API提供了一些注解,可以添加到领域对象的属性上,以便声明校验规则。Hibernate的Validation API实现又添加了一些校验注解。通过将Spring Validation starter添加到构建文件中,我们就能将这两者引入项目中。在Spring Boot Starter向导的I/O区域下面选中Validation复选框就可以实现这一点,但是如果想手动编写构建文件,在Maven pom.xml中添加如下的条目同样可以做到这一点:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

如果你使用Gradle,需要如下的依赖:

implementation 'org.springframework.boot:spring-boot-starter-validation'

我们是否还需要validation starter?

在早期版本的Spring Boot中,Spring Validation starter会自动包含到web starter中。从Spring Boot 2.3.0版本开始,如果想要使用校验,需要显式地将其添加到构建文件中。

validation starter已经准备就绪,我们看一下如何使用其中的一些注解来校验用户提交的Taco和TacoOrder。

对于Taco类来说,我们想要确保name属性不能为空或null,同时希望有至少一项配料被选中。程序清单2.11展示了更新后的Taco类,它使用@NotNull和@Size注解来声明这些校验规则。

程序清单2.11 为Taco领域类添加校验

package tacos;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;

@Data
public class Taco {

  @NotNull
  @Size(min = 5, message = "Name must be at least 5 characters long")
  private String name;
  @NotNull
  @Size(min = 1, message = "You must choose at least 1 ingredient")
  private List<Ingredient> ingredients;

}

我们可以发现,除了要求name属性不为null之外,我们还声明它的值的长度至少为5个字符。

在对提交的taco订单进行校验时,必须要给TacoOrder类添加注解。对于地址相关的属性,我们只想确保用户没有提交空白字段。为此,我们可以使用@NotBlank注解。

但是,支付相关的字段就比较复杂了。我们不仅要确保ccNumber属性不为空,还要保证它所包含的值是一个合法的信用卡号码。ccExpiration属性必须符合MM/YY格式(两位的月份和两位的年份),ccCVV属性需要是3位数字。为了实现这种校验,我们需要其他的一些JavaBean Validation API注解,并结合来自Hibernate Validator的注解。程序清单2.12展现了校验TacoOrder类所需的变更。

程序清单2.12 校验订单的字段

package tacos;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import java.util.List;
import java.util.ArrayList;
import lombok.Data;

@Data
public class TacoOrder {

  @NotBlank(message = "Delivery name is required")
  private String deliveryName;

  @NotBlank(message = "Street is required")
  private String deliveryStreet;

  @NotBlank(message = "City is required")
  private String deliveryCity;

  @NotBlank(message = "State is required")
  private String deliveryState;

  @NotBlank(message = "Zip code is required")
  private String deliveryZip;

  @CreditCardNumber(message = "Not a valid credit card number")
  private String ccNumber;

  @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([2-9][0-9])$",
           message = "Must be formatted MM/YY")
  private String ccExpiration;

  @Digits(integer = 3, fraction = 0, message = "Invalid CVV")
  private String ccCVV;

  private List<Taco> tacos = new ArrayList<>();

  public void addTaco(Taco taco) {
    this.tacos.add(taco);
  }
}

我们可以看到,ccNumber属性添加了@CreditCardNumber注解。这个注解声明该属性的值必须是合法的信用卡号,它要能通过Luhn算法的检查。这能防止用户有意或无意地输入错误的数据,但并不能确保这个信用卡号真的分配给了某个账户,也不能保证这个账号能够用来进行支付。

令人遗憾的是,目前还没有现成的注解来校验ccExpiration属性的MM/YY格式。在这里,我使用了@Pattern注解并为其提供了一个正则表达式,确保属性值符合预期的格式。如果你想知道如何解释这个正则表达式,我建议你参考一些在线的正则表达式指南,比如Regular Expressions Info网站。正则表达式仿佛一种魔法,已经超出了本书的范围。最后,ccCVV属性上添加了@Digits注解,确保它的值包含3位数字。

所有的校验注解都包含了一个message属性,该属性定义了当输入的信息不满足声明的校验规则时,要给用户展现的消息。

现在,我们已经声明了如何校验Taco和TacoOrder,接下来要重新修改每个控制器,让表单在POST提交至对应的控制器方法时,执行对应的校验。

要校验提交的Taco,我们需要为DesignTacoController中processTaco()方法的Taco参数添加一个JavaBean Validation API的@Valid注解,如程序清单2.13所示。

程序清单2.13 校验POST提交的Taco

import javax.validation.Valid;
import org.springframework.validation.Errors;

...

  @PostMapping
  public String processTaco(
          @Valid Taco taco, Errors errors,
          @ModelAttribute TacoOrder tacoOrder) {

    if (errors.hasErrors()) {
      return "design";
    }

    tacoOrder.addTaco(taco);
    log.info("Processing taco: {}", taco);

    return "redirect:/orders/current";
  }

@Valid注解会告诉Spring MVC要对提交的Taco对象进行校验,而校验时机是在它绑定完表单数据之后、调用processTaco()之前。如果存在校验错误,这些错误的细节将会捕获到一个Errors对象中并传递给processTaco()。processTaco()方法的前几行会查阅Errors对象,调用其hasErrors()方法判断是否有校验错误。如果存在校验错误,这个方法将不会处理Taco对象并返回“design”视图名,以使表单重新展现。

为了对提交的TacoOrder对象进行校验,OrderController的processOrder()方法也需要进行类似的变更,如程序清单2.14所示。

程序清单2.14 校验POST提交的TacoOrder

@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
        SessionStatus sessionStatus) {
  if (errors.hasErrors()) {
    return "orderForm";
  }

  log.info("Order submitted: {}", order);
  sessionStatus.setComplete();

  return "redirect:/";
}

在这两个场景中,如果没有校验错误,方法都允许处理提交的数据;如果存在校验错误,请求将会被转发至表单视图上,以便用户纠正他们的错误。

但是,用户该如何知道有哪些要纠正的错误呢?如果我们无法指出表单上的错误,那么用户只能不断猜测如何才能成功提交表单。

Thymeleaf提供了便捷访问Errors对象的方法,这就是借助fields及其th:errors属性。举例来说,为了展现信用卡字段的校验错误,我们可以添加一个<span>元素,该元素会将对校验错误的引用用到订单的表单模板上,如程序清单2.15所示。

程序清单2.15 展现校验错误

<label for = "ccNumber">Credit Card #: </label>
      <input type = "text" th:field = "*{ccNumber}"/>
      <span class = "validationError"
            th:if = "${#fields.hasErrors('ccNumber')}"
            th:errors = "*{ccNumber}">CC Num Error</span>

在这里,<span>元素使用class属性来为错误添加样式,以引起用户的注意。除此之外,它还使用th:if属性来决定是否要显示该元素。fields属性的hasErrors()方法会检查ccNumber域是否存在错误。如果存在,将会渲染<span>。

th:errors属性引用了ccNumber输入域,如果该输入域存在错误,它会将<span>元素的占位符内容替换为校验信息。

在为订单表单的其他输入域都添加类似的<span>标签之后,如果提交错误信息,表单会如图2.5所示。其中,错误信息提示姓名、城市和邮政编码字段为空,而且所有支付相关的输入域均未满足校验条件。

图2.5 在订单表单上展现校验错误

现在,我们的Taco Cloud控制器不仅能够展现和捕获输入,还能校验用户提交的信息是否满足一定的基本验证规则。接下来,我们后退一步,重新考虑第1章中的HomeController,并学习一种替代实现方案。

到目前为止,我们已经为Taco Cloud应用编写了3个控制器。这3个控制器尽管服务于应用程序的不同功能,但基本上遵循相同的编程模型:

它们都使用了@Controller注解,表明它们是控制器类,并且应该被Spring的组件扫描功能自动发现并初始化为Spring应用上下文中的bean;

除HomeController之外的控制器都在类级别使用了@RequestMapping注解,据此定义该控制器所处理的基本请求模式;

它们都有一个或多个带@GetMapping或@PostMapping注解的方法,这些注解指明了该由哪个方法来处理某种类型的请求。

我们所编写的大部分控制器都将遵循这个模式。但是,如果一个控制器非常简单,不需要填充模型或处理输入(在我们的场景中,也就是HomeController),那么还有另外一种方式来定义控制器。请参考程序清单2.16学习如何声明视图控制器——只将请求转发到视图而不做其他事情的控制器。

程序清单2.16 声明视图控制器

package tacos.web;

import org.springframework.context.annotation.Configuration;
import
     org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("home");
  }
}

关于WebConfig,最需要注意的事情就是它实现了WebMvcConfigurer接口。WebMvcConfigurer定义了多个方法来配置Spring MVC,尽管只是一个接口,却提供了所有方法的默认实现,只需要我们覆盖所需的方法。在本例中,我们覆盖了addViewControllers方法。

addViewControllers()方法会接收一个ViewControllerRegistry对象,我们可以使用它注册一个或多个视图控制器。在这里,我们调用registry的addViewController()方法,将“/”传递进去,视图控制器将会针对该路径执行GET请求。这个方法会返回ViewControllerRegistration对象,我们马上基于该对象调用setViewName()方法,用它指明当请求“/”的时候要转发到“home”视图上。

如前文所述,我们用配置类中的几行代码就替换了HomeController类。现在,我们可以删除HomeController了,应用的功能应该和之前完全一样。唯一需要注意的是,我们要重新找到第1章中的HomeControllerTest类,从@WebMvcTest注解中移除对HomeController的引用,这样测试类的编译才不会报错。

在这里,我们创建了一个新的WebConfig配置类来存放视图控制器的声明。但是,所有的配置类都可以实现WebMvcConfigurer接口并覆盖addViewControllers()方法。举例来说,我们可以将相同的视图控制器声明添加到TacoCloudApplication引导类中,如下所示:

@SpringBootApplication
public class TacoCloudApplication implements WebMvcConfigurer {

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

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("home");
  }

}

采用扩展已有配置类的方式能够避免创建新的配置类,从而减少项目中制品的数量。但是,我倾向于为每种配置(Web、数据、安全等)创建新的配置类,来保持应用的引导配置类尽可能整洁和简单。

在视图控制器方面,或者更具体地讲,在控制器将请求所转发到的视图方面,到目前为止,我们都是使用Thymeleaf来实现所有的视图。我很喜欢Thymeleaf,但是你可能想要为你的应用选择不同的模板模型。让我们来看一下Spring所能支持的众多视图方案。

在大多数情况下,视图模板库的选择完全取决于个人喜好。Spring非常灵活,能够支持很多常见的模板方案。除了个别情况,你所选择的模板库本身甚至不知道它在与Spring协作[3]

表2.2列出了Spring Boot自动配置功能所支持的模板方案。

表2.2  支持的模板方案

模板

Spring Boot starter依赖

FreeMarker

spring-boot-starter-freemarker

Groovy Templates

spring-boot-starter-groovy-templates

JavaServer Pages(JSP)

无(由Tomcat或Jetty提供)

Mustache

spring-boot-starter-mustache

Thymeleaf

spring-boot-starter-thymeleaf

通常来讲,你只需要选择想要的视图模板库,将其作为依赖项添加到构建文件中,就可以在“/templates”目录下(在基于Maven或Gradle构建的项目中,它会在“src/main/ resources”目录下)编写模板了。Spring Boot会探测到你所选择的模板库,并自动配置为Spring MVC控制器生成视图所需的各种组件。

在Taco Cloud应用中,我们已经按照这种方式使用了Thymeleaf模板库。在第1章中,在初始化项目的时候,我们选择了Thymeleaf复选框。这样会自动将Spring Boot的Thymeleaf starter依赖添加到pom.xml文件中。应用启动时,Spring Boot的自动配置功能会探测到Thymeleaf并自动为我们配置Thymeleaf bean。我们所需要做的就是在“/templates”中开始编写模板。

如果你想要使用不同的模板库,只需要在项目初始化的时候选择它,或者编辑已有的项目构建文件,将新选择的模板库添加进来。

例如,假设我们想要使用Mustache来替换Thymeleaf,没有问题!只需要找到pom.xml文件,并将

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

替换为

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mustache</artifactId>
</dependency>

当然,还需要确保按照Mustache语法来编写模板,而不是再使用Thymeleaf标签。Mustache的特定用法(以及其他备选模板语言)超出了本书的范围,但是我在这里给你一个直观的印象,让你明白大致会是什么样子,如下代码是Mustache模板的一个片段,它能够渲染taco设计表单中的某个配料组:

<h3>Designate your wrap:</h3>
{{#wrap}}
<div>
  <input name = "ingredients" type = "checkbox" value = "{{id}}" />
  <span>{{name}}</span><br/>
</div>
{{/wrap}}

这是2.1.3小节中Thymeleaf代码片段的Mustache等价实现。{{#wrap}}代码块(结尾对应使用{{/wrap}})会遍历请求中key为wrap的属性并为每个条目渲染嵌入式HTML。{{id}}和{{name}}标签分别会引用每个条目(应该是一个Ingredient)的id和name属性。

你可能已经注意到了,在表2.2中,JSP并不需要在构建文件中添加任何特殊的依赖。这是因为Servlet容器本身(默认是Tomcat)会实现JSP规范,因此不需要额外的依赖。

但是,选择使用JSP,会带来另一个问题。事实上,Java Servlet容器,包括嵌入式的Tomcat和Jetty容器,通常会在“/WEB-INF”目录下寻找JSP文件。如果将应用构建成一个可执行的JAR文件,就无法满足这种需求了。因此,只有在将应用构建为WAR文件并部署到传统的Servlet容器中时,才能选择JSP方案。如果想要构建可执行的JAR文件,那么必须选择Thymeleaf、FreeMarker或表2.2中的其他方案。

默认情况下,模板只有在第一次使用的时候解析一次,解析的结果会被后续的请求所使用。对于生产环境来说,这是一个很棒的特性,它能防止每次请求时多余的模板解析过程,因此有助于提升性能。

但是,在开发期,这个特性就不太友好了。假设我们启动完应用之后访问taco的设计页面,然后决定对它做一些修改,刷新Web浏览器的时候,依然会看到原来的版本。要想看到变更效果,必须要重新启动应用,这当然是非常不方便的。

幸运的是,有一种方法可以禁用缓存。我们所需要做的就是将相关的缓存属性设置为false。表2.3列出了每种模板库所对应的缓存属性。

表2.3  启用或禁用模板缓存的属性

模板

启用或禁用缓存的属性

FreeMarker

spring.freemarker.cache

Groovy Templates

spring.groovy.template.cache

Mustache

spring.mustache.cache

Thymeleaf

spring.thymeleaf.cache

默认情况下,这些属性都设置成了true,以便启用缓存。我们可以将缓存属性设置为false,从而禁用所选模板引擎的缓存。例如,要禁用Thymeleaf缓存,只需要在application.properties中添加这行代码:

 spring.thymeleaf.cache = false

唯一需要注意的是,在将应用部署到生产环境之前,一定要删除这一行代码(或者将其设置为true)。对于这一点,有种方法是将该属性设置到profile中(我们将会在第6章讨论profile)。

另外一种更简单的方式是使用Spring Boot的DevTools,与第1章中的做法一样。DevTools提供了很多非常有用的开发期特性,其中有一项功能就是禁用所有模板库的缓存,但是在应用部署的时候,DevTools会将自身禁用掉(从而能够重新启用模板缓存)。

Spring提供了一个强大的Web框架,名为Spring MVC,它能够用来为Spring应用开发Web前端。

Spring MVC是基于注解的,通过像@RequestMapping、@GetMapping和@PostMapping这样的注解来启用请求处理方法的声明。

大多数的请求处理方法最终会返回一个视图的逻辑名称,比如Thymeleaf模板,请求会转发到这样的视图上(同时会带有任意的模型数据)。

Spring MVC支持校验,这是通过JavaBean Validation API和Validation API的实现(如Hibernate Validator)完成的。

我们可以在WebMvcConfigurer类中通过addViewController方法注册视图控制器,以处理没有模型数据和业务逻辑的HTTP GET请求。

除了Thymeleaf之外,Spring支持各种视图方案,包括FreeMarker、Groovy Templates和Mustache。

[1] 如果想更深入地了解应用领域,推荐阅读Eric Evans的《领域驱动设计》。

[2] 样式表的内容与我们的讨论无关,它只是包含了让配料两列显示的样式,避免出现一个很长的配料列表。

[3] 其中一个这样的例外情况就是Thymeleaf的Spring Security方言,我们将会在第5章讨论。

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59869”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


相关图书

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

相关文章

相关课程