JavaWeb-新版

JavaWeb

参考文章:

https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture/

01、Web基础概念简介

1、服务器与客户端

线下的服务器与客户端

线上的服务器与客户端

客户端的各种形式

PC端网页

移动端

Iot设备

服务器的各种形式

参考文章:https://heavy_code_industry.gitee.io/code_heavy_industry/pro000-dev-story/chapter11/content.html

2、服务器端应用程序

我们要开发的就是服务器端应用程序

3、业务

项目中的功能就是业务。

4、请求和响应

发生在饭馆的请求和响应

项目中的请求和响应

5、项目的逻辑构成

  • 请求:请求是项目中最基本的逻辑单元,就像万事万物都由原子构成

    举例:点超链接跳转到注册页面

  • 功能:一个功能包含很多个请求

    举例:注册用户功能

    • 请求1:点超链接跳转到注册页面
    • 请求2:发送请求获取短信验证码
    • 请求3:检查用户名是否可用
    • 请求4:提交表单完成注册
  • 模块:一个模块包含很多功能

    举例:用户信息管理模块

    • 功能1:用户注册功能
    • 功能2:用户登录功能
    • 功能3:个人中心——账户安全功能
    • 功能4:个人中心——账户绑定功能
    • 功能5:个人中心——收货地址功能
    • 功能6:个人中心——我的银行卡功能
  • 子系统:根据项目规模的不同,子系统这层逻辑概念可能有也可能没有。如果设置了子系统,那么子系统中也必然包含很多模块。其实庞大项目的子系统已经相当于一个项目了,甚至比小型项目整个都大。

    举例:认证中心子系统

    • 模块1:用户信息管理模块
    • 模块2:权限管理模块
    • 模块3:授权管理模块
    • 模块4:权限检查模块
  • 项目:为了解决现实生活中的实际问题开发一个项目,这个项目就是为这个需求提供的一整套解决方案。

    举例:电商项目

    • 子系统1:认证中心子系统
    • 子系统2:商品管理子系统
    • 子系统3:购物车子系统
    • 子系统4:仓储子系统
    • 子系统5:物流子系统
    • 子系统6:订单子系统

6、架构

01、概念

『架构』其实就是项目的『结构』。只不过『结构』这个词太小了,不适合用来描述项目这么大的东西,所以换了另一个更大的词:架构。所以当我们聊一个项目的架构时,我们聊的是项目是由哪些部分组成的。

02、发展演变历程

一个项目就是一个工程,这样的结构就是单一架构,也叫all in one。我们现在的JavaWeb阶段、SSM阶段都是学习单一架构开发技术。

分布式架构

一个项目中包含很多工程,每个工程作为一个模块。模块之间存在调用关系。分布式架构阶段的技术分为两类:

  • Java框架:SpringBoot、SpringCloud、Dubbo等等。
  • 中间件:Redis、ElasticSearch、FastDFS、Nginx、Zookeeper、RabbitMQ等等。

03、单一架构技术体系

  • 视图:用户的操作界面+数据的动态显示
    • 前端技术:HTML/CSS/JavaScript
    • 服务器端页面模板技术:Thymeleaf
  • 控制层:处理请求+跳转页面
    • 服务器:Tomcat
    • 控制器:Servlet
    • 域对象:request、session、servletContext
    • 过滤器:Filter
    • 监听器:Listener
    • 异步交互:Ajax
  • 业务逻辑层:业务逻辑计算
  • 持久化层:操作数据库

7、本阶段技术体系

8、本阶段案例简介

02、拜托了大脑

0、学习中的痛点问题

  • 为什么明明很努力了,但就是没效果?
  • 学完记不住怎么办?
  • 每天上课老师讲的代码我也都敲(抄)了,但是感觉什么都没学会怎么办?
  • 为什么学着学着会感觉跟不上了?
  • 学的东西太多感觉非常混乱怎么办?

1、人和人的差异是『系统』的差异

起点、路径、目标

为什么同样是毕业5年,你和你的同学差距特别大?起点、路径、目标这三个东西哪个对人影响最大?

  • 目标:有的人目标是1亿,有的人目标是够吃饭就行。目标不同活法肯定不同。
  • 路径:有的人根本不知道路在哪,有的人靠两条腿狂奔,有的人开汽车,有的人坐飞机,有的人坐火箭。
  • 起点:最不重要的就是起点。对那些靠两条腿跑的人来说,10公里是非常遥远的距离,但是对开汽车的人来说很近。

如果一个问题的解决办法已经有别人反复验证过可行、有效的路径存在,那这种问题就非常容易解决。但即使这样不同的人应对同样问题时效果也完全不同。从深圳到北京,徒步、开车、高铁、飞机就是效率完全不同的解决方案。

在路上使用的交通工具不同就是人和人之间系统的不同,而系统其实就是一个人的认知水平、思维方式、学习和工作的方法

思维跃迁

世界本身是极其复杂的,即使是细分之后的领域仍然非常复杂,比如一颗小小的芯片背后就是一个非常庞大、复杂的体系:科学、技术、设计、制造、供应链、操作系统、市场等等。面对这么复杂的世界,简单的思维方式是没法理解和应对的。而比我们成功的人一定是从更底层把握了复杂世界中简单规律的人。

穿透现象看到本质

实现同一个功能,代码可以有很多种不同的写法,我们要做的并不是把某一种代码背下来,而是直击本质把握规律以不变应万变

宏观视野

将来大家走上工作岗位,参与开发都是非常庞大的系统,此时要求大家能够把视角拔高,从最高处俯瞰整个项目的架构。而在具体的开发过程中对于微观的代码编写和故障调试又要求能够做到细致认真,对程序员来说这两种看似矛盾的能力必须同时具备。

目标导向

任何项目、任何技术都是为了解决实际问题而生的,所以我们不论是在技术学习过程中还是在项目开发过程中都要锻炼自己理解需求的能力和根据需求设定目标的能力。有了明确的目标下一步才能一步一步分析出实现目标的思路,然后根据思路中的各个环节编写代码,最终把需求落地实现。

抽象空间

一个人每天按时上课,按时自习,从不违纪,身体确实在教室,但是灵魂早已不知到哪里游荡,他算努力吗?

一个人把老师的代码抄了一遍又一遍,把课件背了一回又一回,他算努力吗?

一个人顶着狂风烈日面试了一家又一家公司,屡战屡败,屡败屡战,他算努力吗?

另一个人看着好像也没怎么努力,该打球打球,该拍拖拍拖,面试没几家就拿到N个不错的offer,老师劝他不着急入职,再多面几家肯定有更好的,他说现在这个不错了,懒的面了。你好像确实没法说他*『努力』*,但是你不得不承认至少单纯在**『参加培训找工作』这一件事上他很『成功』**。

所以老师不必费力向你证明前面那三个人并没有*『真正努力』*,而只是*『用战术上的勤奋掩盖战略上的懒惰』*——老师希望你注意到:即使是**『真正的努力』仍然只是『手段』而不是『目的』,我们真正的目的是『成功』**。不管过程中再怎么努力,最后达不成目的不还是白搭?当然,如果因此你得出一个结论就是那我不用努力了我也没有办法,毕竟谁都没法叫醒一个装睡的人不是?

那怎么做算是**『战略正确』**呢?

进军抽象空间,到抽象空间里占领一个又一个知识高地。

学习的真正过程发生在人的意识中,而意识是看不见、摸不着的。所以当一个同学『人在教室、神游天外』时,老师明知道他没在学习可是还真不好说啥,因为他确实没有违反纪律。纪律只能在物质世界设置规则,而没法限制你的想法。

但所谓知识其实就是人类认识世界过程中总结、提炼的结晶,全部都是抽象的概念,同样看不见、摸不着。我们上课的过程就是老师把他自己的意识空间用声音和图像投影到教室里,然后再投影到你的意识空间——毕竟老师没法双掌贴着你的后背把毕生功力传给你。

大家需要接收到声音和图像的信号,然后理解它们的含义,再重新凝练成一个一个的概念结晶,最后还要分门别类、井然有序的保存起来以备将来使用。

所以学习真要比的话应该比的就是在意识空间中知识晶体的\数量**和*秩序***

很多人为什么学习起来非常吃力?就是因为他完全没有办法进入抽象空间,完全没法理解任何抽象的东西。这里我们举个例子:

具象:3×2=6,2×3=6,4×7=28,7×4=28,……

抽象:乘法交换律

用具象的算式描述这个规律可以写出无限多个,可是用抽象的规律短短五个字就概括了。这就是掌握规律的力量,这就是为什么培根说『知识就是力量』。好在老师会用举例、类比、画图等等手段把抽象的东西尽量具象化,但是谁也代替不了你自己的理解、提炼和总结。不过好消息是现在你知道怎么努力了。

所以各位朝气蓬勃的少男少女们,充分调动起你活跃的大脑,一起来提炼闪闪发光的知识晶体吧!

2、『学习能力』就是一个系统

敞开心扉

你真的对技术感兴趣吗?

小明拿瓶子去接水,咕咚咕咚一通灌。完事儿一看,瓶子里一滴水都没有。

为啥?瓶塞没有拔出来。

像不像你叫嚷着学Java,但其实真正的内心对Java没有任何兴趣,只是因为听说它很赚钱才学。你**『真正喜欢』的是『钱』而不是『Java』**。

而真正学的时候内心是**『抗拒』**的,你觉得这东西和你一点关系都没有。禁闭的心扉如同没有拔掉木塞的瓶子。这样谁都帮不了你,不管听了多少课都学不到东西的。

正确的状态是:对『Java本身』贼啦好奇,**『挖空心思』**想闹明白到底是咋回事,谁不让我学我跟谁急。

学会以后干活,不给钱也干,谁不让我干我跟谁急,然后肯定会干得特别好,**『意外』**赚了很多钱。

任何成功都来自于*『内心深处强烈的渴求』*。而程序员的成功秘诀是*『敲代码本身即是最大幸福和最高奖赏』*

很多同学总是问学完Java用不用再学学大数据呀?Java干3年以后转型什么呀?

其实Java也好,大数据也好,都是『路径』,不是『目标』。你的**『好奇心』**决定了你在这条路上能够走多远。而培训完的起薪都只是『起点』,前面说过了,三者中『起点』是最不重要的。

在以后的学习中我们任何技术点都会通过提出问题的方式来引入,大家要留意这个环节,在揭示答案之前用这个问题问问自己,看看这个问题是否能在你的心里引起共鸣激发你的好奇心,这是我们迈向成功的第一步。

大脑的工作机制

大脑的结构

  • 最内层:爬行动物脑,只在乎吃饭和制造下一代,再复杂点的事情就理解不了了。
  • 中间层:哺乳动物脑,感情中枢,很多哺乳动物都进化出了这一层大脑,所以你会感觉到你家狗狗对你非常有感情——相信我,那是真感情。
  • 最外层:大脑皮层,人的理性思维在这里生成,语言、视觉、听觉、逻辑思维、空间想象力等等都在大脑皮层这里有专门的负责区域。其中学Java的时候要用到的理性思维是大脑皮层中的**『前额叶』**这个部分。可以说迄今为止人类文明所造就的所有辉煌成果都是建立在前额叶的基础上。

]死记硬背?

不会吧,不会吧?现在还有同学想靠死记硬背来学Java?请马上停止!

相比较**『记忆』来说,大脑更擅长『思考』**。前面说过思考是由前额叶来负责的,下面说说记忆:

  • 临时记忆:海马体。比如别人和你说忙完给他打个电话,你当时答应了但是一转眼就忘了。信息刚刚进入大脑的时候其实是在海马体临时存储的。
  • 长期记忆:写入大脑皮层。这可不容易,死记硬背就是通过大量的重复把信息强行写入大脑皮层。就好比你非要靠脑子记住一个手机号,大致就是这个办法。但遗憾的是一个孤立的信息即使通过大量重复强行写入到大脑皮层也不会存在太久。小学同学朝夕相处,但是分开20年还是会忘记很多人的名字。

这就是为什么总有同学很困扰:学完了记不住呀!对于这个问题我的回应如下:

  • 你想记住啥?

    你想记住代码怎么写吗?需求会变,代码永远要跟着变。

    你想记住老师说的每一句话吗?老师一天能说一万句话。

    想记住这些这不是痴心妄想吗?

  • 你该记住啥?

    具象:3×2=6,2×3=6,4×7=28,7×4=28,……

    抽象:乘法交换律

    你该记住的是从现象中提取出来的规律而不是现象。甚至这么说也不准确。因为这个规律你理解了自然就记住了,不需要背。

  • 知识的网络

    任何孤立的信息都很容易忘,但是点和点之间组成一个彼此联系的网络就很不容易忘记了。

  • 细节记不住咋办?

    即使理解并且记住了要点,但是知识点相关的细节还是记不住怎么办?比如大段的配置,代码调用的API方法,特定场景需要传递的参数等等。好办,『好记性不如烂笔头』,这些东西记到笔记里永久保存。

外接硬盘

通过前面的分析我们看到,大脑是一个有CPU(前额叶)有内存(海马体)的电脑,但是没有硬盘来做永久存储。所以我们需要给大脑连接一个能够永久存储信息的外部设备:笔记。由此我们就得出了大脑要扮演的第一个角色:搜索引擎

首先,学习的过程就是建设索引库的过程:

然后在解决实际问题时需要运用学过的知识就到索引库中去检索:

快思维和慢思维

大脑的第二个角色是:司令部。而你是司令。对,你没看错,其实『你不是你的大脑,你的大脑也不是你』。就像手、脚、胯骨轴是你的一部分一样,大脑同样只是你的一部分。但是大脑对人来说太重要了,人的很多无意识行为都是大脑来协调和指挥的。比如:人走路时的身体平衡,身体各个系统和脏器的正常工作,应激反应等等都是大脑来指挥的,而这些事情你完全不知道是怎么做的,也无法干涉。司令部协助司令指挥部队,司令把握总体的战略方向

而司令部这个角色和学习有关系的部分就是大脑的快思维和慢思维。

小孩子没法一边穿衣服、系扣子一边走路。因为它穿衣服、系扣子需要慢慢做,一边做一边想。所以事情不熟练、需要慢慢思考的时候,大脑就需要以慢思维的模式工作——这个过程是需要**『你』**参与的。

大人穿衣服、系扣子都很熟练了,不需要想,所以可以一边走路,一边穿衣服。这个过程就不需要『你』参与了,大脑调用现成的『路径』在快思维的工作模式下完成动作。

这是因为**『慢思维』非常消耗能量,而『快思维』能耗非常低,大脑会优先选择能耗低的路径执行动作,节约能量。这就是为什么人会有『习惯』,而习惯一旦养成很难改变。本质原因就是大脑的这种『路径依赖』**的机制。

知识的知识:元知识

知识点

比如:**『黄赤交角导致地球上四季更迭』**这就是一个知识点。

前置知识

想理解『黄赤交角导致地球上四季更迭』这个知识点的前提是知道『赤道』、『黄道』、『地球自转』、『地球公转』等等概念,还必须具备一定的『立体几何』的基础。这就说明知识的大厦是一砖一瓦建成的,没有1楼别想上2楼。

如果想理解B的前提是知道A,那么A就是B的前置知识点。

知识链

把所有前置知识连起来就形成了一条知识的链条。虽然我们学习的时候不需要把这个链条画出来,但是它可以帮助你理解你为什么会跟不上:在知识的链条中前置知识点缺失太多了。不知道『赤道』、『黄道』、『地球自转』、『地球公转』等等概念也不理解两个平面之间的夹角是怎么回事的时候听别人给你讲地球上为什么会有春夏秋冬那就是听天书,完全不明白什么意思。

知识树

为什么老师讲课的课件总是划分成章节,章节里面还有一、二、三、……1、2、3、……①、②、③、……这样的小点?就是因为知识点根据彼此之间的归类和相关性可以『纵向抽取』,从而整理成一个树形结构,这样也是为了大家更好理解。

但是老师讲课的时候还是要从这根葡萄梗上把葡萄一颗一颗摘下来给你。

而你拿到葡萄以后就乱放了,散落一地的葡萄非常不好拿,到用的时候各种颜色的葡萄混杂的胡乱堆放在一起,根本找不到你要的。

所以大家听完课还是要在自己的精神世界里把葡萄往葡萄梗上一颗一颗装回去。用专业术语来说就是把节点按照树形结构重新组装起来。

知识网络

哲学知识点:矛盾是对立统一,矛盾中对立的双方都不能脱离对方而单独存在。

化学知识点:在碱溶液中会有极少量的H+离子,在酸溶液中会有极少量的OH-离子。

横向来看,知识点之间很容易找到相通或类似的情况。有些知识点之间的横向对比能够彼此印证,帮助我们理解;而有些看起来相似但实质不同的知识点却会干扰我们的判断和使用。

所以知识点融会贯通之后必然会打破彼此的界限连成网络,这个网络越发达你的视野越开阔,网络中的具体知识点掌握越牢固

知识体系

在生物学中把生物划分为了界、门、纲、目、科、属、种,这有什么好处呢?不仅让已知的生物更易于研究和学习,将来发现新物种就直接按照这个划分方法纳入体系就好了。

有了知识体系之后,你的学习会非常快速,因为新的知识点会快速被已有的知识体系接纳,旧的知识会帮助你理解新的知识。

  MySQL Redis
数据库连接 Connection Jedis
数据库连接池 Druid JedisPool
连接信息 主协议:子协议://主机地址:端口号/数据库名称 用户名 密码 数据库驱动 主机地址:端口号

当你不断的感受到旧知识点在帮助你理解新知识点,你的知识体系就在不断扩大;知识系统不断扩大之后,旧知识点对新知识点的帮助能力就更大,形成一个*『增强回路』*,让你的学习越来越快,形成一条指数曲线。

卖油翁的微笑

陈康肃公善射,当世无双 ,公亦以此自矜。尝射于家圃,有卖油翁释担而立,睨之久而不去。见其发矢十中八九,但微颔之。 康肃问曰:“汝亦知射乎?吾射不亦精乎?”。翁曰:“无他, 但手熟尔。”康肃忿然曰:“尔安敢轻吾射!”翁曰:“以我酌油知之。”乃取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿。因曰:“我亦无他,惟手熟尔。”康肃笑而遣之。

编程本质上是一个技能,而任何技能都需要通过大量的重复练习才能够熟练掌握。李小龙说:『我不怕会一万种招式的人,我怕把一种招式练一万次的人』。神枪手是用一匣一匣的子弹喂出来的,优秀的程序员就是用一堆又一堆的bug给练出来的。所以学编程肯定要多敲多练。

但是我说的是**『敲』不是『抄』**!很多同学照着老师的代码敲自己的代码,看着是敲了一遍,其实只是抄了一遍。抄完发现没有什么效果。关上电脑什么都不记得。这其实是正常的认知规律的体现。

在『抄』的时候大脑其实不需要多想什么,因为它只需要考虑A处是什么单词,再到B处敲同样的单词就行了,这是一件很简单的事情,大脑调用5%就足够了——这调用深度远远不够。而且你不觉得非常枯燥无聊吗?

真正的学习状态是什么?向知识点提问、打草稿、分析思路、画流程图、和别人讨论、敲代码验证、分析结果为什么和老师不一样、把验证结果写到笔记里……

你看看这是不是热火朝天?这么整的话大脑的利用率可是有95%啊!而且兴趣盎然啊有木有?这么学习既高效又开心,谁不让我学我和谁急!

这时大脑扮演了第三个角色:教练

输出倒逼输入

你有没有发现看老师讲课的视频特别容易睡着?这不是老师讲得不好,也不是你不爱学习,而是单纯的看视频只是被动接受,大脑还是没有充分调动起来。大脑进入闲置状态自然会昏昏欲睡

而当你和别人讨论问题,把你的观点讲出来,就会越聊越兴奋甚至争得脸红脖子粗有木有?

甚至是在和别人表达自己观点的时候会发现自己以前理解的某个知识点其实完全不是那么回事,开始以为自己懂了但是要说的时候才发现自己根本没有懂,理解的不透彻。

其实在大脑里很多角落都在黑暗中,当我们想拿出来的时候才会用手电筒照过去,把那个地方照亮,照亮以后才能看清楚那里到底放着什么。

所以一切形式的**『输出』**都会非常有效的提高学习效率。那么输出的形式包括哪些呢?

  • 做笔记:自己亲手做,不是抄别人的。
  • 敲代码:不是抄啊!
  • 和别人讨论:有问有答的那种
  • 调bug:bug虐我千百遍,我待bug如初恋
  • 发博客:积累多了对职业发展很有帮助的哟

总结

说了这么多,我们具体在学习的时候应该怎么学呢?

  • 第一步:提出问题,激发好奇。
  • 第二步:思考分析,理解原理。
  • 第三步:敲代码、调bug练习,验证原理,修正理解。
  • 第四步:做笔记,既是备忘,又是建设自己的索引库和知识体系。
  • 第五步:遇到相似知识点就详细辨析比对,直到充分区别不会混淆。
  • 第六步:回归到真实场景,在项目中运用,彻底掌握这个技术,让所学技术真正落地。

所以**『学习』两个字中,『学』指的是理解原理,建立知识体系,而『习』则是练习技能,引申到各种形式的输出都算『习』。而只有拿学过、练过的东西真正的解决了现实生活中真正的实际问题才算是『会』。而任何形式的市场都只会为能够真正解决问题的人付『钱』**。

03、单一架构回顾

我们从现在的JavaWeb阶段到后面学习SSM框架阶段都是在学习单一架构项目开发的技术。而在JavaWeb阶段由于重点是探讨如何实现Web开发,所以必须学习一部分前端开发的技术。本节就是让大家明确我们现在要学习的内容在整个架构体系中处于什么位置。

1、单一架构技术体系

2、视图层

严格来说视图分成两层:

  • 前端技术:HTML/CSS/JavaScript
  • 服务器端页面模板技术:Thymeleaf

其中HTML、CSS、JavaScript都是工作在浏览器上的,所以它们都属于前端技术。而Thymeleaf是在服务器上把动态内容计算出具体数据,所以严格来说Thymeleaf是后端技术。

这里大家会有个疑问:为什么在『视图』这个地方已经有HTML、CSS、JavaScript这些前端技术了,能够生成用户可以操作的界面,那为什么还需要Thymeleaf这样一个后端技术呢?

简单来说原因是Thymeleaf=HTML+动态数据,而HTML不支持动态数据,这部分需要借助Thymeleaf来完成。

更进一步的细节咱们讲到那再说啦!

3、Web2.0

Web2.0是相对于更早的网页开发规范而提出的新规范。Web2.0规范之前的网页开发并没有明确的将HTML、CSS、JavaScript代码分开,而是互相之间纠缠在一起,导致代码维护困难,开发效率很低。

在开发中我们把这样彼此纠缠、互相影响的现象称为『耦合』。而把耦合在一起的东西拆解开,让他们彼此独立出来称为『解耦』。各个组成部分独立完成自己负责的功能,和其他模块无关称为『内聚』。

将来大家经常会听到一句话:软件开发提倡『 高内聚,低耦合』。

一个软件项目只有做到了高内聚、低耦合才能算得上结构严谨,模块化程度高,有利于开发和维护。

所以Web2.0规范主张将网页代码分成下面三个部分:

  • 结构:由HTML实现,负责管理网页的内容。将来网页上不管是静态还是动态的数据都是填写到HTML的标签里。
  • 表现:由CSS实现,负责管理网页内容的表现形式。比如:颜色、尺寸、位置、层级等等。也就是给数据穿上一身漂亮的衣服。
  • 行为:由JavaScript实现,负责实现网页的动态交互效果。比如:轮播图、表单验证、鼠标滑过显示下拉菜单、鼠标滑过改变背景颜色等等。

04、HTML&CSS

一、HTML简介

参考文章:https://www.cnblogs.com/zhaostudy/p/16685971.html

1、名词解释

HTML是Hyper Text Markup Language的缩写。意思是*『超文本标记语言』*

2、超文本

HTML文件本质上是文本文件,而普通的文本文件只能显示字符。但是HTML技术则通过HTML标签把其他网页、图片、音频、视频等各种多媒体资源引入到当前网页中,让网页有了非常丰富的呈现方式,这就是超文本的含义——本身是文本,但是呈现出来的最终效果超越了文本。

3、标记语言

说HTML是一种『标记语言』是因为它不是向Java这样的『编程语言』,因为它是由一系列『标签』组成的,没有常量、变量、流程控制、异常处理、IO等等这些功能。HTML很简单,每个标签都有它固定的含义和确定的页面显示效果。

标签是通过一组尖括号+标签名的方式来定义的:

<p>HTML is a very popular fore-end technology.</p>

这个例子中使用了一个p标签来定义一个段落,

叫*『开始标签』*

叫*『结束标签』*。开始标签和结束标签一起构成了一个完整的标签。开始标签和结束标签之间的部分叫*『文本标签体』*,也简称*『标签体』*

有的时候标签里还带有*『属性』*

<a href="http://www.xxx.com">show detail</a>

href="http://www.xxx.com"就是属性,href是**『属性名』**,"http://www.xxx.com"是**『属性值』**。

还有一种标签是*『单标签』*

<input type="text" name="username" />

4、HelloWorld

5、HTML文件结构

文档类型声明

HTML文件中第一行的内容,用来告诉浏览器当前HTML文档的基本信息,其中最重要的就是当前HTML文档遵循的语法标准。这里我们只需要知道HTML有4和5这两个大的版本,HTML4版本的文档类型声明是:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

HTML5版本的文档类型声明是:

<!DOCTYPE html>

现在主流的技术选型都是使用HTML5,之前的版本基本不用了。

历史上HTML的各个版本:

版本名称 年份
HTML 1991
HTML+ 1993
HTML2.0 1995
HTML3.2 1997
HTML4.01 1999
XHTML1.0 2000
HTML5 2012
XHTML5 2013

根标签

html标签是整个文档的根标签,所有其他标签都必须放在html标签里面。上面的文档类型不能当做普通标签看待。

所谓『根』其实是『树根』的意思。在一个树形结构中,根节点只能有一个。

头部

head标签用于定义文档的头部,其他头部元素都放在head标签里。头部元素包括title标签、script标签、style标签、link标签、meta标签等等。

主体

body标签定义网页的主体内容,在浏览器窗口内显示的内容都定义到body标签内。

注释

HTML注释的写法是:

<!-- 注释内容 -->

注释的内容不会显示到浏览器窗口内,是开发人员用来对代码内容进行解释说明。

6、HTML语法规则

  • 根标签有且只能有一个
  • 无论是双标签还是单标签都必须正确关闭
  • 标签可以嵌套但不能交叉嵌套
  • 注释不能嵌套
  • 属性必须有值,值必须加引号,单引号或双引号均可
  • 标签名不区分大小写但建议使用小写

二、使用HTML展示文章

以文章的组织形式展示数据是HTML最基本的功能了,网页上显示的文章在没有做任何CSS样式设定的情况下如下图所示:

本节我们要学习的HTML标签如下表:

标签名称 功能
h1~h6 1级标题~6级标题
p 段落
a 超链接
ul/li 无序列表
img 图片
div 定义一个前后有换行的块
span 定义一个前后无换行的块

为了方便编写代码,我们在IDEA中创建一个静态Web工程来操作:

1、标题标签

代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <h1>这是一级标题</h1>
    <h2>这是二级标题</h2>
    <h3>这是三级标题</h3>
    <h4>这是四级标题</h4>
    <h5>这是五级标题</h5>
    <h6>这是六级标题</h6>

</body>
</html>

页面显示效果

注意:标题标签前后有换行。

2、段落标签

代码

<p>There is clearly a need for CSS to be taken seriously by graphic artists. The Zen Garden aims to excite, inspire, and encourage participation. To begin, view some of the existing designs in the list. Clicking on any one will load the style sheet into this very page. The code remains the same, the only thing that has changed is the external .css file. Yes, really.</p>

页面显示效果

注意:段落标签前后有换行。

3、超链接

代码

<a href="page02-anchor-target.html">点我跳转到下一个页面</a>

页面显示效果

点击后跳转到href属性指定的页面

4、路径

在我们整个Web开发技术体系中,『路径』是一个贯穿始终的重要概念。凡是需要获取另外一个资源的时候都需要用到路径。要想理解路径这个概念,我们首先要认识一个概念:『文件系统』。

文件系统

我们写代码的时候通常都是在Windows系统来操作,而一个项目开发完成后想要让所有人都能够访问到就必须『部署』到服务器上,也叫『发布』。而服务器通常是Linux系统。

Windows系统和Linux系统的文件系统有很大差别,为了让我们编写的代码不会因为从Windows系统部署到了Linux系统而出现故障,实际开发时不允许使用物理路径

物理路径举例:

D:\aaa\pro01-HTML\page01-article-tag.html

D:\aaa\pro01-HTML\page02-anchor-target.html

幸运的是不管是Windows系统还是Linux系统环境下,目录结构都是树形结构,编写路径的规则是一样的。

所以我们以项目的树形目录结构为依据来编写路径就不用担心操作系统平台发生变化之后路径错误的问题了。有了这个大前提,我们具体编写路径时有两种具体写法:

  • 相对路径
  • 绝对路径(建议使用)

相对路径

相对路径都是以\『当前位置』**为基准**来编写的。假设我们现在正在浏览a页面,想在a页面内通过超链接跳转到z页面。

那么按照相对路径的规则,我们现在所在的位置是a.html所在的b目录:

z.html并不在b目录下,所以我们要从b目录出发,向上走,进入b的父目录——c目录:

c目录还是不行,继续向上走,进入c的父目录——d目录:

在从d目录向下经过两级子目录——e目录、f目录才能找到z.html:

所以整个路径的写法是:

<a href="../../e/f/z.html">To z.html</a>

可以看到使用相对路径有可能会很繁琐,而且在后面我们结合了在服务器上运行的Java程序后,相对路径的基准是有可能发生变化的,所以不建议使用相对路径

绝对路径

通过IDEA服务器打开HTML文件

测试绝对路径的前提是通过IDEA的内置服务器访问我们编写的HTML页面——这样访问地址的组成结构才能和我们以后在服务器上运行的Java程序一致。

服务器访问地址的组成

绝对路径的写法

绝对路径要求必须是以**『正斜线』**开头。这个开头的正斜线在整个服务器访问地址中对应的位置如下图所示:

这里标注出的这个位置代表的是*『服务器根目录』*,从这里开始我们就是在服务器的内部查找一个具体的Web应用。

所以我们编写绝对路径时就从这个位置开始,按照目录结构找到目标文件即可。拿前面相对路径中的例子来说,我们想在a.html页面中通过超链接访问z.html。此时路径从正斜线开始,和a.html自身所在位置没有任何关系:

<a href="/d/e/f/z.html">To z.html</a>
具体例子

编写超链接访问下面的页面:

<a href="/aaa/pro01-HTML/animal/cat/miao.html">Cat Page</a>
小结

为了和我们后面学习的内容和正确的编码方式保持一致,建议大家从现在开始就使用绝对路径。

5、换行

代码

We would like to see as much CSS1 as possible. CSS2 should be limited to widely-supported elements only. The css Zen Garden is about functional, practical CSS and not the latest bleeding-edge tricks viewable by 2% of the browsing public. <br/>The only real requirement we have is that your CSS validates.

页面显示效果

6、无序列表

代码

<ul>
        <li>Apple</li>
        <li>Banana</li>
        <li>Grape</li>
    </ul>

页面显示效果

7、图片

准备图片文件

代码

src属性用来指定图片文件的路径,这里同样按我们前面说的使用*『绝对路径』*

<img src="/aaa/pro01-HTML/./images/mi.jpg"/>

页面显示效果

8、块

**『块』**并不是为了显示文章内容的,而是为了方便结合CSS对页面进行布局。块有两种,div是前后有换行的块,span是前后没有换行的块。

把下面代码粘贴到HTML文件中查看他们的区别:

<div style="border: 1px solid black;width: 100px;height: 100px;">This is a div block</div>
<div style="border: 1px solid black;width: 100px;height: 100px;">This is a div block</div>

<span style="border: 1px solid black;width: 100px;height: 100px;">This is a span block</span>
<span style="border: 1px solid black;width: 100px;height: 100px;">This is a span block</span>

页面显示效果为:

9、HTML实体

在HTML文件中,<、>等等这样的符号已经被赋予了特定含义,不会作为符号本身显示到页面上,此时如果我们想使用符号本身怎么办呢?那就是使用HTML实体来转义。

三、使用HTML收集表格数据

参考文章:

HTML DOM Table 对象 (w3school.com.cn)

Table 对象

Table 对象代表一个 HTML 表格。

在 HTML 文档中

标签每出现一次,一个 Table 对象就会被创建。

Table 对象集合

集合 描述
[cells] 返回包含表格中所有单元格的一个数组。
[rows] 返回包含表格中所有行的一个数组。
tBodies[] 返回包含表格中所有 tbody 的一个数组。

Table 对象属性

属性 描述
align 表在文档中的水平对齐方式。(已废弃)
bgColor 表的背景颜色。(已废弃)
border 设置或返回表格边框的宽度。
caption 对表格的元素的引用。
cellPadding 设置或返回单元格内容和单元格边框之间的空白量。
cellSpacing 设置或返回在表格中的单元格之间的空白量。
frame 设置或返回表格的外部边框。
id 设置或返回表格的 id。
rules 设置或返回表格的内部边框(行线)。
summary 设置或返回对表格的描述(概述)。
tFoot 返回表格的 TFoot 对象。如果不存在该元素,则为 null。
tHead 返回表格的 THead 对象。如果不存在该元素,则为 null。
width 设置或返回表格的宽度。

标准属性

属性 描述
className 设置或返回元素的 class 属性。
dir 设置或返回文本的方向。
lang 设置或返回元素的语言代码。
title 设置或返回元素的 title 属性。

Table 对象方法

方法 描述
createCaption() 为表格创建一个 caption 元素。
createTFoot() 在表格中创建一个空的 tFoot 元素。
createTHead() 在表格中创建一个空的 tHead 元素。
deleteCaption() 从表格删除 caption 元素以及其内容。
deleteRow() 从表格删除一行。
deleteTFoot() 从表格删除 tFoot 元素及其内容。
deleteTHead() 从表格删除 tHead 元素及其内容。
insertRow() 在表格中插入一个新行。

水果库存案例

HTML代码

<div id="div_container">
  <div id="div_fruit_list">
    <table id="tbl_fruit">
      <tr>
        <td class="w20">名称</td>
        <td class="w20">单价</td>
        <td class="w20">数量</td>
        <td class="w20">小计</td>
        <td>操作</td>
      </tr>
      <tr>
        <td>苹果</td>
        <td>3</td>
        <td>100</td>
        <td>60</td>
        <td><a href="#">删除</a></td>
      </tr>
      <tr>
        <td>香蕉</td>
        <td>4</td>
        <td>100</td>
        <td>60</td>
        <td><a href="#">删除</a></td>
      </tr>
      <tr>
        <td>菠萝</td>
        <td>5</td>
        <td>110</td>
        <td>60</td>
        <td><a href="#">删除</a></td>
      </tr>
      <tr>
        <td>橘子</td>
        <td>8</td>
        <td>80</td>
        <td>30</td>
        <td><a href="#">删除</a></td>
      </tr>
      <tr>
        <td>总计</td>
        <td colspan="4">999</td>
      </tr>
    </table>
    <hr/>
    <div id="add_friut_div">
      <table>
        <tr>
          <td>名称:</td>
          <td><input type="text" id="fname" value="apple"></td>
        </tr>
        <tr>
          <td>单价:</td>
          <td><input type="text" id="price" value="5"></td>
        </tr>
        <tr>
          <td>数量:</td>
          <td><input type="text" id="fcount" value="100"></td>
        </tr>
        <tr>
          <th colspan="2">
            <input type="button" id="addBtn" class="btn" value="添加">
            <input type="button" class="btn" value="重填">
          </th>
        </tr>
      </table>
    </div>
  </div>
</div>

CSS代码

body {
  margin: 0;
  padding: 0;
  /* background-color: #ccc; */
}

div {
  position: relative;
  float: left;
}

#div_container {
  width: 80%;
  height: 100%;
  margin-left: 10%;
  border: 0px solid red;
  float: left;
  border-radius: 8px;
  /* background-color: #ccc; */
}

#div_fruit_list {
  width: 100%;
  border: 0px darkcyan solid;
}

#tbl_fruit {
  width: 80%;
  line-height: 28px;
  margin-top: 150px;
  margin-left: 15%;
}

#tbl_fruit,
#tbl_fruit tr,
#tbl_fruit td,
#tbl_fruit th {
  border: 1px dodgerblue solid;
  border-collapse: collapse;
  text-align: center;
  font-size: 16px;
  font-family: '黑体';
  color: pink;
}

.wp {
  width: 20%;
}

.btn {
  border: 1px solid sliver;
  width: 80px;
  height: 24px;
}

JS代码

// 这个方法可以显示在外部的js文件中通过外部导入的方式!
window.onload = function() {
    // 当页面加载完成,我们需要绑定各种事件
    var fruitTbl = document.getElementById('tbl_fruit');
    // 获取表格中的所有的行
    var rows = fruitTbl.rows;
    for (var i = 1; i < rows.length - 1; i++) {
        var tr = rows[i];
        trBindEvent(tr);
    }

    document.getElementById("addBtn").onclick = addFruit;
};

