- 书城项目第一阶段
- 书城项目第二阶段
- 第一节 不带数据库的登录注册
- 第二节 三层架构
- 第四节 持久化层
- 第五节 完成带数据库的登录注册
- 1、密码加密
- ①加密方式介绍
- ②加密算法:HASH
- ③执行加密的工具方法
- #[2]实现UserService接口">#[2]实现UserService接口
- [3]修改RegisterServlet
- [2]LoginServlet的doPost()方法
- 1、密码加密
- 书城项目第三阶段
- 书城项目第四阶段
- 书城项目第五阶段
- 书城项目第六阶段
书城项目第一阶段
第一节 事件驱动补充
1、取消控件的默认行为
①控件默认行为
- 点超链接会跳转页面
- 点表单提交按钮会提交表单
本来控件的默认行为是天经地义就该如此的,但是如果我们希望点击之后根据我们判断的结果再看是否要跳转,此时默认行为无脑跳转的做法就不符合我们的预期了。
②取消方式
[1]超链接举例
HTML代码:
<a id="anchor" href="http://www.baidu.com">超链接</a>
JavaScript代码:
document.getElementById("anchor").onclick = function() {console.log("我点击了一个超链接");event.preventDefault();}
[2]表单提交按钮举例
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、正则表达式零起步
①创建正则表达式对象
[1]使用两个斜杠
// 类似创建数组时可以使用[]、创建对象可以使用{}var reg = /a/;
[2]使用new关键字创建RegExp类型的对象
// 类似创建数组可以new Array()、创建对象可以使用new Object()var reg = new RegExp("a");
②正则表达式的组成
正则表达式本身也是一个字符串,它由两种字符组成:
- 普通字符,例如大、小写英文字母;数字等。
- 元字符:被系统赋予特殊含义的字符。例如:^表示以某个字符串开始,$表示以某个字符串结束。
③正则表达式初体验
[1]模式验证
注意:这里是使用正则表达式对象来调用方法。 ```javascript // 创建一个最简单的正则表达式对象 var reg = /o/;
// 创建一个字符串对象作为目标字符串 var str = ‘Hello World!’;
// 调用正则表达式对象的test()方法验证目标字符串是否满足我们指定的这个模式,返回结果true console.log(“/o/.test(‘Hello World!’)=”+reg.test(str));
<a name="bOwl9"></a>##### [2]匹配读取**注意**:这里是使用**字符串对象**来**调用**方法。```javascript// 在目标字符串中查找匹配的字符,返回匹配结果组成的数组var resultArr = str.match(reg);// 数组长度为1console.log("resultArr.length="+resultArr.length);// 数组内容是oconsole.log("resultArr[0]="+resultArr[0]);
[3]替换
注意:这里是使用字符串对象来调用方法。
var newStr = str.replace(reg,'@');// 只有第一个o被替换了,说明我们这个正则表达式只能匹配第一个满足的字符串console.log("str.replace(reg)="+newStr);//Hell@ World!// 原字符串并没有变化,只是返回了一个新字符串console.log("str="+str);//str=Hello World!
④匹配方式
[1]全文查找
如果不使用g对正则表达式对象进行修饰,则使用正则表达式进行查找时,仅返回第一个匹配;使用g后,返回所有匹配。
// 目标字符串var targetStr = 'Hello World!';// 没有使用全局匹配的正则表达式var reg = /[A-Z]/;// 获取全部匹配var resultArr = targetStr.match(reg);// 数组长度为1console.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);// 数组长度为2console.log("resultArr.length="+resultArr.length);// 遍历数组,发现可以获取到“H”和“W”for(var i = 0; i < resultArr.length; i++){console.log("resultArr["+i+"]="+resultArr[i]);}
[2]忽略大小写
//目标字符串var targetStr = 'Hello WORLD!';//没有使用忽略大小写的正则表达式var reg = /o/g;//获取全部匹配var resultArr = targetStr.match(reg);//数组长度为1console.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);//数组长度为2console.log("resultArr.length="+resultArr.length);//遍历数组,得到'o'和'O'for(var i = 0; i < resultArr.length; i++){console.log("resultArr["+i+"]="+resultArr[i]);}
[3]多行查找
不使用多行查找模式,目标字符串中不管有没有换行符都会被当作一行。
//目标字符串1var targetStr01 = 'Hello\nWorld!';//目标字符串2var targetStr02 = 'Hello';//匹配以'Hello'结尾的正则表达式,没有使用多行匹配var reg = /Hello$/;console.log(reg.test(targetStr01));//falseconsole.log(reg.test(targetStr02));//true
对比代码:
//目标字符串1var targetStr01 = 'Hello\nWorld!';//目标字符串2var targetStr02 = 'Hello';//匹配以'Hello'结尾的正则表达式,使用了多行匹配var reg = /Hello$/m;console.log(reg.test(targetStr01));//trueconsole.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]例1
var str = 'one two three four';// 匹配全部空格var reg = /\s/g;// 将空格替换为@var newStr = str.replace(reg,'@'); // one@two@three@fourconsole.log("newStr="+newStr);
[2]例2
var str = '今年是2014年';// 匹配至少一个数字var reg = /\d+/g;str = str.replace(reg,'abcd');console.log('str='+str); // 今年是abcd年
[3]例3
var str01 = 'I love Java';var str02 = 'Java love me';// 匹配以Java开头var reg = /^Java/g;console.log('reg.test(str01)='+reg.test(str01)); // flaseconsole.log("<br />");console.log('reg.test(str02)='+reg.test(str02)); // true
[4]例4
var str01 = 'I love Java';var str02 = 'Java love me';// 匹配以Java结尾var reg = /Java$/g;console.log('reg.test(str01)='+reg.test(str01)); // trueconsole.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));//flaseconsole.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')); // flaseconsole.log("/[a]{3}/.test('aaa')="+/[a]{3}/g.test('aaa')); // trueconsole.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]);//Worldconsole.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+|\s+$/g |
| 电子邮箱 | /^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+[\.]{1})+[a-zA-Z]+$/ |
第三节 第一阶段要实现的功能
0、准备工作
创建目录后,把一整套现成的前端页面复制到新建的目录下,然后用HBuilderX打开这个目录。然后把vue.js文件复制到script目录下。
1、登录页面的表单验证
①规则设定
- 用户名非空
-
②在login.html页面中加入Vue的环境
</body><script src="/bookstoreV01/static/script/vue.js" type="text/javascript" charset="utf-8"></script></html>
③思路
④代码实现
[1]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>
[2]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 ;}}}});
书城项目第二阶段
第一节 不带数据库的登录注册
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页面
- 失败:返回错误消息
②思路
友情提示:分析业务功能,捋清思路最好的办法就是画流程图
③代码
[1]创建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>
[2]完成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("抱歉!用户名或密码不正确,请重新输入!");}}
[3]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、注册功能
①明确目标
用户提交注册表单后,检查用户名是否被占用
- 没有被占用:注册成功
-
②思路
③代码
[1]创建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>
[2]完成doPost()方法中的代码
```java 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");
}
}
<a name="Uqlc4"></a>##### [3]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就是属性名。 ```java public class User {
private String safeUserName;
public String getUserName(){
return this.safeUserName;
}
public void setUserName(String userName){
this.safeUserName = userName;
}
}
在上面例子中,getXxx()、setXxx()方法定义的属性是userName,不是safeUserName。<a name="o7ovt"></a>## 第三节 建模<a name="NR3kL"></a>### 1、创建数据库```sqlCREATE 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_idprivate String userName;// user_nameprivate String userPwd;// user_pwdprivate String email;// email……
第四节 持久化层
1、加入所需jar包
2、创建连接数据库的工具类
3、创建外部属性文件