function trBindEvent(tr) {
    tr.onmouseover = showBGColor;
    tr.onmouseout = clearBGColor;

    var cells = tr.cells;
    var priceTD = cells[1];
    // 当鼠标悬浮在单价单元格变成手势!
    priceTD.onmouseover = showHand;
    // 绑定鼠标点击单价单元格的事件
    priceTD.onclick = editPrice;

    // 绑定删除小图标的点击事件
    var img = cells[4].firstChild;
    if (img && img.tagName == "IMG") {
        img.onclick = delFruit;

    }
}

function addFruit() {
    var fname = document.getElementById("fname").value;
    var price = parseInt(document.getElementById("price").value);
    var fcount = parseInt(document.getElementById("fcount").value);
    var xj = price * fcount;

    var fruitTbl = document.getElementById("tbl_fruit");
    var tr = fruitTbl.insertRow(fruitTbl.rows.length - 1);
    var fnameTD = tr.insertCell();
    fnameTD.innerText = fname;
    var priceTD = tr.insertCell();
    priceTD.innerText = price;
    var fcountTD = tr.insertCell();
    fcountTD.innerText = fcount;
    var xjTD = tr.insertCell();
    xjTD.innerText = xj;
    var imgTD = tr.insertCell();
    imgTD.innerText = "<img src='imgs/del.jpg' class='delImg' />";

    updateZJ();

    trBindEvent(tr);

}

function delFruit() {
    if (event && event.srcElement && event.srcElement.tagName == "IMG") {
        if (window.confirm("是否确认删除当前库存记录")) {
            var img = event.srcElement;
            var tr = img.parentElement.parentElement;
            var fruitTbl = document.getElementById("tbl_fruit");
            fruitTbl.deleteRow(tr.rowIndex);

            updateZJ();
        }
    }
}

// 当鼠标悬浮时,显示背景颜色!
function showBGColor() {
    if (event && event.srcElement && event.srcElement.tagName == 'TD') {
        var td = event.srcElement;
        var tr = td.parentElement;
        tr.style.backgroundColor = 'skyblue';

        // 获取表格中的所有单元格
        var tds = tr.cells;
        for (var i = 0; i < tds.length; i++) {
            tds[i].style.color = 'white';
        }
    }
}

// 当鼠标离开时,恢复背景颜色!
function clearBGColor() {
    if (event && event.srcElement && event.srcElement.tagName == 'TD') {
        var td = event.srcElement;
        var tr = td.parentElement;
        tr.style.backgroundColor = 'transparent';

        var tds = tr.cells;
        for (var i = 0; i < tds.length; i++) {
            tds[i].style.color = 'pink';
        }
    }
}

// 当鼠标在单价单元格时,显示手势
function showHand() {
    if (event && event.srcElement && event.srcElement.tagName == 'TD') {
        var td = event.srcElement;
        td.style.cursor = 'pointer';
    }
}

// 当鼠标点击单价单元格进行价格编辑
function editPrice() {
    if (event && event.srcElement && event.srcElement.tagName == 'TD') {
        var priceTD = event.srcElement;
        // 判断当前priceTD有子节点,而且第一个子节点是文本节点,TypeNode 对应为3,ElementNode 对应为1
        if (priceTD.firstChild && priceTD.firstChild.nodeType == 3) {
            // innerText 表示设置或者获取当前节点的内部文本
            var oldPrice = priceTD.innerText;
            // innerHTML表示设置当前内部节点的HTML
            priceTD.innerHTML = "<input type=‘text’ size='4' />";
            var input = priceTD.firstChild;
            if (input.tagName == 'INPUT') {
                input.value = oldPrice;
                // 选中输入框内部的文本
                input.select();
                // 绑定输入框失去焦点事件,失去焦点,更新单价!
                input.onblur = updatePrice;

                // 在输入框上绑定键盘按下的事件,此处需要保证按下的是数字
                input.onkeydown = ckInput;
            }
        }
    }
}

// 检验键盘按下的事件
function ckInput() {
    var kc = event.KeyCode;
    if (!((kc >= 48 && kc <= 57) || kc == 8 || kc == 13)) {
        event.returnValue = false;
    }

    if (kc == 13) {
        event.srcElement.blur();
    }
}

function updatePrice() {
    if (event && event.srcElement && evnet.srcElement.tagName == 'INPUT') {
        var input = event.srcElement;
        var newPrice = input.value;
        // input节点的父节点是td
        var priceTD = input.parentElement;
        priceTD.innerText = newPrice;

        // 更新当前内部的小计这一个格子的值
        // priceTD。parentElement td的父元素是tr
        updateXJ(priceTD.parentElement);
    }
}

function updateXJ(tr) {
    if (tr && tr.tagName == 'TR') {
        var tds = tr.cells;
        var price = tds[1].innerText;
        var count = tds[2].innerText;
        // innerText获取到的值类型是字符串类型,因此进行数据转换,进行数学运算
        var xj = parseInt(price) * parseInt(count);
        td[3].innerText = xj;

        // 更新总计
        updateZJ();
    }
}

// 更新总计
function updateZJ() {
    var fruitTbl = document.getElementById('tbl_fruit');
    var rows = fruitTbl.rows;
    var sum = 0;
    for (var i = 1; i < rows.length - 1; i++) {
        var tr = rows[i];
        var xj = parseInt(tr.cells[3].innerText);
        sum = sum + xj;
    }
    rows[rows.length - 1].cells[1].innerText = sum;
}

四、使用HTML表单收集数据

1、什么是表单

在项目开发过程中,凡是需要用户填写的信息都需要用到表单。

2、form标签

在HTML中我们使用form标签来定义一个表单。而对于form标签来说有两个最重要的属性:action和method。

<form action="/aaa/pro01-HTML/page05-form-target.html" method="post">
</form>

action属性

用户在表单里填写的信息需要发送到服务器端,对于Java项目来说就是交给Java代码来处理。那么在页面上我们就必须正确填写服务器端的能够接收表单数据的地址。

这个地址要写在form标签的action属性中。但是现在暂时我们还没有服务器端环境,所以先借用一个HTML页面来当作服务器端地址使用。

method属性

『method』这个单词的意思是『方式、方法』,在form标签中method属性用来定义提交表单的*『请求方式』*。method属性只有两个可选值:get或post,没有极特殊情况的话使用post即可。

什么是*『请求方式』*

浏览器和服务器之间在互相通信时有大量的**『数据』**需要传输。但是不论是浏览器还是服务器都有很多不同厂商提供的不同产品。

常见的浏览器有:

  • Chrome
  • Firefox
  • Safari
  • Opera
  • Edge

常见的Java服务器有:

  • Tomcat
  • Weblogic
  • WebSphere
  • Glassfish
  • Jetty

这么多不同厂商各自开发的应用程序怎么能保证它们彼此之间传输的**『数据』**能够被对方正确理解呢?

很简单,我们给这些数据设定*『格式』*,发送端按照格式发送数据,接收端按照格式解析数据,这样就能够实现数据的**『跨平台传输』**了。

而这里定义的**『数据格式』就是应用程序之间的『通信协议』**。

在JavaSE阶段的网络编程章节我们接触过TCP/IP、UDP这样的协议,而我们现在使用的**『HTTP协议』**的底层就是TCP/IP协议。

HTTP1.1中共定义了八种请求方式:

  • GET
  • POST
  • PUT
  • DELETE
  • HEAD
  • CONNECT
  • OPTIONS
  • TRACE

但是在HTML标签中,点击超链接是GET方式的请求,提交一个表单可以通过form标签的method属性指定GET或POST请求,其他请求方式无法通过HTML标签实现。除了GET、POST之外的其他请求方式暂时我们不需要涉及(到我们学习SpringMVC时会用到PUT和DELETE)。至于GET请求和POST请求的区别我们会在讲HTTP协议的时候详细介绍,现在大家可以从表面现象来观察一下。

3、name和value

在用户使用一个软件系统时,需要一次性提交很多数据是非常正常的现象。我们肯定不能要求用户一个数据一个数据的提交,而肯定是所有数据填好后一起提交。那就带来一个问题,服务器怎么从众多数据中识别出来收货人、所在地区、详细地址、手机号码……?

很简单,给每个数据都起一个『名字』,发送数据时用**『名字』携带对应的数据,接收数据时通过『名字』**获取对应的数据。

在各个具体的表单标签中,我们通过**『name属性』来给数据起『名字』,通过『value属性』来保存要发送给服务器的『值』**。

但是名字和值之间既有可能是*『一个名字对应一个值』*,也有可能是*『一个名字对应多个值』*

这么看来这样的关系很像我们Java中的Map,而事实上在服务器端就是使用Map类型来接收请求参数的。具体的是类型是:Map<String,String[]>

name属性就是Map的键,value属性就是Map的值。

有了上面介绍的基础知识,下面我们就可以来看具体的表单标签了。

4、单行文本框

个性签名:<input type="text" name="signal"/><br/>

显示效果

5、密码框

代码

密码:<input type="password" name="secret"/><br/>

显示效果

6、单选框

代码

你最喜欢的季节是:
<input type="radio" name="season" value="spring" />春天
<input type="radio" name="season" value="summer" checked="checked" />夏天
<input type="radio" name="season" value="autumn" />秋天
<input type="radio" name="season" value="winter" />冬天

<br/><br/>

你最喜欢的动物是:
<input type="radio" name="animal" value="tiger" />路虎
<input type="radio" name="animal" value="horse" checked="checked" />宝马
<input type="radio" name="animal" value="cheetah" />捷豹

效果

说明

  • name属性相同的radio为一组,组内互斥
  • 当用户选择了一个radio并提交表单,这个radio的name属性和value属性组成一个键值对发送给服务器
  • 设置checked="checked"属性设置默认被选中的radio

7、多选框

代码

你最喜欢的球队是:
<input type="checkbox" name="team" value="Brazil"/>巴西
<input type="checkbox" name="team" value="German" checked="checked"/>德国
<input type="checkbox" name="team" value="France"/>法国
<input type="checkbox" name="team" value="China" checked="checked"/>中国
<input type="checkbox" name="team" value="Italian"/>意大利

8、下拉列表

代码

你喜欢的运动是:
<select name="interesting">
    <option value="swimming">游泳</option>
    <option value="running">跑步</option>
    <option value="shooting" selected="selected">射击</option>
    <option value="skating">溜冰</option>
</select>

效果

说明

  • 下拉列表用到了两种标签,其中select标签用来定义下拉列表,而option标签设置列表项。
  • name属性在select标签中设置。
  • value属性在option标签中设置。
  • option标签的标签体是显示出来给用户看的,提交到服务器的是value属性的值。
  • 通过在option标签中设置selected="selected"属性实现默认选中的效果。

9、按钮

代码

<button type="button">普通按钮</button>
<button type="reset">重置按钮</button>
<button type="submit">提交按钮</button>

效果

说明

类型 功能
普通按钮 点击后无效果,需要通过JavaScript绑定单击响应函数
重置按钮 点击后将表单内的所有表单项都恢复为默认值
提交按钮 点击后提交表单

10、表单隐藏域

代码

<input type="hidden" name="userId" value="2233"/>

说明

通过表单隐藏域设置的表单项不会显示到页面上,用户看不到。但是提交表单时会一起被提交。用来设置一些需要和表单一起提交但是不希望用户看到的数据,例如:用户id等等。

11、多行文本框

代码

自我介绍:<textarea name="desc"></textarea>

效果

说明

textarea没有value属性,如果要设置默认值需要写在开始和结束标签之间。

五、CSS的简单应用

1、设置CSS样式的三种方式

在HTML标签内设置

仅对当前标签有效

<div style="border: 1px solid black;width: 100px; height: 100px;"> </div>

在head标签内设置

对当前页面有效

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style type="text/css">
        .one {
            border: 1px solid black;
            width: 100px;
            height: 100px;
            background-color: lightgreen;
            margin-top: 5px;
        }
    </style>
</head>
<body>

    <div style="border: 1px solid black;width: 100px; height: 100px;"> </div>

    <div class="one"> </div>
    <div class="one"> </div>
    <div class="one"> </div>

</body>

引入外部CSS样式文件

创建CSS文件

编辑CSS文件
.two {
    border: 1px solid black;
    width: 100px;
    height: 100px;
    background-color: yellow;
    margin-top: 5px;
}
引入外部CSS文件

在需要使用这个CSS文件的HTML页面的head标签内加入:

<link rel="stylesheet" type="text/css" href="/aaa/pro01-HTML/style/example.css" />

于是下面HTML代码的显示效果是:

<div class="two"> </div>
    <div class="two"> </div>
    <div class="two"> </div>

2、CSS代码语法

  • CSS样式由选择器和声明组成,而声明又由属性和值组成。
  • 属性和值之间用冒号隔开。
  • 多条声明之间用分号隔开。
  • 使用/* ... */声明注释。

3、CSS选择器

标签选择器

HTML代码:

<p>Hello, this is a p tag.</p>
<p>Hello, this is a p tag.</p>
<p>Hello, this is a p tag.</p>
<p>Hello, this is a p tag.</p>
<p>Hello, this is a p tag.</p>

CSS代码:

p {
  color: blue;
  font-weight: bold;
}

id选择器

HTML代码:

<p>Hello, this is a p tag.</p>
    <p>Hello, this is a p tag.</p>
    <p id="special">Hello, this is a p tag.</p>
    <p>Hello, this is a p tag.</p>
    <p>Hello, this is a p tag.</p>

CSS代码:

#special {
  font-size: 20px;
  background-color: aqua;
}

类选择器

HTML代码:

<div class="one"> </div>
    <div class="one"> </div>
    <div class="one"> </div>

CSS代码:

.one {
  border: 1px solid black;
  width: 100px;
  height: 100px;
  background-color: lightgreen;
  margin-top: 5px;
}

4、定位与浮动

这里不做太多的简介!

简单来说定位与浮动的关系:

  • relative:定位相对于float进行调试!
  • absoulte:定位与top、left进行调试!

05、JavaScript

一、JavaScript简介

参考文章:

https://www.cnblogs.com/zhaostudy/p/16733510.html

https://www.cnblogs.com/zhaostudy/p/16051571.html

https://www.cnblogs.com/zhaostudy/p/15883773.html

1、起源

1995年时,由Netscape公司的Brendan Eich,在网景导航者浏览器上首次设计实现而成。Netscape在最初将其脚本语言命名为LiveScript,因为Netscape与Sun合作,网景公司管理层希望它外观看起来像Java,因此取名为JavaScript。

2、特性

脚本语言

JavaScript是一种解释型的脚本语言。不同于C、C++、Java等语言先编译后执行, JavaScript不会产生编译出来的字节码文件,而是在程序的运行过程中对源文件逐行进行解释。

基于对象

JavaScript是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象。但是面向对象的三大特性:『封装』、『继承』、『多态』中,JavaScript能够实现封装,可以模拟继承,不支持多态,所以它不是一门面向对象的编程语言。

弱类型

JavaScript中也有明确的数据类型,但是声明一个变量后它可以接收任何类型的数据,并且会在程序执行过程中根据上下文自动转换类型。

事件驱动

JavaScript是一种采用事件驱动的脚本语言,它不需要经过Web服务器就可以对用户的输入做出响应。

跨平台性

JavaScript脚本语言不依赖于操作系统,仅需要浏览器的支持。因此一个JavaScript脚本在编写后可以带到任意机器上使用,前提是机器上的浏览器支持JavaScript脚本语言。目前JavaScript已被大多数的浏览器所支持。

二、JS-HelloWorld

1、功能效果图

2、代码实现

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>HelloWorld</title>
    </head>
    <body>
        <!-- 在HTML代码中定义一个按钮 -->
        <button type="button" id="helloBtn">SayHello</button>
    </body>

    <!-- 在script标签中编写JavaScript代码 -->
    <script type="text/javascript">

        // document对象代表整个HTML文档
        // document对象调用getElementById()方法表示根据id查找对应的元素对象
        var btnElement = document.getElementById("helloBtn");

        // 给按钮元素对象绑定单击响应函数
        btnElement.onclick = function(){

            // 弹出警告框
            alert("hello");
        };
    </script>
</html>

三、JavaScript基本语法

1、JavaScript代码嵌入方式

HTML文档内

  • JavaScript代码要写在script标签内
  • script标签可以写在文档内的任意位置
  • 为了能够方便查询或操作HTML标签(元素)script标签可以写在body标签后面

可以参考简化版的HelloWorld

<!-- 在HBuilderX中,script标签通过打字“sc”两个字母就可以直接完整生成 -->
<script type="text/javascript">

    // 下面是同样实现HelloWorld功能的简化版代码
    document.getElementById("helloBtn").onclick = function() {
        alert("Hello simple");
    };

</script>

引入外部JavaScript文档

在script标签内通过src属性指定外部xxx.js文件的路径即可。但是要注意以下两点:

  • 引用外部JavaScript文件的script标签里面不能写JavaScript代码
  • 先引入,再使用
  • script标签不能写成单标签

引入方式如下:

<body>
</body>

<!-- 使用script标签的src属性引用外部JavaScript文件,和Java中的import语句类似 -->
<!-- 引用外部JavaScript文件的script标签里面不能写JavaScript代码 -->
<!-- 引用外部JavaScript文件的script标签不能改成单标签 -->
<!-- 外部JavaScript文件一定要先引入再使用 -->
<script src="/pro02-JavaScript/scripts/outter.js" type="text/javascript" charset="utf-8"></script>

<script type="text/javascript">

    // 调用外部JavaScript文件中声明的方法
    showMessage();
</script>

2、声明和使用变量

JavaScript数据类型

基本数据类型

  • 数值型:JavaScript不区分整数、小数

  • 字符串:JavaScript不区分字符、字符串;单引号、双引号意思一样。

  • 布尔型:true、false

    在JavaScript中,其他类型和布尔类型的自动转换。

    • true:非零的数值,非空字符串,非空对象
    • false:零,空字符串,null,undefined

    例如:"false"放在if判断中

// "false"是一个非空字符串,直接放在if判断中会被当作『真』处理
if("false"){
    alert("true");
}else{
    alert("false");
}
  • 引用类型
    • 所有new出来的对象
    • 用[]声明的数组
    • 用{}声明的对象

变量

  • 关键字:var

  • 数据类型:JavaScript变量可以接收任意类型的数据

  • 标识符:严格区分大小写

  • 变量使用规则

    • 如果使用了一个没有声明的变量,那么会在运行时报错

      Uncaught ReferenceError: b is not defined

    • 如果声明一个变量没有初始化,那么这个变量的值就是undefined

3、函数

内置函数

内置函数:系统已经声明好了可以直接使用的函数。

弹出警告框
alert("警告框内容");
弹出确认框

用户点击『确定』返回true,点击『取消』返回false

var result = confirm("老板,你真的不加个钟吗?");
if(result) {
    console.log("老板点了确定,表示要加钟");
}else{
    console.log("老板点了确定,表示不加钟");
}
在控制台打印日志
console.log("日志内容");

声明函数

写法1:

function sum(a, b) {
            return a+b;
        }

写法2:

var total = function() {
            return a+b;
        };

写法2可以这样解读:声明一个函数,相当于创建了一个『函数对象』,将这个对象的『引用』赋值给变量total。最后加的分号不是给函数声明加的,而是给整体的赋值语句加的分号。

调用函数

JavaScript中函数本身就是一种对象,函数名就是这个**『对象』『引用』**。而调用函数的格式是:函数引用()

function sum(a, b) {
            return a+b;
        }

        var result = sum(2, 3);
        console.log("result="+result);

或:

var total = function() {
            return a+b;
        }

        var totalResult = total(3,6);
        console.log("totalResult="+totalResult);

4、对象

JavaScript中没有『类』的概念,对于系统内置的对象可以直接创建使用。

使用new关键字创建对象

// 创建对象
        var obj01 = new Object();

        // 给对象设置属性和属性值
        obj01.stuName = "tom";
        obj01.stuAge = 20;
        obj01.stuSubject = "java";

        // 在控制台输出对象
        console.log(obj01);

使用{}创建对象

// 创建对象
        var obj02 = {
            "soldierName":"john",
            "soldierAge":35,
            "soldierWeapon":"gun"
        };

        // 在控制台输出对象
        console.log(obj02);

给对象设置函数属性

// 创建对象
var obj01 = new Object();

// 给对象设置属性和属性值
obj01.stuName = "tom";
obj01.stuAge = 20;
obj01.stuSubject = "java";

obj01.study = function() {
    console.log(this.stuName + " is studying");
};

// 在控制台输出对象
console.log(obj01);
// 调用函数
obj01.study();

或:

// 创建对象
var obj02 = {
    "soldierName":"john",
    "soldierAge":35,
    "soldierWeapon":"gun",
    "soldierShoot":function(){
        console.log(this.soldierName + " is using " + this.soldierWeapon);
    }
};

// 在控制台输出对象
console.log(obj02);
// 调用函数
obj02.soldierShoot();

this关键字

this关键字只有两种情况:

  • 在函数外面:this关键字指向window对象(代表当前浏览器窗口)
  • 在函数里面:this关键字指向调用函数的对象
// 直接打印this
console.log(this);

// 函数中的this
// 1.声明函数
function getName() {
    console.log(this.name);
}

// 2.创建对象
var obj01 = {
    "name":"tom",
    "getName":getName
};
var obj02 = {
    "name":"jerry",
    "getName":getName
};

// 3.调用函数
obj01.getName();
obj02.getName();

5、数组

使用new关键字创建数组

// 1.创建数组对象
var arr01 = new Array();

// 2.压入数据
arr01.push("apple");
arr01.push("orange");
arr01.push("banana");
arr01.push("grape");

// 3.遍历数组
for (var i = 0; i < arr01.length; i++) {
    console.log(arr01[i]);
}

// 4.数组元素反序
arr01.reverse();
for (var i = 0; i < arr01.length; i++) {
    console.log(arr01[i]);
}

// 5.数组元素拼接成字符串
var arrStr = arr01.join(",");
console.log(arrStr);

// 6.字符串拆分成数组
var arr02 = arrStr.split(",");
for (var i = 0; i < arr02.length; i++) {
    console.log(arr02[i]);
}

// 7.弹出数组中最后一个元素
var ele = arr01.pop();
console.log(ele);

使用[]创建数组

// 8.使用[]创建数组
var arr03 = ["cat","dog","tiger"];
console.log(arr03);

6、JSON

JSON格式的用途

在开发中凡是涉及到*『跨平台数据传输』*,JSON格式一定是首选。

JSON格式的说明

  • JSON数据两端要么是*{}*,要么是**[]**
  • **{}**定义JSON对象
  • **[]**定义JSON数组
  • JSON对象的格式是:
{key:value,key:value,...,key:value}
  • JOSN数组的格式是:
[value,value,...,value]
  • key的类型固定是字符串
  • value的类型可以是:
    • 基本数据类型
    • 引用类型:JSON对象或JSON数组

正因为JSON格式中value部分还可以继续使用JSON对象或JSON数组,所以JSON格式是可以**『多层嵌套』**的,所以JSON格式不论多么复杂的数据类型都可以表达。

{
    "stuId":556,
    "stuName":"carl",
    "school":{
        "schoolId":339,
        "schoolName":"atguigu"
    },
    "subjectList":[
        {
            "subjectName":"java",
            "subjectScore":50
        },
        {
            "subjectName":"PHP",
            "subjectScore":35
        },
        {
            "subjectName":"python",
            "subjectScore":24
        }
    ],
    "teacherMap":{
        "aaa":{
            "teacherName":"zhangsan",
            "teacherAge":20
        },
        "bbb":{
            "teacherName":"zhangsanfeng",
            "teacherAge":108
        },
        "ccc":{
            "teacherName":"zhangwuji",
            "teacherAge":25
        }
    }
}

JSON对象和JSON字符串互转

JSON对象转JSON字符串
var jsonObj = {"stuName":"tom","stuAge":20};
var jsonStr = JSON.stringify(jsonObj);

console.log(typeof jsonObj); // object
console.log(typeof jsonStr); // string
JSON字符串转JSON对象
jsonObj = JSON.parse(jsonStr);
console.log(jsonObj); // {stuName: "tom", stuAge: 20}

四、DOM

1、概念

名词解释

DOM是Document Object Model的缩写,意思是*『文档对象模型』*——将HTML文档抽象成模型,再封装成对象方便用程序操作。

这是一种非常常用的编程思想:将现实世界的事物抽象成模型,这样就非常容易使用对象来量化的描述现实事物,从而把生活中的问题转化成一个程序问题,最终实现用应用软件协助解决现实问题。而在这其中**『模型』**就是那个连通现实世界和代码世界的桥梁。

DOM树

浏览器把HTML文档从服务器上下载下来之后就开始按照**『从上到下』的顺序『读取HTML标签』。每一个标签都会被封装成一个『对象』**。

而第一个读取到的肯定是根标签html,然后是它的子标签head,再然后是head标签里的子标签……所以从html标签开始,整个文档中的所有标签都会根据它们之间的**『父子关系』被放到一个『树形结构』**的对象中。

这个包含了所有标签对象的整个树形结构对象就是JavaScript中的一个可以直接使用的内置对象document

例如,下面的标签结构:

会被解析为:

2、具体概念

各个组成部分的类型

整个文档中的一切都可以看做Node。各个具体组成部分的具体类型可以看做Node类型的子类。

其实严格来说,JavaScript并不支持真正意义上的『继承』,这里我们借用Java中的『继承』概念,从逻辑上来帮助我们理解各个类型之间的关系。

组成部分 节点类型 具体类型
整个文档 文档节点 Document
HTML标签 元素节点 Element
HTML标签内的文本 文本节点 Text
HTML标签内的属性 属性节点 Attr
注释 注释节点 Comment

父子关系

先辈后代关系

3、DOM操作

由于实际开发时基本上都是使用JavaScript的各种框架来操作,而框架中的操作方式和我们现在看到的原生操作完全不同,所以下面罗列的API仅供参考,不做要求。

在整个文档范围内查询元素节点

功能 API 返回值
根据id值查询 document.getElementById(“id值”) 一个具体的元素节
根据标签名查询 document.getElementsByTagName(“标签名”) 元素节点数组
根据name属性值查询 document.getElementsByName(“name值”) 元素节点数组

在具体元素节点范围内查找子节点

功能 API 返回值
查找全部子节点 element.childNodes 【W3C考虑换行,IE≤8不考虑】 节点数组
查找第一个子节点 element.firstChild 【W3C考虑换行,IE≤8不考虑】 节点对象
查找最后一个子节点 element.lastChild 【W3C考虑换行,IE≤8不考虑】 节点对象

查找指定元素节点的父节点

功能 API 返回值
查找指定元素节点的父节点 element.parentNode 节点对象

查找指定元素节点的兄弟节点

功能 API 返回值
查找前一个兄弟节点 node.previousSibling 【W3C考虑换行,IE≤8不考虑】 节点对象
查找后一个兄弟节点 node.nextSibling 【W3C考虑换行,IE≤8不考虑】 节点对象

属性操作

需求 操作方式
读取属性值 元素对象.属性名
修改属性值 元素对象.属性名=新的属性值

文本操作

需求 操作方式
读取文本值 element.firstChild.nodeValue
修改文本值 element.firstChild.nodeValue=新文本值

DOM增删改操作

API 功能
document.createElement(“标签名”) 创建元素节点并返回,但不会自动添加到文档中
document.createTextNode(“文本值”) 创建文本节点并返回,但不会自动添加到文档中
element.appendChild(ele) 将ele添加到element所有子节点后面
parentEle.insertBefore(newEle,targetEle) 将newEle插入到targetEle前面
parentEle.replaceChild(newEle, oldEle) 用新节点替换原有的旧子节点
parentEle.removeChild(childNode) 删除指定的子节点
element.innerHTML 读写HTML代码

五、 JavaScript事件驱动

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <style type="text/css">
            #eventArea {
                border: 1px solid black;
                width: 100px;
                height: 100px;
            }
        </style>
    </head>
    <body>
        <!-- 用div作为鼠标移动区域 -->
        <div id="eventArea"></div>

        <!-- 在p标签内显示鼠标坐标 -->
        <p id="showData"></p>
    </body>
    <script type="text/javascript">

        // 根据id找到div标签对应的元素对象
        var divEle = document.getElementById("eventArea");

        // 根据id找到p标签对应的元素对象
        var pEle = document.getElementById("showData");

        // 声明事件响应函数
        function whenMouseMove(event){
            pEle.innerText = event.clientX + " " + event.clientY;
        }

        // 将事件响应函数赋值给对应的事件属性        
        // onmousemove表示在鼠标移动的时候
        divEle.onmousemove = whenMouseMove;
    </script>
</html>

下面是简化的代码:

document.getElementById("eventArea").onmousemove = function (event){
    document.getElementById("showData").innerText = event.clientX + " " + event.clientY;
};

06、Vue.js

一、Vue.js简介

1、框架

任何编程语言在最初的时候都是没有框架的,后来随着在实际开发过程中不断总结*『经验』*,积累*『最佳实践』*,慢慢的人们发现很多**『特定场景』下的『特定问题』总是可以『套用固定解决方案』**。

于是有人把成熟的**『固定解决方案』收集起来,整合在一起,就成了『框架』**。

在使用框架的过程中,我们往往只需要告诉框架*『做什么(声明)』*,而不需要关心框架*『怎么做(编程)』*

对于Java程序来说,我们使用框架就是导入那些封装了**『固定解决方案』的jar包,然后通过『配置文件』**告诉框架做什么,就能够大大简化编码,提高开发效率。我们使用过的junit其实就是一款单元测试框架。

而对于JavaScript程序来说,我们使用框架就是导入那些封装了**『固定解决方案』『js文件』**,然后在框架的基础上编码。

用洗衣服来类比框架:

典型应用场景:洗衣服

输入数据:衣服、洗衣液、水

不使用框架:手洗

使用框架:使用洗衣机,对人来说,只需要按键,具体操作是洗衣机完成的。人只是告诉洗衣机做什么,具体的操作是洗衣机完成的。

实际开发中使用框架时,我们也主要是告诉框架要做什么,具体操作是框架完成的。

2、Vue.js

Vue.js的作者

在为AngularJS工作之后,Vue的作者尤雨溪开Vue.js。他声称自己的思路是提取Angular中自己喜欢的部分,构建出一款相当轻量的框架。

Vue最早发布于2014年2月。作者在Hacker News、Echo JS与 Reddit的JavaScript版块发布了最早的版本。一天之内,Vue 就登上了这三个网站的首页。

Vue是Github上最受欢迎的开源项目之一。同时,在JavaScript框架/函数库中, Vue所获得的星标数已超过React,并高于Backbone.js、Angular 2、jQuery等项目。

Vue.js的官网介绍

Vue (读音 /vjuː/,类似于view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链 (opens new window)以及各种支持类库 (opens new window)结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

官网地址:https://cn.vuejs.org/

二、准备Vue.js环境

1、开发中的最佳实践

『最佳实践』是实际开发中,针对特定问题提炼出来的最好的解决方案。把『最佳实践』抽取出来,封装到各自编程语言的程序包中,就是框架的基础。

  • Java语言的程序包:jar包
  • JavaScript语言的程序包:外部js文件

对于Java程序来说,框架=jar包+配置文件。对于Vue来说,导入Vue的外部js文件就能够使用Vue框架了。

2、Vue框架的js文件获取

官网提供的下载地址:https://cdn.jsdelivr.net/npm/vue/dist/vue.js

3、本地创建vue.js文件

第一步:在HBuilderX中创建工程

第二步:在工程目录下创建script目录用来存放vue.js文件

第三步:创建空vue.js文件

第四步:将官网提供的vue.js文件的内容复制粘贴到本地vue.js文件中

4、创建HTML文档并引入vue.js

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
    </body>
    <script src="/pro03-vue/script/vue.js" type="text/javascript" charset="utf-8"></script>
    <script type="text/javascript">

    </script>
</html>

三、Vue.js基本语法:声明式渲染

1、概念

声明式

**『声明式』是相对于『编程式』**而言的。

  • 声明式:告诉框架做什么,具体操作由框架完成
  • 编程式:自己编写代码完成具体操作

渲染

上图含义解释:

  • 蓝色方框:HTML标签
  • 红色圆形:动态、尚未确定的数据
  • 蓝色圆形:经过程序运算以后,计算得到的具体的,可以直接在页面上显示的数据、
  • 渲染:程序计算动态数据得到具体数据的过程

2、demo

HTML代码

<!-- 使用{
    
    {}}格式,指定要被渲染的数据 -->
        <div id="app">{
    
    {message}}</div>

vue代码

// 1.创建一个JSON对象,作为new Vue时要使用的参数
var argumentJson = {

    // el用于指定Vue对象要关联的HTML元素。el就是element的缩写
    // 通过id属性值指定HTML元素时,语法格式是:#id
    "el":"#app",

    // data属性设置了Vue对象中保存的数据
    "data":{
        "message":"Hello Vue!"
    }
};

// 2.创建Vue对象,传入上面准备好的参数
var app = new Vue(argumentJson);

3、查看声明式渲染的响应式效果

通过验证Vue对象的『响应式』效果,我们看到Vue对象和页面上的HTML标签确实是始终保持着关联的关系,同时看到Vue在背后确实是做了大量的工作。

四、Vue.js基本语法:绑定元素属性

1、基本语法

v-bind:HTML标签的原始属性名

2、demo

HTML代码

<div id="app">
    <!-- v-bind:value表示将value属性交给Vue来进行管理,也就是绑定到Vue对象 -->
    <!-- vueValue是一个用来渲染属性值的表达式,相当于标签体中加{
    
    {}}的表达式 -->
    <input type="text" v-bind:value="vueValue" />

    <!-- 同样的表达式,在标签体内通过{
    
    {}}告诉Vue这里需要渲染; -->
    <!-- 在HTML标签的属性中,通过v-bind:属性名="表达式"的方式告诉Vue这里要渲染 -->
    <p>{
    
    {vueValue}}</p>
</div>

Vue代码

// 创建Vue对象,挂载#app这个div标签
var app = new Vue({
    "el":"#app",
    "data":{
        "vueValue":"太阳当空照"
    }
});

3、小结

本质上,v-bind:属性名="表达式"它们都是用Vue对象来渲染页面。只不过:

  • 文本标签体:使用形式
  • 属性:使用v-bind:属性名="表达式"形式

五、Vue.js基本语法:双向数据绑定

1、提出问题

而使用了双向绑定后,就可以实现:页面上数据被修改后,Vue对象中的数据属性也跟着被修改。

2、demo

HTML代码

<div id="app">
    <!-- v-bind:属性名 效果是从Vue对象渲染到页面 -->
    <!-- v-model:属性名 效果不仅是从Vue对象渲染到页面,而且能够在页面上数据修改后反向修改Vue对象中的数据属性 -->
    <input type="text" v-model:value="vueValue" />

    <p>{
    
    {vueValue}}</p>
</div>

Vue代码

// 创建Vue对象,挂载#app这个div标签
var app = new Vue({
    "el":"#app",
    "data":{
        "vueValue":"太阳当空照"
    }
});

页面效果

p标签内的数据能够和文本框中的数据实现同步修改:

3、去除前后空格

:value可以省略

<input type="text" v-model="vueValue" />

.trim修饰符

实际开发中,要考虑到用户在输入数据时,有可能会包含前后空格。而这些前后的空格对我们程序运行来说都是干扰因素,要去掉。在v-model后面加上.trim修饰符即可实现。

<input type="text" v-model.trim="vueValue" />

Vue会帮助我们在文本框失去焦点时自动去除前后空格。

六、Vue.js基本语法:条件渲染

根据Vue对象中,数据属性的值来判断是否对HTML页面内容进行渲染。

1、v-if

HTML代码

<div id="app">
    <h3>if</h3>
    <img v-if="flag" src="/pro03-vue/./images/one.jpg" />
    <img v-if="!flag" src="/pro03-vue/./images/two.jpg" />
</div>

Vue代码

var app = new Vue({
            "el":"#app",
            "data":{
                "flag":true
            }
        });

2、v-if和v-else

HTML代码

<div id="app02">
    <h3>if/else</h3>
    <img v-if="flag" src="/pro03-vue/./images/one.jpg" />
    <img v-else="flag" src="/pro03-vue/./images/two.jpg" />
</div>

Vue代码

var app02 = new Vue({
            "el":"#app02",
            "data":{
                "flag":true
            }
        });

3、v-show

HTML代码

<div id="app03">
    <h3>v-show</h3>
    <img v-show="flag" src="/pro03-vue/./images/mi.jpg" />
</div>

Vue代码

var app03 = new Vue({
            "el":"#app03",
            "data":{
                "flag":true
            }
        });

七、Vue.js基本语法:列表渲染

1、迭代一个简单的数组

HTML代码

<div id="app01">
    <ul>
        <!-- 使用v-for语法遍历数组 -->
        <!-- v-for的值是语法格式是:引用数组元素的变量名 in Vue对象中的数组属性名 -->
        <!-- 在文本标签体中使用{
    
    {引用数组元素的变量名}}渲染每一个数组元素 -->
        <li v-for="fruit in fruitList">{
    
    {fruit}}</li>
    </ul>
</div>

Vue代码

var app01 = new Vue({
    "el":"#app01",
    "data":{
        "fruitList": [
            "apple",
            "banana",
            "orange",
            "grape",
            "dragonfruit"
        ]
    }
});

2、迭代一个对象数组

HTML代码

<div id="app">
    <table>
        <tr>
            <th>编号</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>专业</th>
        </tr>
        <tr v-for="employee in employeeList">
            <td>{
    
    {employee.empId}}</td>
            <td>{
    
    {employee.empName}}</td>
            <td>{
    
    {employee.empAge}}</td>
            <td>{
    
    {employee.empSubject}}</td>
        </tr>
    </table>
</div>

Vue代码

var app = new Vue({
    "el":"#app",
    "data":{
        "employeeList":[
            {
                "empId":11,
                "empName":"tom",
                "empAge":111,
                "empSubject":"java"
            },
            {
                "empId":22,
                "empName":"jerry",
                "empAge":222,
                "empSubject":"php"
            },
            {
                "empId":33,
                "empName":"bob",
                "empAge":333,
                "empSubject":"python"
            }
        ]
    }
});

八、Vue.js基本语法:事件驱动

1、demo:字符串顺序反转

HTML代码

<div id="app">
    <p>{
    
    {message}}</p>

    <!-- v-on:事件类型="事件响应函数的函数名" -->
    <button v-on:click="reverseMessage">Click me,reverse message</button>
</div>

Vue代码

var app = new Vue({
    "el":"#app",
    "data":{
        "message":"Hello Vue!"                
    },
    "methods":{
        "reverseMessage":function(){
            this.message = this.message.split("").reverse().join("");
        }
    }
});

2、demo:获取鼠标移动时的坐标信息

HTML代码

<div id="app">
    <div id="area" v-on:mousemove="recordPosition"></div>
    <p id="showPosition">{
    
    {position}}</p>
</div>

Vue代码

var app = new Vue({
    "el":"#app",
    "data":{
        "position":"暂时没有获取到鼠标的位置信息"
    },
    "methods":{
        "recordPosition":function(event){
            this.position = event.clientX + " " + event.clientY;
        }
    }
});

九、Vue.js基本语法:侦听属性

1、提出需求

<div id="app">
    <p>尊姓:{
    
    {firstName}}</p>
    <p>大名:{
    
    {lastName}}</p>
    尊姓:<input type="text" v-model="firstName" /><br/>
    大名:<input type="text" v-model="lastName" /><br/>
    <p>全名:{
    
    {fullName}}</p>
</div>

在上面代码的基础上,我们希望firstName或lastName属性发生变化时,修改fullName属性。此时需要对firstName或lastName属性进行*『侦听』*

具体来说,所谓**『侦听』**就是对message属性进行监控,当firstName或lastName属性的值发生变化时,调用我们准备好的函数。

2、Vue代码

在watch属性中声明对firstName和lastName属性进行**『侦听』**的函数:

var app = new Vue({
    "el":"#app",
    "data":{
        "firstName":"jim",
        "lastName":"green",
        "fullName":"jim green"
    },
    "watch":{
        "firstName":function(inputValue){
            this.fullName = inputValue + " " + this.lastName;
        },
        "lastName":function(inputValue){
            this.fullName = this.firstName + " " + inputValue;
        }
    }
});

十、Vue.js基本语法:简化写法

1、v-bind的简化写法

正常写法:

<input type="text" v-bind:value="message" />

简化以后:

<input type="text" :value="message" />

2、v-on的简化写法

正常写法:

<button v-on:click="sayHello">SayHello</button>

简化以后:

<button @click="sayHello">SayHello</button>

十一、练习

1、功能效果演示

2、任务拆解

  • 第一步:显示表格
  • 第二步:显示四个文本框
  • 第三步:创建一个p标签用来显示用户在文本框中实时输入的内容
  • 第四步:点击添加记录按钮实现记录的添加

3、第一步:显示表格

HTML标签

<table id="appTable">
    <tr>
        <th>编号</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>专业</th>
    </tr>
    <tr v-for="employee in employeeList">
        <td>{
    
    {employee.empId}}</td>
        <td>{
    
    {employee.empName}}</td>
        <td>{
    
    {employee.empAge}}</td>
        <td>{
    
    {employee.empSubject}}</td>
    </tr>
</table>

Vue代码

var appTable = new Vue({
    "el": "#appTable",
    "data": {
        "employeeList": [{
                "empId": 1,
                "empName": "tom",
                "empAge": 11,
                "empSubject": "java"
            },
            {
                "empId": 2,
                "empName": "jerry",
                "empAge": 22,
                "empSubject": "php"
            },
            {
                "empId": 3,
                "empName": "peter",
                "empAge": 33,
                "empSubject": "python"
            }
        ]
    }
});

4、第二步:显示四个文本框

HTML标签

<!-- 四个文本框、显示收集到的文本框数据的p标签、按钮这三个部分需要共享数据,所以要放在同一个app中 -->
<div id="appDiv">

    <!-- 第一部分:四个文本框 -->
    编号:<input type="text" v-model="empId" /><br/>
    姓名:<input type="text" v-model="empName" /><br/>
    年龄:<input type="text" v-model="empAge" /><br/>
    专业:<input type="text" v-model="empSubject" /><br/>

    <!-- 第二部分:显示收集到的文本框数据的p标签 -->

    <!-- 第三部分:按钮 -->

</div>

Vue代码

var appDiv = new Vue({
    "el":"#appDiv",
    "data":{
        // 初始值设置空字符串即可
        "empId":"",
        "empName":"",
        "empAge":"",
        "empSubject":""
    }
});

测试是否正确的方式是:在控制台尝试修改Vue对象的数据属性值:

5、第三步:创建一个p标签

HTML标签:

<!-- 四个文本框、显示收集到的文本框数据的p标签、按钮这三个部分需要共享数据,所以要放在同一个app中 -->
<div id="appDiv">

    <!-- 第一部分:四个文本框 -->
    编号:<input type="text" v-model="empId" placeholder="请输入编号" /><br/>
    姓名:<input type="text" v-model="empName" placeholder="请输入姓名" /><br/>
    年龄:<input type="text" v-model="empAge" placeholder="请输入年龄" /><br/>
    专业:<input type="text" v-model="empSubject" placeholder="请输入专业" /><br/>

    <!-- 第二部分:显示收集到的文本框数据的p标签 -->
    <p>{
    
    {empId}} {
    
    {empName}} {
    
    {empAge}} {
    
    {empSubject}}</p>

    <!-- 第三部分:按钮 -->

</div>

6、第四步:点击添加记录按钮

第一小步:给按钮设置事件驱动

HTML标签
<!-- 四个文本框、显示收集到的文本框数据的p标签、按钮这三个部分需要共享数据,所以要放在同一个app中 -->
<div id="appDiv">

    <!-- 第一部分:四个文本框 -->
    编号:<input type="text" v-model="empId" placeholder="请输入编号" /><br/>
    姓名:<input type="text" v-model="empName" placeholder="请输入姓名" /><br/>
    年龄:<input type="text" v-model="empAge" placeholder="请输入年龄" /><br/>
    专业:<input type="text" v-model="empSubject" placeholder="请输入专业" /><br/>

    <!-- 第二部分:显示收集到的文本框数据的p标签 -->
    <p>{
    
    {empId}} {
    
    {empName}} {
    
    {empAge}} {
    
    {empSubject}}</p>

    <!-- 第三部分:按钮 -->
    <button type="button" v-on:click="addRecord">添加记录</button>

</div>
Vue代码
var appDiv = new Vue({
    "el":"#appDiv",
    "data":{
        // 初始值设置空字符串即可
        "empId":"",
        "empName":"",
        "empAge":"",
        "empSubject":""
    },
    "methods":{
        "addRecord":function(){
            console.log("我点击了那个按钮……");
        }
    }
});

第二小步:打印一下文本框输入的数据

var appDiv = new Vue({
    "el":"#appDiv",
    "data":{
        // 初始值设置空字符串即可
        "empId":"",
        "empName":"",
        "empAge":"",
        "empSubject":""
    },
    "methods":{
        "addRecord":function(){
            console.log("我点击了那个按钮……");
            console.log(this.empId);
            console.log(this.empName);
            console.log(this.empAge);
            console.log(this.empSubject);
        }
    }
});

第三小步:将收集到的数据添加到表格中

"addRecord":function(){

    // 确认单击事件是否绑定成功
    console.log("我点击了那个按钮……");

    // 确认是否能够正确收集到文本框数据
    console.log(this.empId);
    console.log(this.empName);
    console.log(this.empAge);
    console.log(this.empSubject);

    // 将收集到的文本框数据封装为一个对象
    var employee = {
        "empId":this.empId,
        "empName":this.empName,
        "empAge":this.empAge,
        "empSubject":this.empSubject
    };

    // 将上面的对象压入表格数据的employeeList数组
    appTable.employeeList.push(employee);
}

十二、Vue对象生命周期

1、概念

在我们各种语言的编程领域中,**『生命周期』**都是一个非常常见的概念。一个对象从创建、初始化、工作再到释放、清理和销毁,会经历很多环节的演变。比如我们在JavaSE阶段学习过线程的生命周期,今天学习Vue对象的生命周期,将来还要学习Servlet、Filter等Web组件的生命周期

2、Vue对象的生命周期

3、生命周期钩子函数

Vue允许我们在特定的生命周期环节中通过钩子函数来加入我们的代码。

<div id="app">
    <p id="content">{
    
    {message}}</p>
    <button @click="changeValue">点我</button>
</div>
new Vue({
    "el":"#app",
    "data":{
        "message":"hello"
    },
    "methods":{
        "changeValue":function(){
            this.message = "new hello";
        }
    },

    // 1.实例创建之前
    "beforeCreate":function(){
        console.log("beforeCreate:"+this.message);
    },

    // 2.实例创建完成
    "created":function(){
        console.log("created:"+this.message);
    },

    // 3.数据挂载前
    "beforeMount":function(){
        console.log("beforeMount:"+document.getElementById("content").innerText);
    },

    // 4.数据已经挂载
    "mounted":function(){
        console.log("mounted:"+document.getElementById("content").innerText);
    },

    // 5.数据更新前
    "beforeUpdate":function(){
        console.log("beforeUpdate:"+document.getElementById("content").innerText);
    },

    // 6.数据更新之后
    "updated":function(){
        console.log("updated:"+document.getElementById("content").innerText);
    }
});

07、书城项目第一阶段

一、事件驱动补充

1、取消控件的默认行为

控件默认行为

  • 点超链接会跳转页面
  • 点表单提交按钮会提交表单

本来控件的默认行为是天经地义就该如此的,但是如果我们希望点击之后根据我们判断的结果再看是否要跳转,此时默认行为无脑跳转的做法就不符合我们的预期了。

取消方式

调用事件对象的**preventDefault()**方法。

超链接举例

HTML代码:

<a id="anchor" href="http://www.baidu.com">超链接</a>

JavaScript代码:

document.getElementById("anchor").onclick = function() {
    console.log("我点击了一个超链接");
    event.preventDefault();
}
表单提交按钮举例

HTML代码:

<form action="http://www.baidu.com" method="post">
    <button id="submitBtn" type="submit">提交表单</button>
</form>

JavaScript代码:

document.getElementById("submitBtn").onclick = function() {
    console.log("我点击了一个表单提交按钮");
    event.preventDefault();
}

2、阻止事件冒泡

图中的两个div,他们的HTML标签是:

<div id="outterDiv">
    <div id="innerDiv"></div>
</div>

点击里面的div同时也等于点击了外层的div,此时如果两个div上都绑定了单击响应函数那么就都会被触发:

document.getElementById("outterDiv").onclick = function() {
    console.log("外层div的事件触发了");
}

document.getElementById("innerDiv").onclick = function() {
    console.log("内层div的事件触发了");
}

所以事件冒泡就是一个事件会不断向父元素传递,直到window对象。

如果这不是我们想要的效果那么可以使用事件对象的**stopPropagation()**函数阻止。

document.getElementById("innerDiv").onclick = function() {
    console.log("内层div的事件触发了");

    event.stopPropagation();
}

3、Vue事件修饰符

对于事件修饰符,Vue官网的描述是:

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节

取消控件的默认行为

控件的默认行为指的是:

  • 点击超链接跳转页面
  • 点击表单提交按钮提交表单

实现这个需求使用的Vue事件修饰符是:.prevent

<a href="http://www.baidu.com" @click.prevent="clickAnchor">超链接</a>

<form action="http://www.baidu.com" method="post">
    <button type="submit" @click.prevent="clickSubmitBtn">提交表单</button>
</form>

取消事件冒泡

实现这个需求使用的Vue事件修饰符是:.stop

<div id="outterDiv" @click="clickOutterDiv">
    <div id="innerDiv" @click.stop="clickInnerDiv"></div>
</div>

二、正则表达式

1、从凤姐的择偶标准说起

本人对伴侣要求如下:

  • 第一、必须为北京大学或清华大学硕士毕业生。必须本科硕士连读,中途无跳级,不留级,不转校。在外参加工作后再回校读书者免。
  • 第二、必须为经济学专业毕业。非经济学专业毕业则必须精通经济学。或对经济学有浓厚的兴趣。
  • 第三、必须具备国际视野,但是无长期定居国外甚至移民的打算。
  • 第四、身高176--183左右。长得越帅越好。
  • 第五、无生育史。过往所有女友均无因自身而致的堕胎史。
  • 第六、东部户籍,即江、浙、沪三地户籍或黑龙江、广东、天津、山东、北京、吉林、辽宁等。
  • 东北三省和内蒙古等地户籍,西南地区即重庆、贵州、云南、西藏和湖南、湖北等地籍贯者不予考虑。
  • 第七、年龄25--28岁左右。即06届,07届,08届,09届毕业生。有一至两年的工作经验,06级毕业生需年龄在28岁左右,09级毕业生则需聪明过人。且具备丰富的社会实践经验。就职于国家机关,国有企事业单位者不愿考虑。但就职于中石油,中石化等世界顶尖型企业或银行者又比较喜欢。现自主创业者要商榷一番了。

2、标准在手,世界我有

模式验证

使用标准衡量一位具体的男士,返回一个布尔值,从而知道这位男士是否满足自己的标准——相当于我们使用正则表达式验证一个字符串是否满足规则。比如验证一个字符串是否是一个身份证号。

匹配读取

对全中国的男士应用这个标准,返回一个数组,遍历这个数组,可以得到所有符合标准的男士——相当于我们使用正则表达式获取一段文本中匹配的子字符串。比如将一篇文章中的电子邮件地址读取出来。

匹配替换

对全中国的男士应用这个标准,把其中已婚的变成未婚,这样凤姐就有机会了——相当于我们使用正则表达式替换所有匹配的部分。比如将一段文字中的”HelloWorld”替换为”HelloJava”。

花絮:

记者:封老师您好!由于您的名字『封捷』和『凤姐』谐音,同学们总是以此来调侃您,说您是尚硅谷『凤姐』,对此您有什么想说的吗?

封老师:太过分了!我咋能和人家比!

记者:呃……太意外了,您的意思是?

封老师:虽然过气了,但人家好歹也是网红呀!

3、正则表达式的概念

使用一段字符串定义的一个规则,用以检测某个字符串是否满足这个规则,或将目标字符串中满足规则的部分读取出来,又或者将目标字符串中满足标准的部分替换为其他字符串。所以正则表达式有三个主要用途:

  • 模式验证
  • 匹配读取
  • 匹配替换

4、正则表达式零起步

创建正则表达式对象

使用两个斜杠
// 类似创建数组时可以使用[]、创建对象可以使用{}
var reg = /a/;
使用new关键字创建RegExp类型的对象
// 类似创建数组可以new Array()、创建对象可以使用new Object()
var reg = new RegExp("a");

正则表达式的组成

正则表达式本身也是一个字符串,它由两种字符组成:

  • 普通字符,例如大、小写英文字母;数字等。
  • 元字符:被系统赋予特殊含义的字符。例如:^表示以某个字符串开始,$表示以某个字符串结束。

正则表达式初体验

模式验证

注意:这里是使用正则表达式对象调用方法。

// 创建一个最简单的正则表达式对象
var reg = /o/;

// 创建一个字符串对象作为目标字符串
var str = 'Hello World!';

// 调用正则表达式对象的test()方法验证目标字符串是否满足我们指定的这个模式,返回结果true
console.log("/o/.test('Hello World!')="+reg.test(str));
匹配读取

注意:这里是使用字符串对象调用方法。

// 在目标字符串中查找匹配的字符,返回匹配结果组成的数组
var resultArr = str.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);

// 数组内容是o
console.log("resultArr[0]="+resultArr[0]);
替换

注意:这里是使用字符串对象调用方法。

var newStr = str.replace(reg,'@');
// 只有第一个o被替换了,说明我们这个正则表达式只能匹配第一个满足的字符串
console.log("str.replace(reg)="+newStr);//Hell@ World!

// 原字符串并没有变化,只是返回了一个新字符串
console.log("str="+str);//str=Hello World!

匹配方式

全文查找

如果不使用g对正则表达式对象进行修饰,则使用正则表达式进行查找时,仅返回第一个匹配;使用g后,返回所有匹配。

// 目标字符串
var targetStr = 'Hello World!';

// 没有使用全局匹配的正则表达式
var reg = /[A-Z]/;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为1
console.log("resultArr.length="+resultArr.length);

// 遍历数组,发现只能得到'H'
for(var i = 0; i < resultArr.length; i++){
    console.log("resultArr["+i+"]="+resultArr[i]);
}

对比代码:

// 目标字符串
var targetStr = 'Hello World!';

// 使用了全局匹配的正则表达式
var reg = /[A-Z]/g;
// 获取全部匹配
var resultArr = targetStr.match(reg);
// 数组长度为2
console.log("resultArr.length="+resultArr.length);

// 遍历数组,发现可以获取到“H”和“W”
for(var i = 0; i < resultArr.length; i++){
    console.log("resultArr["+i+"]="+resultArr[i]);
}
忽略大小写
//目标字符串
var targetStr = 'Hello WORLD!';

//没有使用忽略大小写的正则表达式
var reg = /o/g;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为1
console.log("resultArr.length="+resultArr.length);
//遍历数组,仅得到'o'
for(var i = 0; i < resultArr.length; i++){
    console.log("resultArr["+i+"]="+resultArr[i]);
}

对比代码:

//目标字符串
var targetStr = 'Hello WORLD!';

//使用了忽略大小写的正则表达式
var reg = /o/gi;
//获取全部匹配
var resultArr = targetStr.match(reg);
//数组长度为2
console.log("resultArr.length="+resultArr.length);
//遍历数组,得到'o'和'O'
for(var i = 0; i < resultArr.length; i++){
    console.log("resultArr["+i+"]="+resultArr[i]);
}
多行查找

不使用多行查找模式,目标字符串中不管有没有换行符都会被当作一行。

//目标字符串1
var targetStr01 = 'Hello\nWorld!';
//目标字符串2
var targetStr02 = 'Hello';

//匹配以'Hello'结尾的正则表达式,没有使用多行匹配
var reg = /Hello$/;
console.log(reg.test(targetStr01));//false

console.log(reg.test(targetStr02));//true

对比代码:

//目标字符串1
var targetStr01 = 'Hello\nWorld!';
//目标字符串2
var targetStr02 = 'Hello';

//匹配以'Hello'结尾的正则表达式,使用了多行匹配
var reg = /Hello$/m;
console.log(reg.test(targetStr01));//true

console.log(reg.test(targetStr02));//true

5、元字符

概念

在正则表达式中被赋予特殊含义的字符,不能被直接当做普通字符使用。如果要匹配元字符本身,需要对元字符进行转义,转义的方式是在元字符前面加上“\”,例如:^

常用元字符

代码 说明
. 匹配除换行字符以外的任意字符。
\w 匹配字母或数字或下划线等价于[a-zA-Z0-9_]
\W 匹配任何非单词字符。等价于[^A-Za-z0-9_]
\s 匹配任意的空白符,包括空格、制表符、换页符等等。等价于[\f\n\r\t\v]。
\S 匹配任何非空白字符。等价于[^\f\n\r\t\v]。
\d 匹配数字。等价于[0-9]。
\D 匹配一个非数字字符。等价于[^0-9]
\b 匹配单词的开始或结束
^ 匹配字符串的开始,但在[]中使用表示取反
$ 匹配字符串的结束
例1
var str = 'one two three four';
// 匹配全部空格
var reg = /\s/g;
// 将空格替换为@
var newStr = str.replace(reg,'@'); // one@two@three@four
console.log("newStr="+newStr);
例2
var str = '今年是2014年';
// 匹配至少一个数字
var reg = /\d+/g;
str = str.replace(reg,'abcd');
console.log('str='+str); // 今年是abcd年
例3
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java开头
var reg = /^Java/g;
console.log('reg.test(str01)='+reg.test(str01)); // flase
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // true
例4
var str01 = 'I love Java';
var str02 = 'Java love me';
// 匹配以Java结尾
var reg = /Java$/g;
console.log('reg.test(str01)='+reg.test(str01)); // true
console.log("<br />");
console.log('reg.test(str02)='+reg.test(str02)); // flase

6、字符集合

语法格式 示例 说明
[字符列表] 正则表达式:[abc] 含义:目标字符串包含abc中的任何一个字符 目标字符串:plain 是否匹配:是 原因:plain中的“a”在列表“abc”中 目标字符串中任何一个字符出现在字符列表中就算匹配。
[^字符列表] [^abc] 含义:目标字符串包含abc以外的任何一个字符 目标字符串:plain 是否匹配:是 原因:plain中包含“p”、“l”、“i”、“n” 匹配字符列表中未包含的任意字符。
[字符范围] 正则表达式:[a-z] 含义:所有小写英文字符组成的字符列表 正则表达式:[A-Z] 含义:所有大写英文字符组成的字符列表 匹配指定范围内的任意字符。
var str01 = 'Hello World';
var str02 = 'I am Tom';
//匹配abc中的任何一个
var reg = /[abc]/g;
console.log('reg.test(str01)='+reg.test(str01));//flase
console.log('reg.test(str02)='+reg.test(str02));//true

7、重复

代码 说明
* 重复零次或多次
+ 重复一次或多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或多次
{n,m} 重复n到m次
console.log("/[a]{3}/.test('aa')="+/[a]{3}/g.test('aa')); // flase
console.log("/[a]{3}/.test('aaa')="+/[a]{3}/g.test('aaa')); // true
console.log("/[a]{3}/.test('aaaa')="+/[a]{3}/g.test('aaaa')); // true

8、在正则表达式中表达『或者』

使用符号:|

// 目标字符串
var str01 = 'Hello World!';
var str02 = 'I love Java';
// 匹配'World'或'Java'
var reg = /World|Java/g;
console.log("str01.match(reg)[0]="+str01.match(reg)[0]);//World
console.log("str02.match(reg)[0]="+str02.match(reg)[0]);//Java

9、常用正则表达式

需求 正则表达式
用户名 /^[a-zA-Z_][a-zA-Z_-0-9]{5,9}$/
密码 /^[a-zA-Z0-9_-@#&*]{6,12}$/
前后空格 /^\s+\
电子邮箱 /^[a-zA-Z0-9_.-]+@([a-zA-Z0-9-]+[.]{1})+[a-zA-Z]+$/

三、第一阶段要实现的功能

01、准备工作

创建目录后,把一整套现成的前端页面复制到新建的目录下,然后用HBuilderX打开这个目录。然后把vue.js文件复制到script目录下。

02、登录页面的表单验证

规则设定

  • 用户名非空
  • 密码非空

在login.html页面中加入Vue的环境

</body>
    <script src="/bookstoreV01/static/script/vue.js" type="text/javascript" charset="utf-8"></script>
</html>

思路

代码实现

HTML代码
<form id="loginForm" action="login_success.html">
    <label>用户名称:</label>
    <input class="itxt" type="text" v-model:value="username" placeholder="请输入用户名" autocomplete="off" tabindex="1" name="username" id="username" />
    <br />
    <br />
    <label>用户密码:</label>
    <input class="itxt" type="password" v-model:value="password" placeholder="请输入密码" autocomplete="off" tabindex="1" name="password" id="password" />
    <br />
    <br />
    <button type="submit" id="sub_btn" @click="loginCheck">登录</button>
</form>
Vue代码
new Vue({
    "el":"#loginForm",
    "data":{
        "username":"",
        "password":""
    },
    "methods":{
        "loginCheck":function(){

            // 判断用户名或密码是否为空
            if(this.username == "" || this.password == "") {
                // 如果不满足验证条件,那么阻止表单提交
                event.preventDefault();
            }

        }
    }
});

2、注册页面的表单验证

HTML代码

<form id="registerForm" action="regist_success.html">
    <div class="form-item">
        <div>
            <label>用户名称:</label>
            <input v-model:value="username" type="text" placeholder="请输入用户名" />
            <span></span>
        </div>
        <span>{
    
    {usernameCheckMessage}}</span>
    </div>
    <div class="form-item">
        <div>
            <label>用户密码:</label>
            <input v-model:value="password" type="password" placeholder="请输入密码" />
        </div>
        <span class="errMess">密码的长度至少为8位</span>
    </div>
    <div class="form-item">
        <div>
            <label>确认密码:</label>
            <input v-model:value="passwordConfirm" type="password" placeholder="请输入确认密码" />
        </div>
        <span class="errMess">密码两次输入不一致</span>
    </div>
    <div class="form-item">
        <div>
            <label>用户邮箱:</label>
            <input v-model:value="email" type="text" placeholder="请输入邮箱" />
        </div>
        <span class="errMess">请输入正确的邮箱格式</span>
    </div>
    <div class="form-item">
        <div>
            <label>验证码:</label>
            <div class="verify">
                <input v-model:value="code" type="text" placeholder="" />
                <img src="../../static/img/code.bmp" alt="" />
            </div>
        </div>
        <span class="errMess">请输入正确的验证码</span>
    </div>
    <button type="submit" @click="registerCheck" class="btn">注册</button>
</form>

Vue代码

new Vue({
    "el":"#registerForm",
    "data":{
        "username":"",
        "password":"",
        "passwordConfirm":"",
        "email":"",
        "code":"",
        "usernameCheckMessage":""
    },
    "watch":{
        "username":function(inputValue){
            var usernameRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;

            if (usernameRegExp.test(this.username)) {
                this.usernameCheckMessage = "";                        
            }else{
                this.usernameCheckMessage = "用户名不符合规则";
            }
        }
    },
    "methods":{
        "registerCheck":function(){

            // 1.检查用户名
            var usernameRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;

            if (!usernameRegExp.test(this.username)) {

                // 如果不满足条件,则阻止表单提交
                event.preventDefault();

                // 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
                return ;
            }

            // 2.检查密码
            var passwordRegExp = /^[A-Z,a-z,0-9,_]{5,8}$/;

            if (!passwordRegExp.test(this.password)) {

                // 如果不满足条件,则阻止表单提交
                event.preventDefault();

                // 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
                return ;
            }

            // 3.检查确认密码是否和密码一致
            if (this.password != this.passwordConfirm) {

                // 如果不满足条件,则阻止表单提交
                event.preventDefault();

                // 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
                return ;

            }

            // 4.检查电子邮件
            var emailRegExp = /^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+[\.]{1})+[a-zA-Z]+$/;

            if (!emailRegExp.test(this.email)) {

                // 如果不满足条件,则阻止表单提交
                event.preventDefault();

                // 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
                return ;
            }

            // 5.检查验证码
            var codeRegExp = /[A-Z,a-z,0-9]{5}/;

            if(!codeRegExp.test(this.code)) {
                // 如果不满足条件,则阻止表单提交
                event.preventDefault();

                // 有任何一个条件不满足,后面就没必要检查了,所以函数可以停止执行
                return ;
            }
        }
    }
});

08、Tomcat

一、配置文件

1、配置

设置

所谓设置其实就是通过修改一个一个的参数,告诉应用软件它该怎么工作

配置

本质上配置和设置是一样的,只是对象和形式不同:

  配置 设置
对象 开发中使用的应用程序或框架 应用软件
形式 特定格式的配置文件 应用软件的友好界面

2、配置文件

XML