driverClassName=com.mysql.jdbc.Driverurl=jdbc:mysql://192.168.198.100:3306/bookstore210107username=rootpassword=atguiguinitialSize=10maxActive=20maxWait=10000
4、在JDBCUtils类中创建数据源对象
private static DataSource dataSource;static {// 1.创建一个用于存储外部属性文件信息的Properties对象Properties properties = new Properties();// 2.使用当前类的类加载器加载外部属性文件:jdbc.propertiesInputStream 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 {@Testpublic 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;}}
测试方法:
@Testpublic 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);}@Testpublic void testGetBean() {BaseDao<User> baseDao = new BaseDao<>();// user_id userId// user_name userName// user_pwd userPwdString 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);}@Testpublic 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 {@Overridepublic 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);}@Overridepublic 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());}}
④测试方法
@Testpublic void testUserDaoGetUserByName() {UserDao userDao = new UserDaoImpl();User user = userDao.selectUserByName("陈冠希");System.out.println("user = " + user);}@Testpublic void testUserDaoSaveUser() {UserDao userDao = new UserDaoImpl();User user = new User(null, "陈冠希", "666666", "aaa@qq.com");int count = userDao.insertUser(user);System.out.println("count = " + count);}
第五节 完成带数据库的登录注册
1、密码加密
①加密方式介绍
- 对称加密:在知道密文和加密算法的情况下,能够反推回明文
非对称加密:
特点1:不可逆
- 特点2:加密后,密文长度固定
- 特点3:输入数据不变,输出数据也保证不变;输入数据变化,输出数据一定变化
常见的HASH算法举例:
- MD5
- SHA1
- SHA512
-
③执行加密的工具方法
```java 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; }
}
<a name="KGVII"></a>### 2、注册功能<a name="HIaT5"></a>#### ①目标检查用户名是否可用,如果用户名可用则保存User对象<a name="w2IoO"></a>#### ②思路<a name="F8n9k"></a>#### ③代码<a name="hEx1n"></a>##### [1]创建UserService```javapublic 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();@Overridepublic void doRegister(User userForm) {}@Overridepublic User doLogin(User userForm) {return null;}}
理由:
- 创建对象的操作只执行一次
-
(2)实现注册功能
@Overridepublic 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
```java 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());
}
}
<a name="is5SF"></a>### 3、登录功能<a name="Sxudq"></a>#### ①关键点提示对用户密码进行验证时,无法将密文解密为明文,只能将明文再次加密为密文,**『比较密文是否一致』**。<a name="zCBq9"></a>#### ②思路<a name="VX6Hl"></a>#### ③代码<a name="BYgkZ"></a>##### [1]UserService接口的doLogin()```java@Overridepublic User doLogin(User userForm) {// 1.获取表单提交的用户名String userName = userForm.getUserName();// 2.根据用户名调用UserDao方法查询User对象User userDB = userDao.selectUserByName(userName);// 3.检查数据库查询的User对象是否为nullif (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());}}
书城项目第三阶段
第一节 创建module,迁移代码
1、创建module
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中,将两个基类迁移过来
将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。 ```java 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);
}
}
<a name="IMYVu"></a>#### ③调整index.html- 加入Thymeleaf名称空间```html<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
-
②创建BookService
接口:BookService
-
③创建BookDao
接口:BookDao
-
④组装
给BookManagerServlet组装BookService
-
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()
```java protected void showBookList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.调用Service方法查询图书列表 List
bookList = bookService.getBookList(); // 2.将图书列表数据存入请求域 request.setAttribute(“bookList”, bookList);
// 3.执行视图模板渲染 String viewName = “manager/book_manager”;
processTemplate(viewName, request, response);
}
<a name="mOnGV"></a>#### ④实现:BookService.getBookList()```java@Overridepublic List<Book> getBookList() {return bookDao.selectBookList();}
⑤实现:BookDao.selectBookList()
@Overridepublic 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()
```java 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”);
}
<a name="DUCaB"></a>#### ④实现:BookService.removeBook()```java@Overridepublic void removeBook(String bookId) {bookDao.deleteBook(bookId);}
⑤实现:BookDao.deleteBook()
@Overridepublic 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()
@Overridepublic void saveBook(Book book) {bookDao.insertBook(book);}
⑦实现:BookDao.insertBook()
@Overridepublic 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.从请求参数中获取bookIdString 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()
@Overridepublic Book getBookById(String bookId) {return bookDao.selectBookByPrimaryKey(bookId);}
⑤实现:BookDao.selectBookByPrimaryKey()
@Overridepublic 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()
@Overridepublic void updateBook(Book book) {bookDao.updateBook(book);}
⑨实现:BookDao.updateBook()
注意:这里不修改imgPath字段
@Overridepublic 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>
书城项目第四阶段
第一节 保持登录状态
1、创建module
- 迁移src目录下的Java代码
- 迁移web目录下的static目录
- 迁移web/WEB-INF目录下的lib目录和pages目录
- 将lib目录下的jar包添加到运行时环境
将旧的web.xml中的配置复制到新module的web.xml中
2、将登录成功的User对象存入会话域
```java
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);
}
}
<a name="sVFb3"></a>### 3、修改欢迎信息<a name="N8RRo"></a>#### ①登录成功页面```html<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包
②配置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对象存入cartItemMapcartItemMap.put(bookId, cartItem);}}// 从购物车中删除CartItempublic 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的数量+1public void itemCountIncrease(String bookId) {CartItem cartItem = cartItemMap.get(bookId);cartItem.setCount(cartItem.getCount() + 1);}// 把某个CartItem的数量-1public 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
```java
public class CartServlet extends ModelBaseServlet {private BookService bookService = new BookServiceImpl();
protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.从请求参数中获取bookIdString 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");
}
}
<a name="GOucd"></a>##### [2]index.html页面购物车数量显示:```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的逻辑
-
②思路
减号span绑定单击响应函数→获取文本框中的数据:当前数量→检查数量是否大于1
数量大于1:发送请求访问CartServlet.decrease()方法→回到cart.html
数量等于1:confirm确认→发送请求访问CartServlet.removeItem()方法→回到cart.html
③后端代码
CartServlet.decrease() ```java 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);
}
<a name="O80p6"></a>#### ④前端代码HTML代码:```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是否大于1if (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、删除
①目标
②思路
cart.html→删除超链接→CartServlet.removeItem()→回到cart.html
③代码实现
[1]后端代码
CartServlet.removeItem()
protected void removeItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 1.从请求参数获取bookIdString 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和newCountString 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.通过请求参数获取bookIdString 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.发送请求执行+1window.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();}
书城项目第五阶段
第一节 登录检查
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类
```java
/**- 判断当前请求是否已登录的Filter
- 由于在web.xml的配置中,这个Filter只拦截/protected开头的路径,
所以,凡是被这个Filter拦截的请求都要求必须登录 */ public class LoginFilter implements Filter { public void destroy() {} public void init(FilterConfig config) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
// 1.将request对象的类型转换为HttpServletRequestHttpServletRequest httpRequest = (HttpServletRequest) request;// 2.通过httpRequest对象获取HttpSession对象HttpSession session = httpRequest.getSession();// 3.尝试从Session域获取User对象User user = (User) session.getAttribute("user");// 4.判断User对象是否存在if (user == null) {// 5.转发到登录页面,提示用户登录后再操作request.setAttribute("message", "请登录后再操作");request.getRequestDispatcher("/UserServlet?method=toLoginPage").forward(request, response);}else{// 6.如果User对象存在,说明用户已登录,直接放行chain.doFilter(request, response);}
}
}
<a name="eC1p9"></a>##### [2]注册Filter类```xml<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表
| 字段名称 | 字段作用 | | —- | —- | | item_id | 主键 | | book_name | 书名 | | price | 单价 | | item_count | 当前订单项的数量 | | item_amount | 当前订单项的金额 | | order_id | 当前订单项关联的订单表的主键 |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));
说明: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()
@Overridepublic String checkout(Cart cart, User user) {// 从User对象中获取userIdInteger 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_idorderItemParam[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() + "";// bookIdbookUpdateInfoArr[2] = cartItem.getBookId();// 将数组存入List集合updateBookParamArr[i] = bookUpdateInfoArr;}// 把OrderItem对象的集合保存到数据库:批量操作orderItemDao.insertOrderItemArr(saveOrderItemParamArr);// 使用bookUpdateInfoList对图书数据的表执行批量更新操作bookDao.updateBookByParamArr(updateBookParamArr);// 返回订单号return orderSequence;}
[4]orderDao.insertOrder(order)
```java @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);
}
}
<a name="tLslB"></a>##### [5]BaseDao.batchUpdate()```java/*** 通用的批量增删改方法* @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)
@Overridepublic 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)
@Overridepublic 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)包住,就能够包住将来要执行的所有方法 ```java 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();
}
}
<a name="Q6uie"></a>#### ⑤在Filter中获取数据库连接```javapublic 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对象时都能拿到同一个对象,需要做到:将唯一的对象存入一个大家都能接触到的地方。
结论:使用线程本地化技术实现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);elsecreateMap(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.propertiesInputStream 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中) ```java /**
- 各个具体Dao类的基类,泛型T对应具体实体类类型
@param
*/ public class BaseDao { 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
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
getBeanList(Class clazz, String sql, Object … param) { Connection connection = JDBCUtils.getConnection();
List
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()方法也要去掉关闭数据库连接的操作。```java@Overridepublic 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]创建页面
[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.获取当前请求的ServletPathString 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() {}}
第四节 后台查看订单列表&发货
第五节 前台查看订单列表&确认收货
书城项目第六阶段
第一节 注册页面用户名唯一性检查优化
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
@Overridepublic 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.messagelayer.msg(response.data.message);}}).catch(function (error) {console.log(error);});}}});
④后端代码
CartServlet
protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 1.从请求参数中获取bookIdString 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]调整方式
[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>