先来个例子:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 配置SpringMVC前端控制器 -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <!-- 在初始化参数中指定SpringMVC配置文件位置 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>

        <!-- 设置当前Servlet创建对象的时机是在Web应用启动时 -->
        <load-on-startup>1</load-on-startup>

    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>

        <!-- url-pattern配置斜杠表示匹配所有请求 -->
        <!-- 两种可选的配置方式:
                1、斜杠开头:/
                2、包含星号:*.atguigu
             不允许的配置方式:前面有斜杠,中间有星号
                /*.app
         -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
名词解释

XML是eXtensible Markup Language的缩写,翻译过来就是可扩展标记语言。所以很明显,XML和HTML一样都是标记语言,也就是说它们的基本语法都是标签。

可扩展

可扩展三个字表面上的意思是XML允许自定义格式。但是别美,这不代表可以随便写

在XML基本语法规范的基础上,你使用的那些第三方应用程序、框架会通过设计**『XML约束』的方式『强制规定』**配置文件中可以写什么和怎么写,规定之外的都不可以写。

XML基本语法这个知识点的定位是:我们不需要从零开始,从头到尾的一行一行编写XML文档,而是在第三方应用程序、框架已提供的配置文件的基础上修改。要改成什么样取决于你的需求,而怎么改取决于XML基本语法具体的XML约束

XML基本语法
  • XML声明

这部分基本上就是固定格式,大家知道就好

<?xml version="1.0" encoding="UTF-8"?>
  • 根标签

根标签有且只能有一个。

  • 标签关闭
    • 双标签:开始标签和结束标签必须成对出现。
    • 单标签:单标签在标签内关闭。
  • 标签嵌套
    • 可以嵌套,但是不能交叉嵌套。
  • 注释不能嵌套
  • 标签名、属性名建议使用小写字母
  • 属性
    • 属性必须有值
    • 属性值必须加引号,单双都行

看到这里大家一定会发现XML的基本语法和HTML的基本语法简直如出一辙。其实这不是偶然的,XML基本语法+HTML约束=HTML语法。在逻辑上HTML确实是XML的子集。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

从HTML4.01版本的文档类型声明中可以看出,这里使用的DTD类型的XML约束。也就是说http://www.w3.org/TR/html4/loose.dtd这个文件定义了HTML文档中可以写哪些标签,标签内可以写哪些属性,某个标签可以有什么样的子标签。

XML约束

将来我们主要就是根据XML约束中的规定来编写XML配置文件。而XML约束主要包括DTD和Schema两种。如果XML配置文件使用的是DTD,那么对我们几乎没有影响。如果是Schema约束,需要我们稍微参与一点点。

  • DTD

将来在IDEA中有代码提示的协助,在DTD文档的约束下进行配置非常简单。

<!ENTITY % fontstyle
 "TT | I | B | U | S | STRIKE | BIG | SMALL">

<!ENTITY % phrase "EM | STRONG | DFN | CODE |
                   SAMP | KBD | VAR | CITE | ABBR | ACRONYM" >

<!ENTITY % special
   "A | IMG | APPLET | OBJECT | FONT | BASEFONT | BR | SCRIPT |
    MAP | Q | SUB | SUP | SPAN | BDO | IFRAME">

<!ENTITY % formctrl "INPUT | SELECT | TEXTAREA | LABEL | BUTTON">

<!-- %inline; covers inline or "text-level" elements -->
<!ENTITY % inline "#PCDATA | %fontstyle; | %phrase; | %special; | %formctrl;">

<!ELEMENT (%fontstyle;|%phrase;) - - (%inline;)*>
<!ATTLIST (%fontstyle;|%phrase;)
  %attrs;                              -- %coreattrs, %i18n, %events --
  >
  • Schema

我们将来使用SSM框架中的Spring、SpringMVC框架时,会涉及到一点点对Schema约束的设置。不过不必紧张,有IDEA的支持操作会非常简单,我们现在只需要理解基本概念即可。

首先我们要理解一个概念:『名称空间』,英文:name space

Schema约束要求我们一个XML文档中,所有标签,所有属性都必须在约束中有明确的定义。

下面我们以web.xml的约束声明为例来做个说明:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
属性名 作用
xmlns 指出当前XML文档约束规则的名称空间在哪里 我们就是通过这个属性来引用一个具体的名称空间
xmlns:xsi 指出xmlns这个属性是在哪个约束文档中被定义的
xsi:schemaLocation 语法格式:在xsi名称空间下引用schemaLocation属性 配置含义:指定当前XML文档中所用到的约束文档本身的文件的地址

xmlns和xsi:schemaLocation对应关系如下图:

引入多个名称空间的例子如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <context:component-scan base-package="com.atguigu.crud.component"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <mvc:default-servlet-handler/>
    <mvc:annotation-driven/>

</beans>

看到这么复杂,这么长的名称空间字符串,我们会觉得很担心,根本记不住。但是其实不需要记,在IDEA中编写配置文件时,IDEA会协助我们导入,会有提示。我们理解各个部分的含义能够调整即可。

属性文件

以properties作为扩展名的文件。

yykk.jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis
yykk.jdbc.driver=com.mysql.jdbc.Driver
yykk.jdbc.username=root
yykk.jdbc.password=123456
  • 由键值对组成
  • 键和值之间的符号是等号
  • 每一行都必须顶格写,前面不能有空格之类的其他符号

也有人叫资源文件。

其他形式

YAML语言的配置文件:在SpringBoot中使用。

spring:
  profiles:
    active: fc
  datasource:
    name: mydb
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://192.168.41.100:3306/spring_boot?serverTimezone=UTC
    username: root
    password: atguigu
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: classpath*:/mybatis-mapper/*Mapper.xml
logging:
  level:
    com.atguigu.function.compute.mapper: debug

JSON格式的配置文件:一般是前端使用。

二、Tomcat的部署和启动

1、Tomcat扮演的角色

对外:Web服务器

对内:Servlet容器

2、部署

前提

Tomcat本身是一个Java程序,所以当前系统中必须正确配置了JAVA_HOME环境变量。我们可以通过下面的命令检测:

C:\Users\Administrator>java -version java version "1.8.0_141" Java(TM) SE Runtime Environment (build 1.8.0_141-b15) Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

C:\Users\Administrator>echo %JAVA_HOME% D:\software\Java

C:\Users\Administrator>echo %PATH% D:\software\xftp;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0;D:\software\Java\bin;D:\software\apache-maven-3.5.4\bin;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;

解压

将Tomcat压缩包解压到一个非中文无空格的目录下。

Tomcat目录结构

启动Tomcat并访问首页

启动Tomcat:运行Tomcat解压后根目录下\bin\startup.bat即可,例如:

D:\software\apache-tomcat-7.0.79\bin\startup.bat

如果需要停止Tomcat,则运行shutdown.bat程序,例如:

D:\software\apache-tomcat-7.0.79\bin\shutdown.bat

小提示:将来我们在IDEA中启动Tomcat,如果IDEA卡死强关,Tomcat不会正常退出。下次再启动Tomcat会因为残留进程仍然占用8080端口,导致新的进程无法启动。此时可以使用shutdown.bat结束残留进程。

部署一个war包并启动Tomcat

在Tomcat启动过程中,会打印启动日志,其中我们能看到hello.war被部署的日志信息:

信息: Deployment of web application archive D:\software\apache-tomcat-7.0.79\webapps\hello.war has finished in 1,150 ms

此时,我们回到webapps目录,能看到hello.war被解压了:

访问刚才部署的Web应用

关于Tomcat端口号

配置文件节选:

……
<!-- 第22行 -->
<Server port="8005" shutdown="SHUTDOWN">
……
<!-- 第71行 -->
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
……
<!-- 第93行 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

三个位置共定义了3个端口号,Tomcat启动后,这个三个端口号都会被占用。

三、在IDEA中关联Tomcat

四、创建动态Web工程并部署运行

1、第一步

创建empty project

2、第二步

以下操作基于IDEA2018.3

创建动态Web工程:

创建好的工程如下所示:

注意:src目录应该是蓝色的,web目录的图标上有个蓝色的圆点。

3、动态Web工程目录结构介绍

目录或文件名 功能
src目录 存放Java源文件
web目录 存放Web开发相关资源
web/WEB-INF目录 存放web.xml文件、classes目录、lib目录
web/WEB-INF/web.xml文件 别名:部署描述符deployment descriptor 作用:Web工程的核心配置文件
web/WEB-INF/classes目录 存放编译得到的*.class字节码文件
web/WEB-INF/lib目录 存放第三方jar包

4、创建用来部署Web工程的Tomcat实例

通常我们会觉得现在这个Application context太长了,改简短一些会更方便。

5、编写简单的测试代码

Java代码

public class Hello {

    public String getMessage() {
        return "年少不知软饭香";
    }

}

JSP代码

<%=new Hello().getMessage() %>

启动专门为这个工程创建的Tomcat实例

错误提示

提示信息的含义:未指定编译结果的保存目录。

错误原因:

  • 父工程只是一个empty project
  • 当前模块继承父工程的编译输出路径

为了解决这个问题我们可以在父工程中设置输出路径:

6、IDEA运行程序时界面布局

五、在IDEA中重新部署运行

1、为什么需要重新部署?

对于已经运行过的Web项目,如果我们增加了目录和文件,那么部署目录有可能不会自动同步过来,从而造成实际运行的效果和我们期望的不同。

如下图中,我们在工程目录已经新增了./images目录和两个图片:

但是在部署目录并没有出现:

哪怕我们执行了重新部署也没有起作用。

2、如何重新部署?

清理部署目录

构建

效果

六、导入别人的web module

如果你想把老师发给你的module导入自己的project中运行起来,可以参考下面的操作:

做下面操作前,需要把要导入的module复制到project目录下。

09、HTTP协议

1、介绍

HTTP:Hyper Text Transfer Protocol超文本传输协议。HTTP最大的作用就是确定了请求和响应数据的格式。浏览器发送给服务器的数据:请求报文;服务器返回给浏览器的数据:响应报文。

2、请求报文

在开发者工具中浏览报文源码

请求报文的三个部分

请求行

作用:展示当前请求的最基本信息

POST /dynamic/target.jsp HTTP/1.1

  • 请求方式
  • 访问地址
  • HTTP协议的版本

请求消息头

作用:通过具体的参数对本次请求进行详细的说明

格式:键值对,键和值之间使用冒号隔开

相对比较重要的请求消息头:

名称 功能
Host 服务器的主机地址
Accept 声明当前请求能够接受的『媒体类型』
Referer 当前请求来源页面的地址
Content-Length 请求体内容的长度
Content-Type 请求体的内容类型,这一项的具体值是媒体类型中的某一种
Cookie 浏览器访问服务器时携带的Cookie数据

请求体

作用:作为请求的主体,发送数据给服务器。具体来说其实就是POST请求方式下的请求参数。

格式:

form data

含义:当前请求体是一个表单提交的请求参数。

查看源码后,发现格式如下:

username=tom&password=123456

  • 每一组请求参数是一个键值对
  • 键和值中间是等号
  • 键值对之间是&号

Request Payload

含义:整个请求体以某种特定格式来组织数据,例如JSON格式。

3、请求方式

HTTP协议已定义的请求方式

HTTP1.1中共定义了八种请求方式:

  • GET:从服务器端获取数据
  • POST:将数据保存到服务器端
  • PUT:命令服务器对数据执行更新
  • DELETE:命令服务器删除数据
  • HEAD
  • CONNECT
  • OPTIONS
  • TRACE

GET请求

  • 特征1:没有请求体
  • 特征2:请求参数附着在URL地址后面
  • 特征3:请求参数在浏览器地址栏能够直接被看到,存在安全隐患
  • 特征4:在URL地址后面携带请求参数,数据容量非常有限。如果数据量大,那么超出容量的数据会丢失
  • 特征5:从报文角度分析,请求参数是在请求行中携带的,因为访问地址在请求行

POST请求

  • 特征1:有请求体
  • 特征2:请求参数放在请求体中
  • 特征3:请求体发送数据的空间没有限制
  • 特征4:可以发送各种不同类型的数据
  • 特征5:从报文角度分析,请求参数是在请求体中携带的
  • 特征6:由于请求参数是放在请求体中,所以浏览器地址栏看不到

4、媒体类型

HTTP协议中的MIME类型

Multipurpose Internet Mail Extensions

用途

为了让用户通过浏览器和服务器端交互的过程中有更好、更丰富的体验,HTTP协议需要支持丰富的数据类型。

MIME类型定义参考

我们可以通过查看Tomcat解压目录下conf/web.xml配置文件,了解HTTP协议中定义的MIME类型。

<mime-mapping>
    <extension>mp4</extension>
    <mime-type>video/mp4</mime-type>
</mime-mapping>
<mime-mapping>
    <extension>doc</extension>
    <mime-type>application/msword</mime-type>
</mime-mapping>
<mime-mapping>
    <extension>json</extension>
    <mime-type>application/json</mime-type>
</mime-mapping>
<mime-mapping>
    <extension>html</extension>
    <mime-type>text/html</mime-type>
</mime-mapping>

从上面的例子中可以看出:MIME的基本格式是

大类/具体类型

MIME类型在HTTP报文中对应的是内容类型:Content-type

5、响应报文

响应状态行

HTTP/1.1 200 OK

  • HTTP协议版本
  • 响应状态码
  • 响应状态的说明文字

响应消息头

  • 响应体的说明书。
  • 服务器端对浏览器端设置数据,例如:服务器端返回Cookie信息。
名称 功能
Content-Type 响应体的内容类型
Content-Length 响应体的内容长度
Set-Cookie 服务器返回新的Cookie信息给浏览器
location 重定向的情况下,告诉浏览器访问下一个资源的地址

响应体

服务器返回的数据主体,有可能是各种数据类型。

  • HTML页面
  • 图片
  • 视频
  • 以下载形式返回的文件
  • CSS文件
  • JavaScript文件

响应状态码

作用:以编码的形式告诉浏览器当前请求处理的结果

状态码 含义
200 服务器成功处理了当前请求,成功返回响应
302 重定向
400 [SpringMVC特定环境]请求参数问题
403 没有权限
404 找不到目标资源
405 请求方式和服务器端对应的处理方式不一致
406 [SpringMVC特定环境]请求扩展名和实际返回的响应体类型不一致
50X 服务器端内部错误,通常都是服务器端抛异常了

404产生的具体原因:

  • 访问地址写错了,确实是没有这个资源
  • 访问了WEB-INF目录下的资源
  • Web应用启动的时候,控制台已经抛出异常,导致整个Web应用不可用,访问任何资源都是404
  • 服务器端缓存

10、Servlet

一、Servlet简介

1、Servlet名字

Servlet=Server+applet

Server:服务器

applet:小程序

Servlet含义是服务器端的小程序

2、Servlet在整个Web应用中起到的作用

生活中的例子

对应Web应用

具体细节

Servlet扮演角色

在整个Web应用中,Servlet主要负责处理请求、协调调度功能。我们可以把Servlet称为Web应用中的*『控制器』*

二、 Servlet HelloWorld

1、HelloWorld分析

目标

在页面上点击超链接,由Servlet处理这个请求,并返回一个响应字符串:Hello,I am Servlet

思路

2、具体操作

第一步:创建动态Web module

第二步:创建超链接

<!-- /Web应用地址/Servlet地址 -->
<a href="/app/helloServlet">Servlet Hello World</a>

第三步:创建HelloServlet的Java类

public class HelloServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        // 控制台打印,证明这个方法被调用了
        System.out.println("我是HelloServlet,我执行了!");

        // 返回响应字符串
        // 1、获取能够返回响应数据的字符流对象
        PrintWriter writer = servletResponse.getWriter();

        // 2、向字符流对象写入数据
        writer.write("Hello,I am Servlet");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

第四步:配置HelloServlet

配置文件位置:WEB-INF/web.xml

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>
</servlet>

<!-- 将Servlet和访问地址关联起来 -->
<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/helloServlet</url-pattern>
</servlet-mapping>

『虚拟路径』:Servlet并不是文件系统中实际存在目录或文件,所以为了方便浏览器访问,我们创建了虚拟出来的路径来访问它。

小结

  • 需求:在浏览器上点超链接能够访问Java程序

3、梳理概念

原生Tomcat

IDEA中的Tomcat实例

IDEA中的Web工程

根据Web工程生成的war包

Web工程中的资源

静态资源
  • HTML文件
  • CSS文件
  • JavaScript文件
  • 图片文件
动态资源
  • Servlet

访问资源的地址

静态资源

/Web应用名称/静态资源本身的路径

动态资源

/Web应用名称/虚拟路径

Web应用名称

总体的逻辑结构

三、Servlet 生命周期

1、从Servlet接口说起

2、Servlet创建对象的时机

验证方式

在HelloServlet的构造器中执行控制台打印

public HelloServlet(){
    System.out.println("我来了!HelloServlet对象创建!");
}

打印结果

我来了!HelloServlet对象创建! 我是HelloServlet,我执行了! 我是HelloServlet,我执行了! 我是HelloServlet,我执行了! 我是HelloServlet,我执行了! 我是HelloServlet,我执行了! 我是HelloServlet,我执行了!

结论

  • 默认情况下:Servlet在第一次接收到请求的时候才创建对象
  • 创建对象后,所有的URL地址匹配的请求都由这同一个对象来处理
  • Tomcat中,每一个请求会被分配一个线程来处理,所以可以说:Servlet是单实例,多线程方式运行的。
  • 既然Servlet是多线程方式运行,所以有线程安全方面的可能性,所以不能在处理请求的方法中修改公共属性。

修改Servlet创建对象的时机

修改web.xml中Servlet的配置:

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>

    <!-- 配置Servlet启动顺序 -->
    <load-on-startup>1</load-on-startup>
</servlet>

效果:Web应用启动的时候创建Servlet对象

友情提示:将来配置SpringMVC的时候会看到这样的配置。

3、其他环节

4、Servlet容器

容器

在开发使用的各种技术中,经常会有很多对象会放在容器中。

容器提供的功能

容器会管理内部对象的整个生命周期。对象在容器中才能够正常的工作,得到来自容器的全方位的支持。

  • 创建对象
  • 初始化
  • 工作
  • 清理

容器本身也是对象

  • 特点1:往往是非常大的对象
  • 特点2:通常的单例的

典型Servlet容器产品举例

  • Tomcat
  • jetty
  • jboss
  • Weblogic
  • WebSphere
  • glassfish

5、总结

名称 时机 次数
创建对象 默认情况:接收到第一次请求 修改启动顺序后:Web应用启动过程中 一次
初始化操作 创建对象之后 一次
处理请求 接收到请求 多次
销毁操作 Web应用卸载之前 一次

小提示:

我们学习任何一章的知识,通常都包括两类:

  • 现在用得上的——优先级高
  • 以后才用的——优先级低

生命周期部分就属于以后才用的知识。

四、ServletConfig和ServletContext

1、类比

2、ServletConfig接口

接口概览

接口方法介绍

方法名 作用
getServletName() 获取HelloServlet定义的Servlet名称
getServletContext() 获取ServletContext对象
getInitParameter() 获取配置Servlet时设置的『初始化参数』,根据名字获取值
getInitParameterNames() 获取所有初始化参数名组成的Enumeration对象

初始化参数举例

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>

    <!-- 配置初始化参数 -->
    <init-param>
        <param-name>goodMan</param-name>
        <param-value>me</param-value>
    </init-param>

    <!-- 配置Servlet启动顺序 -->
    <load-on-startup>1</load-on-startup>
</servlet>

体验

在HelloServlet中增加代码:

public class HelloServlet implements Servlet {

    // 声明一个成员变量,用来接收init()方法传入的servletConfig对象
    private ServletConfig servletConfig;

    public HelloServlet(){
        System.out.println("我来了!HelloServlet对象创建!");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

        System.out.println("HelloServlet对象初始化");

        // 将Tomcat调用init()方法时传入的servletConfig对象赋值给成员变量
        this.servletConfig = servletConfig;

    }

    @Override
    public ServletConfig getServletConfig() {

        // 返回成员变量servletConfig,方便使用
        return this.servletConfig;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        // 控制台打印,证明这个方法被调用了
        System.out.println("我是HelloServlet,我执行了!");

        // 返回响应字符串
        // 1、获取能够返回响应数据的字符流对象
        PrintWriter writer = servletResponse.getWriter();

        // 2、向字符流对象写入数据
        writer.write("Hello,I am Servlet");

        // =============分割线===============
        // 测试ServletConfig对象的使用
        // 1.获取ServletConfig对象:在init()方法中完成
        System.out.println("servletConfig = " + servletConfig.getClass().getName());

        // 2.通过servletConfig对象获取ServletContext对象
        ServletContext servletContext = this.servletConfig.getServletContext();
        System.out.println("servletContext = " + servletContext.getClass().getName());

        // 3.通过servletConfig对象获取初始化参数
        Enumeration<String> enumeration = this.servletConfig.getInitParameterNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            System.out.println("name = " + name);

            String value = this.servletConfig.getInitParameter(name);
            System.out.println("value = " + value);
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("HelloServlet对象即将销毁,现在执行清理操作");
    }
}

打印效果:

我是HelloServlet,我执行了! servletConfig = org.apache.catalina.core.StandardWrapperFacade servletContext = org.apache.catalina.core.ApplicationContextFacade name = goodMan value = me

引申:

  • 广义Servlet:javax.servlet包下的一系列接口定义的一组Web开发标准。遵循这一套标准,不同的Servlet容器提供了不同的实现。
  • 狭义Servlet:javax.servlet.Servlet接口和它的实现类,也就是实际开发时使用的具体的Servlet。

Servlet标准和JDBC标准对比:

Servlet标准 JDBC标准
javax.servlet包下的一系列接口 javax.sql包下的一系列接口
Servlet容器厂商提供的具体实现类 数据库厂商提供的实现类(数据库驱动)

同样都体现了面向接口编程的思想,同时也体现了解耦的思想:只要接口不变,下层方法有任何变化,都不会影响上层方法。

3、ServletContext接口

简介

  • 代表:整个Web应用
  • 是否单例:是
  • 典型的功能:
    • 获取某个资源的真实路径:getRealPath()
    • 获取整个Web应用级别的初始化参数:getInitParameter()
    • 作为Web应用范围的域对象
      • 存入数据:setAttribute()
      • 取出数据:getAttribute()

体验

配置Web应用级别的初始化参数
<!-- 配置Web应用的初始化参数 -->
    <context-param>
        <param-name>handsomeMan</param-name>
        <param-value>alsoMe</param-value>
    </context-param>

获取参数

String handsomeMan = servletContext.getInitParameter("handsomeMan");
System.out.println("handsomeMan = " + handsomeMan);

五、使用IDEA创建Servlet

<!-- IDEA会自动生成servlet标签 -->
<servlet>
    <servlet-name>QuickServlet</servlet-name>
    <servlet-class>com.atguigu.servlet.QuickServlet</servlet-class>
</servlet>

<!-- 我们自己补充servlet-mapping标签 -->
<servlet-mapping>
    <servlet-name>QuickServlet</servlet-name>
    <url-pattern>/QuickServlet</url-pattern>
</servlet-mapping>

IDEA生成的Servlet自动继承了HttpServlet

public class QuickServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }
}

下面是测试代码:

public class QuickServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("doPost method processed");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("doGet method processed");
    }
}
<form action="/app/QuickServlet" method="get">
    <button type="submit">发送GET请求</button>
</form>

<br/>

<form action="/app/QuickServlet" method="post">
    <button type="submit">发送POST请求</button>
</form>

六、Servlet 继承关系[了解]

1、提出问题

  • 为什么IDEA创建Servlet之后不再实现Servlet接口而是继承HttpServlet类
  • Servlet接口HttpServlet类有什么关系?
  • doXxx()方法service()方法有什么关系?

2、类型关系

3、方法关系

七、动态Web工程内编写路径

1、为什么要写路径?

  • 整个系统要根据功能拆分成许许多多独立资源
  • 资源之间既要完成自身的功能又要和其他资源配合
  • 写路径就是为了从一个资源跳转到下一个资源

2、为什么写路径这事有点复杂?

先开发再部署

  • 工程目录:我们写代码的地方,但是在服务器上运行的不是这个。
  • 部署目录:经过Java源文件编译目录重组后,IDEA就替我们准备好了可以在服务器上运行的部署目录。
  • 区别:因为从工程目录到部署目录经过了目录重组,所以它们的目录结构是不同的。
  • 基准:用户通过浏览器访问服务器,而服务器上运行的是部署目录,所以写路径的时候参考部署目录而不是工程目录。
  • 对应关系工程目录下的web目录对应部署目录的根目录,同时部署目录的根目录也是路径中的Web应用根目录

路径的各个组成部分

从最前面一直到Web应用名称这里都是固定写法,到资源名这里要看具体是什么资源。

具体文件

我们写代码的时候都是在工程目录下操作,所以参照工程目录来说最方便。按照工程目录的目录结构来说,从web目录开始按照实际目录结构写就好了(不包括web目录本身)。

Servlet

访问Servlet的路径是我们在web.xml中配置的,大家可能注意到了,url-pattern里面的路径我们也是斜杠开头的,但是这个开头的斜杠代表Web应用根目录

同样是开头的斜杠,超链接路径中的开头斜杠代表服务器根目录,Servlet地址开头的斜杠,代表Web应用根目录,怎么记呢?请看下面的准则:

3、准则

用通俗的大白话来解释:一个路径由谁来解析,其实就是这个路径是谁来用。

路径类型 解析方式
由浏览器解析的路径 开头斜杠代表服务器根目录
由服务器解析的路径 开头斜杠代表Web应用根目录

那么具体来说,哪些路径是浏览器解析的,哪些路径是服务器解析的呢?

  • 浏览器解析的路径举例:
    • 所有HTML标签中的路径
    • 重定向过程中指定的路径
  • 服务器解析的路径举例:
    • 所有web.xml中配置的路径
    • 请求转发过程中指定的路径

4、写路径的步骤

5、动态获取上下文路径

上下文路径的概念

上下文路径(context path)=/Web应用名称

动态获取

由于项目部署的时候,上下文路径是可以变化的,所以写死有可能发生错误。此时我们通过request对象动态获取上下文路径就不用担心这个问题了。调用下面这个方法,每一次获取的都是当前环境下实际的上下文路径的值。

request.getContextPath()

如果本节让你感觉很复杂,建议你放慢节奏,尝试下面的步骤:

  • 第一步:先弄清楚每个『名词概念』,清楚的知道我们提到的每一个名词指的是什么。
  • 第二步:弄清楚底层运行原理,其实就是工程目录和部署目录的区别。
  • 第三步:按照我们介绍的步骤一步一步慢慢写,写一步想一想,弄清楚各个部分的对应关系。

八、请求转发和重定向

1、接力

发一个请求给Servlet,接力棒就传递到了Servlet手中。而绝大部分情况下,Servlet不能独自完成一切,需要把接力棒继续传递下去,此时我们就需要请求的**『转发』『重定向』**。

2、转发

本质:转交

完整定义:在请求的处理过程中,Servlet完成了自己的任务,需要把请求转交给下一个资源继续处理。

代码:

request.getRequestDispatcher("/fruit/apple/red/sweet/big.html").forward(request, response);

类比:

代码 类比
request 小货车
getRequestDispatcher("转发地址") 告诉司机要去哪
forward(request, response) 出发

关键:由于转发操作的核心部分是在服务器端完成的,所以浏览器感知不到,整个过程中浏览器只发送一次请求

3、重定向

本质:一种特殊的响应

完整定义:在请求的处理过程中,Servlet完成了自己的任务,然后以一个响应的方式告诉浏览器:“要完成这个任务还需要你另外再访问下一个资源”。

代码:

response.sendRedirect("/app/fruit/apple/red/sweet/big.html");

关键:由于重定向操作的核心部分是在浏览器端完成的,所以整个过程中浏览器共发送两次请求

4、对比

转发 重定向
一次请求 两次请求
浏览器地址栏显示的是第一个资源的地址 浏览器地址栏显示的是第二个资源的地址
全程使用的是同一个request对象 全程使用的是不同的request对象
在服务器端完成 在浏览器端完成
目标资源地址由服务器解析 目标资源地址由浏览器解析
目标资源可以在WEB-INF目录下 目标资源不能在WEB-INF目录下
目标资源仅限于本应用内部 目标资源可以是外部资源

5、转发和重定向的应用场景

可以简单的判断:能用转发的先用转发,如果转发不行,再使用重定向。

  • 需要通过同一个request对象把数据携带到目标资源:只能用转发
  • 如果希望前往下一个资源之后,浏览器刷新访问的是第二个资源:只能用重定向

九、获取请求参数

1、请求参数的概念

浏览器在给服务器发送请求的同时,携带的参数数据。

2、浏览器端发送请求参数的基本形式

  • URL地址后面附着的请求参数

/orange/CharacterServlet?username=汤姆

  • 表单
  • Ajax请求(将来会学到)

3、服务器端对请求参数的封装

总体上来说,服务器端将请求参数封装为Map<String, String[]>

  • 键:请求参数的名字
  • 值:请求参数的值组成的数组

4、获取请求参数的方法

方法名 返回值类型
request.getParameterMap() Map<String, String[]>
request.getParameter("请求参数的名字") String
request.getParameterValues("请求参数的名字") String []
request.getParameterNames() Enumeration

5、测试

HTML代码

<!-- 测试请求参数的表单 -->
<form action="/orange/ParamServlet" method="post">

    <!-- 单行文本框 -->
    <!-- input标签配合type="text"属性生成单行文本框 -->
    <!-- name属性定义的是请求参数的名字 -->
    <!-- 如果设置了value属性,那么这个值就是单行文本框的默认值 -->
    个性签名:<input type="text" name="signal" value="单行文本框的默认值" /><br/>

    <!-- 密码框 -->
    <!-- input标签配合type="password"属性生成密码框 -->
    <!-- 用户在密码框中填写的内容不会被一明文形式显示 -->
    密码:<input type="password" name="secret" /><br/>

    <!-- 单选框 -->
    <!-- input标签配合type="radio"属性生成单选框 -->
    <!-- name属性一致的radio会被浏览器识别为同一组单选框,同一组内只能选择一个 -->
    <!-- 提交表单后,真正发送给服务器的是name属性和value属性的值 -->
    <!-- 使用checked="checked"属性设置默认被选中 -->
    请选择你最喜欢的季节:
    <input type="radio" name="season" value="spring" />春天
    <input type="radio" name="season" value="summer" checked="checked" />夏天
    <input type="radio" name="season" value="autumn" />秋天
    <input type="radio" name="season" value="winter" />冬天

    <br/><br/>

    你最喜欢的动物是:
    <input type="radio" name="animal" value="tiger" />路虎
    <input type="radio" name="animal" value="horse" checked="checked" />宝马
    <input type="radio" name="animal" value="cheetah" />捷豹

    <br/>

    <!-- 多选框 -->
    <!-- input标签和type="checkbox"配合生成多选框 -->
    <!-- 多选框被用户选择多个并提交表单后会产生『一个名字携带多个值』的情况 -->
    你最喜欢的球队是:
    <input type="checkbox" name="team" value="Brazil"/>巴西
    <input type="checkbox" name="team" value="German" checked="checked"/>德国
    <input type="checkbox" name="team" value="France"/>法国
    <input type="checkbox" name="team" value="China" checked="checked"/>中国
    <input type="checkbox" name="team" value="Italian"/>意大利

    <br/>

    <!-- 下拉列表 -->
    <!-- 使用select标签定义下拉列表整体,在select标签内设置name属性 -->
    你最喜欢的运动是:
    <select name="sport">
        <!-- 使用option属性定义下拉列表的列表项 -->
        <!-- 使用option标签的value属性设置提交给服务器的值,在option标签的标签体中设置给用户看的值 -->
        <option value="swimming">游泳</option>
        <option value="running">跑步</option>

        <!-- 使用option标签的selected="selected"属性设置这个列表项默认被选中 -->
        <option value="shooting" selected="selected">射击</option>
        <option value="skating">溜冰</option>
    </select>

    <br/>

    <br/><br/>

    <!-- 表单隐藏域 -->
    <!-- input标签和type="hidden"配合生成表单隐藏域 -->
    <!-- 表单隐藏域在页面上不会有任何显示,用来保存要提交到服务器但是又不想让用户看到的数据 -->
    <input type="hidden" name="userId" value="234654745" />

    <!-- 多行文本框 -->
    自我介绍:<textarea name="desc">多行文本框的默认值</textarea>

    <br/>

    <!-- 普通按钮 -->
    <button type="button">普通按钮</button>

    <!-- 重置按钮 -->
    <button type="reset">重置按钮</button>

    <!-- 表单提交按钮 -->
    <button type="submit">提交按钮</button>
</form>

Java代码

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 获取包含全部请求参数的Map
    Map<String, String[]> parameterMap = request.getParameterMap();

    // 遍历这个包含全部请求参数的Map
    Set<String> keySet = parameterMap.keySet();

    for (String key : keySet) {

        String[] values = parameterMap.get(key);

        System.out.println(key + "=" + Arrays.asList(values));
    }

    System.out.println("---------------------------");

    // 根据请求参数名称获取指定的请求参数值
    // getParameter()方法:获取单选框的请求参数
    String season = request.getParameter("season");
    System.out.println("season = " + season);

    // getParameter()方法:获取多选框的请求参数
    // 只能获取到多个值中的第一个
    String team = request.getParameter("team");
    System.out.println("team = " + team);

    // getParameterValues()方法:取单选框的请求参数
    String[] seasons = request.getParameterValues("season");
    System.out.println("Arrays.asList(seasons) = " + Arrays.asList(seasons));

    // getParameterValues()方法:取多选框的请求参数
    String[] teams = request.getParameterValues("team");
    System.out.println("Arrays.asList(teams) = " + Arrays.asList(teams));

}

十、请求响应设置字符集

1、请求

GET请求

设置之前

发送请求参数:

<a href="/orange/CharacterServlet?username=汤姆">测试请求字符集设置:GET请求</a>

接收到的数据:

username = ?±¤?§?

设置方式

在server.xml第71行的Connector标签中增加URIEncoding属性:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" 
           URIEncoding="UTF-8"
           />

重启Tomcat实例即可。再重新测试的结果是:

username = 汤姆

POST请求

设置之前

发送请求参数:

<!-- 测试请求字符集设置:POST请求 -->
<form action="/orange/CharacterServlet" method="post">

    用户名:<input type="text" name="username" /><br/>

    <button type="submit">保存</button>

</form>

接收到的数据:

username = ?????????

设置方式
// 使用request对象设置字符集
request.setCharacterEncoding("UTF-8");

// 获取请求参数
String username = request.getParameter("username");

System.out.println("username = " + username);

接收到的数据:

username = 林志玲

需要注意的问题

不能在设置字符集之前获取请求参数!下面是错误的示范

// 获取请求参数(先获取请求参数会导致设置字符集失效)
String username = request.getParameter("username");

// 使用request对象设置字符集
request.setCharacterEncoding("UTF-8");

System.out.println("username = " + username);

2、响应

设置之前

服务器端代码:

PrintWriter writer = response.getWriter();

writer.write("志玲姐姐你好!");

浏览器显示:

???????

设置方式一

编码字符集和解码字符集一致

// 设置服务器端的编码字符集
response.setCharacterEncoding("UTF-8");

PrintWriter writer = response.getWriter();

writer.write("<!DOCTYPE html>                  ");
writer.write("<html>                           ");
writer.write("<head>                           ");
writer.write("<!-- 设置浏览器端的解码字符集 -->");
writer.write("    <meta charset='UTF-8'>       ");
writer.write("    <title>Title</title>         ");
writer.write("</head>                          ");
writer.write("<body>                           ");
writer.write("<p>志玲姐姐你好!</p>            ");
writer.write("</body>                          ");
writer.write("</html>                          ");

设置方式二

response.setContentType("text/html;charset=UTF-8");

需要注意的问题

response.getWriter()不能出现在设置字符集操作的前面(两种方式都不行)。

11、书城项目第二阶段

一、不带数据库的登录注册

1、创建动态Web工程

2、把V1中的页面粘贴过来

3、使用base标签统一设置路径基准

base标签的语法规则

  • base标签要写在head标签内
  • base标签必须写在所有其他有路径的标签的前面
  • base标签使用href属性设置路径的基准
  • base标签生效的机制是:最终的访问地址=base标签href属性设置的基准+具体标签内的路径
  • 对于想要参考base标签的具体标签,如果路径是以斜杠开头,那么它将不参考base标签

base标签使用举例

<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>书城首页</title>
    <base href="/bookstore/"/>
    <link rel="stylesheet" href="static/css/minireset.css"/>
    <link rel="stylesheet" href="static/css/common.css"/>
    <link rel="stylesheet" href="static/css/iconfont.css"/>
    <link rel="stylesheet" href="static/css/index.css"/>
    <link rel="stylesheet" href="static/css/swiper.min.css"/>
</head>

4、基于base标签调整整个页面的路径

base标签的代码

在需要的页面把下面的base标签代码粘贴到head标签内、需要路径的标签前即可

<base href="/bookstore/"/>

对需要统一调整的路径执行替换

Ctrl+r调出替换操作窗口

具体操作时请参考页面的实际情况进行替换。

5、基本假设

为了实现『不带数据库』的登录注册,我们需要假设:系统中目前已有用户:

用户名 密码
tom 123456

6、登录功能

明确目标

在服务器端检查用户通过表单提交的用户名、密码是否正确。

  • 成功:跳转到login_success.html页面
  • 失败:返回错误消息

思路

友情提示:分析业务功能,捋清思路最好的办法就是画流程图

代码

创建LoginServlet

创建Packages时的规范:

公司或组织域名倒序.项目名.模块名.具体包名

公司或组织域名倒序.项目名.具体包名

下面是完整的Servlet配置信息:

<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>
完成doPost()方法中的代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.声明两个变量,用于存储假设的用户名、密码
    String usernameSystem = "tom";
    String passwordSystem = "123456";

    // 2.获取请求参数中的用户名、密码
    String usernameForm = request.getParameter("username");
    String passwordForm = request.getParameter("password");

    // 3.执行判断
    if(usernameSystem.equals(usernameForm) && passwordSystem.equals(passwordForm)) {

        // 4.登录成功:重定向到登录成功页面
        response.sendRedirect(request.getContextPath() + "/pages/user/login_success.html");

    }else{

        // 5.登录失败
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("抱歉!用户名或密码不正确,请重新输入!");

    }

}
HTML页面设置表单提交地址
<form id="loginForm" action="LoginServlet" method="post">
    <label>用户名称:</label>
    <input class="itxt" type="text" v-model:value="username" placeholder="请输入用户名" autocomplete="off" tabindex="1" name="username" id="username" />
    <br />
    <br />
    <label>用户密码:</label>
    <input class="itxt" type="password" v-model:value="password" placeholder="请输入密码" autocomplete="off" tabindex="1" name="password" id="password" />
    <br />
    <br />
    <button type="submit" id="sub_btn" @click="loginCheck">登录</button>
</form>

提示消息改进探索

以下代码仅供参考:

// 5.登录失败
// 返回提示消息方案一:过于简陋
// response.setContentType("text/html;charset=UTF-8");
// response.getWriter().write("抱歉!用户名或密码不正确,请重新输入!");

// 返回提示消息方案二:没有提示消息,让用户非常困惑
// request.getRequestDispatcher("/pages/user/login.html").forward(request, response);

// 返回提示消息方案三:确实能在登录页面显示提示消息,但是实现的方式让我想骂人
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("<!DOCTYPE html>");
writer.write("<html>");
writer.write("    <head>");
writer.write("        <base href='/bookstore/' />");

7、注册功能

明确目标

用户提交注册表单后,检查用户名是否被占用

  • 没有被占用:注册成功
  • 已经被占用:注册失败

思路

代码

创建RegisterServlet

完整配置信息:

<servlet>
    <servlet-name>RegisterServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>RegisterServlet</servlet-name>
    <url-pattern>/RegisterServlet</url-pattern>
</servlet-mapping>
完成doPost()方法中的代码
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.声明变量保存系统内置的用户名
    String usernameSystem = "jerry";

    // 2.获取请求参数
    String usernameForm = request.getParameter("username");

    // 3.比较
    if (usernameSystem.equals(usernameForm)) {

        // 4.说明用户名已经被占用,需要提示错误消息
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write("抱歉!用户名已经被占用,请重新输入!");

    }else{

        // 5.说明用户名可用,跳转到注册成功页面
        response.sendRedirect(request.getContextPath() + "/pages/user/regist_success.html");

    }

}
HTML页面调整表单中的设置
<form id="registerForm" action="RegisterServlet" method="post">
    <div class="form-item">
        <div>
            <label>用户名称:</label>
            <input v-model:value="username" type="text" name="username" placeholder="请输入用户名" />
            <span></span>
        </div>
        <span>{
    
    {usernameCheckMessage}}</span>
    </div>
    <div class="form-item">
        <div>
            <label>用户密码:</label>
            <input v-model:value="password" type="password" name="password" placeholder="请输入密码" />
        </div>
        <span class="errMess">密码的长度至少为8位</span>
    </div>
    <div class="form-item">
        <div>
            <label>确认密码:</label>
            <input v-model:value="passwordConfirm" type="password" placeholder="请输入确认密码" />
        </div>
        <span class="errMess">密码两次输入不一致</span>
    </div>
    <div class="form-item">
        <div>
            <label>用户邮箱:</label>
            <input v-model:value="email" type="text" name="email" placeholder="请输入邮箱" />
        </div>
        <span class="errMess">请输入正确的邮箱格式</span>
    </div>
    <div class="form-item">
        <div>
            <label>验证码:</label>
            <div class="verify">
                <input v-model:value="code" type="text" name="code" placeholder="" />
                <img src="static/img/code.bmp" alt="" />
            </div>
        </div>
        <span class="errMess">请输入正确的验证码</span>
    </div>
    <button type="submit" @click="registerCheck" class="btn">注册</button>
</form>

二、三层架构

1、三层架构划分

  • 表述层:负责处理浏览器请求、返回响应、页面调度
  • 业务逻辑层:负责处理业务逻辑,根据业务逻辑把持久化层从数据库查询出来的数据进行运算、组装,封装好后返回给表述层,也可以根据业务功能的需要调用持久化层把数据保存到数据库、修改数据库中的数据、删除数据库中的数据
  • 持久化层:根据上一层的调用对数据库中的数据执行增删改查的操作

2、三层架构好处

如果不做三层架构形式的拆分:

所有和当前业务功能需求相关的代码全部耦合在一起,如果其中有任何一个部分出现了问题,牵一发而动全身,导致其他无关代码也要进行相应的修改。这样的话代码会非常难以维护。

所以为了提高开发效率,需要对代码进行模块化的拆分。整个项目模块化、组件化程度越高,越容易管理和维护,出现问题更容易排查。

3、三层架构和模型的关系

模型对整个项目中三层架构的每一层都提供支持,具体体现是使用模型对象封装业务功能数据

Java实体类有很多不同名称:

  • POJO:Plain old Java Object,传统的普通的Java对象
  • entity:实体类
  • bean或Java bean
  • domain:领域模型

4、模型开发的要求

ORM

ORM:Object Relative Mapping对象关系映射

对象:Java对象

关系:关系型数据库

映射:Java对象和数据库表之间的对应关系

Java类 数据库表
属性 字段/列
对象 记录/行
属性按照驼峰式命名 字段名各个单词之间用下划线分开

Java实体类的要求

  • 必须有一个无参构造器

    将来使用框架后,大量的对象都是框架通过反射来创建的。

    Class clazz = Class.forName("全类名");

    clazz.newInstance();

  • 通过getXxx()、setXxx()方法定义属性:getXxx()或setXxx()方法去掉get或set后,Xxx把首字母小写,得到的xxx就是属性名。

public class User {

    private String safeUserName;

    public String getUserName(){
        return this.safeUserName;
    }

    public void setUserName(String userName){
        this.safeUserName = userName;
    }

}

在上面例子中,getXxx()、setXxx()方法定义的属性是userName,不是safeUserName。

三、建模

1、创建数据库

CREATE DATABASE bookstore210107 CHARACTER SET utf8;
USE `bookstore210107`;

2、创建数据库表

物理建模的思路参考这里

CREATE TABLE t_user(
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    user_name CHAR(100),
    user_pwd CHAR(100),
    email CHAR(100)
);

3、创建Java实体类

public class User {

    private Integer userId;// user_id
    private String userName;// user_name
    private String userPwd;// user_pwd
    private String email;// email
    ……

四、持久化层

1、加入所需jar包

2、创建连接数据库的工具类

3、创建外部属性文件

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/bookstore210107
username=root
password=123456
initialSize=10
maxActive=20
maxWait=10000

注意:这里是上课演示时使用的参数,大家操作时要改成自己的。

4、在JDBCUtils类中创建数据源对象

private static DataSource dataSource;

static {

    // 1.创建一个用于存储外部属性文件信息的Properties对象
    Properties properties = new Properties();

    // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
    InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");

    try {

        // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
        properties.load(inputStream);

        // 4.创建数据源对象
        dataSource = DruidDataSourceFactory.createDataSource(properties);

    } catch (Exception e) {
        e.printStackTrace();
    }

}

5、声明工具方法操作数据库连接

/**
     * 从数据源中获取数据库连接
     * @return 数据库连接对象
     */
    public static Connection getConnection() {

        Connection connection = null;

        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        return connection;

    }

    /**
     * 释放数据库连接
     * @param connection 要执行释放操作的连接对象
     */
    public static void releaseConnection(Connection connection) {

        if (connection != null) {

            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }

        }

    }

测试代码如下:

public class BookstoreTest {

    @Test
    public void testConnection() {

        Connection connection = JDBCUtils.getConnection();
        System.out.println("connection = " + connection);

    }

}

6、创建BaseDao

DAO概念

DAO:Data Access Object数据访问对象

DAL:Data Access Layer数据访问层

创建Java类

编写通用方法

/**
 * 各个具体Dao类的基类,泛型T对应具体实体类类型
 * @param <T>
 */
public class BaseDao<T> {

    private QueryRunner queryRunner = new QueryRunner();

    /**
     * 通用的增删改方法
     * @param sql 要执行的SQL语句
     * @param param 为SQL语句准备好的参数
     * @return 受影响的行数
     */
    public int update(String sql, Object ... param) {

        int updatedRowCount = 0;

        Connection connection = JDBCUtils.getConnection();

        try {

            updatedRowCount = queryRunner.update(connection, sql, param);

        }
        // 为了让上层方法调用方便,将编译时异常捕获
        catch (SQLException e) {
            e.printStackTrace();

            // 为了不掩盖问题,将编译时异常封装为运行时异常抛出
            throw new RuntimeException(e);
        } finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }

        return updatedRowCount;

    }

    /**
     * 查询单个对象
     * @param clazz 单个对象所对应的实体类类型
     * @param sql   查询单个对象所需要的SQL语句
     * @param param SQL语句的参数
     * @return      查询到的单个对象
     */
    public T getBean(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtils.getConnection();

        T t = null;

        try {

            t = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        } finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }

        return t;
    }

    /**
     * 查询集合对象
     * @param clazz 集合中单个对象所对应的实体类类型
     * @param sql   查询集合所需要的SQL语句
     * @param param SQL语句的参数
     * @return      查询到的集合对象
     */
    public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtils.getConnection();

        List<T> list = null;

        try {

            list = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        } finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }

        return list;
    }

}

测试方法:

@Test
public void testUpdate() {
    BaseDao<Object> baseDao = new BaseDao<>();

    String sql = "insert into t_user(user_name,user_pwd) values(?,?)";

    int count = baseDao.update(sql, "罗志祥", "789456");

    System.out.println("count = " + count);
}

@Test
public void testGetBean() {
    BaseDao<User> baseDao = new BaseDao<>();

    // user_id userId
    // user_name userName
    // user_pwd userPwd
    String sql = "select user_id userId,user_name userName,user_pwd userPwd from t_user where user_id=?";

    User user = baseDao.getBean(User.class, sql, 2);

    System.out.println("user = " + user);
}

@Test
public void testGetBeanList() {
    BaseDao<User> baseDao = new BaseDao<>();

    String sql = "select user_id userId,user_name userName,user_pwd userPwd from t_user";

    List<User> userList = baseDao.getBeanList(User.class, sql);

    for (User user : userList) {
        System.out.println("user = " + user);
    }
}

7、创建UserDao

用户登录注册功能中的组件关系图

声明UserDao接口

public interface UserDao {

    /**
     * 根据用户名查询User对象
     * @param username 用户名
     * @return 查询到的User对象
     */
    User selectUserByName(String username);

    /**
     * 将User对象保存到数据库
     * @param user 要保存的User对象
     * @return 受影响的行数
     */
    int insertUser(User user);

}

声明UserDaoImpl实现类

public class UserDaoImpl extends BaseDao<User> implements UserDao {
    @Override
    public User selectUserByName(String username) {

        String sql = "select user_id userId,user_name userName,user_pwd userPwd,email from t_user where user_name=?";

        return super.getBean(User.class, sql, username);
    }

    @Override
    public int insertUser(User user) {

        String sql = "insert into t_user(user_name,user_pwd,email) values(?,?,?)";

        return super.update(sql, user.getUserName(), user.getUserPwd(), user.getEmail());
    }
}

测试方法

@Test
public void testUserDaoGetUserByName() {

    UserDao userDao = new UserDaoImpl();

    User user = userDao.selectUserByName("陈冠希");

    System.out.println("user = " + user);

}

@Test
public void testUserDaoSaveUser() {
    UserDao userDao = new UserDaoImpl();

    User user = new User(null, "陈冠希", "666666", "[email protected]");

    int count = userDao.insertUser(user);

    System.out.println("count = " + count);
}

五、完成带数据库的登录注册

1、密码加密

加密方式介绍

  • 对称加密:在知道密文和加密算法的情况下,能够反推回明文
  • 非对称加密:
    • 加密:使用私钥加密
    • 解密:使用公钥解密

加密算法:HASH

  • 特点1:不可逆
  • 特点2:加密后,密文长度固定
  • 特点3:输入数据不变,输出数据也保证不变;输入数据变化,输出数据一定变化

常见的HASH算法举例:

  • MD5
  • SHA1
  • SHA512
  • CRC32

执行加密的工具方法

public class MD5Util {

    /**
     * 针对明文字符串执行MD5加密
     * @param source
     * @return
     */
    public static String encode(String source) {

        // 1.判断明文字符串是否有效
        if (source == null || "".equals(source)) {
            throw new RuntimeException("用于加密的明文不可为空");
        }

        // 2.声明算法名称
        String algorithm = "md5";

        // 3.获取MessageDigest对象
        MessageDigest messageDigest = null;
        try {
            messageDigest = MessageDigest.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        // 4.获取明文字符串对应的字节数组
        byte[] input = source.getBytes();

        // 5.执行加密
        byte[] output = messageDigest.digest(input);

        // 6.创建BigInteger对象
        int signum = 1;
        BigInteger bigInteger = new BigInteger(signum, output);

        // 7.按照16进制将bigInteger的值转换为字符串
        int radix = 16;
        String encoded = bigInteger.toString(radix).toUpperCase();

        return encoded;
    }

}

2、注册功能

目标

检查用户名是否可用,如果用户名可用则保存User对象

思路

代码

[1]创建UserService

public interface UserService {

    void doRegister(User userForm);

    User doLogin(User userForm);

}

开发中,接口设计和接口中方式定义的理念:

  • 方法的返回值应该对应这个方法本身的业务功能
    • 写操作:没有返回值
    • 读操作:有返回值,返回值就是查询的结果
  • 方法执行是否成功
    • 成功:不抛异常
    • 失败:抛异常

启发:

上层方法向下层方法布置任务:方法名、方法的参数

下层方法向上层方法反馈结果:返回值、是否抛异常

[2]实现UserService接口

(1)UserDao声明为成员变量

说明:将来在Servlet中使用Service的时候,也是同样声明为成员变量,那么从Servlet、Service到Dao就都是**『单实例,多线程』**方式运行。

public class UserServiceImpl implements UserService {

    private UserDao userDao = new UserDaoImpl();

    @Override
    public void doRegister(User userForm) {

    }

    @Override
    public User doLogin(User userForm) {

        return null;
    }
}

理由:

  • 创建对象的操作只执行一次
  • 对象在内存中只保存一份,不会过多占用内存空间
(2)实现注册功能
@Override
public void doRegister(User userForm) {

    // 1.从userForm对象中获取用户通过表单提交的用户名
    String userName = userForm.getUserName();

    // 2.根据用户名调用UserDao方法查询对应的User对象
    User userDB = userDao.selectUserByName(userName);

    // 3.检查User对象是否为空
    if (userDB != null) {
        // 4.如果User对象不为空,则抛出异常,通知上层调用方法:用户名已经被占用
        throw new RuntimeException("用户名已经被占用");
    }

    // 5.对表单提交的密码执行MD5加密
    // ①取出表单的密码
    String userPwd = userForm.getUserPwd();

    // ②执行加密
    String encode = MD5Util.encode(userPwd);

    // ③将加密得到的密文字符串设置回userForm对象
    userForm.setUserPwd(encode);

    // 6.调用UserDao方法将userForm对象保存到数据库
    userDao.insertUser(userForm);
}
[3]修改RegisterServlet
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取数据封装为User对象
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    String email = request.getParameter("email");

    User userForm = new User(null, username, password, email);

    // 2.调用UserService的方法执行注册
    try {
        userService.doRegister(userForm);

        // 3.如果没有抛出异常那么就跳转到注册成功的页面
        // 选择重定向的原因:跳转到regist_success.html页面后,用户刷新浏览器不会重复提交注册表单
        response.sendRedirect(request.getContextPath()+"/pages/user/regist_success.html");
    } catch (Exception e) {
        e.printStackTrace();

        // 4.如果抛出了异常
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("注册失败:" + e.getMessage());
    }

}

3、登录功能

关键点提示

对用户密码进行验证时,无法将密文解密为明文,只能将明文再次加密为密文,『比较密文是否一致』

思路

代码

[1]UserService接口的doLogin()
@Override
public User doLogin(User userForm) {

    // 1.获取表单提交的用户名
    String userName = userForm.getUserName();

    // 2.根据用户名调用UserDao方法查询User对象
    User userDB = userDao.selectUserByName(userName);

    // 3.检查数据库查询的User对象是否为null
    if (userDB == null) {
        // 4.如果数据库查询的User对象为null,说明用户名不正确,抛出异常:登录失败
        throw new RuntimeException("用户名或密码不正确");
    }

    // 5.密码验证
    // ①获取表单提交的密码
    String userPwdForm = userForm.getUserPwd();

    // ②对表单提交的密码进行加密
    String encode = MD5Util.encode(userPwdForm);

    // ③获取数据库查询到的密码
    String userPwdDB = userDB.getUserPwd();

    // ④比较表单密码和数据库密码
    if (Objects.equals(encode, userPwdDB)) {
        // 6.如果密码验证成功,则将从数据库查询出来的User对象返回
        return userDB;

    }else{
        // 7.如果密码验证失败,说明密码不正确,抛出异常:登录失败
        throw new RuntimeException("用户名或密码不正确");
    }

}
[2]LoginServlet的doPost()方法
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");

    // 2.封装为User对象
    User userForm = new User(null, username, password, null);

    // 3.调用UserService的方法执行登录验证
    try {
        User userDB = userService.doLogin(userForm);

        // 4.登录成功后跳转到登录成功页面
        response.sendRedirect(request.getContextPath() + "/pages/user/login_success.html");
    } catch (Exception e) {
        e.printStackTrace();

        // 5.登录失败则显示提示消息
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("登录失败:" + e.getMessage());
    }

}

12、Thymeleaf

官网:https://www.thymeleaf.org/

github:https://github.com/thymeleaf/

中文文档:https://fanlychie.github.io/post/thymeleaf.html

参考文章:https://www.cnblogs.com/msi-chen/p/10974009.html

一、MVC

1、提出问题

我们对HTML的新的期待:既能够正常显示页面,又能在页面中包含动态数据部分。而动态数据单靠HTML本身是无法做到的,所以此时我们需要引入服务器端动态视图模板技术。

2、从三层结构到MVC

MVC概念

M:Model模型

V:View视图

C:Controller控制器

MVC是在表述层开发中运用的一种设计理念。主张把封装数据的『模型』显示用户界面的『视图』、**协调调度的『控制器』**分开。

好处:

  • 进一步实现各个组件之间的解耦
  • 让各个组件可以单独维护
  • 将视图分离出来以后,我们后端工程师和前端工程师的对接更方便

MVC和三层架构之间关系

3、前后端工程师对接方式

  • 服务器端渲染:前端工程师把前端页面一整套做好交给后端工程师
  • 前后端分离:开会商量JSON格式,然后分头开发。在后端程序尚不可用时,前端工程师会使用Mock.js生成假数据使用,在后端程序可用后再连接实际后端程序获取真实数据。

查看详细内容

二、Thymeleaf简介

1、Thymeleaf的同行

JSP、Freemarker、Velocity等等,它们有一个共同的名字:服务器端模板技术

2、Thymeleaf官方文档中的介绍

官网地址(opens new window)

官方文档

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide an elegant and highly-maintainable way of creating templates. To achieve this, it builds on the concept of Natural Templates to inject its logic into template files in a way that doesn’t affect the template from being used as a design prototype. This improves communication of design and bridges the gap between design and development teams. Thymeleaf has also been designed from the beginning with Web Standards in mind – especially HTML5 – allowing you to create fully validating templates if that is a need for you.

3、Thymeleaf优势

  • SpringBoot官方推荐使用的视图模板技术,和SpringBoot完美整合。
  • 不经过服务器运算仍然可以直接查看原始值,对前端工程师更友好。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <p th:text="${hello}">Original Value</p>

</body>
</html>

4、物理视图和逻辑视图

物理视图

在Servlet中,将请求转发到一个HTML页面文件时,使用的完整的转发路径就是物理视图

/pages/user/login_success.html

如果我们把所有的HTML页面都放在某个统一的目录下,那么转发地址就会呈现出明显的规律:

/pages/user/login.html /pages/user/login_success.html /pages/user/regist.html /pages/user/regist_success.html

……

路径的开头都是:/pages/user/

路径的结尾都是:.html

所以,路径开头的部分我们称之为视图前缀,路径结尾的部分我们称之为视图后缀

逻辑视图

物理视图=视图前缀+逻辑视图+视图后缀

上面的例子中:

视图前缀 逻辑视图 视图后缀 物理视图
/pages/user/ login .html /pages/user/login.html
/pages/user/ login_success .html /pages/user/login_success.html

三、在服务器端引入Thymeleaf环境

1、加入jar包

2、配置上下文参数

物理视图=视图前缀+逻辑视图+视图后缀

<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
    <param-name>view-prefix</param-name>
    <param-value>/WEB-INF/view/</param-value>
</context-param>
<context-param>
    <param-name>view-suffix</param-name>
    <param-value>.html</param-value>
</context-param>

说明:param-value中设置的前缀、后缀的值不是必须叫这个名字,可以根据实际情况和需求进行修改。

为什么要放在WEB-INF目录下?

原因:WEB-INF目录不允许浏览器直接访问,所以我们的视图模板文件放在这个目录下,是一种保护。以免外界可以随意访问视图模板文件。

访问WEB-INF目录下的页面,都必须通过Servlet转发过来,简单说就是:不经过Servlet访问不了。

这样就方便我们在Servlet中检查当前用户是否有权限访问。

那放在WEB-INF目录下之后,重定向进不去怎么办?

重定向到Servlet,再通过Servlet转发到WEB-INF下。

3、创建Servlet基类

这个类大家直接复制粘贴即可,将来使用框架后,这些代码都将被取代。

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ViewBaseServlet extends HttpServlet {

    private TemplateEngine templateEngine;

    @Override
    public void init() throws ServletException {

        // 1.获取ServletContext对象
        ServletContext servletContext = this.getServletContext();

        // 2.创建Thymeleaf解析器对象
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);

        // 3.给解析器对象设置参数
        // ①HTML是默认模式,明确设置是为了代码更容易理解
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // ②设置前缀
        String viewPrefix = servletContext.getInitParameter("view-prefix");

        templateResolver.setPrefix(viewPrefix);

        // ③设置后缀
        String viewSuffix = servletContext.getInitParameter("view-suffix");

        templateResolver.setSuffix(viewSuffix);

        // ④设置缓存过期时间(毫秒)
        templateResolver.setCacheTTLMs(60000L);

        // ⑤设置是否缓存
        templateResolver.setCacheable(true);

        // ⑥设置服务器端编码方式
        templateResolver.setCharacterEncoding("utf-8");

        // 4.创建模板引擎对象
        templateEngine = new TemplateEngine();

        // 5.给模板引擎对象设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);

    }

    protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1.设置响应体内容类型和字符集
        resp.setContentType("text/html;charset=UTF-8");

        // 2.创建WebContext对象
        WebContext webContext = new WebContext(req, resp, getServletContext());

        // 3.处理模板数据
        templateEngine.process(templateName, webContext, resp.getWriter());
    }
}

4、HelloWorld

创建index.html

在index.html编写超链接

<a href="/view/TestThymeleafServlet">初步测试Thymeleaf</a>

创建Servlet

<servlet>
    <servlet-name>TestThymeleafServlet</servlet-name>
    <servlet-class>com.atguigu.thymeleaf.servlet.TestThymeleafServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>TestThymeleafServlet</servlet-name>
    <url-pattern>/TestThymeleafServlet</url-pattern>
</servlet-mapping>

修改Servlet继承的父类

在doGet()方法中跳转到Thymeleaf页面

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.声明当前请求要前往的视图名称
    String viewName = "target";

    // 2.调用ViewBaseServlet父类中的解析视图模板的方法
    super.processTemplate(viewName, request, response);

}

Thymeleaf页面

<!DOCTYPE html>

<!-- 在html标签内加入Thymeleaf名称空间的声明 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <!-- 在p标签的基础上,使用Thymeleaf的表达式,解析了一个URL地址 -->
    <p th:text="@{'/aaa/bbb/ccc'}">Thymeleaf将在这里显示一个解析出来的URL地址</p>

</body>
</html>

四、基本语法:th名称空间

五、基本语法:表达式语法

1、修改标签文本值

代码示例:

<p th:text="标签体新值">标签体原始值</p>

th:text作用

  • 不经过服务器解析,直接用浏览器打开HTML文件,看到的是『标签体原始值』
  • 经过服务器解析,Thymeleaf引擎根据th:text属性指定的『标签体新值』去替换『标签体原始值』

字面量

『字面量』是一个经常会遇到的概念,我们可以对照『变量』来理解它的含义。

// a是变量,100是字面量
int a = 100;
System.out.println("a = " + a);
  • 变量:变量名字符串本身不是它的值,它指向的才是它的值
  • 字面量:它就是字面上的含义,我们从『字面』上看到的直接就是它的值

现在我们在th:text属性中使用的就是『字面量』,它不指代任何其他值

2、修改指定属性值

代码示例:

<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />

语法:任何HTML标签原有的属性,前面加上『th:』就都可以通过Thymeleaf来设定新值。

3、解析URL地址

基本语法

代码示例:

<p th:text="@{/aaa/bbb/ccc}">标签体原始值</p>

经过解析后得到:

/view/aaa/bbb/ccc

所以@{}的作用是在字符串前附加『上下文路径』

这个语法的好处是:实际开发过程中,项目在不同环境部署时,Web应用的名字有可能发生变化。所以上下文路径不能写死。而通过@{}动态获取上下文路径后,不管怎么变都不怕啦!

首页使用URL地址解析

如果我们直接访问index.html本身,那么index.html是不需要通过Servlet,当然也不经过模板引擎,所以index.html上的Thymeleaf的任何表达式都不会被解析。

解决办法:通过Servlet访问index.html,这样就可以让模板引擎渲染页面了:

进一步的好处:

通过上面的例子我们看到,所有和业务功能相关的请求都能够确保它们通过Servlet来处理,这样就方便我们统一对这些请求进行特定规则的限定。

给URL地址后面附加请求参数

参照官方文档说明:

4、直接执行表达式

Servlet代码:

request.setAttribute("reqAttrName", "<span>hello-value</span>");

页面代码:

<p>有转义效果:[[${reqAttrName}]]</p>
<p>无转义效果:[(${reqAttrName})]</p>

执行效果:

<p>有转义效果:<span>hello-value</span></p>
<p>无转义效果:<span>hello-value</span></p>

六、基本语法:访问域对象

1、域对象

请求域

在请求转发的场景下,我们可以借助HttpServletRequest对象内部给我们提供的存储空间,帮助我们携带数据,把数据发送给转发的目标资源。

请求域:HttpServletRequest对象内部给我们提供的存储空间

会话域

应用域

PS:在我们使用的视图是JSP的时候,域对象有4个

  • pageContext
  • request:请求域
  • session:会话域
  • application:应用域

所以在JSP的使用背景下,我们可以说域对象有4个,现在使用Thymeleaf了,没有pageContext。

2、在Servlet中将数据存入属性域

操作请求域

Servlet中代码:

String requestAttrName = "helloRequestAttr";
String requestAttrValue = "helloRequestAttr-VALUE";

request.setAttribute(requestAttrName, requestAttrValue);

Thymeleaf表达式:

<p th:text="${helloRequestAttr}">request field value</p>

作会话域

Servlet中代码:

// ①通过request对象获取session对象
HttpSession session = request.getSession();

// ②存入数据
session.setAttribute("helloSessionAttr", "helloSessionAttr-VALUE");

Thymeleaf表达式:

<p th:text="${session.helloSessionAttr}">这里显示会话域数据</p>

操作应用域

Servlet中代码:

// ①通过调用父类的方法获取ServletContext对象
ServletContext servletContext = getServletContext();

// ②存入数据
servletContext.setAttribute("helloAppAttr", "helloAppAttr-VALUE");

Thymeleaf表达式:

<p th:text="${application.helloAppAttr}">这里显示应用域数据</p>

七、基本语法:获取请求参数

具体来说,我们这里探讨的是在页面上(模板页面)获取请求参数。底层机制是:

1、一个名字一个值

页面代码:

<p th:text="${param.username}">这里替换为请求参数的值</p>

页面显示效果:

2、一个名字多个值

页面代码:

<p th:text="${param.team}">这里替换为请求参数的值</p>

页面显示效果:

如果想要精确获取某一个值,可以使用数组下标。页面代码:

<p th:text="${param.team[0]}">这里替换为请求参数的值</p>
<p th:text="${param.team[1]}">这里替换为请求参数的值</p>

页面显示效果:

八、基本语法:内置对象

1、概念

所谓内置对象其实就是在表达式中可以直接使用的对象。

2、基本内置对象

用法举例:

<h3>表达式的基本内置对象</h3>
<p th:text="${#request.getClass().getName()}">这里显示#request对象的全类名</p>
<p th:text="${#request.getContextPath()}">调用#request对象的getContextPath()方法</p>
<p th:text="${#request.getAttribute('helloRequestAttr')}">调用#request对象的getAttribute()方法,读取属性域</p>

基本思路:

  • 如果不清楚这个对象有哪些方法可以使用,那么就通过getClass().getName()获取全类名,再回到Java环境查看这个对象有哪些方法
  • 内置对象的方法可以直接调用
  • 调用方法时需要传参的也可以直接传入参数

3、公共内置对象

Servlet中将List集合数据存入请求域:

request.setAttribute("aNotEmptyList", Arrays.asList("aaa","bbb","ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());

页面代码:

<p>#list对象isEmpty方法判断集合整体是否为空aNotEmptyList:<span th:text="${#lists.isEmpty(aNotEmptyList)}">测试#lists</span></p>
<p>#list对象isEmpty方法判断集合整体是否为空anEmptyList:<span th:text="${#lists.isEmpty(anEmptyList)}">测试#lists</span></p>

公共内置对象对应的源码位置:

九、基本语法:${}中的表达式本质是OGNL

1、OGNL

OGNL:Object-Graph Navigation Language对象-图 导航语言

2、对象图

从根对象触发,通过特定的语法,逐层访问对象的各种属性。

3、OGNL语法

起点

在Thymeleaf环境下,${}中的表达式可以从下列元素开始:

  • 访问属性域的起点
    • 请求域属性名
    • session
    • application
  • param
  • 内置对象
    • #request
    • #session
    • #lists
    • #strings

属性访问语法

  • 访问对象属性:使用getXxx()、setXxx()方法定义的属性
    • 对象.属性名
  • 访问List集合或数组
    • 集合或数组[下标]
  • 访问Map集合
    • Map集合.key
    • Map集合['key']

十、基本语法:分支与迭代

1、分支

if和unless

让标记了th:if、th:unless的标签根据条件决定是否显示。

示例的实体类:

public class Employee {

    private Integer empId;
    private String empName;
    private Double empSalary;

示例的Servlet代码:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.创建ArrayList对象并填充
    List<Employee> employeeList = new ArrayList<>();

    employeeList.add(new Employee(1, "tom", 500.00));
    employeeList.add(new Employee(2, "jerry", 600.00));
    employeeList.add(new Employee(3, "harry", 700.00));

    // 2.将集合数据存入请求域
    request.setAttribute("employeeList", employeeList);

    // 3.调用父类方法渲染视图
    super.processTemplate("list", request, response);
}

示例的HTML代码:

<table>
    <tr>
        <th>员工编号</th>
        <th>员工姓名</th>
        <th>员工工资</th>
    </tr>
    <tr th:if="${#lists.isEmpty(employeeList)}">
        <td colspan="3">抱歉!没有查询到你搜索的数据!</td>
    </tr>
    <tr th:if="${not #lists.isEmpty(employeeList)}">
        <td colspan="3">有数据!</td>
    </tr>
    <tr th:unless="${#lists.isEmpty(employeeList)}">
        <td colspan="3">有数据!</td>
    </tr>
</table>

if配合not关键词和unless配合原表达式效果是一样的,看自己的喜好。

switch

<h3>测试switch</h3>
<div th:switch="${user.memberLevel}">
    <p th:case="level-1">银牌会员</p>
    <p th:case="level-2">金牌会员</p>
    <p th:case="level-3">白金会员</p>
    <p th:case="level-4">钻石会员</p>
</div>

2、迭代

<h3>测试each</h3>
<table>
    <thead>
        <tr>
            <th>员工编号</th>
            <th>员工姓名</th>
            <th>员工工资</th>
        </tr>
    </thead>
    <tbody th:if="${#lists.isEmpty(employeeList)}">
        <tr>
            <td colspan="3">抱歉!没有查询到你搜索的数据!</td>
        </tr>
    </tbody>
    <tbody th:if="${not #lists.isEmpty(employeeList)}">
        <!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
        <tr th:each="employee : ${employeeList}">
            <td th:text="${employee.empId}">empId</td>
            <td th:text="${employee.empName}">empName</td>
            <td th:text="${employee.empSalary}">empSalary</td>
        </tr>
    </tbody>
</table>

在迭代过程中,可以参考下面的说明使用迭代状态:

<h3>测试each</h3>
<table>
    <thead>
        <tr>
            <th>员工编号</th>
            <th>员工姓名</th>
            <th>员工工资</th>
            <th>迭代状态</th>
        </tr>
    </thead>
    <tbody th:if="${#lists.isEmpty(employeeList)}">
        <tr>
            <td colspan="3">抱歉!没有查询到你搜索的数据!</td>
        </tr>
    </tbody>
    <tbody th:if="${not #lists.isEmpty(employeeList)}">
        <!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
        <tr th:each="employee,empStatus : ${employeeList}">
            <td th:text="${employee.empId}">empId</td>
            <td th:text="${employee.empName}">empName</td>
            <td th:text="${employee.empSalary}">empSalary</td>
            <td th:text="${empStatus.count}">count</td>
        </tr>
    </tbody>
</table>

十一、基本语法:包含其他模板文件

1、应用场景

抽取各个页面的公共部分:

2、创建页面的代码片段

使用th:fragment来给这个片段命名:

<div th:fragment="header">
    <p>被抽取出来的头部内容</p>
</div>

3、包含到有需要的页面

语法 效果
th:insert 把目标的代码片段整个插入到当前标签内部
th:replace 用目标的代码替换当前标签
th:include 把目标的代码片段去除最外层标签,然后再插入到当前标签内部

页面代码举例:

<!-- 代码片段所在页面的逻辑视图 :: 代码片段的名称 -->
<div id="badBoy" th:insert="segment :: header">
    div标签的原始内容
</div>

<div id="worseBoy" th:replace="segment :: header">
    div标签的原始内容
</div>

<div id="worstBoy" th:include="segment :: header">
    div标签的原始内容
</div>

加餐 最凝练的CRUD

1、建模

物理建模

CREATE DATABASE `view-demo`CHARACTER SET utf8;
USE `view-demo`;
CREATE TABLE t_soldier(
    soldier_id INT PRIMARY KEY AUTO_INCREMENT,
    soldier_name CHAR(100),
    soldier_weapon CHAR(100)
);

逻辑建模

public class Soldier {

    private Integer soldierId;
    private String soldierName;
    private String soldierWeapon;

2、总体架构

3、搭建持久化层所需环境

导入jar包

commons-dbutils-1.6.jar druid-1.1.9.jar hamcrest-core-1.3.jar junit-4.12.jar mysql-connector-java-5.1.37-bin.jar

创建jdbc.properties

维护基本连接信息

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/view-demo
username=root
password=123456
initialSize=10
maxActive=20
maxWait=10000

创建JDBCUtils工具类

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class JDBCUtil {

    // 将数据源对象设置为静态属性,保证大对象的单一实例
    private static DataSource dataSource;

    static {

        // 1.创建一个用于存储外部属性文件信息的Properties对象
        Properties properties = new Properties();

        // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
        InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");

        try {

            // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
            properties.load(inputStream);

            // 4.创建数据源对象
            dataSource = DruidDataSourceFactory.createDataSource(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 从数据源中获取数据库连接
     * @return 数据库连接对象
     */
    public static Connection getConnection() {

        Connection connection = null;

        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }

        return connection;

    }

    /**
     * 释放数据库连接
     * @param connection 要执行释放操作的连接对象
     */
    public static void releaseConnection(Connection connection) {

        if (connection != null) {

            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();

                throw new RuntimeException(e);
            }
        }
    }
}

测试能否正常连接数据库:

public class DemoTest {

    @Test
    public void testConnection() {
        Connection connection = JDBCUtil.getConnection();
        System.out.println("connection = " + connection);
    }
}

BaseDao

public class BaseDao<T> {

    private QueryRunner queryRunner = new QueryRunner();

    /**
     * 通用的增删改方法
     * @param sql
     * @param param
     * @return
     */
    public int update(String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        int count = 0;
        try {
            count = queryRunner.update(connection, sql, param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);

        } finally {

            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);

        }

        return count;
    }

    /**
     * 查询单个对象的通用方法
     * @param clazz
     * @param sql
     * @param param
     * @return
     */
    public T getBean(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        T bean = null;
        try {
            bean = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {

            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);

        }

        return bean;
    }

    /**
     * 查询集合对象的通用方法
     * @param clazz
     * @param sql
     * @param param
     * @return
     */
    public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        List<T> beanList = null;

        try {
            beanList = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {

            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);

        }

        return beanList;
    }
}

4、搭建表述层所需环境

本质上就是Thymeleaf所需要的环境

导入jar包

attoparser-2.0.5.RELEASE.jar javassist-3.20.0-GA.jar log4j-1.2.15.jar ognl-3.1.26.jar slf4j-api-1.7.25.jar slf4j-log4j12-1.7.25.jar thymeleaf-3.0.12.RELEASE.jar unbescape-1.1.6.RELEASE.jar

创建ViewBaseServlet

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ViewBaseServlet extends HttpServlet {

    private TemplateEngine templateEngine;

    @Override
    public void init() throws ServletException {

        // 1.获取ServletContext对象
        ServletContext servletContext = this.getServletContext();

        // 2.创建Thymeleaf解析器对象
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);

        // 3.给解析器对象设置参数
        // ①HTML是默认模式,明确设置是为了代码更容易理解
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // ②设置前缀
        String viewPrefix = servletContext.getInitParameter("view-prefix");

        templateResolver.setPrefix(viewPrefix);

        // ③设置后缀
        String viewSuffix = servletContext.getInitParameter("view-suffix");

        templateResolver.setSuffix(viewSuffix);

        // ④设置缓存过期时间(毫秒)
        templateResolver.setCacheTTLMs(60000L);

        // ⑤设置是否缓存
        templateResolver.setCacheable(true);

        // ⑥设置服务器端编码方式
        templateResolver.setCharacterEncoding("utf-8");

        // 4.创建模板引擎对象
        templateEngine = new TemplateEngine();

        // 5.给模板引擎对象设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);

    }

    protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1.设置响应体内容类型和字符集
        resp.setContentType("text/html;charset=UTF-8");

        // 2.创建WebContext对象
        WebContext webContext = new WebContext(req, resp, getServletContext());

        // 3.处理模板数据
        templateEngine.process(templateName, webContext, resp.getWriter());
    }
}

配置web.xml

<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
    <param-name>view-prefix</param-name>
    <param-value>/WEB-INF/view/</param-value>
</context-param>
<context-param>
    <param-name>view-suffix</param-name>
    <param-value>.html</param-value>
</context-param>

创建view目录

5、功能清单

  • 显示首页:浏览器通过index.html访问首页Servlet,然后再解析对应的模板视图
  • 显示列表:在首页点击超链接,跳转到目标页面把所有士兵的信息列表显示出来
  • 删除信息:在列表上点击删除超链接,执行信息的删除操作
  • 新增信息:
    • 在列表页面点击超链接跳转到新增士兵信息的表单页面
    • 在新增信息的表单页面点击提交按钮执行保存
  • 更新信息:
    • 在列表上点击更新超链接,跳转到更新士兵信息的表单页面:表单回显
    • 在更新信息的表单页面点击提交按钮执行更新

6、显示首页

目标

浏览器访问index.html,通过首页Servlet,渲染视图,显示首页。

思路

代码

[1]创建PortalServlet
<servlet>
    <servlet-name>PortalServlet</servlet-name>
    <servlet-class>com.atguigu.demo.servlet.PortalServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PortalServlet</servlet-name>
    <url-pattern>/index.html</url-pattern>
</servlet-mapping>

Servlet代码:

public class PortalServlet extends ViewBaseServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String viewName = "portal";

        super.processTemplate(viewName, request, response);

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}
[2]创建portal.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>士兵信息管理系统</title>
</head>
<body>

    <a th:href="@{/SoldierServlet?method=showList}">显示士兵信息列表</a>

</body>
</html>

7、显示列表

目标

在目标页面显示所有士兵信息,士兵信息是从数据库查询出来的

思路

代码

[1]ModelBaseServlet

创建这个基类的原因是:我们希望每一个模块能够对应同一个Servlet,这个模块所需要调用的所有方法都集中在同一个Servlet中。如果没有这个ModelBaseServlet基类,我们doGet()、doPost()方法可以用来处理请求,这样一来,每一个方法都需要专门创建一个Servlet(就好比咱们之前的LoginServlet、RegisterServlet其实都应该合并为UserServlet)。

public class ModelBaseServlet extends ViewBaseServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 在doGet()方法中调用doPost()方法,这样就可以在doPost()方法中集中处理所有请求
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.从请求参数中获取method对应的数据
        String method = request.getParameter("method");

        // 2.通过反射调用method对应的方法
        // ①获取Class对象
        Class<? extends ModelBaseServlet> clazz = this.getClass();

        try {
            // ②获取method对应的Method对象
            Method methodObject = clazz.getDeclaredMethod(method, HttpServletRequest.class, HttpServletResponse.class);

            // ③打开访问权限
            methodObject.setAccessible(true);

            // ④通过Method对象调用目标方法
            methodObject.invoke(this, request, response);
        } catch (Exception e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }
    }

}
[2]SoldierDao.selectSoldierList()

接口方法:

public interface SoldierDao {

    List<Soldier> selectSoldierList();
}

实现类方法:

public class SoldierDaoImpl extends BaseDao<Soldier> implements SoldierDao {
    @Override
    public List<Soldier> selectSoldierList() {

        String sql = "select soldier_id soldierId,soldier_name soldierName,soldier_weapon soldierWeapon from t_soldier";

        return getBeanList(Soldier.class, sql);
    }
}
[3]SoldierService.getSoldierList()

接口方法:

public interface SoldierService {

    List<Soldier> getSoldierList();
}

实现类方法:

public class SoldierServiceImpl implements SoldierService {

    private SoldierDao soldierDao = new SoldierDaoImpl();

    @Override
    public List<Soldier> getSoldierList() {

        List<Soldier> soldierList = soldierDao.selectSoldierList();

        return soldierList;
    }
}
[4]SoldierServlet.showList()
protected void showList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.调用Service方法获取集合数据
    List<Soldier> soldierList = soldierService.getSoldierList();

    // 2.将集合数据存入请求域
    request.setAttribute("soldierList", soldierList);

    // 3.渲染视图(在渲染的过程中,已经包含了转发)
    processTemplate("list", request, response);
}

8、删除功能

目标

点击页面上的超链接,把数据库表中的记录删除。

思路

[1]先不考虑后续

[2]加上后续返回响应页面

代码

[1]完成删除超链接

<a th:href="@{/SoldierServlet(soldierId=${soldier.soldierId},method='remove')}">删除</a>

关于@{地址}附加请求参数的语法格式:

  • 只有一个请求参数:@{地址(请求参数名=普通字符串)}或@{地址(请求参数名=${需要解析的表达式})}
  • 多个请求参数:@{地址(名=值,名=值)}

官方文档中的说明如下:

[2]Servlet方法
protected void remove(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取士兵信息的id值
    String soldierId = request.getParameter("soldierId");

    // 2.调用Service方法执行删除操作
    soldierService.remove(soldierId);

    // 3.后续……
    // 方案一:还是直接前往list.html,需要重新查询soldierList数据,代码重复
    // 1.调用Service方法获取集合数据
    // List<Soldier> soldierList = soldierService.getSoldierList();

    // 2.将集合数据存入请求域
    // request.setAttribute("soldierList", soldierList);

    // processTemplate("list", request, response);

    // 方案二:直接调用隔壁的showList()
    // 也能实现需求,但是总感觉这样调用方法破坏了程序的结构
    // showList(request, response);

    // 方案三:通过请求转发的方式间接调用showList()方法
    // request.getRequestDispatcher("/SoldierServlet?method=showList").forward(request, response);

    // 方案四:通过请求重定向的方式间接调用showList()方法
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}
[3]Service方法
@Override
    public void remove(String soldierId) {
        soldierDao.delete(soldierId);
    }
[4]Dao方法
@Override
    public void delete(String soldierId) {
        String sql = "delete from t_soldier where soldier_id=?";

        update(sql, soldierId);
    }

9、前往新增信息的表单页面

创建超链接

<a th:href="@{/SoldierServlet?method=toAddPage}">前往新增页面</a>

Servlet

protected void toAddPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    processTemplate("add-page", request, response);
}

创建表单页面

<form th:action="@{/SoldierServlet}" method="post">

    <input type="hidden" name="method" value="saveSoldier" />

    士兵姓名:<input type="text" name="soldierName" /><br/>
    士兵武器:<input type="text" name="soldierWeapon" /><br/>

    <button type="submit">保存</button>

</form>

10、执行保存

目标

提交表单后,将表单数据封装为Soldier对象,然后将Soldier对象保存到数据库。

思路

代码

[1]Servlet方法
protected void saveSoldier(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取请求参数
    String soldierName = request.getParameter("soldierName");
    String soldierWeapon = request.getParameter("soldierWeapon");

    // 2.创建Soldier对象
    Soldier soldier = new Soldier(null, soldierName, soldierWeapon);

    // 3.调用Service方法
    soldierService.saveSoldier(soldier);

    // 4.重定向请求
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}
[2]Service方法
@Override
    public void saveSoldier(Soldier soldier) {

        soldierDao.insertSoldier(soldier);

    }
[3]Dao方法
@Override
    public void insertSoldier(Soldier soldier) {

        String sql = "insert into t_soldier(soldier_name,soldier_weapon) values(?,?)";

        update(sql, soldier.getSoldierName(), soldier.getSoldierWeapon());
    }

11、前往修改信息的表单页面

创建超链接

<a th:href="@{/SoldierServlet(soldierId=${soldier.soldierId},method=toEditPage)}">编辑</a>

Servlet方法

protected void toEditPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数获取soldierId
    String soldierId = request.getParameter("soldierId");

    // 2.根据soldierId查询Soldier对象
    Soldier soldier = soldierService.getSoldierById(soldierId);

    // 3.将Soldier对象存入请求域
    request.setAttribute("soldier", soldier);

    // 4.前往更新的表单页面
    processTemplate("edit-page", request, response);

}

Service方法

@Override
    public Soldier getSoldierById(String soldierId) {
        return soldierDao.selectSoldierByPrimaryKey(soldierId);
    }

Dao方法

@Override
public Soldier selectSoldierByPrimaryKey(String soldierId) {
    String sql = "select soldier_id soldierId,soldier_name soldierName,soldier_weapon soldierWeapon from t_soldier where soldier_id=?";

    return getBean(Soldier.class, sql, soldierId);
}

表单页面

<form th:action="@{/SoldierServlet}" method="post">

    <input type="hidden" name="method" value="updateSoldier" />
    <input type="hidden" name="soldierId" th:value="${soldier.soldierId}" />

    士兵姓名:<input type="text" name="soldierName" th:value="${soldier.soldierName}" /><br/>
    士兵武器:<input type="text" name="soldierWeapon" th:value="${soldier.soldierWeapon}" /><br/>

    <button type="submit">更新</button>

</form>

12、执行更新

Servlet方法

protected void updateSoldier(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取请求参数
    String soldierIdOrigin = request.getParameter("soldierId");
    Integer soldierId = Integer.parseInt(soldierIdOrigin);
    String soldierName = request.getParameter("soldierName");
    String soldierWeapon = request.getParameter("soldierWeapon");

    // 2.封装对象
    Soldier soldier = new Soldier(soldierId, soldierName, soldierWeapon);

    // 3.调用Service方法执行更新
    soldierService.updateSoldier(soldier);

    // 4.重定向请求
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}

Service方法

@Override
    public void updateSoldier(Soldier soldier) {

        soldierDao.updateSoldier(soldier);

    }

Dao方法

@Override
public void updateSoldier(Soldier soldier) {
    String sql = "update t_soldier set soldier_name=?,soldier_weapon=? where soldier_id=?";
    update(sql, soldier.getSoldierName(), soldier.getSoldierWeapon(), soldier.getSoldierId());
}

13、请求字符集设置

  • 设置请求体字符集需要调用request.setCharacterEncoding("UTF-8");
  • request.setCharacterEncoding("UTF-8");要求在所有request.getParameter()前面
  • 在执行子类Servlet方法时,其实都是先调用父类ModelBaseServlet的doPost()方法
  • doPost()方法中包含获取method请求参数的操作
  • 所以最前面的request.getParameter()在doPost()方法中
  • 所以request.setCharacterEncoding("UTF-8");要放在doPost()方法的request.getParameter()前面
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 0.在所有request.getParameter()前面设置解析请求体的字符集
    request.setCharacterEncoding("UTF-8");

    // 1.从请求参数中获取method对应的数据
    String method = request.getParameter("method");

    // ……

13、书城项目第三阶段

一、创建module,迁移代码

1、创建module

把index.jsp删除

2、加入jar包

attoparser-2.0.5.RELEASE.jar commons-dbutils-1.6.jar druid-1.1.9.jar hamcrest-core-1.3.jar javassist-3.20.0-GA.jar junit-4.12.jar log4j-1.2.15.jar mysql-connector-java-5.1.37-bin.jar ognl-3.1.26.jar slf4j-api-1.7.25.jar slf4j-log4j12-1.7.25.jar thymeleaf-3.0.12.RELEASE.jar unbescape-1.1.6.RELEASE.jar

3、从V02迁移代码

①src目录下的Java源代码

  • 在V02的module中的src目录上点右键
  • show in explorer在操作系统的窗口内打开
  • 在操作系统的窗口内复制package目录和jdbc.properties
  • 在操作系统的窗口内粘贴到V03的module中的src目录下
  • 在V03的src目录下,找到上一个版本的Servlet,全部删除
  • 创建两个子包
    • 存放Servlet基类:com.atguigu.bookstore.servlet.base
    • 存放Servlet子类:com.atguigu.bookstore.servlet.model
  • 从view-demo中,将两个基类迁移过来
    • 视图基类:ViewBaseServlet
    • 方法分发基类:ModelBaseServlet

②前端页面

  • 将V02中的pages目录整体复制到V03 module的WEB-INF目录
  • 将V02中的static目录整体复制到V03 module的web目录
  • 将V02中的index.html复制到V03 module的WEB-INF/pages目录下,将来通过Servlet访问

4、显示首页

配置web.xml

<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
    <param-name>view-prefix</param-name>
    <param-value>/WEB-INF/pages/</param-value>
</context-param>
<context-param>
    <param-name>view-suffix</param-name>
    <param-value>.html</param-value>
</context-param>

注意:这里需要将WEB-INF下的view改成pages,和当前项目环境的目录结构一致。

创建PortalServlet

注意:这个PortalServlet映射的地址是/index.html,这样才能保证访问首页时访问它。

<servlet>
    <servlet-name>PortalServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.model.PortalServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PortalServlet</servlet-name>
    <url-pattern>/index.html</url-pattern>
</servlet-mapping>

注意:PortalServlet服务于首页的显示,为了降低用户访问首页的门槛,不能附加任何请求参数,所以不能继承ModelBaseServlet,只能继承ViewBaseServlet。

public class PortalServlet extends ViewBaseServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 将两种请求方式的处理逻辑合并到一个方法
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 声明视图名称
        String viewName = "index";

        // 解析模板视图
        processTemplate(viewName, request, response);
    }
}

调整index.html

  • 加入Thymeleaf名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  • 修改base标签
<base th:href="@{/}" href="/bookstore/"/>

二、完成用户模块

1、重构登录功能

①思路

②实现:创建并组装组件

[1]创建UserServlet

web.xml中的配置:

<servlet>
    <servlet-name>UserServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.model.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>UserServlet</servlet-name>
    <url-pattern>/UserServlet</url-pattern>
</servlet-mapping>

Java代码:

public class UserServlet extends ModelBaseServlet {
    protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    protected void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    protected void toLoginSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }
}

注意:记得修改UserServlet继承的类ModelBaseServlet。

[2]把UserService组件组装到UserServlet中
public class UserServlet extends ModelBaseServlet {

    private UserService userService = new UserServiceImpl();

③实现:前往登录页面

[1]修改首页中登录超链接
<a href="UserServlet?method=toLoginPage" class="login">登录</a>
[2]完成UserServlet.toLoginPage()方法
protected void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String viewName = "user/login";

    processTemplate(viewName, request, response);
}

[3]调整登录页面代码

  • 加入Thymeleaf名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  • 修改base标签
<base th:href="@{/}" href="/bookstore/" />
  • 修改form标签action属性
<form id="loginForm" action="UserServlet" method="post">
  • 增加method请求参数的表单隐藏域
<input type="hidden" name="method" value="doLogin" />
  • 根据条件显示登录失败消息
<p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(message)}" th:text="${message}">这里根据条件显示登录失败消息</p>
[4]回显表单中的用户名

遇到问题:使用th:value="${param.username}"确实实现了服务器端渲染,但是实际打开页面并没有看到。原因是页面渲染顺序:

  • 服务器端渲染
  • 服务器端将渲染结果作为响应数据返回给浏览器
  • 浏览器加载HTML文档
  • 读取到Vue代码后,执行Vue代码
  • Vue又进行了一次浏览器端渲染,覆盖了服务器端渲染的值

解决办法:将服务器端渲染的结果设置到Vue对象的data属性中。

new Vue({
    "el":"#loginForm",
    "data":{
        "username":"[[${param.username}]]",
        "password":""
    },

实现:前往登录成功页面

UserServlet.toLoginSuccessPage()

protected void toLoginSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String viewName = "user/login_success";

    processTemplate(viewName, request, response);
}

login_success.html

<html lang="en" xmlns:th="http://www.thymeleaf.org">
    ……
<base th:href="@{/}" href="/bookstore/"/>

⑤实现:完成登录操作

protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");

    // 2.封装为User对象
    User userForm = new User(null, username, password, null);

    // 3.调用UserService的方法执行登录验证
    try {
        User userDB = userService.doLogin(userForm);

        // 4.登录成功后跳转到登录成功页面
        response.sendRedirect(request.getContextPath() + "/UserServlet?method=toLoginSuccessPage");
    } catch (Exception e) {
        e.printStackTrace();

        // 5.登录失败则显示提示消息
        // ①将登录失败的提示消息存入请求域
        request.setAttribute("message", e.getMessage());

        // ②执行登录页面的模板渲染
        String viewName = "user/login";

        processTemplate(viewName, request, response);
    }

}

2、重构注册功能

①思路

②实现:前往注册页面

[1]修改首页中注册超链接
<a href="UserServlet?method=toRegisterPage" class="register">注册</a>
[2]完成UserServlet.toRegisterPage()方法
protected void toRegisterPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewName = "user/regist";

    processTemplate(viewName, request, response);

}
[3]调整注册页面代码
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    ……
<base th:href="@{/}" href="/bookstore/"/>
    ……
    <form id="registerForm" action="UserServlet" method="post">
                    <input type="hidden" name="method" value="doRegister" />
        ……
        <p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(message)}" th:text="${message}">这里根据条件显示注册失败消息</p>
new Vue({
    "el":"#registerForm",
    "data":{
        "username":"[[${param.username}]]",
        "password":"",
        "passwordConfirm":"",
        "email":"[[${param.email}]]",
        "code":"",
        "usernameCheckMessage":""
    }

③实现:前往注册成功页面

UserServlet:

protected void toRegisterSuccessPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewName = "user/regist_success";

    processTemplate(viewName, request, response);

}

regist_success.html:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
    ……
<base th:href="@{/}" href="/bookstore/"/>

④实现:完成注册操作

protected void doRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 1.从请求参数中获取数据封装为User对象
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    String email = request.getParameter("email");

    User userForm = new User(null, username, password, email);

    // 2.调用UserService的方法执行注册
    try {
        userService.doRegister(userForm);

        // 3.如果没有抛出异常那么就跳转到注册成功的页面
        // 选择重定向的原因:跳转到regist_success.html页面后,用户刷新浏览器不会重复提交注册表单
        response.sendRedirect(request.getContextPath()+"/UserServlet?method=toRegisterSuccessPage");
    } catch (Exception e) {
        e.printStackTrace();

        // 4.如果抛出了异常
        request.setAttribute("message", e.getMessage());

        String viewName = "user/regist";

        processTemplate(viewName, request, response);
    }
}

三、进入后台开发

1、概念辨析

2、访问后台首页

①思路

首页→后台系统超链接→AdminServlet.toPortalPage()→manager.html

②实现:创建AdminServlet

web.xml

<servlet>
    <servlet-name>AdminServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.model.AdminServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AdminServlet</servlet-name>
    <url-pattern>/AdminServlet</url-pattern>
</servlet-mapping>

Java代码:

public class AdminServlet extends ModelBaseServlet {
    protected void toPortalPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String viewName = "manager/manager";

        processTemplate(viewName, request, response);

    }
}

③实现:调整manager.html

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <base th:href="@{/}" href="/bookstore/"/>

然后去除页面上的所有“../”。

④实现:抽取页面公共部分

[1]公共部分内容

三个超链接:

<a href="./book_manager.html" class="order">图书管理</a>
          <a href="./order_manager.html" class="destory">订单管理</a>
          <a href="../../index.html" class="gohome">返回商城</a>
[2]抽取它们的理由

为了实现链接地址修改时:一处修改,处处生效

[3]创建包含代码片段的页面

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

    <!-- 使用th:fragment属性给代码片段命名 -->
    <div th:fragment="navigator">
        <a href="book_manager.html" class="order">图书管理</a>
        <a href="order_manager.html" class="destory">订单管理</a>
        <a href="index.html" class="gohome">返回商城</a>
    </div>

</body>
</html>
[4]在有需要的页面引入片段
<div th:include="segment/admin-navigator :: navigator"></div>

四、后台图书CRUD

C:Create 增

R:Retrieve 查

U:Update 改

D:Delete 删

1、建模

①物理建模

点击这里查看SQL文件

注:上面链接建议点右键→目标另存为,直接打开会显示乱码

②逻辑建模

package com.atguigu.bookstore.entity;

public class Book {

    private Integer bookId;
    private String bookName;
    private String author;
    private Double price;
    private Integer sales;
    private Integer stock;
    private String imgPath;

2、创建并组装组件

①创建Servlet

注意:由于项目分成『前台』和『后台』,所以Servlet也分成两个:

  • 前台:BookPortalServlet
  • 后台:BookManagerServlet

②创建BookService

  • 接口:BookService
  • 实现类:BookServiceImpl

③创建BookDao

  • 接口:BookDao
  • 实现类:BookDaoImpl

④组装

  • 给BookManagerServlet组装BookService
  • 给BookService组装BookDao

3、图书列表显示功能

①思路

manager.html→图书管理超链接→BookManagerServlet→showBookList()→book_manager.html

②实现:修改图书管理超链接

超链接所在文件位置:

WEB-INF/pages/segment/admin-navigator.html

<a href="BookManagerServlet?method=showBookList" class="order">图书管理</a>

③实现:BookManagerServlet.showBookList()

protected void showBookList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.调用Service方法查询图书列表
    List<Book> bookList = bookService.getBookList();

    // 2.将图书列表数据存入请求域
    request.setAttribute("bookList", bookList);

    // 3.执行视图模板渲染
    String viewName = "manager/book_manager";

    processTemplate(viewName, request, response);

}

④实现:BookService.getBookList()

@Override
    public List<Book> getBookList() {
        return bookDao.selectBookList();
    }

⑤实现:BookDao.selectBookList()

@Override
public List<Book> selectBookList() {

    String sql = "select book_id bookId, book_name bookName, author, price, sales, stock, img_path imgPath from t_book order by book_Id desc";

    return getBeanList(Book.class, sql);
}

⑥实现:调整book_manager.html

  • Thymeleaf名称空间
  • base标签
  • 路径中的../和./
  • 包含进来的代码片段

⑦实现:在book_manager.html中迭代显示图书列表

<tbody th:if="${#lists.isEmpty(bookList)}">
    <tr>
        <td colspan="7">抱歉,没有查询到您要的数据!</td>
    </tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(bookList)}">
    <tr th:each="book : ${bookList}">
        <td>
            <img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt=""/>
        </td>
        <td th:text="${book.bookName}">活着</td>
        <td th:text="${book.price}">100.00</td>
        <td th:text="${book.author}">余华</td>
        <td th:text="${book.sales}">200</td>
        <td th:text="${book.stock}">400</td>
        <td>
            <a href="book_edit.html">修改</a><a href="" class="del">删除</a>
        </td>
    </tr>
</tbody>

4、图书删除功能

①思路

book_manager.html→删除超链接→BookManagerServlet.removeBook()→重定向显示列表功能

②实现:删除超链接

<a th:href="@{/BookManagerServlet(method=removeBook,bookId=${book.bookId})}" class="del">删除</a>

③实现:BookManagerServlet.removeBook()

protected void removeBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取bookId
    String bookId = request.getParameter("bookId");

    // 2.调用Service方法执行删除
    bookService.removeBook(bookId);

    // 3.重定向到显示列表功能
    response.sendRedirect(request.getContextPath() + "/BookManagerServlet?method=showBookList");

}

④实现:BookService.removeBook()

@Override
    public void removeBook(String bookId) {
        bookDao.deleteBook(bookId);
    }

⑤实现:BookDao.deleteBook()

@Override
    public void deleteBook(String bookId) {
        String sql = "delete from t_book where book_id=?";

        update(sql, bookId);
    }

5、新增图书功能

①思路

book_manager.html→添加图书超链接→BookManagerServlet.toAddPage()→book_add.html

book_add.html→提交表单→BookManagerServlet.saveBook()→重定向显示列表功能

②实现:添加图书超链接

<a href="BookManagerServlet?method=toAddPage">添加图书</a>

③实现:BookManagerServlet.toAddPage()

protected void toAddPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewName = "book_add";

    processTemplate(viewName, request, response);

}

④实现:book_add.html

由book_edit.html复制出来,然后调整表单标签:

<form action="BookManagerServlet" method="post">
    <input type="hidden" name="method" value="saveBook" />
    <input type="text" name="bookName" placeholder="请输入名称"/>
    <input type="number" name="price" placeholder="请输入价格"/>
    <input type="text" name="author" placeholder="请输入作者"/>
    <input type="number" name="sales" placeholder="请输入销量"/>
    <input type="number" name="stock" placeholder="请输入库存"/>

⑤实现:BookManagerServlet.saveBook()

protected void saveBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取请求参数
    String bookName = request.getParameter("bookName");
    String price = request.getParameter("price");
    String author = request.getParameter("author");
    String sales = request.getParameter("sales");
    String stock = request.getParameter("stock");

    // 2.封装对象

    // ※imgPath按说应该通过文件上传的方式提供,但是现在这个技术还没学
    // 所以暂时使用一个固定值
    String imgPath = "static/uploads/mi.jpg";

    Book book = new Book(null, bookName, author, Double.parseDouble(price), Integer.parseInt(sales), Integer.parseInt(stock), imgPath);

    // 3.调用Service方法执行保存
    bookService.saveBook(book);

    // 4.重定向到显示列表页面
    response.sendRedirect(request.getContextPath() + "/BookManagerServlet?method=showBookList");
}

⑥实现:BookService.saveBook()

@Override
    public void saveBook(Book book) {
        bookDao.insertBook(book);
    }

⑦实现:BookDao.insertBook()

@Override
public void insertBook(Book book) {
    String sql = "insert  into `t_book`(`book_name`,`author`,`price`,`sales`,`stock`,`img_path`) values (?,?,?,?,?,?)";

    update(sql, book.getBookName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
}

6、修改图书功能

①思路

book_manager.html→修改图书超链接→BookManagerServlet.toEditPage()→book_edit.html(表单回显)

book_edit.html→提交表单→BookManagerServlet.updateBook()→重定向显示列表功能

②实现:修改图书超链接

<a th:href="@{/BookManagerServlet(method=toEditPage,bookId=${book.bookId})}">删除</a>

③实现:BookManagerServlet.toEditPage()

protected void toEditPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取bookId
    String bookId = request.getParameter("bookId");

    // 2.根据bookId查询对应的Book对象
    Book book = bookService.getBookById(bookId);

    // 3.把Book对象存入请求域
    request.setAttribute("book", book);

    // 4.渲染视图
    String viewName = "manager/book_edit";

    processTemplate(viewName, request, response);

}

④实现:BookService.getBookById()

@Override
    public Book getBookById(String bookId) {
        return bookDao.selectBookByPrimaryKey(bookId);
    }

⑤实现:BookDao.selectBookByPrimaryKey()

@Override
public Book selectBookByPrimaryKey(String bookId) {

    String sql = "select book_id bookId, book_name bookName, author, price, sales, stock, img_path imgPath from t_book where book_id=?";

    return getBean(Book.class, sql, bookId);
}

⑥实现:book_edit.html(表单回显)

<form action="BookManagerServlet" method="post">
    <input type="hidden" name="method" value="updateBook" />
    <input type="text" name="bookName" th:value="${book.bookName}" placeholder="请输入名称"/>
    <input type="number" name="price" th:value="${book.price}" placeholder="请输入价格"/>
    <input type="text" name="author" th:value="${book.author}" placeholder="请输入作者"/>
    <input type="number" name="sales" th:value="${book.sales}" placeholder="请输入销量"/>
    <input type="number" name="stock" th:value="${book.stock}" placeholder="请输入库存"/>

    <button class="btn">更新</button>

⑦实现:BookManagerServlet.updateBook()

protected void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取请求参数
    String bookId = request.getParameter("bookId");
    String bookName = request.getParameter("bookName");
    String price = request.getParameter("price");
    String author = request.getParameter("author");
    String sales = request.getParameter("sales");
    String stock = request.getParameter("stock");

    // 2.封装对象
    Book book = new Book(Integer.parseInt(bookId), bookName, author, Double.parseDouble(price), Integer.parseInt(sales), Integer.parseInt(stock), null);

    // 3.调用Service方法执行更新
    bookService.updateBook(book);

    // 4.渲染视图
    response.sendRedirect(request.getContextPath() + "/BookManagerServlet?method=showBookList");
}

⑧实现:BookService.updateBook()

@Override
    public void updateBook(Book book) {
        bookDao.updateBook(book);
    }

⑨实现:BookDao.updateBook()

注意:这里不修改imgPath字段

@Override
public void updateBook(Book book) {
    String sql = "update t_book set book_name=?, author=?, price=?, sales=?, stock=? where book_id=?";

    update(sql, book.getBookName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getBookId());
}

五、前台图书展示

1、思路

index.html→PortalServlet.doPost()→把图书列表数据查询出来→渲染视图→页面迭代显示图书数据

2、实现:PortalServlet.doPost()

public class PortalServlet extends ViewBaseServlet {

    private BookService bookService = new BookServiceImpl();

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 将两种请求方式的处理逻辑合并到一个方法
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 查询图书列表数据
        List<Book> bookList = bookService.getBookList();

        // 将图书列表数据存入请求域
        request.setAttribute("bookList", bookList);

        // 声明视图名称
        String viewName = "index";

        // 解析模板视图
        processTemplate(viewName, request, response);

    }

}

3、实现:页面迭代显示图书数据

页面文件:index.html

<div class="list-content" th:if="${#lists.isEmpty(bookList)}">
    抱歉,本商城现在没有上架任何商品
</div>
<div class="list-content" th:if="${not #lists.isEmpty(bookList)}">
    <div class="list-item" th:each="book : ${bookList}">
        <img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
        <p>书名:<span th:text="${book.bookName}">活着</span></p>
        <p>作者:<span th:text="${book.author}">余华</span></p>
        <p>价格:¥<span th:text="${book.price}">66.6</span></p>
        <p>销量:<span th:text="${book.sales}">230</span></p>
        <p>库存:<span th:text="${book.stock}">1000</span></p>
        <button>加入购物车</button>
    </div>
</div>

14、会话控制

一、提出问题与解决方案核心代码

1、提出问题

保持用户登录状态,背后的底层逻辑是:服务器在接收到用户请求的时候,有办法判断这个请求来自于之前的某一个用户。所以保持登录状态,本质上是保持*『会话状态』*

2、解决方案

①结论

使用HttpSession对象,将数据存入会话域就能保持会话状态。

HttpSession session = request.getSession();
session.setAttribute("user", user);

②demo体验

[1]准备环境

参考会话控制demo原始纯净版

[2]创建将数据存入会话域的Servlet

public class HelloWorldServlet extends ModelBaseServlet {
    protected void setValue(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.通过request对象获取session对象
        HttpSession session = request.getSession();

        // 2.设置数据名称和数据的值
        String attrName = "sessionHelloAttrName";
        String attrValue = "sessionHelloAttrValue";

        // 3.将数据存入会话域
        session.setAttribute(attrName, attrValue);

        // 4.渲染视图
        processTemplate("page-target", request, response);
    }
}

[3]在其他页面从会话域取出数据

<div th:if="${#strings.isEmpty(session.sessionHelloAttrName)}">
    没有从会话域获取到数据
</div>
<div th:if="${not #strings.isEmpty(session.sessionHelloAttrName)}">
    <p>从会话域读取到:<span th:text="${session.sessionHelloAttrName}"></span></p>
</div>

[4]操作效果

二、Cookie的工作机制

1、HTTP协议和会话控制

HTTP协议本身是无状态的。单靠HTTP协议本身无法判断一个请求来自于哪一个浏览器,所以也就没法识别用户的身份状态。

2、Cookie介绍

①本质

  • 在浏览器端临时存储数据
  • 键值对
  • 键和值都是字符串类型
  • 数据量很小

②Cookie在浏览器和服务器之间的传递

[1]没有Cookie的状态

在服务器端没有创建Cookie并返回的情况下,浏览器端不会保存Cookie信息。双方在请求和响应的过程中也不会携带Cookie的数据。

[2]创建Cookie对象并返回
// 1.创建Cookie对象
Cookie cookie = new Cookie("cookie-message", "hello-cookie");

// 2.将Cookie对象添加到响应中
response.addCookie(cookie);

// 3.返回响应
processTemplate("page-target", request, response);
[3]服务器端返回Cookie的响应消息头

[4]浏览器拿到Cookie之后

浏览器拿到Cookie之后,以后的每一个请求都会携带Cookie信息。

[5]服务器端读取Cookie的信息
// 1.通过request对象获取Cookie的数组
Cookie[] cookies = request.getCookies();

// 2.遍历数组
for (Cookie cookie : cookies) {
    System.out.println("cookie.getName() = " + cookie.getName());
    System.out.println("cookie.getValue() = " + cookie.getValue());
    System.out.println();
}

③Cookie时效性

[1]理论
  • 会话级Cookie
    • 服务器端并没有明确指定Cookie的存在时间
    • 在浏览器端,Cookie数据存在于内存中
    • 只要浏览器还开着,Cookie数据就一直都在
    • 浏览器关闭,内存中的Cookie数据就会被释放
  • 持久化Cookie
    • 服务器端明确设置了Cookie的存在时间
    • 在浏览器端,Cookie数据会被保存到硬盘上
    • Cookie在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
    • 持久化Cookie到达了预设的时间会被释放

服务器端返回Cookie时附带过期时间的响应消息头如下:

服务器通知浏览器删除Cookie时的响应消息头如下:

[2]代码
// ※给Cookie设置过期时间
// 正数:Cookie的过期时间,以秒为单位
// 负数:表示这个Cookie是会话级的Cookie,浏览器关闭时释放
// 0:通知浏览器立即删除这个Cookie
cookie.setMaxAge(20);
[3]会话和持久化Cookie对比

④Cookie的domain和path

上网时间长了,本地会保存很多Cookie。对浏览器来说,访问互联网资源时不能每次都把所有Cookie带上。浏览器会使用Cookie的domain和path属性值来和当前访问的地址进行比较,从而决定是否携带这个Cookie。

三、Session的工作机制

1、文字描述

前提:浏览器正常访问服务器

  • 服务器端没调用request.getSession()方法:什么都不会发生
  • 服务器端调用了request.getSession()方法
    • 服务器端检查当前请求中是否携带了JSESSIONID的Cookie
      • 有:根据JSESSIONID在服务器端查找对应的HttpSession对象
        • 能找到:将找到的HttpSession对象作为request.getSession()方法的返回值返回
        • 找不到:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回
      • 无:服务器端新建一个HttpSession对象作为request.getSession()方法的返回值返回

2、流程图描述

3、代码验证

// 1.调用request对象的方法尝试获取HttpSession对象
HttpSession session = request.getSession();

// 2.调用HttpSession对象的isNew()方法
boolean wetherNew = session.isNew();

// 3.打印HttpSession对象是否为新对象
System.out.println("wetherNew = " + (wetherNew?"HttpSession对象是新的":"HttpSession对象是旧的"));

// 4.调用HttpSession对象的getId()方法
String id = session.getId();

// 5.打印JSESSIONID的值
System.out.println("JSESSIONID = " + id);

4、时效性

①为什么Session要设置时限

用户量很大之后,Session对象相应的也要创建很多。如果一味创建不释放,那么服务器端的内存迟早要被耗尽。

②设置时限的难点

从服务器端的角度,很难精确得知类似浏览器关闭的动作。而且即使浏览器一直没有关闭,也不代表用户仍然在使用。

③服务器端给Session对象设置最大闲置时间

  • 默认值:1800秒

最大闲置时间生效的机制如下:

④代码验证

// ※测试时效性
// 获取默认的最大闲置时间
int maxInactiveIntervalSecond = session.getMaxInactiveInterval();
System.out.println("maxInactiveIntervalSecond = " + maxInactiveIntervalSecond);

// 设置默认的最大闲置时间
session.setMaxInactiveInterval(15);

⑤强制Session立即失效

session.invalidate();

15、书城项目第四阶段

一、保持登录状态

1、创建module

  • 迁移src目录下的Java代码
  • 迁移web目录下的static目录
  • 迁移web/WEB-INF目录下的lib目录和pages目录
  • 将lib目录下的jar包添加到运行时环境
  • 将旧的web.xml中的配置复制到新module的web.xml中

2、将登录成功的User对象存入会话域

protected void doLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取用户名和密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");

    // 2.封装为User对象
    User userForm = new User(null, username, password, null);

    // 3.调用UserService的方法执行登录验证
    try {
        User userDB = userService.doLogin(userForm);

        // ※保持登录状态,将User对象存入会话域
        HttpSession session = request.getSession();

        // 注意:不要放错,这里要使用从数据库查询得到的User对象
        session.setAttribute("user", userDB);

        // 4.登录成功后跳转到登录成功页面
        response.sendRedirect(request.getContextPath() + "/UserServlet?method=toLoginSuccessPage");
    } catch (Exception e) {
        e.printStackTrace();

        // 5.登录失败则显示提示消息
        // ①将登录失败的提示消息存入请求域
        request.setAttribute("message", e.getMessage());

        // ②执行登录页面的模板渲染
        String viewName = "user/login";

        processTemplate(viewName, request, response);
    }

}

3、修改欢迎信息

①登录成功页面

<span>欢迎<span class="um_span" th:text="${session.user.username}">张总</span>光临尚硅谷书城</span>

②首页

<div class="topbar-right" th:if="${session.user == null}">
    <a href="UserServlet?method=toLoginPage" class="login">登录</a>
    <a href="UserServlet?method=toRegisterPage" class="register">注册</a>
    <a href="pages/cart/cart.html" class="cart iconfont icon-gouwuche">
        购物车
        <div class="cart-num">3</div>
    </a>
    <a href="AdminServlet?method=toPortalPage" class="admin">后台管理</a>
</div>
<!--          登录后风格-->
<div class="topbar-right" th:if="${session.user != null}">
    <span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
    <a href="#" class="register">注销</a>
    <a href="pages/cart/cart.jsp" class="cart iconfont icon-gouwuche">
        购物车
        <div class="cart-num">3</div>
    </a>
    <a href="pages/manager/book_manager.html" class="admin">后台管理</a>
</div>

4、退出登录功能

①页面超链接

<a href="UserServlet?method=logout">注销</a>

②UserServlet.logout()

protected void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    HttpSession session = request.getSession();

    // 退出登录方案一:从会话域移除User对象
    // session.removeAttribute("user");

    // 退出登录方案二:强制Session对象失效
    session.invalidate();

    processTemplate("index", request, response);
}

二、验证码

1、目标

通过让用户填写验证码并在服务器端检查,防止浏览器端使用程序恶意访问。

2、思路

3、操作

①导入jar包

kaptcha-2.3.2.jar

②配置KaptchaServlet

jar包中已经写好了Servlet的Java类,我们只需要在web.xml中配置这个Servlet即可。

<servlet>
    <servlet-name>KaptchaServlet</servlet-name>
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>KaptchaServlet</servlet-name>
    <url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>

③通过页面访问测试

http://localhost:8080/bookstore/KaptchaServlet

④在注册页面显示验证码图片

<img src="KaptchaServlet" alt="" />

⑤调整验证码图片的显示效果

[1]去掉边框

KaptchaServlet会在初始化时读取init-param,而它能够识别的init-param在下面类中:

com.google.code.kaptcha.util.Config

web.xml中具体配置如下:

<servlet>
    <servlet-name>KaptchaServlet</servlet-name>
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>

    <!-- 通过配置初始化参数影响KaptchaServlet的工作方式 -->
    <!-- 可以使用的配置项参考com.google.code.kaptcha.util.Config类 -->
    <!-- 配置kaptcha.border的值为false取消图片边框 -->
    <init-param>
        <param-name>kaptcha.border</param-name>
        <param-value>no</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>KaptchaServlet</servlet-name>
    <url-pattern>/KaptchaServlet</url-pattern>
</servlet-mapping>

开发过程中的工程化细节:

no、false、none等等单词从含义上来说都表示『没有边框』这个意思,但是这里必须使用no。

参考的依据是下面的源码:

public boolean getBoolean(String paramName, String paramValue, boolean defaultValue) {
    boolean booleanValue;
    if (!"yes".equals(paramValue) && !"".equals(paramValue) && paramValue != null) {
        if (!"no".equals(paramValue)) {
            throw new ConfigException(paramName, paramValue, "Value must be either yes or no.");
        }

        booleanValue = false;
    } else {
        booleanValue = defaultValue;
    }

    return booleanValue;
}
[2]设置图片大小
<img style="width: 150px; height: 40px;" src="KaptchaServlet" alt="" />

⑥点击图片刷新

[1]目的

验证码图片都是经过刻意扭曲、添加了干扰、角度偏转,故意增加了识别的难度。所以必须允许用户在看不出来的时候点击图片刷新,生成新的图片重新辨认。

[2]实现的代码

修改图片的img标签:

<img @click="refreshCodeImage" style="width: 150px; height: 40px;" src="KaptchaServlet" alt="" />

Vue代码:将refreshCodeImage()单击响应函数声明到注册表单验证功能的Vue对象的methods属性中

,
"refreshCodeImage":function () {

    // 通过event事件对象的target属性获取当前正在点击的img标签
    var imgEle = event.target;

    // 设置img标签的src属性
    imgEle.src = "KaptchaServlet?random=" + Math.random();
}

⑦执行注册前检查验证码

[1]确认KaptchaServlet将验证码存入Session域时使用的属性名

通过查看源码,找到验证码存入Session域时使用的属性名是:

KAPTCHA_SESSION_KEY

[2]在执行注册的方法中添加新的代码
protected void doRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // ※检查验证码
        // 1.从请求参数中获取用户提交的验证码
        String codeForm = request.getParameter("code");

        // 2.获取Session域中保存的验证码
        HttpSession session = request.getSession();
        String codeSystem = (String) session.getAttribute("KAPTCHA_SESSION_KEY");

        // 3.将表单验证码和系统验证码进行比较
        if (Objects.equals(codeForm, codeSystem)) {

            // 4.如果比较后发现二者一致,则将用过的验证码从Session域移除
            session.removeAttribute("KAPTCHA_SESSION_KEY");

        }else{

            // 5.如果比较后发现二者不一致,则返回注册的表单页面显示提示信息
            request.setAttribute("message", "验证码不正确,请重新填写");

            String viewName = "user/regist";

            processTemplate(viewName, request, response);

            // 6.停止执行当前方法
            return ;
        }

        // 后续是原来的代码……

三、购物车

1、功能清单

  • 添加购物车
  • 显示购物车信息
  • 修改购物车中具体商品的数量
  • 删除购物车中某个具体商品
  • 清空购物车

2、创建购物车模型

①购物车详情类:CartItem

public class CartItem {

    private String bookId;
    private String bookName;
    private String imgPath;
    private Double price;
    private Integer count;
    private Double amount;

    // 获取金额时需要计算得到
    public Double getAmount() {
        return this.count * this.price;
    }

②购物车类:Cart

public class Cart {

    private Map<String, CartItem> cartItemMap = new HashMap<>();

    // 添加购物车
    public void addCartItem(Book book) {

        // 1.获取当前Book对象的id值
        String bookId = book.getBookId() + "";

        // 2.根据id值到Map中检查是否已存在
        if (cartItemMap.containsKey(bookId)) {
            // 3.如果存在,则给原有的CartItem增加数量
            CartItem cartItem = cartItemMap.get(bookId);

            int newCount = cartItem.getCount() + 1;

            cartItem.setCount(newCount);
        } else {
            // 4.如果不存在,则创建新的CartItem对象
            CartItem cartItem = new CartItem();
            cartItem.setBookId(bookId);
            cartItem.setBookName(book.getBookName());
            cartItem.setCount(1);
            cartItem.setImgPath(book.getImgPath());
            cartItem.setPrice(book.getPrice());

            // 5.将新的CartItem对象存入cartItemMap
            cartItemMap.put(bookId, cartItem);
        }

    }

    // 从购物车中删除CartItem
    public void removeCartItem(String bookId) {

        cartItemMap.remove(bookId);

    }

    // 把某个CartItem的数量修改为指定值
    public void updateItemCount(String bookId, Integer newCount) {

        // 根据bookId从Map中获取对应的CartItem对象
        CartItem cartItem = cartItemMap.get(bookId);

        // 设置新的数量值
        cartItem.setCount(newCount);

    }

    // 把某个CartItem的数量+1
    public void itemCountIncrease(String bookId) {

        CartItem cartItem = cartItemMap.get(bookId);

        cartItem.setCount(cartItem.getCount() + 1);

    }

    // 把某个CartItem的数量-1
    public void itemCountDecrease(String bookId) {

        CartItem cartItem = cartItemMap.get(bookId);

        cartItem.setCount(cartItem.getCount() - 1);

        if (cartItem.getCount() == 0) {
            removeCartItem(bookId);
        }
    }

    public Map<String, CartItem> getCartItemMap() {
        return cartItemMap;
    }

    // 计算总数量
    public Integer getTotalCount() {

        // 1.声明一个变量用于存储累加结果
        Integer sum = 0;

        // 2.遍历Map集合
        Set<String> keySet = cartItemMap.keySet();

        for (String key : keySet) {
            CartItem cartItem = cartItemMap.get(key);
            Integer count = cartItem.getCount();
            sum = sum + count;
        }

        // 3.返回累加结果
        return sum;
    }

    // 计算总金额
    public Double getTotalAmount() {

        // 1.声明一个变量用于存储累加结果
        Double sum = 0.0;

        // 2.遍历Map集合
        Set<String> keySet = cartItemMap.keySet();

        for (String key : keySet) {
            CartItem cartItem = cartItemMap.get(key);
            Double amount = cartItem.getAmount();

            sum = sum + amount;
        }

        // 3.返回累加结果
        return sum;

    }
}

3、添加购物车功能

①目标

在首页点击『加入购物车』将该条图书加入『购物车』。

②思路

首页→加入购物车→CartServlet.addCart()→执行添加操作→回到首页

③代码实现

[1]创建CartServlet

public class CartServlet extends ModelBaseServlet {

    private BookService bookService = new BookServiceImpl();

    protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.从请求参数中获取bookId
        String bookId = request.getParameter("bookId");

        // 2.根据bookId查询图书数据
        Book book = bookService.getBookById(bookId);

        // 3.获取Session对象
        HttpSession session = request.getSession();

        // 4.尝试从Session域获取购物车对象
        Cart cart = (Cart) session.getAttribute("cart");

        // 5.判断Cart对象是否存在
        if (cart == null) {

            // 6.如果不存在,则创建新的Cart对象
            cart = new Cart();

            // 7.将新创建的Cart对象存入Session域
            session.setAttribute("cart", cart);
        }

        // 8.添加购物车
        cart.addCartItem(book);

        // 9.回首页
        response.sendRedirect(request.getContextPath() + "/index.html");
    }

}
[2]index.html页面

购物车数量显示:

<!--          登录后风格-->
<div class="topbar-right" th:if="${session.user != null}">
    <span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
    <a href="#" class="register">注销</a>
    <a href="pages/cart/cart.jsp" class="cart iconfont icon-gouwuche">
        购物车
        <div class="cart-num" th:if="${session.cart != null}" th:text="${session.cart.totalCount}">3</div>
    </a>
    <a href="pages/manager/book_manager.html" class="admin">后台管理</a>
</div>

加入购物车:

<div class="list-content" th:if="${not #lists.isEmpty(bookList)}">
    <div class="list-item" th:each="book : ${bookList}">
        <img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
        <p>书名:<span th:text="${book.bookName}">活着</span></p>
        <p>作者:<span th:text="${book.author}">余华</span></p>
        <p>价格:¥<span th:text="${book.price}">66.6</span></p>
        <p>销量:<span th:text="${book.sales}">230</span></p>
        <p>库存:<span th:text="${book.stock}">1000</span></p>
        <!--<button>加入购物车</button>-->
        <a th:href="@{/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
    </div>
</div>

④CSS样式

.books-list .list-content .list-item a {
  display: block;
  width: 80px;
  height: 30px;
  border: none;
  line-height: 30px;
  background-color: #39987c;
  margin-top: 5px;
  outline: none;
  color: #fff;
  cursor:pointer;
  font-size:12px;
  text-align:center;
}

如果修改完成后页面效果没有改变,可以使用Ctrl+F5强制刷新。

4、显示购物车页面

①目标

把购物车信息在专门的页面显示出来

②思路

首页→购物车超链接→CartServlet.showCart()→cart.html

③代码实现

[1]购物车超链接

登录状态和未登录状态

<a href="CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
[2]CartServlet
protected void showCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewName = "cart/cart";

    processTemplate(viewName, request, response);

}
[3]cart.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <base th:href="@{/}"/>
    ……
    <div class="header-right" th:include="segment/welcome :: welcome-page"></div>
    ……
    <tbody th:if="${session.cart == null || session.cart.cartItemMap.size() == 0}">
        <tr>
            <td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
        </tr>
    </tbody>
    <tbody th:if="${session.cart != null && session.cart.cartItemMap.size() > 0}">
    <tr th:each="cartItemEntry : ${session.cart.cartItemMap}">
        <td>
            <img th:src="${cartItemEntry.value.imgPath}" src="static/uploads/huozhe.jpg" alt=""/>
        </td>
        <td th:text="${cartItemEntry.value.bookName}">活着</td>
        <td>
            <span class="count">-</span>
            <input class="count-num" type="text" th:value="${cartItemEntry.value.count}" value="1"/>
            <span class="count">+</span>
        </td>
        <td th:text="${cartItemEntry.value.price}">36.8</td>
        <td th:text="${cartItemEntry.value.amount}">36.8</td>
        <td><a href="">删除</a></td>
    </tr>
    </tbody>
</table>
<div class="footer" th:if="${session.cart != null && session.cart.cartItemMap.size() > 0}">
    <div class="footer-left">
        <a href="#" class="clear-cart">清空购物车</a>
        <a href="index.html">继续购物</a>
    </div>
    <div class="footer-right">
        <div>共<span th:text="${session.cart.totalCount}">3</span>件商品</div>
        <div class="total-price">总金额<span th:text="${session.cart.totalAmount}">99.9</span>元</div>
        <a class="pay" href="checkout.html">去结账</a>
    </div>
</div>

5、清空购物车

①目标

当用户确定点击清空购物车,将Session域中的Cart对象移除。

②思路

cart.html→清空购物车超链接→绑定单击响应函数→confirm()确认→确定→CartServlet.clearCart()→从Session域移除Cart对象

③代码实现

[1]清空购物车超链接
<a href="CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
[2]绑定单击响应函数
(1)引入Vue
<script src="static/script/vue.js" type="text/javascript" charset="utf-8"></script>
(2)编写Vue代码
new Vue({
    "el":"#appCart",
    "methods":{
        "clearCart":function () {
            // 1.使用confirm()弹框向用户确认是否要清空
            var confirmResult = confirm("您真的要清空购物车吗?");

            // 2.如果用户点击了取消
            if (!confirmResult) {

                // 3.调用事件对象的方法阻止超链接跳转页面
                event.preventDefault();
            }

        }
    }
});
[3]CartServlet.clearCart()
protected void clearCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.通过request对象获取Session对象
    HttpSession session = request.getSession();

    // 2.将Session域的Cart对象移除
    session.removeAttribute("cart");

    // 3.渲染视图
    String viewName = "cart/cart";

    processTemplate(viewName, request, response);
}

6、减号

①目标

  • 在大于1的数值基础上-1:执行-1的逻辑
  • 在1的基础上-1:执行删除item的逻辑

②思路

减号span绑定单击响应函数→获取文本框中的数据:当前数量→检查数量是否大于1

  • 数量大于1:发送请求访问CartServlet.decrease()方法→回到cart.html
  • 数量等于1:confirm确认→发送请求访问CartServlet.removeItem()方法→回到cart.html

③后端代码

CartServlet.decrease()

protected void decrease(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.通过请求参数获取bookId
    String bookId = request.getParameter("bookId");

    // 2.通过request对象获取Session对象
    HttpSession session = request.getSession();

    // 3.从Session域获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
    if (cart != null) {

        // 4.从购物车对象中根据bookId执行-1操作
        cart.itemCountDecrease(bookId);

    }

    // 5.回到购物车页面
    String viewName = "cart/cart";

    processTemplate(viewName, request, response);

}

④前端代码

HTML代码:

<td>
    <input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
    <span @click="itemDecrease" class="count">-</span>
    ……
</td>

Vue代码:

,
"itemDecrease":function () {

    // 0.不管执行两种逻辑中的哪一种,都需要将当前item的bookId发送到Servlet
    // event:事件对象
    // event.target:当前事件操作的控件(HTML元素)
    // previousSibling:上一个兄弟节点。当前span标签的上一个兄弟节点是空格,所以要再找上一个兄弟
    var bookId = event.target.previousSibling.previousSibling.value;

    console.log("bookId="+bookId);

    // 1.获取文本框中的value属性值:当前item的数量
    // event:事件对象
    // event.target:当前事件操作的控件(HTML元素)
    // nextSibling:下一个兄弟节点。当前span标签的下一个兄弟节点是空格,所以要再找下一个兄弟
    // value:获取input标签的value属性值
    var currentCount = event.target.nextSibling.nextSibling.value;

    console.log("currentCount="+currentCount);

    // 2.判断currentCount是否大于1
    if (currentCount > 1) {

        // 3.应用-1逻辑
        console.log("currentCount > 1");

        window.location.href = "CartServlet?method=decrease&bookId="+bookId;

    } else {

        // 通过DOM树形结构获取图书名称数据
        // event:事件对象
        // event.target:当前事件操作的控件(HTML元素)
        // parentNode:访问父节点
        // parentNode.getElementsByTagName():在父节点范围内根据标签名称查找元素,返回数组
        // innerHTML:返回标签内部HTML代码
        var bookName = event.target.parentNode.parentNode.getElementsByTagName("td")[1].innerHTML;

        // 4.应用删除item的逻辑
        var confirmResult = confirm("你真的要从购物车中删除『"+bookName+"』这条记录吗?");

        if (confirmResult) {

            window.location.href = "CartServlet?method=removeItem&bookId="+bookId;
        }

    }
}

7、删除

①目标

点击删除超链接后,把对应的CartItem从Cart中删除

②思路

cart.html→删除超链接→CartServlet.removeItem()→回到cart.html

③代码实现

[1]后端代码

CartServlet.removeItem()

protected void removeItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数获取bookId
    String bookId = request.getParameter("bookId");

    // 2.通过request对象获取Session对象
    HttpSession session = request.getSession();

    // 3.从Session域获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
    if (cart != null) {

        // 4.执行删除item操作
        cart.removeCartItem(bookId);

    }

    // 5.回到购物车页面
    String viewName = "cart/cart";

    processTemplate(viewName, request, response);

}

[2]前端代码

HTML代码:

<a @click="removeConfirm" th:href="@{/CartServlet(method=removeItem,bookId=${cartItemEntry.value.bookId})}">删除</a>

Vue代码:

,
"removeConfirm":function () {

    // 通过DOM树形结构获取图书名称数据
    // event:事件对象
    // event.target:当前事件操作的控件(HTML元素)
    // parentNode:访问父节点
    // parentNode.getElementsByTagName():在父节点范围内根据标签名称查找元素,返回数组
    // innerHTML:返回标签内部HTML代码
    var bookName = event.target.parentNode.parentNode.getElementsByTagName("td")[1].innerHTML;
    console.log("bookName=" + bookName);

    if(!confirm("你真的要从购物车中删除『"+bookName+"』这条记录吗?")) {
        event.preventDefault();
    }

}

8、文本框修改

①目标

用户在文本框输入新数据后,根据用户输入在Session中的Cart中修改CartItem中的count

②思路

cart.html→前端数据校验→CartServlet.changeCount()→回到cart.html

③代码实现

[1]后端代码

CartServlet.changeCount()

protected void changeCount(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数获取bookId和newCount
    String bookId = request.getParameter("bookId");
    Integer newCount = Integer.parseInt(request.getParameter("newCount"));

    // 2.通过request对象获取Session对象
    HttpSession session = request.getSession();

    // 3.从Session域获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
    if (cart != null) {

        // 4.执行修改数量操作
        cart.updateItemCount(bookId, newCount);

    }

    // 5.回到购物车页面
    String viewName = "cart/cart";

    processTemplate(viewName, request, response);

}
[2]前端代码

HTML代码:

<td>
    <input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
    ……
    <input @change="itemCountChange" class="count-num" type="text" th:value="${cartItemEntry.value.count}" value="1"/>
    ……
</td>

Vue代码:

,
"itemCountChange":function () {

    // 1.获取文本框中输入的数据
    var newCount = event.target.value;

    // 6.排除小数或零或负数的情况
    var regExp = /^[0-9]+$/;

    if (!regExp.test(newCount) || newCount == 0) {

        // 7.如果用户输入的数据不是数字,给出提示
        alert("请输入正整数!");

        // 8.将文本框恢复为默认值
        event.target.value = event.target.defaultValue;

        // 9.当前函数停止执行
        return ;
    }

    // 10.获取bookId值
    var bookId = event.target.parentNode.getElementsByTagName("input")[0].value;
    console.log("bookId="+bookId);

    // 11.发送请求执行修改
    window.location.href = "CartServlet?method=changeCount&bookId="+bookId+"&newCount="+newCount;
}

9、加号

①目标

告诉Servlet将Session域中Cart对象里面对应的CartItem执行count+1操作

②思路

加号span绑定单击响应函数→获取文本框中的数据:当前数量→CartServlet.increase()→回到cart.html

③代码实现

[1]后端代码

CartServlet.increase()

protected void increase(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.通过请求参数获取bookId
    String bookId = request.getParameter("bookId");

    // 2.通过request对象获取Session对象
    HttpSession session = request.getSession();

    // 3.从Session域获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // ※考虑到如果Session超时会导致购物车数据丢失,所以先判断购物车数据是否为空
    if (cart != null) {

        // 4.从购物车对象中根据bookId执行-1操作
        cart.itemCountIncrease(bookId);

    }

    // 5.回到购物车页面
    String viewName = "cart/cart";

    processTemplate(viewName, request, response);

}
[2]前端代码

HTML代码:

<td>
    <input type="hidden" name="bookId" th:value="${cartItemEntry.value.bookId}" />
    ……
    <span @click="itemIncrease" class="count">+</span>
</td>

Vue代码:

,
"itemIncrease":function () {

    // 1.获取bookId值
    var bookId = event.target.parentNode.getElementsByTagName("input")[0].value;

    // 2.发送请求执行+1
    window.location.href = "CartServlet?method=increase&bookId="+bookId;

}

10、Double数据运算过程中精度调整

①问题现象

②解决方案

  • 使用BigDecimal类型来进行Double类型数据运算
  • 创建BigDecimal类型对象时将Double类型的数据转换为字符串

Cart类:

// 计算总金额
public Double getTotalAmount() {

    // 1.声明一个变量用于存储累加结果
    BigDecimal sum = new BigDecimal("0.0");

    // 2.遍历Map集合
    Set<String> keySet = cartItemMap.keySet();

    for (String key : keySet) {
        CartItem cartItem = cartItemMap.get(key);
        Double amount = cartItem.getAmount();

        sum = sum.add(new BigDecimal(amount + ""));
    }

    // 3.返回累加结果
    return sum.doubleValue();

}

CartItem类:

// 获取金额时需要计算得到
public Double getAmount() {

    BigDecimal bigDecimalCount = new BigDecimal(this.count + "");
    BigDecimal bigDecimalPrice = new BigDecimal(this.price + "");

    BigDecimal multiReuslt = bigDecimalCount.multiply(bigDecimalPrice);

    return multiReuslt.doubleValue();
}

16、过滤器

一、过滤器简介

1、通过类比了解过滤器作用

①坐地铁

②登录检查

2、过滤器的三要素

①拦截

过滤器之所以能够对请求进行预处理,关键是对请求进行拦截,把请求拦截下来才能够做后续的操作。而且对于一个具体的过滤器,它必须明确它要拦截的请求,而不是所有请求都拦截。

②过滤

根据业务功能实际的需求,看看在把请求拦截到之后,需要做什么检查或什么操作,写对应的代码即可。

③放行

过滤器完成自己的任务或者是检测到当前请求符合过滤规则,那么可以将请求放行。所谓放行,就是让请求继续去访问它原本要访问的资源。

友情提示:将来学习SpringMVC时,会学习SpringMVC中的『拦截器』,同样具备三要素。

二、HelloWord

1、思路

2、操作步骤

①准备工作

  • 创建module
  • 加入Thymeleaf环境
  • 完成首页访问功能
  • 创建Target01Servlet以及target01.html
  • 创建SpecialServlet以及special.html

②创建Filter

[1]创建Target01Filter类
  • 要点1:实现javax.servlet.Filter接口
  • 要点2:在doFilter()方法中执行过滤
  • 要点3:如果满足过滤条件使用 chain.doFilter(request, response);放行
  • 要点4:如果不满足过滤条件转发或重定向请求
    • 附带问题:Thymeleaf模板渲染。这里我们选择的解决办法是跳转到一个Servlet,由Servlet负责执行模板渲染返回页面。
public class Target01Filter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1.打印一句话表明Filter执行了
        System.out.println("过滤器执行:Target01Filter");

        // 2.检查是否满足过滤条件
        // 人为设定一个过滤条件:请求参数message是否等于monster
        // 等于:放行
        // 不等于:将请求跳转到另外一个页面
        // ①获取请求参数
        String message = request.getParameter("message");

        // ②检查请求参数是否等于monster
        if ("monster".equals(message)) {

            // ③执行放行
            // FilterChain对象代表过滤器链
            // chain.doFilter(request, response)方法效果:将请求放行到下一个Filter,
            // 如果当前Filter已经是最后一个Filter了,那么就将请求放行到原本要访问的目标资源
            chain.doFilter(request, response);

        }else{

            // ④跳转页面
            request.getRequestDispatcher("/SpecialServlet?method=toSpecialPage").forward(request, response);

        }

    }

    @Override
    public void destroy() {

    }
}
[2]配置Target01Filter类

这一步也可以叫『注册』。

<!-- 配置Target01Filter -->
<filter>
    <!-- 配置Filter的友好名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 配置Filter的全类名,便于Servlet容器创建Filter对象 -->
    <filter-class>com.atguigu.filter.filter.Target01Filter</filter-class>
</filter>

<!-- 配置Filter要拦截的目标资源 -->
<filter-mapping>
    <!-- 指定这个mapping对应的Filter名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 通过请求地址模式来设置要拦截的资源 -->
    <url-pattern>/Target01Servlet</url-pattern>
</filter-mapping>

三、过滤器生命周期

1、回顾Servlet生命周期

Servlet生命周期

2、Filter生命周期

和Servlet生命周期类比,Filter生命周期的关键区别是:在Web应用启动时创建对象

生命周期阶段 执行时机 执行次数
创建对象 Web应用启动时 一次
初始化 创建对象后 一次
拦截请求 接收到匹配的请求 多次
销毁 Web应用卸载前 一次

四、过滤器匹配规则

本节要探讨的是在filter-mapping中如何将Filter同它要拦截的资源关联起来。

1、精确匹配

指定被拦截资源的完整路径:

<!-- 配置Filter要拦截的目标资源 -->
<filter-mapping>
    <!-- 指定这个mapping对应的Filter名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 通过请求地址模式来设置要拦截的资源 -->
    <url-pattern>/Target01Servlet</url-pattern>
</filter-mapping>

2、模糊匹配

相比较精确匹配,使用模糊匹配可以让我们创建一个Filter就能够覆盖很多目标资源,不必专门为每一个目标资源都创建Filter,提高开发效率。

①前杠后星

在我们配置了url-pattern为/user/*之后,请求地址只要是/user开头的那么就会被匹配。

<filter-mapping>
    <filter-name>Target02Filter</filter-name>

    <!-- 模糊匹配:前杠后星 -->
    <!--
        /user/Target02Servlet
        /user/Target03Servlet
        /user/Target04Servlet
    -->
    <url-pattern>/user/*</url-pattern>
</filter-mapping>

极端情况:/*匹配所有请求

②前星后缀

下面我们使用png图片来测试后缀拦截的效果,并不是只能拦截png扩展名。

[1]创建一组img标签
<img th:src="@{/./images/img017.png}"/><br/>
    <img th:src="@{/./images/img018.png}"/><br/>
    <img th:src="@{/./images/img019.png}"/><br/>
    <img th:src="@{/./images/img020.png}"/><br/>
    <img th:src="@{/./images/img024.png}"/><br/>
    <img th:src="@{/./images/img025.png}"/><br/>
[2]创建Filter
<filter>
    <filter-name>Target04Filter</filter-name>
    <filter-class>com.atguigu.filter.filter.Target04Filter</filter-class>
</filter>
<filter-mapping>
    <filter-name>Target04Filter</filter-name>
    <url-pattern>*.png</url-pattern>
</filter-mapping>

③前杠后缀,星号在中间

配置方式如下:

<url-pattern>/*.png</url-pattern>

按照这个配置启动Web应用时会抛出异常:

java.lang.IllegalArgumentException: Invalid /*.png in filter mapping

结论:这么配是\不允许*的!*

3、匹配Servlet名称[了解]

<filter-mapping>
    <filter-name>Target05Filter</filter-name>

    <!-- 根据Servlet名称匹配 -->
    <servlet-name>Target01Servlet</servlet-name>
</filter-mapping>

五、过滤器链

1、概念

  • 多个Filter的拦截范围如果存在重合部分,那么这些Filter会形成Filter链
  • 浏览器请求重合部分对应的目标资源时,会依次经过Filter链中的每一个Filter。
  • Filter链中每一个Filter执行的顺序是由web.xml中filter-mapping配置的顺序决定的。

2、测试

①准备工作

创建超链接访问一个普通的Servlet即可。

②创建多个Filter拦截Servlet

<filter-mapping>
    <filter-name>TargetChain03Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>TargetChain02Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>TargetChain01Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>

控制台打印效果:

过滤器执行:Target03Filter[模糊匹配 前杠后星 /*] 测试Filter链:TargetChain03Filter 测试Filter链:TargetChain02Filter 测试Filter链:TargetChain01Filter

17、书城项目第五阶段

一、登录检查

1、目标

把项目中需要保护的功能保护起来,没有登录不允许访问。但是我们不考虑后台登录检查,仅完成前台登录检查。

  • 购物车功能
  • 订单功能

2、思路

3、代码实现

①拦截受保护资源的请求

购物车资源地址:/protected/CartServlet

订单资源地址:/protected/OrderServlet

Filter拦截的地址:/protected/*

②对访问购物车资源的地址进行修改

[1]首页加入购物车
<a th:href="@{/protected/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
[2]首页显示购物车
<a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
[3]cart.html清空购物车
<a @click="clearCart" href="protected/CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
[4]cart.html删除超链接
<a @click="removeConfirm" th:href="@{/protected/CartServlet(method=removeItem,bookId=${cartItemEntry.value.bookId})}">删除</a>
[5]cart.html中Vue代码

凡是涉及到window.location.href都需要修改:

window.location.href = "protected/CartServlet?method=decrease&bookId="+bookId;
……

③web.xml中修改CartServlet的url-pattern

<servlet>
    <servlet-name>CartServlet</servlet-name>
    <servlet-class>com.atguigu.bookstore.servlet.model.CartServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>CartServlet</servlet-name>
    <url-pattern>/protected/CartServlet</url-pattern>
</servlet-mapping>

④创建执行登录检查的Filter

[1]Filter类

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 扫描service相关的bean -->
    <context:component-scan base-package="com.kuang.service" />

    <!--BookServiceImpl注入到IOC容器中-->
    <bean id="BookServiceImpl" class="com.kuang.service.BookServiceImpl">
        <property name="bookMapper" ref="bookMapper"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource" />
    </bean>

 </beans>
[2]注册Filter类
<filter>
    <filter-name>LoginFilter</filter-name>
    <filter-class>com.atguigu.bookstore.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LoginFilter</filter-name>
    <url-pattern>/protected/*</url-pattern>
</filter-mapping>

二、结账

1、创建订单模型

①物理建模

[1]t_order表
CREATE TABLE t_order(
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    order_sequence VARCHAR(200),
    create_time VARCHAR(100),
    total_count INT,
    total_amount DOUBLE,
    order_status INT,
    user_id INT
);
字段名 字段作用
order_id 主键
order_sequence 订单号
create_time 订单创建时间
total_count 订单的总数量
total_amount 订单的总金额
order_status 订单的状态
user_id 下单的用户的id
  • 虽然order_sequence也是一个不重复的数值,但是不使用它作为主键。数据库表的主键要使用没有业务功能的字段来担任。
  • 订单的状态
    • 待支付(书城项目中暂不考虑)
    • 已支付,待发货:0
    • 已发货:1
    • 确认收货:2
    • 发起退款或退货(书城项目中暂不考虑)
  • 用户id
    • 从逻辑和表结构的角度来说,这其实是一个外键。
    • 但是开发过程中建议先不要加外键约束:因为开发过程中数据尚不完整,加了外键约束开发过程中使用测试数据非常不方便,建议项目预发布时添加外键约束测试。
[2]t_order_item表
CREATE TABLE t_order_item(
    item_id INT PRIMARY KEY AUTO_INCREMENT,
    book_name VARCHAR(20),
    price DOUBLE,
    img_path VARCHAR(50),
    item_count INT,
    item_amount DOUBLE,
    order_id VARCHAR(20)
);
字段名称 字段作用
item_id 主键
book_name 书名
price 单价
item_count 当前订单项的数量
item_amount 当前订单项的金额
order_id 当前订单项关联的订单表的主键

说明:book_name、author、price这三个字段其实属于t_book表,我们把它们加入到t_order_item表中,其实并不符合数据库设计三大范式。这里做不符合规范的操作的原因是:将这几个字段加入当前表就不必在显示数据时和t_book表做关联查询,提高查询的效率,这是一种变通的做法。

②逻辑模型

[1]Order类
public class Order {

    private Integer orderId;
    private String orderSequence;
    private String createTime;
    private Integer totalCount;
    private Double totalAmount;
    private Integer orderStatus = 0;
    private Integer userId;
[2]OrdrItem类
public class OrderItem {

    private Integer itemId;
    private String bookName;
    private Double price;
    private String imgPath;
    private Integer itemCount;
    private Double itemAmount;
    private Integer orderId;

2、创建组件

①持久化层

②业务逻辑层

③表述层

3、结账功能

①具体操作清单

  • 创建订单对象
  • 给订单对象填充数据
    • 生成订单号
    • 生成订单的时间
    • 从购物车迁移总数量和总金额
    • 从已登录的User对象中获取userId并设置到订单对象中
  • 将订单对象保存到数据库中
  • 获取订单对象在数据库中自增主键的值
  • 根据购物车中的CartItem集合逐个创建OrderItem对象
    • 每个OrderItem对象对应的orderId属性都使用前面获取的订单数据的自增主键的值
  • 把OrderItem对象的集合保存到数据库
  • 每一个item对应的图书增加销量
  • 每一个item对应的图书减少库存
  • 清空购物车

②思路

③代码实现

[1]购物车页面结账超链接

cart.html

<a class="pay" href="protected/OrderClientServlet?method=checkout">去结账</a>
[2]OrderClientServlet.checkout()
protected void checkout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取HttpSession对象
    HttpSession session = request.getSession();

    // 2.获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");
    if (cart == null) {
        String viewName = "cart/cart";

        processTemplate(viewName, request, response);

        return ;
    }

    // 3.获取已登录的用户对象
    User user = (User) session.getAttribute("user");

    // 4.调用Service方法执行结账的业务逻辑
    String orderSequence = orderService.checkout(cart, user);

    // 5.清空购物车
    session.removeAttribute("cart");

    // 6.将订单号存入请求域
    request.setAttribute("orderSequence", orderSequence);

    // 7.将页面跳转到下单成功页面
    String viewName = "cart/checkout";
    processTemplate(viewName, request, response);
}
[3]OrderService.checkout()
@Override
public String checkout(Cart cart, User user) {

    // 从User对象中获取userId
    Integer userId = user.getUserId();

    // 创建订单对象
    Order order = new Order();

    // 给订单对象填充数据
    // 生成订单号=系统时间戳
    String orderSequence = System.currentTimeMillis() + "_" + userId;

    // 生成订单的时间
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    String createTime = simpleDateFormat.format(new Date());

    // 从购物车迁移总数量和总金额
    Integer totalCount = cart.getTotalCount();
    Double totalAmount = cart.getTotalAmount();

    order.setOrderSequence(orderSequence);
    order.setCreateTime(createTime);
    order.setTotalCount(totalCount);
    order.setTotalAmount(totalAmount);

    // 将订单对象保存到数据库中
    // ※说明:这里对insertOrder()方法的要求是获取自增的主键并将自增主键的值设置到Order对象的orderId属性中
    orderDao.insertOrder(order);

    // 获取订单对象在数据库中自增主键的值
    Integer orderId = order.getOrderId();

    // 根据购物车中的CartItem集合逐个创建OrderItem对象
    Map<String, CartItem> cartItemMap = cart.getCartItemMap();
    Collection<CartItem> cartItems = cartItemMap.values();
    List<CartItem> cartItemList = new ArrayList<>(cartItems);

    // 为了便于批量保存OrderItem,创建Object[][]
    // 二维数组第一维:SQL语句的数量
    // 二维数组第二维:SQL语句中参数的数量
    Object[][] saveOrderItemParamArr = new Object[cartItems.size()][6];

    // 为了便于批量更新Book,创建Object[][]
    Object[][] updateBookParamArr = new Object[cartItems.size()][3];

    for (int i = 0;i < cartItemList.size(); i++) {

        CartItem cartItem = cartItemList.get(i);

        // 为保存OrderItem创建Object[]
        Object[] orderItemParam = new Object[6];

        // book_name,price,img_path,item_count,item_amount,order_id
        orderItemParam[0] = cartItem.getBookName();
        orderItemParam[1] = cartItem.getPrice();
        orderItemParam[2] = cartItem.getImgPath();
        orderItemParam[3] = cartItem.getCount();
        orderItemParam[4] = cartItem.getAmount();
        orderItemParam[5] = orderId;

        // 将一维数组存入二维数组中
        saveOrderItemParamArr[i] = orderItemParam;

        // 创建数组用于保存更新Book数据的信息
        String[] bookUpdateInfoArr = new String[3];

        // 增加的销量
        bookUpdateInfoArr[0] = cartItem.getCount() + "";

        // 减少的库存
        bookUpdateInfoArr[1] = cartItem.getCount() + "";

        // bookId
        bookUpdateInfoArr[2] = cartItem.getBookId();

        // 将数组存入List集合
        updateBookParamArr[i] = bookUpdateInfoArr;
    }

    // 把OrderItem对象的集合保存到数据库:批量操作
    orderItemDao.insertOrderItemArr(saveOrderItemParamArr);

    // 使用bookUpdateInfoList对图书数据的表执行批量更新操作
    bookDao.updateBookByParamArr(updateBookParamArr);

    // 返回订单号
    return orderSequence;
}
[4]orderDao.insertOrder(order)
@Override
public void insertOrder(Order order) {

    // ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成
    // 1.获取数据库连接
    Connection connection = JDBCUtils.getConnection();

    // 2.创建PreparedStatement对象
    String sql = "INSERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)";

    try {

        // ①创建PreparedStatement对象,指明需要自增的主键
        PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);

        // ②给PreparedStatement对象设置SQL语句的参数
        preparedStatement.setString(1, order.getOrderSequence());
        preparedStatement.setString(2, order.getCreateTime());
        preparedStatement.setInt(3, order.getTotalCount());
        preparedStatement.setDouble(4, order.getTotalAmount());
        preparedStatement.setInt(5, order.getOrderStatus());
        preparedStatement.setInt(6, order.getUserId());

        // ③执行更新
        preparedStatement.executeUpdate();

        // ④获取封装了自增主键的结果集
        ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys();

        // ⑤解析结果集
        if (generatedKeysResultSet.next()) {
            int orderId = generatedKeysResultSet.getInt(1);

            order.setOrderId(orderId);
        }

    } catch (SQLException e) {
        e.printStackTrace();

        throw new RuntimeException(e);
    } finally {
        JDBCUtils.releaseConnection(connection);
    }

}
[5]BaseDao.batchUpdate()
/**
 * 通用的批量增删改方法
 * @param sql
 * @param params 执行批量操作的二维数组
 *               每一条SQL语句的参数是一维数组
 *               多条SQL语句的参数就是二维数组
 * @return 每一条SQL语句返回的受影响的行数
 */
public int[] batchUpdate(String sql, Object[][] params) {

    Connection connection = JDBCUtils.getConnection();

    int[] rowCountArr = null;

    try {
        rowCountArr = queryRunner.batch(connection, sql, params);
    } catch (SQLException e) {
        e.printStackTrace();

        throw new RuntimeException(e);
    } finally {
        JDBCUtils.releaseConnection(connection);
    }

    return rowCountArr;

}
[6]orderItemDao.insertOrderItemArr(saveOrderItemParamArr)
@Override
public void insertOrderItemArr(Object[][] saveOrderItemParamArr) {
    String sql = "INSERT INTO t_order_item(book_name,price,img_path,item_count,item_amount,order_id) VALUES(?,?,?,?,?,?)";

    super.batchUpdate(sql, saveOrderItemParamArr);
}
[7]bookDao.updateBookByParamArr(updateBookParamArr)
@Override
public void updateBookByParamArr(Object[][] updateBookParamArr) {
    String sql = "update t_book set sales=sales+?,stock=stock-? where book_id=?";
    super.batchUpdate(sql, updateBookParamArr);
}

三、结账过程中使用事务(重要)

1、事务回顾

①ACID属性

  • A:原子性 事务中包含的数据库操作缺一不可,整个事务是不可再分的。
  • C:一致性 事务执行之前,数据库中的数据整体是正确的;事务执行之后,数据库中的数据整体仍然是正确的。
    • 事务执行成功:提交(commit)
    • 事务执行失败:回滚(rollback)
  • I:隔离性 数据库系统同时执行很多事务时,各个事务之间基于不同隔离级别能够在一定程度上做到互不干扰。简单说就是:事务在并发执行过程中彼此隔离。
  • D:持久性 事务一旦提交,就永久保存到数据库中,不可撤销。

②隔离级别

[1]并发问题
并发问题 问题描述
脏读 当前事务读取了其他事务尚未提交的修改 如果那个事务回滚,那么当前事务读取到的修改就是错误的数据
不可重复读 当前事务读取同一个数据,第一次和第二次不一致
幻读 当前事务在执行过程中,数据库表增减或减少了一些记录,感觉像是出现了幻觉
[2]隔离级别
隔离级别 描述 能解决的并发问题
读未提交 允许当前事务读取其他事务尚未提交的修改 啥问题也解决不了
读已提交 允许当前事务读取其他事务已经提交的修改 脏读
可重复读 当前事务执行时锁定当前记录,不允许其他事务操作 脏读、不可重复读
串行化 当前事务执行时锁定当前表,不允许其他事务操作 脏读、不可重复读、幻读

2、JDBC事务控制

①同一个数据库连接

②关闭事务的自动提交

connection.setAutoCommit(false);

③提交事务

connection.commit();

④回滚事务

connection.rollBack();

⑤事务整体的代码块

try{

    // 关闭事务的自动提交
    connection.setAutoCommit(false);

    // 事务中包含的所有数据库操作

    // 提交事务
    connection.commit();
}catch(Excetion e){

    // 回滚事务
    connection.rollBack();

}finally{

    // 释放数据库连接
    connection.close();

}

3、将事务对接到书城项目中

①三层架构中事务要对接的位置

从逻辑上来说,一个事务对应一个业务方法(Service层的一个方法)。

②假想

每一个Service方法内部,都套用了事务操作所需要的try...catch...finally块。

③假想代码的缺陷

  • 会出现大量的冗余代码:我们希望能够抽取出来,只写一次
  • 对核心业务功能是一种干扰:我们希望能够在编写业务逻辑代码时专注于业务本身,而不必为辅助性质的套路代码分心
  • 将持久化层对数据库的操作写入业务逻辑层,是对业务逻辑层的一种污染,导致持久化层和业务逻辑层耦合在一起

④事务代码抽取

  • 只要是Filter拦截到的请求都会从doFilter()方法经过
  • chain.doFilter(req, resp);可以包裹住将来要执行的所有方法
  • 事务操作的try...catch...finally块只要把chain.doFilter(req, resp)包住,就能够包住将来要执行的所有方法
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

    try{

        // 关闭事务的自动提交
        connection.setAutoCommit(false);

        // 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中
        // 所以用事务的try...catch...finally块包住chain.doFilter(req, resp);
        // 就能让所有事务方法都『享受』到事务功能的『服务』。
        // 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。
        chain.doFilter(req, resp);

        // 提交事务
        connection.commit();
    }catch(Excetion e){

        // 回滚事务
        connection.rollBack();

    }finally{

        // 释放数据库连接
        connection.close();

    }

}

⑤在Filter中获取数据库连接

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

    try{

        // 获取数据库连接
        Connection connection = JDBCUtils.getConnection();

        // 关闭事务的自动提交
        connection.setAutoCommit(false);

        // 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中
        // 所以用事务的try...catch...finally块包住chain.doFilter(req, resp);
        // 就能让所有事务方法都『享受』到事务功能的『服务』。
        // 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。
        chain.doFilter(req, resp);

        // 提交事务
        connection.commit();
    }catch(Excetion e){

        // 回滚事务
        connection.rollBack();

    }finally{

        // 释放数据库连接
        connection.close();

    }

}

⑥保证所有数据库操作使用同一个连接

『重要发现』:在书城项目中所有执行SQL语句的代码都是通过**JDBCUtils.getConnection()方法获取数据库连接。所以我们可以通过重构JDBCUtils.getConnection()**方法实现:所有数据库操作使用同一个连接。

[1]从数据源中只拿出一个

为了保证各个需要Connection对象的地方使用的都是同一个对象,我们从数据源中只获取一个Connection。不是说整个项目只用一个Connection,而是说调用JDBCUtils.getConnection()方法时,只使用一个。所以落实到代码上就是:每次调用getConnection()方法时先检查是否已经拿过了,拿过就给旧的,没拿过给新的。

[2]公共区域

为了保证各个方法中需要Connection对象时都能拿到同一个对象,需要做到:将唯一的对象存入一个大家都能接触到的地方。

⑦线程本地化

[1]确认同一个线程

在从Filter、Servlet、Service一直到Dao运行的过程中,我们始终都没有做类似new Thread().start()这样开启新线程的操作,所以整个过程在同一个线程中。

[2]一条小河

[3]一个线程

[4]代码

java.lang.ThreadLocal的set()方法:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

java.lang.TheadLocal的get()方法:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

所以TheadLocal的基本原理是:它在内部维护了一个Map,需要存入数据时,就以this为键,要存入的数据为值,存入Map。需要取出数据时,就以this为键,从Map中取出数据。

[5]结论

如果我们需要将数据在整个项目中按照从上到下的方式传递,但是又没法通过方法的参数来实现,这时使用线程本地化技术是一个非常好的选择。

⑧异常向上抛出的线路

上图中标记颜色的位置都是有try...catch块的代码,需要逐个检查一下,catch块捕获的异常是否转换为运行时异常又再次抛出。

如果没有抛出,异常就不会传递到Filter中,TransactionFilter就会认为代码执行过程中没有发生问题,从而提交事务,但是实际上应该回滚。下面是一个例子:

/**
 * 通用的批量增删改方法
 * @param sql
 * @param params 执行批量操作的二维数组
 *               每一条SQL语句的参数是一维数组
 *               多条SQL语句的参数就是二维数组
 * @return 每一条SQL语句返回的受影响的行数
 */
public int[] batchUpdate(String sql, Object[][] params) {

    Connection connection = JDBCUtils.getConnection();

    int[] rowCountArr = null;

    try {
        rowCountArr = queryRunner.batch(connection, sql, params);
    } catch (SQLException e) {
        e.printStackTrace();

        throw new RuntimeException(e);
    } finally {
        JDBCUtils.releaseConnection(connection);
    }

    return rowCountArr;

}

4、代码实现

①重构JDBCUtils类

  • 要点1:将ThreadLocal对象声明为静态成员变量
  • 要点2:重构获取数据库连接的方法
  • 要点3:重构释放数据库连接的方法
/**
 * 功能1:创建数据源对象
 * 功能2:获取数据库连接并绑定到当前线程上
 * 功能3:释放数据库连接并从当前线程移除
 */
public class JDBCUtils {

    // 将数据源对象设置为静态属性,保证大对象的单一实例
    private static DataSource dataSource;

    // 将ThreadLocal对象设置为静态成员变量,保证以此为键时从Map中取值能够取到同一个值
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    static {

        // 1.创建一个用于存储外部属性文件信息的Properties对象
        Properties properties = new Properties();

        // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
        InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");

        try {

            // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
            properties.load(inputStream);

            // 4.创建数据源对象
            dataSource = DruidDataSourceFactory.createDataSource(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 从数据源中获取数据库连接
     * @return 数据库连接对象
     */
    public static Connection getConnection() {

        // 1.尝试从当前线程获取Connection对象
        Connection connection = threadLocal.get();

        if (connection == null) {

            try {
                // 2.如果从当前线程上没有获取到Connection对象那么从数据源获取
                connection = dataSource.getConnection();

                // 3.将Connection对象绑定到当前线程
                threadLocal.set(connection);

            } catch (SQLException e) {
                e.printStackTrace();

                throw new RuntimeException(e);
            }

        }

        // 4.返回Connection对象
        return connection;

    }

    /**
     * 释放数据库连接
     * @param connection 要执行释放操作的连接对象
     */
    public static void releaseConnection(Connection connection) {

        if (connection != null) {

            try {
                connection.close();

                // 将Connection对象从当前线程移除
                threadLocal.remove();

            } catch (SQLException e) {
                e.printStackTrace();

                throw new RuntimeException(e);
            }

        }

    }
}

②重构BaseDao

  • 要点:去除释放数据库连接的操作(转移到TransactionFilter中)
/**
 * 各个具体Dao类的基类,泛型T对应具体实体类类型
 * @param <T>
 */
public class BaseDao<T> {

    private QueryRunner queryRunner = new QueryRunner();

    /**
     * 通用的批量增删改方法
     * @param sql
     * @param params 执行批量操作的二维数组
     *               每一条SQL语句的参数是一维数组
     *               多条SQL语句的参数就是二维数组
     * @return 每一条SQL语句返回的受影响的行数
     */
    public int[] batchUpdate(String sql, Object[][] params) {

        Connection connection = JDBCUtils.getConnection();

        int[] rowCountArr = null;

        try {
            rowCountArr = queryRunner.batch(connection, sql, params);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }/* finally {
            JDBCUtils.releaseConnection(connection);
        }*/

        return rowCountArr;

    }

    /**
     * 通用的增删改方法
     * @param sql 要执行的SQL语句
     * @param param 为SQL语句准备好的参数
     * @return 受影响的行数
     */
    public int update(String sql, Object ... param) {

        int updatedRowCount = 0;

        Connection connection = JDBCUtils.getConnection();

        try {

            updatedRowCount = queryRunner.update(connection, sql, param);

        }
        // 为了让上层方法调用方便,将编译时异常捕获
        catch (SQLException e) {
            e.printStackTrace();

            // 为了不掩盖问题,将编译时异常封装为运行时异常抛出
            throw new RuntimeException(e);
        }/* finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }*/

        return updatedRowCount;

    }

    /**
     * 查询单个对象
     * @param clazz 单个对象所对应的实体类类型
     * @param sql   查询单个对象所需要的SQL语句
     * @param param SQL语句的参数
     * @return      查询到的单个对象
     */
    public T getBean(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtils.getConnection();

        T t = null;

        try {

            t = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }/* finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }*/

        return t;
    }

    /**
     * 查询集合对象
     * @param clazz 集合中单个对象所对应的实体类类型
     * @param sql   查询集合所需要的SQL语句
     * @param param SQL语句的参数
     * @return      查询到的集合对象
     */
    public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtils.getConnection();

        List<T> list = null;

        try {

            list = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }/* finally {

            // 关闭数据库连接
            JDBCUtils.releaseConnection(connection);

        }*/

        return list;
    }

}

注意:OrderDao中insertOrder()方法也要去掉关闭数据库连接的操作。

@Override
public void insertOrder(Order order) {

    // ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成
    // 1.获取数据库连接
    Connection connection = JDBCUtils.getConnection();

    // 2.创建PreparedStatement对象
    String sql = "INS222ERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)";

    try {

        // ①创建PreparedStatement对象,指明需要自增的主键
        PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);

        // ②给PreparedStatement对象设置SQL语句的参数
        preparedStatement.setString(1, order.getOrderSequence());
        preparedStatement.setString(2, order.getCreateTime());
        preparedStatement.setInt(3, order.getTotalCount());
        preparedStatement.setDouble(4, order.getTotalAmount());
        preparedStatement.setInt(5, order.getOrderStatus());
        preparedStatement.setInt(6, order.getUserId());

        // ③执行更新
        preparedStatement.executeUpdate();

        // ④获取封装了自增主键的结果集
        ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys();

        // ⑤解析结果集
        if (generatedKeysResultSet.next()) {
            int orderId = generatedKeysResultSet.getInt(1);

            order.setOrderId(orderId);
        }

    } catch (SQLException e) {
        e.printStackTrace();

        throw new RuntimeException(e);
    } /*finally {
        JDBCUtils.releaseConnection(connection);
    }*/

}

③创建一个用于显示通用错误信息的页面

[1]创建页面

这个页面可以从login_success.html复制过来

[2]创建Servlet跳转到页面

protected void showSystemError(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewName = "error";

    processTemplate(viewName, request, response);

}

④创建TransactionFilter

<filter>
    <filter-name>TransactionFilter</filter-name>
    <filter-class>com.atguigu.bookstore.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>TransactionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Java代码如下:

public class TransactionFilter implements Filter {

    private static final Set<String> PUBLIC_STATIC_RESOURCE_EXT_NAME_SET = new HashSet<>();

    static {
        PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".png");
        PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".css");
        PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".js");
        PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".jpg");
        PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".gif");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

        // 排除掉静态资源,它们和数据库操作没有关系
        // 1.给请求和响应对象转换类型
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        // 2.获取当前请求的ServletPath
        String servletPath = request.getServletPath();

        // 3.检查servletPath中是否包含“.”
        if (servletPath.contains(".")) {

            int index = servletPath.lastIndexOf(".");

            String extensionName = servletPath.substring(index);

            if (PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.contains(extensionName)) {
                chain.doFilter(request, response);

                return ;
            }
        }

        // 执行事务操作
        // 1.获取数据库连接
        Connection connection = JDBCUtils.getConnection();

        // 2.使用try...catch...finally块管理事务
        try{

            // 3.关闭事务的自动提交
            connection.setAutoCommit(false);

            // 4.尝试执行目标代码
            chain.doFilter(request, response);

            // 5.如果上面的操作没有抛出异常
            connection.commit();

        }catch (Exception e){

            // 6.如果上面的操作抛出了异常
            try {
                connection.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }

            // 7.捕获到异常后,跳转到专门的页面显示提示消息
            String message = e.getMessage();
            request.setAttribute("error", message);
            request.getRequestDispatcher("/ErrorServlet?method=showSystemError").forward(request, response);

        }finally {

            // 8.不管前面操作是成功还是失败,到这里都要释放数据库连接
            JDBCUtils.releaseConnection(connection);

        }

    }

    public void init(FilterConfig config) throws ServletException {}
    public void destroy() {}
}

18、Listener 监听器

一、观察者模式

二十三种设计模式之一:

  • 观察者:监控『被观察者』的行为,一旦发现『被观察者』触发了事件,就会调用事先准备好的方法执行操作。
  • 被观察者:『被观察者』一旦触发了被监控的事件,就会被『观察者』发现。

二、监听器简介

1、概念

监听器:专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。 Servlet监听器:Servlet规范中定义的一种特殊类,它用于监听Web应用程序中的ServletContext,HttpSession 和HttpServletRequest等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。

2、分类

  • 域对象监听器
  • 域对象的属性域监听器
  • Session域中数据的监听器

3、监听器列表

①ServletContextListener

作用:监听ServletContext对象的创建与销毁

方法名 作用
contextInitialized(ServletContextEvent sce) ServletContext创建时调用
contextDestroyed(ServletContextEvent sce) ServletContext销毁时调用

ServletContextEvent对象代表从ServletContext对象身上捕获到的事件,通过这个事件对象我们可以获取到ServletContext对象。

②HttpSessionListener

作用:监听HttpSession对象的创建与销毁

方法名 作用
sessionCreated(HttpSessionEvent hse) HttpSession对象创建时调用
sessionDestroyed(HttpSessionEvent hse) HttpSession对象销毁时调用

HttpSessionEvent对象代表从HttpSession对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的HttpSession对象。

③ServletRequestListener

作用:监听ServletRequest对象的创建与销毁

方法名 作用
requestInitialized(ServletRequestEvent sre) ServletRequest对象创建时调用
requestDestroyed(ServletRequestEvent sre) ServletRequest对象销毁时调用

ServletRequestEvent对象代表从HttpServletRequest对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的HttpServletRequest对象。另外还有一个方法可以获取到当前Web应用的ServletContext对象。

④ServletContextAttributeListener

作用:监听ServletContext中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletContextAttributeEvent scab) 向ServletContext中添加属性时调用
attributeRemoved(ServletContextAttributeEvent scab) 从ServletContext中移除属性时调用
attributeReplaced(ServletContextAttributeEvent scab) 当ServletContext中的属性被修改时调用

ServletContextAttributeEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletContext() 获取ServletContext对象

⑤HttpSessionAttributeListener

作用:监听HttpSession中属性的创建、修改和销毁

方法名 作用
attributeAdded(HttpSessionBindingEvent se) 向HttpSession中添加属性时调用
attributeRemoved(HttpSessionBindingEvent se) 从HttpSession中移除属性时调用
attributeReplaced(HttpSessionBindingEvent se) 当HttpSession中的属性被修改时调用

HttpSessionBindingEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getSession() 获取触发事件的HttpSession对象

⑥ServletRequestAttributeListener

作用:监听ServletRequest中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletRequestAttributeEvent srae) 向ServletRequest中添加属性时调用
attributeRemoved(ServletRequestAttributeEvent srae) 从ServletRequest中移除属性时调用
attributeReplaced(ServletRequestAttributeEvent srae) 当ServletRequest中的属性被修改时调用

ServletRequestAttributeEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletRequest () 获取触发事件的ServletRequest对象

⑦HttpSessionBindingListener

作用:监听某个对象在Session域中的创建与移除

方法名 作用
valueBound(HttpSessionBindingEvent event) 该类的实例被放到Session域中时调用
valueUnbound(HttpSessionBindingEvent event) 该类的实例从Session中移除时调用

HttpSessionBindingEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取当前事件涉及的属性名
getValue() 获取当前事件涉及的属性值
getSession() 获取触发事件的HttpSession对象

⑧HttpSessionActivationListener

作用:监听某个对象在Session中的序列化与反序列化。

方法名 作用
sessionWillPassivate(HttpSessionEvent se) 该类实例和Session一起钝化到硬盘时调用
sessionDidActivate(HttpSessionEvent se) 该类实例和Session一起活化到内存时调用

HttpSessionEvent对象代表事件对象,通过getSession()方法获取事件涉及的HttpSession对象。

三、ServletContextListener

1、实用性

将来学习SpringMVC的时候,会用到一个ContextLoaderListener,这个监听器就实现了ServletContextListener接口,表示对ServletContext对象本身的生命周期进行监控。

2、具体用法

①创建监听器类

public class AtguiguListener implements ServletContextListener {
    @Override
    public void contextInitialized(
            // Event对象代表本次事件,通过这个对象可以获取ServletContext对象本身
            ServletContextEvent sce) {
        System.out.println("Hello,我是ServletContext,我出生了!");

        ServletContext servletContext = sce.getServletContext();
        System.out.println("servletContext = " + servletContext);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("Hello,我是ServletContext,我打算去休息一会儿!");
    }
}

②注册监听器

<!-- 每一个listener标签对应一个监听器配置,若有多个监听器,则配置多个listener标签即可 -->
<listener>
    <!-- 配置监听器指定全类名即可 -->
    <listener-class>com.atguigu.listener.AtguiguListener</listener-class>
</listener>

事件触发过程中控制台日志的打印:

Connected to server [2021-03-20 04:23:20,982] Artifact pro10-listener:war exploded: Artifact is being deployed, please wait... 三月 20, 2021 4:23:21 下午 org.apache.catalina.deploy.WebXml setVersion 警告: Unknown version string [4.0]. Default version will be used. Hello,我是ServletContext,我出生了! servletContext = org.apache.catalina.core.ApplicationContextFacade@6a66017e [2021-03-20 04:23:21,426] Artifact pro10-listener:war exploded: Artifact is deployed successfully [2021-03-20 04:23:21,426] Artifact pro10-listener:war exploded: Deploy took 444 milliseconds 三月 20, 2021 4:23:30 下午 org.apache.catalina.startup.HostConfig deployDirectory 信息: Deploying web application directory D:\software\apache-tomcat-7.0.57\webapps\manager 三月 20, 2021 4:23:31 下午 org.apache.catalina.startup.HostConfig deployDirectory 信息: Deployment of web application directory D:\software\apache-tomcat-7.0.57\webapps\manager has finished in 124 ms [2021-03-20 04:24:06,422] Artifact pro10-listener:war exploded: Artifact is being deployed, please wait... Hello,我是ServletContext,我打算去休息一会儿! Hello,我是ServletContext,我出生了! servletContext = org.apache.catalina.core.ApplicationContextFacade@2a55374c [2021-03-20 04:24:07,115] Artifact pro10-listener:war exploded: Artifact is deployed successfully [2021-03-20 04:24:07,115] Artifact pro10-listener:war exploded: Deploy took 694 milliseconds

19、Axios Ajax

一、Ajax概述

1、服务器端渲染

2、Ajax渲染(局部更新)

3、前后端分离

彻底舍弃服务器端渲染,数据全部通过Ajax方式以JSON格式来传递。

4、同步与异步

Ajax本身就是Asynchronous JavaScript And XML的缩写,直译为:异步的JavaScript和XML。在实际应用中Ajax指的是:不刷新浏览器窗口不做页面跳转局部更新页面内容的技术。

**『同步』『异步』**是一对相对的概念,那么什么是同步,什么是异步呢?

①同步

多个操作按顺序执行,前面的操作没有完成,后面的操作就必须等待。所以同步操作通常是串行的。

②异步

多个操作相继开始并发执行,即使开始的先后顺序不同,但是由于它们各自是在自己独立的进程或线程中完成,所以互不干扰谁也\不用等*谁*

5、Axios简介

使用原生的JavaScript程序执行Ajax极其繁琐,所以一定要使用框架来完成。而Axios就是目前最流行的前端Ajax框架。

Axios官网:http://www.axios-js.com/

使用Axios和使用Vue一样,导入对应的*.js文件即可。官方提供的script标签引入方式为:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

我们可以把这个axios.min.js文件下载下来保存到本地来使用。

二、 Axios基本用法

0、在前端页面引入开发环境

<script type="text/javascript" src="/demo/static/vue.js"></script>
<script type="text/javascript" src="/demo/static/axios.min.js"></script>

1、发送普通请求参数

①前端代码

HTML标签:

<div id="app">
        <button @click="commonParam">普通请求参数</button>
    </div>

Vue+axios代码:

new Vue({
    "el":"#app",
    "data":{},
    "methods":{
        "commonParam":function () {
            axios({
                "method":"post",
                "url":"/demo/AjaxServlet?method=commonParam",
                "params":{
                    "userName":"tom",
                    "userPwd":"123456"
                }
            }).then(function (response) {
                console.log(response);
            }).catch(function (error) {
                console.log(error);
            });
        }
    }
});

效果:所有请求参数都被放到URL地址后面了,哪怕我们现在用的是POST请求方式。

②后端代码

public class AjaxServlet extends ModelBaseServlet {
    protected void commonParam(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String userName = request.getParameter("userName");
        String userPwd = request.getParameter("userPwd");

        System.out.println("userName = " + userName);
        System.out.println("userPwd = " + userPwd);

        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("服务器端返回普通文本字符串作为响应");

    }
}

P.S.:由于我们不需要Thymeleaf了,所以ModelBaseServlet可以跳过ViewBaseServlet直接继承HttpServlet。

③axios程序接收到的响应对象结构

属性名 作用
config 调用axios(config对象)方法时传入的JSON对象
data 服务器端返回的响应体数据
headers 响应消息头
request 原生JavaScript执行Ajax操作时使用的XMLHttpRequest
status 响应状态码
statusText 响应状态码的说明文本

④服务器端处理请求失败后

catch(function (error) {     // catch()服务器端处理请求出错后,会调用

    console.log(error);         // error就是出错时服务器端返回的响应数据
    console.log(error.response);        // 在服务器端处理请求失败后,获取axios封装的JSON格式的响应数据对象
    console.log(error.response.status); // 在服务器端处理请求失败后,获取响应状态码
    console.log(error.response.statusText); // 在服务器端处理请求失败后,获取响应状态说明文本
    console.log(error.response.data);   // 在服务器端处理请求失败后,获取响应体数据

});

在给catch()函数传入的回调函数中,error对象封装了服务器端处理请求失败后相应的错误信息。其中,axios封装的响应数据对象,是error对象的response属性。response属性对象的结构如下图所示:

可以看到,response对象的结构还是和then()函数传入的回调函数中的response是一样的:

回调函数:开发人员声明,但是调用时交给系统来调用。像单击响应函数、then()、catch()里面传入的都是回调函数。回调函数是相对于普通函数来说的,普通函数就是开发人员自己声明,自己调用:

function sum(a, b) {
    return a+b;
}

var result = sum(3, 2);
console.log("result="+result);

2、发送请求体JSON

①前端代码

HTML代码:

<button @click="requestBodyJSON">请求体JSON</button>

Vue+axios代码:

……
"methods":{
    "requestBodyJSON":function () {
        axios({
            "method":"post",
            "url":"/demo/AjaxServlet?method=requestBodyJSON",
            "data":{
                "stuId": 55,
                "stuName": "tom",
                "subjectList": [
                    {
                        "subjectName": "java",
                        "subjectScore": 50.55
                    },
                    {
                        "subjectName": "php",
                        "subjectScore": 30.26
                    }
                ],
                "teacherMap": {
                    "one": {
                        "teacherName":"tom",
                        "tearcherAge":23
                    },
                    "two": {
                        "teacherName":"jerry",
                        "tearcherAge":31
                    },
                },
                "school": {
                    "schoolId": 23,
                    "schoolName": "atguigu"
                }
            }
        }).then(function (response) {
            console.log(response);
        }).catch(function (error) {
            console.log(error);
        });
    }
}
……

效果:

P.S.:Chrome浏览器中将『请求负载』显示为英文:『Request Payload』。

②后端代码

[1]加入Gson包

Gson是Google研发的一款非常优秀的JSON数据解析和生成工具,它可以帮助我们将数据在JSON字符串和Java对象之间互相转换。

[2]Servlet代码
protected void requestBodyJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.由于请求体数据有可能很大,所以Servlet标准在设计API的时候要求我们通过输入流来读取
    BufferedReader reader = request.getReader();

    // 2.创建StringBuilder对象来累加存储从请求体中读取到的每一行
    StringBuilder builder = new StringBuilder();

    // 3.声明临时变量
    String bufferStr = null;

    // 4.循环读取
    while((bufferStr = reader.readLine()) != null) {
        builder.append(bufferStr);
    }

    // 5.关闭流
    reader.close();

    // 6.累加的结果就是整个请求体
    String requestBody = builder.toString();

    // 7.创建Gson对象用于解析JSON字符串
    Gson gson = new Gson();

    // 8.将JSON字符串还原为Java对象
    Student student = gson.fromJson(requestBody, Student.class);
    System.out.println("student = " + student);

    System.out.println("requestBody = " + requestBody);

    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().write("服务器端返回普通文本字符串作为响应");
}

P.S.:看着很麻烦是吧?别担心,将来我们有了SpringMVC之后,一个**@RequestBody**注解就能够搞定,非常方便!

3、服务器端返回JSON数据

①前端代码

axios({
    "method":"post",
    "url":"/demo/AjaxServlet?method=responseBodyJSON"
}).then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

then()中获取到的response在控制台打印效果如下:我们需要通过data属性获取响应体数据

②后端代码

[1]加入Gson包

仍然需要Gson支持,不用多说

[2]Servlet代码
protected void responseBodyJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.准备数据对象
    Student student = new Student();
    student.setStuId(10);
    student.setStuName("tom");
    student.setSchool(new School(11,"atguigu"));
    student.setSubjectList(Arrays.asList(new Subject("java", 95.5), new Subject("php", 93.3)));

    Map<String, Teacher> teacherMap = new HashMap<>();
    teacherMap.put("t1", new Teacher("lili", 25));
    teacherMap.put("t2", new Teacher("mary", 26));
    teacherMap.put("t3", new Teacher("katty", 27));

    student.setTeacherMap(teacherMap);

    // 2.创建Gson对象
    Gson gson = new Gson();

    // 3.将Java对象转换为JSON对象
    String json = gson.toJson(student);

    // 4.设置响应体的内容类型
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(json);

}

书城项目第六阶段

一、注册页面用户名唯一性检查优化

1、准备工作

  • 创建module
  • 迁移代码

2、加入Ajax开发环境

①前端所需axios库

②后端所需Gson库

3、封装AjaxCommonsResult

①模型的作用

在整个项目中,凡是涉及到给Ajax请求返回响应,我们都封装到AjaxCommonsResult类型中。

②模型的代码

public class AjaxCommonResult<T> {

    public static final String SUCCESS = "SUCCESS";
    public static final String FAILED = "FAILED";

    private String result;
    private String message;
    private T data;

各个属性的含义:

属性名 含义
SUCCESS 代表服务器端处理请求成功
FAILED 代表服务器端处理请求失败
result 服务器端处理请求的结果,取值在SUCCESS和FAILED二者中选择一个
message 失败消息
data 针对查询操作返回的数据

③模型的好处

  • 作为整个团队开发过程中,前后端交互时使用的统一的数据格式
  • 有利于团队成员之间的协助,提高开发效率

4、功能实现

①定位功能的位置

在用户输入用户名之后,立即检查这个用户名是否可用。

②思路

[1]给用户名输入框绑定的事件类型

结论:不能在针对username设定的watch中发送Ajax请求。

原因:服务器端响应的速度跟不上用户输入的速度,而且服务器端异步返回响应数据,无法保证和用户输入的顺序完全一致。此时有下面几点问题:

  • 给服务器增加不必要的压力
  • 用户输入的数据在输入过程中是不断发生变化的
  • 响应数据和输入顺序不对应,会发生错乱

解决办法:绑定的事件类型使用『值改变』事件。

[2]流程图

③代码实现

[0]在当前页面引入axios库文件
<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
[1]给用户名输入框绑定值改变事件
<input v-model:value="username" @change="usernameUniqueCheck" type="text" name="username" placeholder="请输入用户名" />
[2]JavaScript代码
var registerApp = new Vue({
    "el":"#registerForm",
    "data":{
        "username":"[[${param.username}]]",
        "password":"",
        "passwordConfirm":"",
        "email":"[[${param.email}]]",
        "code":"",
        "usernameCheckMessage":""
    },
    "watch":{……},
    "methods":{
        ……,
        ……,
        "usernameUniqueCheck":function () {

            // 获取用户在文本框中输入的数据
            var username = this.username;

            // 发送Ajax请求执行检查
            axios({
                "method":"post",
                "url":"UserServlet",
                "params":{
                    "method":"checkUsernameUnique",
                    "username":username
                }
            }).then(function (response) {

                // 1.从响应数据中获取请求处理结果
                var result = response.data.result;

                // 2.判断result的值
                if (result == "SUCCESS") {

                    // 3.用户名可用
                    // 注意:现在我们在then()的回调函数中,this已经不再指向Vue对象了
                    // 所以,我们通过Vue对象的变量名来访问Vue对象
                    registerApp.usernameCheckMessage = "用户名可用";

                } else {

                    // 4.用户名不可用
                    registerApp.usernameCheckMessage = response.data.message;

                }

            }).catch(function (error) {
                console.log(error);
            });

        }
    }
});
[3]UserServlet
protected void checkUsernameUnique(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    AjaxCommonResult<String> ajaxResult = null;

    // 1.从请求参数中获取用户名
    String username = request.getParameter("username");

    try {
        // 2.调用Service方法检查用户名是否被占用
        userService.checkUsernameUnique(username);

        // 3.按照检测成功的结果创建AjaxCommonResult对象
        ajaxResult = new AjaxCommonResult<>(AjaxCommonResult.SUCCESS, null, null);
    } catch (Exception e) {
        e.printStackTrace();

        // 4.按照检测失败的结果创建AjaxCommonResult对象
        ajaxResult = new AjaxCommonResult<>(AjaxCommonResult.FAILED, e.getMessage(), null);
    }

    // 5.根据ajaxResult对象返回响应数据
    // ①创建Gson对象
    Gson gson = new Gson();

    // ②执行JSON数据转换
    String json = gson.toJson(ajaxResult);

    // ③设置响应体内容类型
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(json);

}
[4]UserService
@Override
public void checkUsernameUnique(String username) {

    User user = userDao.selectUserByName(username);

    if (user != null) {
        throw new RuntimeException("用户名已经被占用");
    }

}

二、加入购物车

1、思路

2、代码实现

①加入layer弹层组件

<script type="text/javascript" src="static/script/jquery-1.7.2.js"></script>
<script type="text/javascript" src="static/layer/layer.js"></script>

②顶层bar绑定Vue对象

Thymeleaf在服务器端渲染的过程中将购物车总数量计算得到,通过表达式设置写入JavaScript代码,作为Vue对象的初始值。然后由Vue对象通过v-show判断是否显示数量标签。

[1]在HTML标签上标记id

由于要考虑是否登录的情况,所以id加到了两种情况外层的div

<div id="topBarApp" class="w">
    <div class="topbar-left">
        <i>送至:</i>
        <i>北京</i>
        <i class="iconfont icon-ai-arrow-down"></i>
    </div>
    <div class="topbar-right" th:if="${session.user == null}">
        <a href="UserServlet?method=toLoginPage" class="login">登录</a>
        <a href="UserServlet?method=toRegisterPage" class="register">注册</a>
        <a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">购物车</a>
        <a href="AdminServlet?method=toPortalPage" class="admin">后台管理</a>
    </div>
    <!--          登录后风格-->
    <div class="topbar-right" th:if="${session.user != null}">
        <span>欢迎你<b th:text="${session.user.userName}">张总</b></span>
        <a href="#" class="register">注销</a>
        <a href="protected/CartServlet?method=showCart" class="cart iconfont icon-gouwuche">
            购物车
            <div class="cart-num" v-show="totalCount > 0">{
    
    {totalCount}}</div>
        </a>
        <a href="pages/manager/book_manager.html" class="admin">后台管理</a>
    </div>
</div>
[2]创建Vue对象
// topBarApp对象的totalCount属性的初始值是Thymeleaf在服务器端运算出来用表达式设置的
var topBarApp = new Vue({
    "el": "#topBarApp",
    "data": {
        "totalCount": [[${(session.cart == null)?"0":session.cart.totalCount}]]
    }
});

③图书列表div绑定Vue对象

[1]在HTML标签上标记id

目的是为了便于创建Vue对象

<div id="bookListApp" class="list-content" th:if="${not #lists.isEmpty(bookList)}">
    <div class="list-item" th:each="book : ${bookList}">
        <img th:src="${book.imgPath}" src="static/uploads/huozhe.jpg" alt="">
        <p>书名:<span th:text="${book.bookName}">活着</span></p>
        <p>作者:<span th:text="${book.author}">余华</span></p>
        <p>价格:¥<span th:text="${book.price}">66.6</span></p>
        <p>销量:<span th:text="${book.sales}">230</span></p>
        <p>库存:<span th:text="${book.stock}">1000</span></p>
        <!--<button>加入购物车</button>-->
        <a th:href="@{/protected/CartServlet(method=addCart,bookId=${book.bookId})}">加入购物车</a>
    </div>
</div>
[2]在首页引入Vue和axios库文件
<script src="static/script/vue.js" type="text/javascript" charset="utf-8"></script>
<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
[3]创建Vue对象
<script type="text/javascript">
    new Vue({
        "el":"#bookListApp"
    });
</script>
[4]绑定单击响应函数

给加入购物车按钮绑定单击响应函数

<button @click="addToCart">加入购物车</button>

Vue代码:

new Vue({
    "el":"#bookListApp",
    "methods":{
        "addToCart":function () {

        }
    }
});
[5]将bookId设置到按钮中

为了便于在按钮的单击响应函数中得到bookId的值

<button th:id="${book.bookId}" @click="addToCart">加入购物车</button>
[6]在单击响应函数中发送Ajax请求
new Vue({
    "el":"#bookListApp",
    "methods":{
        "addToCart":function () {

            // event:事件对象
            // event.target:当前事件操作的对象
            // event.target.id:前事件操作的对象的id属性的值
            var bookId = event.target.id;

            axios({
                "method":"post",
                "url":"protected/CartServlet",
                "params":{
                    "method":"addCart",
                    "bookId":bookId
                }
            }).then(function (response) {

                var result = response.data.result;

                if (result == "SUCCESS") {
                    // 给出提示:加入购物车成功
                    layer.msg("加入购物车成功");

                    // 从响应数据中获取购物车总数量
                    // response.data其实就是AjaxCommonResult对象的JSON格式
                    // response.data.data就是访问AjaxCommonResult对象的data属性
                    var totalCount = response.data.data;

                    // 修改页头位置购物车的总数量
                    topBarApp.totalCount = totalCount;

                }else {

                    // 给出提示:response.data.message
                    layer.msg(response.data.message);

                }

            }).catch(function (error) {
                console.log(error);
            });
        }
    }
});

④后端代码

CartServlet

protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取bookId
    String bookId = request.getParameter("bookId");

    // 2.根据bookId查询图书数据
    Book book = bookService.getBookById(bookId);

    // 3.获取Session对象
    HttpSession session = request.getSession();

    // 4.尝试从Session域获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // 5.判断Cart对象是否存在
    if (cart == null) {

        // 6.如果不存在,则创建新的Cart对象
        cart = new Cart();

        // 7.将新创建的Cart对象存入Session域
        session.setAttribute("cart", cart);
    }

    // 8.添加购物车
    cart.addCartItem(book);

    // 9.给Ajax返回JSON格式响应
    // ①创建AjaxCommonResult对象
    AjaxCommonResult<Integer> result = new AjaxCommonResult<>(AjaxCommonResult.SUCCESS, null, cart.getTotalCount());

    // ②创建Gson对象
    Gson gson = new Gson();

    // ③将AjaxCommonResult对象转换为JSON字符串
    String json = gson.toJson(result);

    // ④设置响应体的内容类型
    response.setContentType("application/json;charset=UTF-8");

    // ⑤返回响应
    response.getWriter().write(json);

}

三、显示购物车数据

1、思路

2、代码实现

①CartServlet增加getCartJSON()方法

[1]Cart模型的局限性

目前的Cart对象转换为JSON后,没有totalCount、totalAmount这样的属性,Map结构也不如LIst遍历方便。

[2]调整方式

把前端页面需要的属性,存入Map中即可。

[3]方法代码
protected void getCartJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    AjaxCommonResult<Map<String, Object>> result = null;

    // 1.获取Session对象
    HttpSession session = request.getSession();

    // 2.尝试获取购物车对象
    Cart cart = (Cart) session.getAttribute("cart");

    // 3.检查cart对象是否为空
    if (cart == null) {
        result = new AjaxCommonResult<>(AjaxCommonResult.FAILED, null, null);
    } else {

        Map<String, Object> cartJSONMap = new HashMap<>();
        cartJSONMap.put("totalCount", cart.getTotalCount());
        cartJSONMap.put("totalAmount", cart.getTotalAmount());
        cartJSONMap.put("cartItemList", cart.getCartItemMap().values());

        result = new AjaxCommonResult<Map<String, Object>>(AjaxCommonResult.SUCCESS, null, cartJSONMap);
    }

    // 4.将AjaxCommonResult对象转换为JSON作为响应返回
    Gson gson = new Gson();
    String json = gson.toJson(result);
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(json);

}

②前端代码

[1]去除Thymeleaf痕迹

将cart.html页面中,由Thymeleaf渲染数据的部分去掉。

[2]使用Vue对象初步接管页面渲染
new Vue({
        "el":"#appCart",
        "data":{
            "cart":"empty"
        },

HTML标签:

<tbody v-if="cart == 'empty'">
    <tr>
        <td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
    </tr>
</tbody>

[3]在mounted生命周期环境发Ajax请求

记得加入axios库:

<script src="static/script/axios.js" type="text/javascript" charset="utf-8"></script>
var cartApp = new Vue({
    "el":"#appCart",
    "data":{
        "cart":"empty"
    },
    "mounted":function () {
        axios({
            "method":"post",
            "url":"protected/CartServlet",
            "params":{
                "method":"getCartJSON"
            }
        }).then(function (response) {

            // 1.从响应数据中获取请求处理结果
            var result = response.data.result;

            // 2.检查结果是成功还是失败
            if (result == "SUCCESS") {

                // 3.获取购物车数据并赋值给Vue对象
                cartApp.cart = response.data.data;

                console.log(cartApp.cart);
            }

        }).catch(function (error) {
            console.log(error);
        });
    },
    ……
[4]完成Vue页面渲染
<div id="appCart" class="w">
    <table>
        <thead>
        <tr>
            <th>图片</th>
            <th>商品名称</th>
            <th>数量</th>
            <th>单价</th>
            <th>金额</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody v-if="cart == 'empty'">
            <tr>
                <td colspan="6">购物车还是空空的,赶紧去添加吧!</td>
            </tr>
        </tbody>
        <tbody v-if="cart != 'empty'">
        <tr v-for="cartItem in cart.cartItemList">
            <td>
                <img :src="cartItem.imgPath" alt=""/>
            </td>
            <td>{
    
    {cartItem.bookName}}</td>
            <td>
                <input type="hidden" name="bookId" :value="cartItem.bookId" />
                <span @click="itemDecrease" class="count">-</span>
                <input @change="itemCountChange" class="count-num" type="text" :value="cartItem.count"/>
                <span @click="itemIncrease" class="count">+</span>
            </td>
            <td>{
    
    {cartItem.price}}</td>
            <td>{
    
    {cartItem.amount}}</td>
            <td><a @click="removeConfirm" href="protected/CartServlet">删除</a></td>
        </tr>
        </tbody>
    </table>
    <div class="footer">
        <div class="footer-left">
            <a @click="clearCart" href="protected/CartServlet?method=clearCart" class="clear-cart">清空购物车</a>
            <a href="index.html">继续购物</a>
        </div>
        <div class="footer-right">
            <div>共<span>{
    
    {cart.totalCount}}</span>件商品</div>
            <div class="total-price">总金额{
    
    {cart.totalAmount}}元</div>
            <a class="pay" href="protected/OrderClientServlet?method=checkout">去结账</a>
        </div>
    </div>
</div>

猜你喜欢

转载自blog.csdn.net/qq_53729147/article/details/127967447