书城项目
书城项目--第一阶段(实现表单验证的功能)
表单验证的说明
- 登录
- 注册
表单验证的实现
-
将静态资源拷贝到book_static模块下
-
index.html一般表示网页的首页
-
static目录中存储了各种静态资源。图片,视频等
-
pages目录中存储了各种页面,如用户页面,后台页面等
我们使用注册页面来演示(登录页面与这个类似)
-
我们使用jquery来使用,我们将jquery的js文件拷贝放在static目录下面
-
关于如果我们跳转的很慢以至于“不合法信息”还存在的问题
我们演示的跳转是跳转到本地,所以会跳转的很快,如果我们是跳转到外网网速慢将会跳转的很慢,会导致下面的问题
-
实现表单(注册页面)的验证
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>尚硅谷会员注册页面</title>
<link type="text/css" rel="stylesheet" href="../../static/css/style.css" >
<style type="text/css">
.login_form{
height:420px;
margin-top: 25px;
}
</style>
<script type="text/javascript" src="../../static/jquery-1.7.2.js"> </script>
<script type="text/javascript">
//给注册绑定单击事件
$(function (){
$("#sub_btn").click(function (){
//验证用户名:必须由字母,数字下划线组成,并且长度为 5 到 12 位
//1.获取用户名输入框里面的内容
var usernameText =$("#username").val();
//2.创建正则表达式对象
var usernamePatt = /^\w{5,12}$/;
//3.使用test方法验证
if(!usernamePatt.test(usernameText)){//不匹配时提示用户
//4.提示用户结果
//我们已经有一个专门的span标签专门用来描述错误的提示信息
// <span class="errorMsg"></span>,我们只需要给span标签赋值即可
$("span.errorMsg").text("用户名不合法");
return false;//禁止其默认行为跳转
}
$("span.errorMsg").text("");//用户名正确去掉提示信息
// 验证密码:必须由字母,数字下划线组成,并且长度为 5 到 12 位
//1.获取用户名输入框里面的内容
var passwordText =$("#password").val();
//2.创建正则表达式对象
var passwordPatt = /^\w{5,12}$/;
//3.使用test方法验证
if(!passwordPatt.test(passwordText)){//不匹配时提示用户
//4.提示用户结果
//我们已经有一个专门的span标签专门用来描述错误的提示信息
// <span class="errorMsg"></span>,我们只需要给span标签赋值即可
$("span.errorMsg").text("密码不合法");
return false;//禁止其默认行为跳转
}
$("span.errorMsg").text("");//用户名正确去掉提示信息
// 验证确认密码:和密码相同
//1.获取确认密码内容
var repwdText =$("#repwd").val();
//2.和密码比较
if(passwordText != repwdText){//不相等时
//3.提示用户
$("span.errorMsg").text("确认密码和密码不一致");
return false;//禁止其默认行为跳转
}
// 邮箱验证:xxxxx@xxx.com
//1.获取邮箱的内容
var emailText =$("#email").val();
//2.创建正则表达式对象
var emailPatt = /^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+(\.{1,2}[a-z]+)+$/;
//3.使用test方法验证是否合法
if(!emailPatt.test(emailText)){
$("span.errorMsg").text("邮箱格式不合法");
return false;//禁止其默认行为跳转
}
//4.提示用户
//验证码:现在只需要验证用户已输入。因为还没讲到服务器。验证码由服务器生成
//只需要验证用户是否输入了验证码
//获取验证码的内容
var codeText =$("#code").val();
//去除空格(防止输入空格)
codeText = $.trim(codeText);
if(codeText == ""||codeText == null){//输入的验证码为空
//提示用户
$("span.errorMsg").text("验证码不能为空");
return false;//禁止其默认行为跳转
}
});
});
</script>
</head>
<body>
<div id="login_header">
<img class="logo_img" alt="" src="../../static/img/logo.gif" >
</div>
<div class="login_banner">
<div id="l_content">
<span class="login_word">欢迎注册</span>
</div>
<div id="content">
<div class="login_form">
<div class="login_box">
<div class="tit">
<h1>注册尚硅谷会员</h1>
<span class="errorMsg"></span>
</div>
<div class="form">
<form action="regist_success.html">
<label>用户名称:</label>
<input class="itxt" type="text" placeholder="请输入用户名" autocomplete="off" tabindex="1" name="username" id="username" />
<br />
<br />
<label>用户密码:</label>
<input class="itxt" type="password" placeholder="请输入密码" autocomplete="off" tabindex="1" name="password" id="password" />
<br />
<br />
<label>确认密码:</label>
<input class="itxt" type="password" placeholder="确认密码" autocomplete="off" tabindex="1" name="repwd" id="repwd" />
<br />
<br />
<label>电子邮件:</label>
<input class="itxt" type="text" placeholder="请输入邮箱地址" autocomplete="off" tabindex="1" name="email" id="email" />
<br />
<br />
<label>验证码:</label>
<input class="itxt" type="text" style="width: 150px;" id="code"/>
<img alt="" src="../../static/img/code.bmp" style="float: right; margin-right: 40px">
<br />
<br />
<input type="submit" value="注册" id="sub_btn" />
</form>
</div>
</div>
</div>
</div>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©2015
</span>
</div>
</body>
</html>
第二阶段----用户注册和登录
- 前面已经实现了注册表单的验证
- 前面的登录表单的验证还没有完成
现在要做的就是客户端数据校验通过(注册和登录一样)后,将数据发送给服务器,然后服务器将数据保存到数据库当中
JavaEE三层架构介绍
创建数据库和t_uesr用户表
我们的系统调用是从最上层一层一层的往下进行调用,而我们在书写代码的时候应该从最底层往最上层进行书写
- 首先我们需要创建注册所需要的数据库和表
- SQL语句
-- 创建书城项目的数据库
CREATE DATABASE book;
-- 创建注册页面需要的数据表
CREATE TABLE t_user(
id INT PRIMARY KEY AUTO_INCREMENT,-- id为主键且自增
username VARCHAR(20)NOT NULL UNIQUE, -- 用户名非空且唯一
password VARCHAR(32)NOT NULL,-- 密码非空
email VARCHAR(200));
-- 往我们创建的表中插入一条数据
INSERT INTO t_user(username,password,email)VALUES('admain','admain','admain@atguigu.com');
SELECT * FROM t_user
编写数据库表对应的JavaBean类
数据库中的表和javaBean类之间是映射关系
- t_uesr表的javaBean类User
package com.atguigu.pojo;
public class User {
private Integer id;
private String userName;
private String password;
private String email;
public User() {
}
public User(Integer id, String userName, String password, String email) {
this.id = id;
this.userName = userName;
this.password = password;
this.email = email;
}
/**
* 获取
* @return id
*/
public Integer getId() {
return id;
}
/**
* 设置
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* 获取
* @return userName
*/
public String getUserName() {
return userName;
}
/**
* 设置
* @param userName
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* 获取
* @return password
*/
public String getPassword() {
return password;
}
/**
* 设置
* @param password
*/
public void setPassword(String password) {
this.password = password;
}
/**
* 获取
* @return email
*/
public String getEmail() {
return email;
}
/**
* 设置
* @param email
*/
public void setEmail(String email) {
this.email = email;
}
public String toString() {
return "User{id = " + id + ", userName = " + userName + ", password = " + password + ", email = " + email + "}";
}
}
jdbcUtils工具类的编写和测试
-
JdbcUtils用户管理从数据库连接池获取和关闭链接
-
需要将jar包放在web-INF目录下面的lib(自己创建)目录下面
使用德鲁伊需要德鲁伊的配置文件放在src目录下面
小点注意:数据库连接池使用后一定要及时释放,要不然达到数据库的最大连接数据,数据库将会瘫痪
编写BaseDao
- BaseDao类
package com.atguigu.dao.impl;
/*
1.Dao负责和数据库进行交互
2.BaseDao作为所有DAO的公共父类,里面定义对数据库增删改查的通用方法
3.使用DBUtils操作数据库
*/
import com.atguigu.utils.JdbcUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
//开发BasicDAO,是其他DAO的父类
public abstract class BaseDao<T> {//使用泛型类表示
//定义属性
private QueryRunner queryRunner = new QueryRunner();//用于执行sql
//通用的DML方法(返回所影响的行数)
//适用于任意表
//parames:表示传入的?的具体值
public int update(String sql, Object... parames) {//这里连接的获取放在在函数里面比较好
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
final int affectedRow = queryRunner.update(connection, sql, parames);//可变参数本质是数组
return affectedRow;
//执行sql
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.close(connection);
}
//在update方法的内部,connection resultSet preparedStatement都已经被关闭
return -1;//返回-1说明执行失败
}
//查询的结果返回多个对象(即返回多行结果,使用List包装)
//适用于任意表
/*
参数解释:
clazz:表示传入一个类的Class对象,比如Actor.class
params:传入?的具体值
返回对应的ArrayList集合
*/
public List<T> queryMulti(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
final List<T> list = queryRunner.query(connection, sql, new BeanListHandler<T>(clazz), params);
return list;//返回一个List集合
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
//返回单行结果的通用方法(返回一个javaBean对象 )
public T querySingle(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
T t = queryRunner.query(connection, sql, new BeanHandler<T>(clazz), params);
return t;//返回一个javaBean对象
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.close(connection);
}
return null;
}
//返回单行单列,返回一个单值
//因为是返回一个单值不需要封装,所有不用传入Class对象
public Object queryScalar(String sql,Object...parames){
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
Object object = queryRunner.query(connection, sql, new ScalarHandler<>(), parames);
return object;//返回任意一个单值
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.close(connection);
}
return null;
}
}
编写UserDao和测试
UserDao里面的功能主要由用户对数据库的需求决定的,在该项目里面主要是完成用户的注册和登录功能对数据库的操作
对UserDao接口中需要定义的方法的原因的解释
-
1.`queryUserByUsername()查询用户信息
在注册页面,当我们输入数据,我们需要根据用户名(username唯一)查询是否存在该用户的信息,然后提示下一步的操作 -
2.注册成功需要保存用户数据到数据库
-
1.登陆时根据用户名和密码查询用户信息
编写UserService和测试
包含对UserService接口的制定和UserServiceImpl实现类的编写,和测试
编写Web层
1.实现用户注册的功能
- 图解用户注册的流程:
在web目录下面编写RegistServlet用于处理注册业务
使用get请求还是使用post请求:我们需要发送密码,并不希望别人看到,所以我们使用post请求
-
在这里可能使用servlet相关的类使用不了,但是IDEA可以自动下载JavaEE的依赖并添加到项目中
-
在web.xml中配置servlet程序的地址
-
servlet程序的访问地址
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>RegistServlet</servlet-name>
<servlet-class>com.atguigu.web.RegistServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RegistServlet</servlet-name>
<url-pattern>/registServlet</url-pattern>
</servlet-mapping>
</web-app>
-
部署服务器
-
所以我们使用base标签+相对路径的方案
-
对使用base+相对路径修改原路径的解析
当写到这里:本来想将路径都改成base+相对路径的形式就完了,但是页面的资源一直报错
- 关于base标签的运行原理查看之前的笔记
错误1:配置服务器的时候工程路径(应用程序上下文)没有进行配置
这样就造成了即使我的资源路径都可以访问,但是如果我们使用服务器进行运行,但是也是访问不到任何资源(index.html)也不行
错误2:servlet程序中没有写doGet方法,访问报405
我们将工程路径配置好了之后,我们尝试在地址栏中访问servlet程序
因为在我们的网页地址栏中通过回车直接访问servlet程序的,默认为是GET请求,而我们在servlet程序中并没有重写doGet方法,所以报405 - 文心一言
一个小注意点
理解servlet程序的访问
我们注册还需要使用注册成功页面,也要将注册成功页面的路径使用base+相当路径的方式
- 小注意:加上base标签后,我们可以在浏览器页面按f12看哪里报错修改哪里的路径
现在开始写服务器端servlet程序的处理代码
- 我们的验证码是由服务器生成的,现在还没有学,目前将验证码写死
在写servlet程序,然后进行测试的时候,又遇到了bug:
这个主要是加载德鲁伊的配置文件一直加载不成功,以至于dataSource 是空
- 得出结论
- 得出在初始化数据库连接池加载配置文件该怎么写:
关于对getResourceAsStream方法的解释
IDEA 中Debug调试的使用
- 在debug调试时,断点一般打在出错的前一行
经验之举:text类型的input的value值表示的是默认值,这在我们测试的时候就不用频繁输入了
关于断点调试的内容很重要,需要经常练习,笔记查看官方笔记即可了
用户登录功能的实现
- 流程分析
1.我们先创建LoginServlet程序,并配置他的访问路径
2.我们的资源路径方案是base标签+相对路径(我们需要修改用到的login.html和 login_sucess.html文件的资源路径)
- 书写servlet程序
package com.atguigu.web;
import com.atguigu.pojo.User;
import com.atguigu.service.UserService;
import com.atguigu.service.impl.UserServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//处理登录的请求
public class LoginServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
//2.调用service.login()处理登录业务
final User loginUser = userService.login(new User(null, username, password, null));
if (loginUser==null){
//登录失败
//跳回登录页面
req.getRequestDispatcher("/pages/user/login.html").forward(req, resp);
}else {
//登录成功
//跳回登录成功页面login_success.html
req.getRequestDispatcher("/pages/user/login_success.html").forward(req, resp);
}
}
}
项目第三阶段
1.修改所有的html页面为jsp页面
- 具体修改
在前面的阶段中,我们已经完成了注册和登录的基本功能。可以实现用户注册和登录时,和数据库进行交互。接下来就是进行优化处理
- 发现问题
通过前面的测试我们发现,当我们该账号已经注册时,再次注册该账号时,将会重新跳转到注册的页面。但是我们发现在这个过程没有任何提示,用户根本就不知道该账号已经被注册。这个需要去进行改进
- 解释为什么要将html修改为jsp页面
servelt程序用于和客户端进行交互,可以将数据回写到客户端,但是单纯的servlet程序对这个回写数据却很疲惫。但是jsp的优势就是在可便宜代替servlet回传html页面数据
注意:对于批量修改替换可以使用1.ctrl+R2.ctrl+shift+R进行操作实现
2.抽取jsp页面中相同的内容
我们通过观察现有代码可以发现:在现有代码中多处页面的内容都相同,比较臃肿。可以抽取成一份,然后引用即可
我们在有该内容的页面,使用静态包含语句,引用该页面即可
- 被引用的login_sucess_menu.jsp文件
<%--
Created by IntelliJ IDEA.
User: SWT
Date: 2023/10/8
Time: 18:58
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<div>
<span>欢迎<span class="um_span">韩总</span>光临尚硅谷书城</span>
<a href="../order/order.jsp">我的订单</a>
<a href="../../index.jsp">注销</a>
<a href="../../index.jsp">返回</a>
</div>
</head>
<body>
</body>
</html>
- 内容抽取2
- 将他们抽取成头部信息head.jsp
这三个头部信息,是没有jsp文件都具备的,所以全部都要被替换 - 被抽取成的head.jsp文件
<%--
Created by IntelliJ IDEA.
User: SWT
Date: 2023/10/8
Time: 19:24
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!--
base标签只对当前页面使用
base标签:永远固定相对路径的跳转结果
在项目里面一般base的值写到工程路径
>
-->
<base href="http://localhost:8080/book/">
<link type="text/css" rel="stylesheet" href="static/css/style.css" >
<script type="text/javascript" src="static/jquery-1.7.2.js"> </script>
- 抽取3:页脚抽取
- 被抽取成的footer.jsp
<%--
Created by IntelliJ IDEA.
User: SWT
Date: 2023/10/8
Time: 19:41
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©2015
</span>
</div>
- 抽取4:后台菜单
- 被抽取成的文件manager_menu.jsp
<%--
Created by IntelliJ IDEA.
User: SWT
Date: 2023/10/8
Time: 19:56
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div>
<a href="book_manager.jsp">图书管理</a>
<a href="order_manager.jsp">订单管理</a>
<a href="../../index.jsp">返回商城</a>
</div>
动态的base标签值
- 一个现象
当我们访问这个项目的时候,使用的是ip地址而不是,localhost(localhost表示的是本机Ip,别人访问则需要使用Ip)时 - 我们在浏览器中按f12查看一下访问的情况
原因解释
因为我们通过抽取jsp页面的头部信息,将base标签都设成成了:<base href="http://localhost:8080/book/">
,这样当导致其他电脑通过ip访问时,资源的ip还是localhost。这样就访问到本机了。所以访问不到
修改:我们将base标签的所有内容都进行动态获取就可以了(包含:协议名,ip,端口号,工程名)
遗留问题
当我们将base标签进行动态化处理后,发现我们使用ip地址进行访问时,首页index.jsp的图片等资源无法正常显示
登录,注册错误提示,及表单回显
1.(注册失败提示信息)当我们在登录时密码错误获取在注册时用户名已经存在时,目录将会重新跳转到登录或者注册页面。但是这对用户是不友好的,他们不知道怎么回事。页面没有提示信息
2.表单回显。将用户名和密码显示在跳转的页面中
-
修改概况
我们在显示注册和登录页面的时候,将一些填写在页面中的数据,保存在域对象中。当我们登录和注册失败的时候,回显页面的时候也将域对象中的数据一起显示 -
修改登录页面
-
回写错误信息和用户名
-
修改注册页面
-
回写错误信息和用户名和邮箱
代码优化:合并LoginServlet和RegistServlet为UserServlet程序
我们的LoginServlet和RegiestServlet都属于用户模块,可以将二者合并
-
原理解释
-
修改点1:在注册和登录的表单中分别添加隐藏域
-
修改点2:给UerServlet添加访问地址
-
修改点3:我们测试好UserServlet可以处理登录和注册业务后,将原来LoginServlet和RegistSerlvet中业务处理代复制到UserServlet中就可以了
-
新创建的后的UserServlet(集合LoginServlet和RegistServlet的功能)
package com.atguigu.web;
import com.atguigu.pojo.User;
import com.atguigu.service.UserService;
import com.atguigu.service.impl.UserServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
//2.调用service.login()处理登录业务
final User loginUser = userService.login(new User(null, username, password, null));
if (loginUser==null){
//登录失败
//将错误信息和回显表单项信息,保存到request域中
req.setAttribute("msg", "用户名或密码错误");
req.setAttribute("username", username);
//跳回登录页面
req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
}else {
//登录成功
//跳回登录成功页面login_success.html
req.getRequestDispatcher("/pages/user/login_success.jsp").forward(req, resp);
}
}
public void regist(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//根据前面的分析,完成servlet程序
//1.获取请求(由表单传递)的参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
final String email = req.getParameter("email");
final String code = req.getParameter("code");//在表单验证码没有写name书写将他补充
//2.检查验证码是否正确:写死,要求验证码为abcde
if ("abcde".equals(code)) {
//正确
//3.检查用户名是否可用(调用service层)
if (userService.existUsername(username)) {
//用户名不可用
System.out.println("用户名["+username+"]已经存在");
//回写信息,保存到reqquest域中
req.setAttribute("msg", "用户名已经存在");//错误信息
req.setAttribute("username", username);//用户名
req.setAttribute("email", email);//邮箱
//跳回注册页面
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}else {
//可用(调用service将用户信息保存到数据库)
userService.registUser(new User(null, username, password, email));
//跳转到注册成功页面
req.getRequestDispatcher("/pages/user/regist_success.jsp").forward(req, resp);
}
}else {
//错误
//跳回注册页面(使用请求转发来完成)
System.out.println("验证码["+code+"]错误");
//回写信息,保存到request域中
req.setAttribute("msg", "验证码错误");
req.setAttribute("username", username);
req.setAttribute("email", email);
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取隐藏域的参数值
final String action = req.getParameter("action");
if("login".equals(action)){
login(req,resp);
}else if("regist".equals(action)) {
regist(req,resp);
}
}
}
- 此时LoginServlet和RegistServlet就没有用来,可以删除了
代码优化:使用反射优化大量else if代码
- 发现问题
在此处反射机制原理举例
package com.atguigu.test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class UserServletTest {
public void login(){
System.out.println("这是调用了login方法");
}
public void regist(){
System.out.println("这是调用了regist方法");
}
public void update(){
System.out.println("这是调用了update方法");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//获取方法(通过获取的方法不同,就可以使得调用的方法不同)
final Method method = UserServletTest.class.getMethod("login");
//调用获取的方法
method.invoke(new UserServletTest());
}
}
我们可以通过获取的方法不同,进而调用的方法不同。进行实现了动态调用方法。因为我们可以不用if else判断是哪个jsp发送了请求。只需要通过发送的隐藏域参数值,来动态调用不同的方法即可
- 优化后的UserServlet代码
package com.atguigu.web;
import com.atguigu.pojo.User;
import com.atguigu.service.UserService;
import com.atguigu.service.impl.UserServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
//2.调用service.login()处理登录业务
final User loginUser = userService.login(new User(null, username, password, null));
if (loginUser==null){
//登录失败
//将错误信息和回显表单项信息,保存到request域中
req.setAttribute("msg", "用户名或密码错误");
req.setAttribute("username", username);
//跳回登录页面
req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
}else {
//登录成功
//跳回登录成功页面login_success.html
req.getRequestDispatcher("/pages/user/login_success.jsp").forward(req, resp);
}
}
public void regist(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//根据前面的分析,完成servlet程序
//1.获取请求(由表单传递)的参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
final String email = req.getParameter("email");
final String code = req.getParameter("code");//在表单验证码没有写name书写将他补充
//2.检查验证码是否正确:写死,要求验证码为abcde
if ("abcde".equals(code)) {
//正确
//3.检查用户名是否可用(调用service层)
if (userService.existUsername(username)) {
//用户名不可用
System.out.println("用户名["+username+"]已经存在");
//回写信息,保存到reqquest域中
req.setAttribute("msg", "用户名已经存在");//错误信息
req.setAttribute("username", username);//用户名
req.setAttribute("email", email);//邮箱
//跳回注册页面
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}else {
//可用(调用service将用户信息保存到数据库)
userService.registUser(new User(null, username, password, email));
//跳转到注册成功页面
req.getRequestDispatcher("/pages/user/regist_success.jsp").forward(req, resp);
}
}else {
//错误
//跳回注册页面(使用请求转发来完成)
System.out.println("验证码["+code+"]错误");
//回写信息,保存到request域中
req.setAttribute("msg", "验证码错误");
req.setAttribute("username", username);
req.setAttribute("email", email);
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取隐藏域的参数值
final String action = req.getParameter("action");
//获取action业务识别字符串,获取相应的业务 反射对象
Method method = null;
try {
method = UserServlet.class.getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
//调用业务方法
//this表示的是UserServlet的对象,表示是UserServlet调用的method方法
method.invoke(this,req,resp);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
这样在我们添加User模块功能的时候就不用写if else判断了
代码优化:抽取BaseServlet程序
- 抽取成的BaseServlet程序
package com.atguigu.web;
import com.atguigu.pojo.User;
import com.atguigu.service.UserService;
import com.atguigu.service.impl.UserServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public abstract class BaseServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取隐藏域的参数值
final String action = req.getParameter("action");
//获取action业务识别字符串,获取相应的业务 反射对象
Method method = null;
try {
method = UserServlet.class.getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
//调用业务方法
//this表示的是UserServlet的对象,表示是UserServlet调用的method方法
method.invoke(this,req,resp);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
- 被抽取后的UserServlet程序
package com.atguigu.web;
import com.atguigu.pojo.User;
import com.atguigu.service.UserService;
import com.atguigu.service.impl.UserServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class UserServlet extends BaseServlet {
private UserService userService = new UserServiceImpl();
public void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
//2.调用service.login()处理登录业务
final User loginUser = userService.login(new User(null, username, password, null));
if (loginUser==null){
//登录失败
//将错误信息和回显表单项信息,保存到request域中
req.setAttribute("msg", "用户名或密码错误");
req.setAttribute("username", username);
//跳回登录页面
req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
}else {
//登录成功
//跳回登录成功页面login_success.html
req.getRequestDispatcher("/pages/user/login_success.jsp").forward(req, resp);
}
}
public void regist(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//根据前面的分析,完成servlet程序
//1.获取请求(由表单传递)的参数
final String username = req.getParameter("username");
final String password = req.getParameter("password");
final String email = req.getParameter("email");
final String code = req.getParameter("code");//在表单验证码没有写name书写将他补充
//2.检查验证码是否正确:写死,要求验证码为abcde
if ("abcde".equals(code)) {
//正确
//3.检查用户名是否可用(调用service层)
if (userService.existUsername(username)) {
//用户名不可用
System.out.println("用户名["+username+"]已经存在");
//回写信息,保存到reqquest域中
req.setAttribute("msg", "用户名已经存在");//错误信息
req.setAttribute("username", username);//用户名
req.setAttribute("email", email);//邮箱
//跳回注册页面
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}else {
//可用(调用service将用户信息保存到数据库)
userService.registUser(new User(null, username, password, email));
//跳转到注册成功页面
req.getRequestDispatcher("/pages/user/regist_success.jsp").forward(req, resp);
}
}else {
//错误
//跳回注册页面(使用请求转发来完成)
System.out.println("验证码["+code+"]错误");
//回写信息,保存到request域中
req.setAttribute("msg", "验证码错误");
req.setAttribute("username", username);
req.setAttribute("email", email);
req.getRequestDispatcher("/pages/user/regist.jsp").forward(req, resp);
}
}
}
在原有的UserServlet程序中doPost方法负责识别需求(哪个jsp文件的请求)并调用相应的函数。经过我们反射机制的优化,已经可以做到动态的识别并调用相应的函数。所以在不同的模块中这部分的代码肯定是相同的,所有我们可以将这部分的代码抽取出来,形成一个BaseServlet父类继承即可。要求BaseServlet继承HttpServlt.然后子类继承BaseServlet即可
- 可以使用debug来验证这一过程
BeanUtils工具类的使用
-
BeanUtils可以便捷的将我们获取的请求参数,封装成对应的JavaBean对象
-
需要改进的现状缺点
但是将参数封装成javaBean对象,或者理解为对象属性值的拷贝,这一操作,在很多模块的代码中都会使用到,如果没有都这样修改将太麻烦了。所以我们可以将这一功能封装成一个功能类,哪个类需要使用直接调用即可
注入原理的解释(为什么可以注入) -
集成的工具类WebUtils
package com.atguigu.utils;
import com.atguigu.pojo.User;
import org.apache.commons.beanutils.BeanUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
//实现对javaBean对象的注入(对象属性值的拷贝操作)
public class WebUtils {
public static void copyParamToBean(HttpServletRequest req, Object object){
System.out.println("注入之前"+object);
//把所以的请求参数都注入到user对象中
try {
BeanUtils.populate(object,req.getParameterMap());
System.out.println("注入之后"+object);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
req.getParameterMap()方法获取出来的到底是什么呢
我们通过debug来分析代码走向
可以发现:他们是通过属性名和参数名对应注入的
- 我们修改userName对应的setxxx方法,还能成功的注入吗
- 发现此时username的值注入失败了,为默认值null
对于工具类中 copyParamToBean(复制参数)的改进
-
修改点1(将参数HttpServletRequest改成Map)
-
原因
我们的复制参数的操作是很常用的。而我们的代码分为web层,dao层,service层,而不是每一层都有HttpServletRequest
这个参数。所以我需要换成Map,代码的重用性更强
HttpServletRequest属于web层,如果我们把参数写成HttpServeltRequsst
将会造成web层的耦合度高
- 修改点2
- 修改点2后的代码:
- WebUntils类的最后代码
package com.atguigu.utils;
import com.atguigu.pojo.User;
import org.apache.commons.beanutils.BeanUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
//实现对javaBean对象的注入(对象属性值的拷贝操作)
public class WebUtils {
public static <T> T copyParamToBean(Map value, T bean){
System.out.println("注入之前"+bean);
//把所以的请求参数都注入到user对象中
try {
BeanUtils.populate(bean,value);
System.out.println("注入之后"+bean);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
return bean;
}
}
- 问题:为啥用户名没有注入成功?
本质上就是因为错误的参数名导致没有找到对应的setter方法给属性进行赋值
书城项目第四阶段 使用 EL 表达式修改表单回显
将login和regist.jsp文件的表达式脚本改成EL表达式
书城项目第五阶段 图书模块
第五阶段内容介绍
- 功能包含
MVC概念的介绍
创建图书模块的数据库表
- 我们通过分析需要哪些表,表里面定义哪些列
- 创建book表的代码
-- 创建书城项目所需要的图书表
CREATE TABLE t_book(
id INT PRIMARY KEY AUTO_INCREMENT,-- 图书id
`name` VARCHAR(100),-- 书名
price DECIMAL(11,2),-- 图书价格
author VARCHAR(100),-- 作者
sales INT,-- 销量
stock INT ,-- 库存
img_path VARCHAR(200) -- 图书图片路径
);
- 测试或者初始化的数据
插入初始化测试数据
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'java从入门到放弃' , '国哥' , 80 , 9999 , 9 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '数据结构与算法' , '严敏君' , 78.5 , 6 , 13 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '怎样拐跑别人的媳妇' , '龙伍' , 68, 99999 , 52 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '木虚肉盖饭' , '小胖' , 16, 1000 , 50 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'C++编程思想' , '刚哥' , 45.5 , 14 , 95 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '蛋炒饭' , '周星星' , 9.9, 12 , 53 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '赌神' , '龙伍' , 66.5, 125 , 535 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'Java编程思想' , '阳哥' , 99.5 , 47 , 36 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'JavaScript从入门到精通' , '婷姐' , 9.9 , 85 , 95 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'cocos2d-x游戏编程入门' , '国哥' , 49, 52 , 62 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'C语言程序设计' , '谭浩强' , 28 , 52 , 74 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'Lua语言程序设计' , '雷丰阳' , 51.5 , 48 , 82 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '西游记' , '罗贯中' , 12, 19 , 9999 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '水浒传' , '华仔' , 33.05 , 22 , 88 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '操作系统原理' , '刘优' , 133.05 , 122 , 188 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '数据结构 java版' , '封大神' , 173.15 , 21 , 81 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'UNIX高级环境编程' , '乐天' , 99.15 , 210 , 810 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , 'javaScript高级编程' , '国哥' , 69.15 , 210 , 810 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '大话设计模式' , '国哥' , 89.15 , 20 , 10 , 'static/img/default.jpg');
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`)
values(null , '人月神话' , '刚哥' , 88.15 , 20 , 80 , 'static/img/default.jpg');
## 查看表内容
select id,name,author,price,sales,stock,img_path from t_book;
编写图书模块的JavaBean类Book类
javaBean类和我们的表是一一对应的
- 所以我们给图片路径设置默认值
- javaBean Book.java
package com.atguigu.pojo;
import java.math.BigDecimal;
//t_book表对应的javaBean类
public class Book {
private Integer id;//图书编号
private String name;//图书名
private BigDecimal price;//价格
private Integer sales;//销量
private Integer stock;//库存
//
private String imgPath="static/img/default.jpg";//图书图片路径
public Book() {
}
public Book(Integer id, String name, BigDecimal price, Integer sales, Integer stock, String imgPath) {
this.id = id;
this.name = name;
this.price = price;
this.sales = sales;
this.stock = stock;
this.imgPath = imgPath;
}
/**
* 获取
* @return id
*/
public Integer getId() {
return id;
}
/**
* 设置
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return price
*/
public BigDecimal getPrice() {
return price;
}
/**
* 设置
* @param price
*/
public void setPrice(BigDecimal price) {
this.price = price;
}
/**
* 获取
* @return sales
*/
public Integer getSales() {
return sales;
}
/**
* 设置
* @param sales
*/
public void setSales(Integer sales) {
this.sales = sales;
}
/**
* 获取
* @return stock
*/
public Integer getStock() {
return stock;
}
/**
* 设置
* @param stock
*/
public void setStock(Integer stock) {
this.stock = stock;
}
/**
* 获取
* @return imgPath
*/
public String getImgPath() {
return imgPath;
}
/**
* 设置
* @param imgPath
*/
public void setImgPath(String imgPath) {
//要求给定的图书封面路径不能为空才给他赋值(有点疑惑,感觉用处不大)
if (imgPath == null || "".equals(imgPath)) {
this.imgPath = imgPath;
}
}
public String toString() {
return "Book{id = " + id + ", name = " + name + ", price = " + price + ", sales = " + sales + ", stock = " + stock + ", imgPath = " + imgPath + "}";
}
}
编写图书模块的DaO和测试
- BookDao接口
package com.atguigu.dao;
import com.atguigu.pojo.Book;
import java.util.List;
//UserDao接口中定义图书模块可能会操作数据库的方法
public interface BookDao {
public int addBook(Book book);
public int deleteBook(Integer id);
public int updateBook(Book book);
public Book queryBookById(Integer id);//根据图书编号查询图书
public List<Book>queryBooks();
}
- BookDaoIml接口实现类
package com.atguigu.dao.impl;
import com.atguigu.dao.BookDao;
import com.atguigu.pojo.Book;
import java.util.List;
public class BookDaoImpl extends BaseDao<Book> implements BookDao {
@Override
public int addBook(Book book) {
String sql = "insert into t_book( `name` , `author` , `price` , `sales` , `stock` , `img_path`) values(?,?,?,?,?,?);";
return super.update(sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(),book.getStock(),book.getImgPath());
}
@Override
public int deleteBook(Integer id) {
String sql = "delete from t_book where id=?";
return super.update(sql, id);
}
@Override
public int updateBook(Book book) {
String sql = "update t_book set `name`=?,`author`=?,`price`=?,`sales`=?,`stock`=?,`img_path`=? where id = ?";
return super.update(sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(),book.getStock(),book.getImgPath(),book.getId());
}
@Override
public Book queryBookById(Integer id) {
String sql = "select `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath from t_book where id = ?";
return super.querySingle(sql, Book.class, id);
}
@Override
public List<Book> queryBooks() {
String sql = "select `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath from t_book";
return super.queryMulti(sql, Book.class);
}
}
- BookDao测试类
package com.atguigu.test;
import com.atguigu.dao.BookDao;
import com.atguigu.dao.impl.BookDaoImpl;
import com.atguigu.pojo.Book;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.Assert.*;
public class BookDaoTest {
private BookDao bookDao = new BookDaoImpl();
@Test
public void addBook() {
bookDao.addBook(new Book(null,"国哥","天下第一帅哥",new BigDecimal(9999),234,345,"static/img/default.jpg"));
}
@Test
public void deleteBook() {
bookDao.deleteBook(22);
}
@Test
public void updateBook() {
bookDao.updateBook(new Book(22,"天才","天下第一帅哥",new BigDecimal(100000),234,345,"static/img/default.jpg"));
}
@Test
public void queryBookById() {
final Book book = bookDao.queryBookById(22);
System.out.println(book);
}
@Test
public void queryBooks() {
final List<Book> books = bookDao.queryBooks();
System.out.println(books);
}
}
编写图书模块的Service和测试Service
- 通过分析Service在业务层面需要提供增上改查等方法
- bookService接口
package com.atguigu.service;
import com.atguigu.pojo.Book;
import java.util.List;
public interface BookService {
public int addBook(Book book);//添加图书
public int deleteBook(Integer id);//删除图书
public int updateBook(Book book);//修改图书
public Book queryBookById(Integer id);//根据图书编号查询图书
public List<Book> queryBooks();//查询所有图书
}
- bookServieImpl
package com.atguigu.service;
import com.atguigu.pojo.Book;
import java.util.List;
public interface BookService {
public int addBook(Book book);//添加图书
public int deleteBook(Integer id);//删除图书
public int updateBook(Book book);//修改图书
public Book queryBookById(Integer id);//根据图书编号查询图书
public List<Book> queryBooks();//查询所有图书
}
- 测试类
package com.atguigu.test;
import com.atguigu.dao.BookDao;
import com.atguigu.dao.impl.BookDaoImpl;
import com.atguigu.pojo.Book;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.Assert.*;
public class BookServiceTest {
BookDao bookDao = new BookDaoImpl();
@Test
public void addBook() {
bookDao.addBook(new Book(null,"swt",
"天下第一帅哥",new BigDecimal(9999),234,345,null));
}
@Test
public void deleteBook() {
bookDao.deleteBook(24);
}
@Test
public void updateBook() {//更新指定id的书籍信息
bookDao.updateBook(new Book(24,"天才","天下第一帅哥",new BigDecimal(100000),234,345,"static/img/default.jpg"));
}
@Test
public void queryBookById() {
System.out.println(bookDao.queryBookById(24));
}
@Test
public void queryBooks() {
final List<Book> books = bookDao.queryBooks();
for (Book book : books) {
System.out.println(book);
}
}
}
编写图书模块的Web层
图书列表功能的实现
- 实现列表功能的流程
列表功能是可以展示图书信息。只有有图书才能进行进一步的增删改查,所以首先需要完成图书列表功能
- 1.修改1:根据上面流程分析:manger.jsp的请求地址是BookServlet(需要将manager.jsp地址该为BookServlet地址)
- 他需要调用Servlet中的list方法去查询全部图书
-2.编写BookServlt程序并配置地址
package com.atguigu.web;
import com.atguigu.pojo.Book;
import com.atguigu.service.BookService;
import com.atguigu.service.impl.BookServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
//用来处理图书模块页面的请求
public class BookServlet extends BaseServlet{
private BookService bookService= new BookServiceImpl();
protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.通过BookService查询所有图书
final List<Book> books = bookService.queryBooks();
//2.将图书列表保存到request域中
req.setAttribute("books", books);
//3.转发到pages/manager/book_manager.jsp
req.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(req, resp);
}
}
- 3.编写book_manager.jsp页面
- book_manager.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>图书管理</title>
<%--静态包含:base标签,css样式,Jquery文件--%>
<%@include file="/pages/common/head.jsp"%>
</head>
<body>
<div id="header">
<img class="logo_img" alt="" src="../../static/img/logo.gif" >
<span class="wel_word">图书管理系统</span>
<%--静态包含manager管理模块的菜单--%>
<%@include file="/pages/common/manager_menu.jsp"%>
</div>
<div id="main">
<table>
<tr>
<td>名称</td>
<td>价格</td>
<td>作者</td>
<td>销量</td>
<td>库存</td>
<td colspan="2">操作</td>
</tr>
<c:forEach items="${requestScope.books}" var ="book">
<tr>
<td>book.name</td>
<td>book.price</td>
<td>book.author</td>
<td>book.sales</td>
<td>book.stock</td>
<td><a href="book_edit.jsp">修改</a></td>
<td><a href="#">删除</a></td>
</tr>
</c:forEach>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td><a href="book_edit.jsp">添加图书</a></td>
</tr>
</table>
</div>
<%--静态包含页脚信息--%>
<%@include file="/pages/common/footer.jsp"%>
</body>
</html>
前后台的简单介绍
添加图书功能的实现
- 实现流程分析
注意:此处如果乱码在BaseServelt中的doGet或者doPost方法(取决于你是哪一种提交方式)的第一行加上 req.setCharacterEncoding("utf-8");
- 表单重复提交Bug
而我们的请求转发是属于一次请求,所以最后一次请求就是添加图书。所以将会执行2次添加图书的操作。而请求重定向是一次独立的请求,所以就算按f5执行最后一次操作也不会怎么样
删除图书功能的实现
-
删除图书的流程
-
修改1.将图书管理页面book_manager.jsp的请求地址改为BookServlet的地址
-
修改为重写pareInt方法
-
封装在WebUtils中的paareInt()方法
//将字符串转化为int类型的数据(转化失败将返回默认值)
public static int parseInt(String str, int defaultValue){
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
e.printStackTrace();
}
return defaultValue;
}
在删除时,当用户点击删除时,应该提示用户是否真的删除,防止用于误触删除
- BookServlet程序中添加的delete方法
//定义删除图书的方法
protected void delete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求id(图书编号)
int id = WebUtils.parseInt(req.getParameter("id"), 0);
//2.调用BookService.deleteBook()删除图书
bookService.deleteBook(id);
//3.重定向到图书列表页面
resp.sendRedirect(req.getContextPath() + "/manager/bookServlet?action=list");
}
- 需要将所有的删除键绑上单击事件
- 具体绑定单击事件
<script type="text/javascript">
$(function (){
//给删除的a标签绑定单击事件,用于删除的提示确认操作
$("a.deleteClass").click(function (){
// 在事件的 function 函数中,有一个 this 对象。这个 this 对象,是当前正在响应事件的 dom 对象。
/*
confirm是确认提示框函数
参数是它提示的内容
他有2个按钮,一个确认,一个取消
返回true表示点击了确认,返回false表示顶级了取消
*/
//这里直接返回:return false阻止去默认行为,return true则允许
return confirm("你确定要删除【"+ $(this).parent().parent().find("td:first").text() + "】")
});
});
</script>
- 注意:可能会出现乱码
修改图书
修改图书第一步:回显回显修改的信息
- 修改1:修改点击修改后跳转的页面
-
点击后应该跳转到编辑图书的页面
-
将信息变成要修改的图书的流程图
-
根据流程图我们需要先跳转到bookServlet中,将上面地址改过来
-
bookServlet中用到的getBook方法
//定义获取图书的方法
protected void getBook(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数图书编号
int id = WebUtils.parseInt(req.getParameter("id"), 0);
//3.调用bookService.queryBookById()查询图书
Book book = bookService.queryBookById(id);
//2.保存到request域中
req.setAttribute("book", book);
//4.转发到pages/manager/book_edit.jsp页面
req.getRequestDispatcher("/pages/manager/book_edit.jsp").forward(req, resp);
}
修改图书的第二步:提交给服务器保存修改
- 修改流程分析
不同的方案目的都是达到动态修改隐藏域的值
方案二
方案三
当我们点击添加操作时,会先访问servelt程序,并将添加的对象放到request域中。所以我们可以判断request域中是否有对象来判断执行的是什么操作
- bookServlet中更新图书信息的方法
//修改图书信息的方法
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求参数并封装成Book对象
Book book = WebUtils.copyParamToBean(req.getParameterMap(), new Book());
//2.调用BookService.updateBook()修改图书
bookService.updateBook(book);
//3.重定向到图书列表页面
//地址:/manager/bookServlet?action=list
resp.sendRedirect(req.getContextPath() + "/manager/bookServlet?action=list");
}
项目第五阶段-图书分页
在前面我们将图书的列表全部显示在了页面上,但在实际开发中全部显示在页面上这个过程中数据处理是非常慢的,并且数据显示的也会非常凌乱。所以在开发中我们一般做分页处理
- 分页原理图
分页模式Page对象的创建
- 对分页数据使用泛型
- Page模型类
package com.atguigu.pojo;
import java.util.List;
//表示分页模型对象
public class Page<T> {
//每页显示数量
public static final Integer PAGE_SIZE = 4;
//当前页码
private Integer pageNo;
//总页码
private Integer pageTotal;
//当前页显示数量
private Integer pageSize = PAGE_SIZE;
//总记录数
private Integer pageTotalCount;
//当前页数据
private List<T> items;
/**
* 获取
* @return pageNo
*/
public Integer getPageNo() {
return pageNo;
}
/**
* 设置
* @param pageNo
*/
public void setPageNo(Integer pageNo) {
this.pageNo = pageNo;
}
/**
* 获取
* @return pageTotal
*/
public Integer getPageTotal() {
return pageTotal;
}
/**
* 设置
* @param pageTotal
*/
public void setPageTotal(Integer pageTotal) {
this.pageTotal = pageTotal;
}
/**
* 获取
* @return pageSize
*/
public Integer getPageSize() {
return pageSize;
}
/**
* 设置
* @param pageSize
*/
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
/**
* 获取
* @return pageTotalCount
*/
public Integer getPageTotalCount() {
return pageTotalCount;
}
/**
* 设置
* @param pageTotalCount
*/
public void setPageTotalCount(Integer pageTotalCount) {
this.pageTotalCount = pageTotalCount;
}
/**
* 获取
* @return items
*/
public List<T> getItems() {
return items;
}
/**
* 设置
* @param items
*/
public void setItems(List<T> items) {
this.items = items;
}
public String toString() {
return "Page{PAGE_SIZE = " + PAGE_SIZE + ", pageNo = " + pageNo + ", pageTotal = " + pageTotal + ", pageSize = " + pageSize + ", pageTotalCount = " + pageTotalCount + ", items = " + items + "}";
}
}
分页初步实现
- 修改
- 修改遍历的数据源
我们发现首页有分页条,我们把他复制到我们的manager.jsp页面下面就可以了
- 将分页中的一些数据动态的显示出来
- wen层booservlet中的page()
//web层处理分页的方法
protected void page(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求的参数pageNo和pageSize(默认当前页面为1)
int pageNo = WebUtils.parseInt(req.getParameter("pageNo"), 1);
int pageSize = WebUtils.parseInt(req.getParameter("pageSize"), Page.PAGE_SIZE);
//2.调用BookService.page(pageNo, pageSize):Page
Page<Book> page = bookService.page(pageNo, pageSize);
//3.保存Page对象到request域中
req.setAttribute("page", page);
//4.转发到pages/manager/book_manager.jsp
req.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(req, resp);
}
- service层:page方法
//分页方法
@Override
public Page<Book> page(int pageNo, int pageSize) {
Page<Book> page = new Page<>();
page.setPageNo(pageNo);//设置当前页面
page.setPageSize(pageSize);//设置每页显示数量
//获取总记录数
Integer pageTotalCount = bookDao.queryForPageTotalCount();
page.setPageTotalCount(pageTotalCount);//设置总记录数
//获取当前页数据
//1.求当前也数据开始索引begin
Integer begin = (page.getPageNo() - 1) * pageSize;
List<Book> items = bookDao.queryForItems( begin, pageSize);
page.setItems(items);//设置当前数据
//求总页码
Integer pageTotal = pageTotalCount / pageSize;
if(pageTotalCount % pageSize > 0){
pageTotal++;
}
page.setPageTotal(pageTotal);
return page;
}
- dao层
//求总记录数
@Override
public Integer queryForPageTotalCount() {
String sql = "select count(*) from t_book";
Number count =(Number) super.queryScalar(sql);//这里需要强转成Number以便转成Integer
return count.intValue();
}
//求当前页的数据
@Override
public List<Book> queryForItems(Integer begin, int pageSize) {
String sql = "select `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath from t_book limit ?,?";
return new BookDaoImpl().queryMulti(sql, Book.class, begin, pageSize);
}
还有写完了一定要测试........从dao---->web
首页,上一页,下一页,末页跳转的的实现
我们的首页,上页,末页跳转的实现和前面分页的原理一样,需要传递2个参数即当前页面和每页显示数量,将这2个参数发送给servlet程序处理
- 对于如果我们正在首页/末页,然后点击上一页或者下一页怎样处理
在本项目中,对于这个采用当当前页面为首页/末页的时候将不会显示首页 上一页或者末页 下一页的按钮
修改:
跳转到指定页码功能的实现
输入框对象location
-
页面跳转到了百度
-
我们可以根据这个为思路直接将当前页面的地址修改为bookServlet的地址并将页面数(想要跳转的页码)传递给它**
-
绑定事件
<script type="text/javascript">
$(function () {
// 跳到指定的页码
$("#searchPageBtn").click(function () {
/*获取当前页面,即输入框中的页面值*/
var pageNo = $("#pn_input").val();
<%--var pageTotal = ${requestScope.page.pageTotal};--%>
<%--alert(pageTotal);--%>
// javaScript 语言中提供了一个 location 地址栏对象
// 它有一个属性叫 href.它可以获取浏览器地址栏中的地址
// href 属性可读,可写
location.href = "http://localhost:8080/book/manager/bookServlet?action=page&pageNo=" +
pageNo;
});
});
</script>
数据有效边境检查
- 改进1:base标签动态化
- 没有进行修改
- 改进2:非法数据控制
- 页面校验
有的情况下有的人可能会直接在地址栏输入地址,这样就会跳过页面直接访问到了服务器,所以我们的服务器也需要进行校验
其实像这样的有效数据检查,每一个模块只要用分页都是需要做的。所以我们可以在设置pageNo这个值(set方法)的时候就自动会进行检查,这样代码适用范围更大,更优雅
- 小点:服务器的检查也会检查在框中设置页码这一过程
但是感觉还是在页码也设置检查把,因为如果页码检查没有通过就不会通过服务器了,这样可以减少服务器的压力
页码段的页码合理性的检查(待完成)
对于JSTL的choose...when标签还有问题。
分页条页码是输出
- 页码的输出设计到一定的算法, 输出原理见官方文档
修改分页后,增加,删除,修改图书信息的回显页面
- 1.我们需要将所以跳转到列表页面都改为跳转到分页(都是booServlet中的)
- 修改后我们再次测试一下
以添加图书为例
- book_edit页码需要接收发送的参数
- 接下来pageNo作为参数发送给了BookServlet
删除和修改的情况和添加类似,只是不需要考虑pageNo+=1的情况
前台分页的初步实现
- 前台分页原理
-
ClientBookServlet的地址配置
-
ClientBookServlet程序(将BookServlet中的page方法复制过来,完全一样)
package com.atguigu.web;
import com.atguigu.pojo.Book;
import com.atguigu.pojo.Page;
import com.atguigu.service.BookService;
import com.atguigu.service.impl.BookServiceImpl;
import com.atguigu.utils.WebUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//处理前台的请求
public class ClientBookServlet extends BaseServlet{
private BookService bookService = new BookServiceImpl();
protected void page(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我被访问了");
//1.获取请求的参数pageNo和pageSize(默认当前页面为1)
int pageNo = WebUtils.parseInt(req.getParameter("pageNo"), 1);
int pageSize = WebUtils.parseInt(req.getParameter("pageSize"), Page.PAGE_SIZE);
//2.调用BookService.page(pageNo, pageSize):Page
Page<Book> page = bookService.page(pageNo, pageSize);
//3.保存Page对象到request域中
req.setAttribute("page", page);
//4.转发到pages/manager/book_manager.jsp
req.getRequestDispatcher("/pages/client/index.jsp").forward(req, resp);
}
}
-
接下来在client.index.jsp中遍历输出数据
-
client中的index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>书城首页</title>
<%--静态包含:base标签,css样式,Jquery文件--%>
<%@include file="/pages/common/head.jsp"%>
</head>
<body>
<div id="header">
<img class="logo_img" alt="" src="static/img/logo.gif" >
<span class="wel_word">网上书城</span>
<div>
<a href="pages/user/login.jsp">登录</a> |
<a href="pages/user/regist.jsp">注册</a>
<a href="pages/cart/cart.jsp">购物车</a>
<a href="pages/manager/manager.jsp">后台管理</a>
</div>
</div>
<div id="main">
<div id="book">
<div class="book_cond">
<form action="" method="get">
价格:<input id="min" type="text" name="min" value=""> 元 -
<input id="max" type="text" name="max" value=""> 元
<input type="submit" value="查询" />
</form>
</div>
<div style="text-align: center">
<span>您的购物车中有3件商品</span>
<div>
您刚刚将<span style="color: red">时间简史</span>加入到了购物车中
</div>
</div>
<c:forEach items="${requestScope.page.items}" var="book">
<div class="b_list">
<div class="img_div">
<img class="book_img" alt="" src="${book.imgPath}" />
</div>
<div class="book_info">
<div class="book_name">
<span class="sp1">${book.name}</span>
<span class="sp2"></span>
</div>
<div class="book_author">
<span class="sp1">${book.author}:</span>
<span class="sp2"></span>
</div>
<div class="book_price">
<span class="sp1">价格:</span>
<span class="sp2">${book.price}</span>
</div>
<div class="book_sales">
<span class="sp1">销量:</span>
<span class="sp2">${book.sales}</span>
</div>
<div class="book_amount">
<span class="sp1">库存:</span>
<span class="sp2">${book.stock}</span>
</div>
<div class="book_add">
<button>加入购物车</button>
</div>
</div>
</div>
</c:forEach>
</div>
<%--分页条的开始--%>
<div id="page_nav">
<%--当页面大于首页才显示首页和上一页这2个按钮--%>
<c:if test="${requestScope.page.pageNo>1}">
<a href="client/bookServlet?action=page&pageNo=1">首页</a>
<a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageNo-1}">上一页</a>
</c:if>
<%--页码输出的开始--%>
<c:choose>
<%--情况 1:如果总页码小于等于 5 的情况,页码的范围是:1-总页码--%>
<c:when test="${ requestScope.page.pageTotal <= 5 }">
<c:set var="begin" value="1"/>
<c:set var="end" value="${requestScope.page.pageTotal}"/>
</c:when>
<%--情况 2:总页码大于 5 的情况--%>
<c:when test="${requestScope.page.pageTotal > 5}">
<c:choose>
<%--小情况 1:当前页码为前面 3 个:1,2,3 的情况,页码范围是:1-5.--%>
<c:when test="${requestScope.page.pageNo <= 3}">
<c:set var="begin" value="1"/>
<c:set var="end" value="5"/>
</c:when>
<%--小情况 2:当前页码为最后 3 个,8,9,10,页码范围是:总页码减 4 - 总页码--%>
<c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal-3}">
<c:set var="begin" value="${requestScope.page.pageTotal-4}"/>
<c:set var="end" value="${requestScope.page.pageTotal}"/>
</c:when>
<%--小情况 3:4,5,6,7,页码范围是:当前页码减 2 - 当前页码加 2--%>
<c:otherwise>
<c:set var="begin" value="${requestScope.page.pageNo-2}"/>
<c:set var="end" value="${requestScope.page.pageNo+2}"/>
</c:otherwise>
</c:choose>
</c:when>
</c:choose>
<c:forEach begin="${begin}" end="${end}" var="i">
<c:if test="${i == requestScope.page.pageNo}">
【${i}】
</c:if>
<c:if test="${i != requestScope.page.pageNo}">
<a href="client/bookServlet?action=page&pageNo=${i}">${i}</a>
</c:if>
</c:forEach>
<%--页码输出的结束--%>
<c:if test="${requestScope.page.pageNo<requestScope.page.pageTotal}">
<a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一页</a>
<a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末页</a>
</c:if>
<a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一页</a>
<a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末页</a>
共${ requestScope.page.pageTotal }页,${requestScope.page.pageTotalCount}条记录 到第<input value="${requestScope.page.pageNo}" name="pn" id="pn_input"/>页
<input id="searchPageBtn" type="button" value="确定">
<script type="text/javascript">
$(function () {
// 跳到指定的页码
$("#searchPageBtn").click(function () {
/*获取当前页面,即输入框中的页面值*/
var pageNo = $("#pn_input").val();
<%--var pageTotal = ${requestScope.page.pageTotal};--%>
<%--alert(pageTotal);--%>
// javaScript 语言中提供了一个 location 地址栏对象
// 它有一个属性叫 href.它可以获取浏览器地址栏中的地址
// href 属性可读,可写
/*获取总页码*/
/*页码检查页码是否合理
* 待完成
* */
location.href = "client/bookServlet?action=page&pageNo=" +
pageNo;
});
});
</script>
</div>
<%--分页条的结束--%>
</div>
<%--静态包含页脚信息--%>
<%@include file="/pages/common/footer.jsp"%>
</body>
</html>
1.在首页中利用分页循环输出每一本书即可
2.将之前学号的分页条复制到首页(注意将分页条中的地址改为client/bookServlet)
分页条的抽取
1.因为前台中和后台中分页条的访问地址一样,所以抽取分页条中请求地址为 url 属性
- 并修改toString 方法
**当进行分页请求的时候我们统一在Servlet程序中的page方法分别给他们进行赋值
2.当我们用`${requestScope.page.url}去替代之后,我们发现此时前台和后台的分页条已经完全一样了,此时我们可以将他们抽取出来,然后静态包含即可
当我们将分页条抽取出来后,以后就只有维护一份就可以了,并且其他的模块分页条是可以通用的
价格区间搜索并分页的分析
- 实现原理
和我们后台分页区别不大,仅仅是添加了一个价格的查询条件
价格区间搜索并分页功能的实现
- ClientBookServlet程序中的pageByPrice方法
//处理按价格搜索并进行分页
protected void pageByPrice(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我被访问了");
//1.获取请求的参数pageNo和pageSize(默认当前页面为1)
int pageNo = WebUtils.parseInt(req.getParameter("pageNo"), 1);
int pageSize = WebUtils.parseInt(req.getParameter("pageSize"), Page.PAGE_SIZE);
//获取请求参数max min
int min = WebUtils.parseInt(req.getParameter("min"), 0);
int max = WebUtils.parseInt(req.getParameter("max"), Integer.MAX_VALUE);
//2.调用BookService.page(pageNo, pageSize):PageByPrice
Page<Book> page = bookService.pageByPrice(pageNo, pageSize, min, max);
//给url赋值(此时调用的方法变为了pageByPrice)
page.setUrl("client/bookServlet?action=pageByPrice");
//3.保存Page对象到request域中
req.setAttribute("page", page);
//4.转发到pages/manager/book_manager.jsp
req.getRequestDispatcher("/pages/client/index.jsp").forward(req, resp);
}
- Service层的pageByPrice方法
@Override
public Page<Book> pageByPrice(int pageNo, int pageSize, int min, int max) {
Page<Book> page = new Page<>();
page.setPageSize(pageSize);//设置每页显示数量
//获取总记录数
Integer pageTotalCount = bookDao.queryForPageTotalCountByPrice(min, max);
page.setPageTotalCount(pageTotalCount);//设置总记录数
//求总页码
Integer pageTotal = pageTotalCount / pageSize;
if(pageTotalCount % pageSize > 0){
pageTotal++;
}
page.setPageTotal(pageTotal);
page.setPageNo(pageNo);//设置当前页面
//获取当前页数据
//1.求当前也数据开始索引begin
Integer begin = (page.getPageNo() - 1) * pageSize;
List<Book> items = bookDao.queryForItemsByPrice( begin, pageSize, min, max);
page.setItems(items);//设置当前数据
return page;
}
- dao层中需要添加的方法
//求总记录数(通过价格区间)
@Override
public Integer queryForPageTotalCountByPrice(int min, int max) {
String sql = "select count(*) from t_book where price between ? and ?";
Number count =(Number) super.queryScalar(sql,min,max);//这里需要强转成Number以便转成Integer
return count.intValue();
}
//根据价格查询当前页数据
@Override
public List<Book> queryForItemsByPrice(Integer begin, int pageSize, int min, int max) {
String sql = "select `id`,`name`,`author`, `price`, `sales`,`stock`,`img_path` imgPath" +
" from t_book where price between ? and ? order by price limit ?,?";
return new BookDaoImpl().queryMulti(sql, Book.class,min,max, begin, pageSize);
}
注意当我们写完各个层次的方法后,一定要进行测试。测试的顺序是dao-->service--->servlet(和页面进行联调)
搜索价格区间的回显
- 问题所在
- 修改
解决分页条中不带价格区间的bug
- 问题描述
- 问题分析
1.在我们在输出框中选择价格区间发送请求时,该页面将价格区间当成参数发送给了ClientBookServlet程序,servlet将参数(max,min)传递给 bookService.pageByPrice
进行查询返回的是在价格区间的页面,然后请求转发给了index.jsp进行了显示。参数我们看到的数据是带有价格限制条件的页面
-
- 当要点击2将会跳转的地址
可以发现我们发送的地址,并没有包含max min。此时我们的ClientBookServlet程序得到请求后,没有max min按照默认值处理
此时我们查询到的页面是包含所有数据的页面,所以我们的分页条将会不一样
而此时我们的index.jsp的价格输出框没有值,所以param.max,param.min所以输出为空
- 当要点击2将会跳转的地址
解决方案
解决的方案是当我们点击页数的时候可以将max min参数页发送过去。进而可以解决这个问题
具体是:当我们第一次输出框输出价格时,我们使用一个容器将价格保存起来。然后当我们点击数字时将这些参数附加在url中, 给分页条中的地址附加上max min参数最后形成我们的param.max param.min就有值了
书城项目第六阶段
显示登录的用户信息
-
问题所在
思路:我们应该在登录成功的时候就将用户信息保存在域中,然后再页面中进行输出 -
注意:
所有当我们点击我的订单跳转到我的订单页面时,用户的信息也需要显示,而这一操作已经是2次请求了,所以我们不能保存在request域中,而是选择范围更大的session域 -
上面修改看一下文档就可以
-
发现问题
-
修改首页
注销登录
我们在登录成功页面点击注销后,应该显示的是只有登录/注册的首页或者是登录页面
我们只需要在servelt添加方法,将session中的数据删除即可
- 具体代码见文档
表单重复提交的三种常见情况
第一种情况
一:提交完表单。服务器使用请求转来进行页面跳转。这个时候,用户按下功能键 F5,就会发起最后一次的请求。
造成表单重复提交问题。解决方法:使用重定向来进行跳转
-
演示第一种情况
-
一个表单提交jsp
<%--
Created by IntelliJ IDEA.
User: SWT
Date: 2023/10/25
Time: 20:15
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<form action="http://localhost:8080/booktest/loginServlet" method="get">
用户名:<input type="text" username="username"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
- 一个表单提交成功的页面
- 当我们提交成功后,点击f5发现,发现数据库又保存了一次数据
情况二:出现网络延迟,用户多次提交
此时如果用户多次点击登录将会多次提交表单,造成数据多次保存
验证码解决表单提交的底层原理
- 使用验证码解决表单重复提交后面2种情况
- 原理图
谷歌验证码的使用
我们知道我们的生成的验证码图片被谷歌的(导入的jar)KaptchaServlet
保存
- loginServlet程序(实现原理图中的RegiestServlet程序)
package com.atguigu.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY;
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
//获取session中的验证码
String code = (String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY);
//删除session中的验证码
req.getSession().removeAttribute(KAPTCHA_SESSION_KEY);
//获取发送的参数
final String username = req.getParameter("username");
final String code1 = req.getParameter("code");
//判断code和code1是否相等
if(code1!=null&&code1.equalsIgnoreCase(code)){
//将用户名保存到数据库中
System.out.println("将用户名保存到了数据库中:"+username);
//请求转发到首页
req.getRequestDispatcher("/ok.jsp").forward(req,resp);
}else {
System.out.println("请不要重复提交表单");
}
}
}
把谷歌验证码加入到书城使用
- 我们使用验证码来改进,解决这个表单重复提交所带来的问题
验证码的切换
- 我们需要给这个验证码绑上单击事件
我们发现我们绑定的单击事件,在谷歌中完全没有问题,但是在火狐中使用时,当我们点击一次后再也不能进行点击了 - 上面问题分析以及解决方案
每次请求的时候会将请求地址和缓存中资源的地址进行比较决定是否发起请求
- 绑定的单击事件的完整代码
//给注册绑定单击事件
$(function (){
/*给验证码的图片绑定单击事件*/
$("#code_img").click(function (){
// 在事件响应的 function 函数中有一个 this 对象。这个 this 对象,是当前正在响应事件的 dom 对象(表示img标签的对象)
// src 属性表示验证码 img 标签的 图片路径。它可读,可写(表示Img的src属性)
// alert(this.src);表示输出img的路径
//在后面加上参数参数的值将会动态变化,每次请求的地址将不同
this.src="${basePath}kaptcha.jpg?d="+new Date()/*重新对他进行请求,将会重新发起一次请求*/
});
第六阶段 购物车
购物车模块的分析
- 购物车模型
去结账这个按钮点击之后将会生成一个账单,所以这个功能不属于购物车模块
购物车模型的创建
- 一共有2个模型
- 1.购物车商品项模型
package com.atguigu.pojo;
import java.math.BigDecimal;
//购物车商品项的模型
public class CartItem {
private Integer id;//商品id
private String name;//商品名称
private Integer count;//商品数量
private BigDecimal price;//商品单价
private BigDecimal totalPrice;//商品总价
public CartItem() {
}
public CartItem(Integer id, String name, Integer count, BigDecimal price, BigDecimal totalPrice) {
this.id = id;
this.name = name;
this.count = count;
this.price = price;
this.totalPrice = totalPrice;
}
/**
* 获取
* @return id
*/
public Integer getId() {
return id;
}
/**
* 设置
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return count
*/
public Integer getCount() {
return count;
}
/**
* 设置
* @param count
*/
public void setCount(Integer count) {
this.count = count;
}
/**
* 获取
* @return price
*/
public BigDecimal getPrice() {
return price;
}
/**
* 设置
* @param price
*/
public void setPrice(BigDecimal price) {
this.price = price;
}
/**
* 获取
* @return totalPrice
*/
public BigDecimal getTotalPrice() {
return totalPrice;
}
/**
* 设置
* @param totalPrice
*/
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
public String toString() {
return "CartItem{id = " + id + ", name = " + name + ", count = " + count + ", price = " + price + ", totalPrice = " + totalPrice + "}";
}
}
- 2.购物车模型
package com.atguigu.pojo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
//购物车的封装
public class Cart {
private Integer totalCount;//总商品数量
private BigDecimal totalPrice;//总商品金额
private List<CartItem>items = new ArrayList<>();//购物车商品项的集合
public Cart() {
}
public Cart(Integer totalCount, BigDecimal totalPrice, List<CartItem> items) {
this.totalCount = totalCount;
this.totalPrice = totalPrice;
this.items = items;
}
/**
* 获取
* @return totalCount
*/
public Integer getTotalCount() {
return totalCount;
}
/**
* 设置
* @param totalCount
*/
public void setTotalCount(Integer totalCount) {
this.totalCount = totalCount;
}
/**
* 获取
* @return totalPrice
*/
public BigDecimal getTotalPrice() {
return totalPrice;
}
/**
* 设置
* @param totalPrice
*/
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
/**
* 获取
* @return items
*/
public List<CartItem> getItems() {
return items;
}
/**
* 设置
* @param items
*/
public void setItems(List<CartItem> items) {
this.items = items;
}
public String toString() {
return "Cart{totalCount = " + totalCount + ", totalPrice = " + totalPrice + ", items = " + items + "}";
}
}
购物车方法的实现与测试、
由于购物车相关的方法只有web层,而没有service和dao。所以我们将经过servlet转发后调用的具体方法放置在Cart(domain)类中,接下来我们将实现这个方法并测试
我们将设置购物车商品总金额和设置购物车商品总数量的set方法删除后,我们在对应的get方法中进行累加以实现需要的功能。
- cart类
package com.atguigu.pojo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
//购物车的封装
public class Cart {
//private Integer totalCount;//总商品数量(使用局部遍历替代)
//private BigDecimal totalPrice;//总商品金额(使用局部遍历替代)
/*
* key存储商品项的编号id
* value存储商品信息
* 这样存储有利于对数据从查找
* */
private Map<Integer,CartItem> items =new LinkedHashMap<Integer,CartItem>();//购物车商品项的集合
/*购物车模块具体调用的方法*/
//添加商品项
public void addItem(CartItem cartItem){
//先查看购物车中是否有该商品,如果有则数量加1,价格更新。如果没有这添加这个商品项
final CartItem item = items.get(cartItem.getId());//查看购物车中是否有这个商品
if(item==null){//没有该商品
items.put(cartItem.getId(),cartItem);//将该商品项添加进购物车中
}else {//购物车中有该商品
item.setCount(item.getCount()+1);//数量加1
item.setTotalPrice(item.getPrice().multiply(new BigDecimal(item.getCount())));//价格更新(单价*数量)
}
}
//删除商品项
public void deleteItem(Integer id){
items.remove(id);
}
//清空购物车
public void clear(){
items.clear();
}
//修改商品数量
public void updateCount(Integer id,Integer count){
//先查找购物车中是否有这个商品,如果有则修改商品的数量,并更新商品金额
final CartItem cartItem = items.get(id);
if(cartItem!=null){
cartItem.setCount(count);//修改商品的数量
cartItem.setTotalPrice(cartItem.getPrice().multiply(new BigDecimal(count)));//修改商品的金额
}
}
public Cart() {
}
public Cart(Integer totalCount, BigDecimal totalPrice, List<CartItem> items) {
this.items = (Map<Integer, CartItem>) items;
}
/**
* 获取
* @return totalCount
*/
public Integer getTotalCount() {
//将购物车中所有的商品项的商品数量进行累加
Integer totalCount = 0;
for(Map.Entry<Integer,CartItem> entry:items.entrySet()){
totalCount += entry.getValue().getCount();
}
return totalCount;
}
/**
* 获取
* @return totalPrice
*/
public BigDecimal getTotalPrice() {
BigDecimal totalPrice = new BigDecimal(0);
//遍历购物车将每个商品项的价格进行累加
for(Map.Entry<Integer,CartItem> entry:items.entrySet()){
totalPrice = totalPrice.add(entry.getValue().getTotalPrice());
}
return totalPrice;
}
/**
* 获取
* @return items
*/
public List<CartItem> getItems() {
return (List<CartItem>) items;
}
/**
* 设置
* @param items
*/
public void setItems(List<CartItem> items) {
this.items = (Map<Integer, CartItem>) items;
}
public String toString() {
return "Cart{totalCount = " + getTotalCount() + ", totalPrice = " + getTotalPrice() + ", items = " + items + "}";
}
}
- 对写好的方法的测试
package com.atguigu.test;
import com.atguigu.pojo.Cart;
import com.atguigu.pojo.CartItem;
import org.junit.Test;
import java.math.BigDecimal;
import static org.junit.Assert.*;
public class CartTest {
@Test
public void addItem() {
Cart cart = new Cart();
cart.addItem(new CartItem(1, "java从入门到放弃", 1, new BigDecimal(100), new BigDecimal(100)));
cart.addItem(new CartItem(2, "数据结构与算法", 98, new BigDecimal(10), new BigDecimal(34)));
System.out.println(cart);
}
@Test
public void deleteItem() {
Cart cart = new Cart();
cart.addItem(new CartItem(1, "java从入门到放弃", 1, new BigDecimal(100), new BigDecimal(100)));
cart.addItem(new CartItem(2, "数据结构与算法", 98, new BigDecimal(10), new BigDecimal(34)));
//使用前面的数据
cart.deleteItem(1);
System.out.println(cart);
}
@Test
public void clear() {
Cart cart = new Cart();
cart.addItem(new CartItem(1, "java从入门到放弃", 1, new BigDecimal(100), new BigDecimal(100)));
cart.addItem(new CartItem(2, "数据结构与算法", 98, new BigDecimal(10), new BigDecimal(34)));
//使用前面的数据
cart.clear();
System.out.println(cart);
}
@Test
public void updateCount() {
Cart cart = new Cart();
cart.addItem(new CartItem(1, "java从入门到放弃", 1, new BigDecimal(100), new BigDecimal(100)));
cart.addItem(new CartItem(2, "数据结构与算法", 98, new BigDecimal(10), new BigDecimal(34)));
//使用前面的数据
cart.deleteItem(2);
cart.updateCount(1,1000);
System.out.println(cart);
}
}
添加商品到购物车功能的实现
先实现添加购物车,因为只有添加到购物车后才能实现对购物车删除,修改等操作
注意:我们前面说过我们使用Session来实现购物车。Cart购物车,具有唯一性,不能直接在程序中new 出来,我们创建cart对象并放在session中,在使用的时候判断是否有有购物车对象,如果没有才new ,并把他放在session中,以此保证是同一个cart对象
- CartServlet程序
package com.atguigu.web;
import com.atguigu.pojo.Book;
import com.atguigu.pojo.Cart;
import com.atguigu.pojo.CartItem;
import com.atguigu.service.BookService;
import com.atguigu.service.impl.BookServiceImpl;
import com.atguigu.utils.WebUtils;
import com.mysql.cj.Session;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
//用来处理购物车业务的servlet
public class CartServlet extends BaseServlet{
private BookService bookService= new BookServiceImpl();
//处理添加商品项的方法
protected void addItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求的图书Id
int id = WebUtils.parseInt(req.getParameter("id"), 0);
//调用BookService的queryBookById(id)找到图书
Book book = bookService.queryBookById(id);
//将图书包装成CartItem对象
CartItem cartItem = new CartItem(book.getId(),book.getName(),1,book.getPrice(),book.getPrice());
//调用addItem方法添加商品项
Cart cart = (Cart) req.getSession().getAttribute("cart");
if(cart==null){
cart = new Cart();
req.getSession().setAttribute("cart",cart);
}
cart.addItem(cartItem);
//重定向回原来商品所在页面的地址
System.out.println("跳转的页面的地址:"+req.getHeader("Referer"));
resp.sendRedirect(req.getHeader("Referer"));
System.out.println(cart);
}
}
购物车的展示
经验:要获取某些数据如果得不到先让他经过servlet程序
但是我们的cart.jsp(展示购物车)页面是可以得到数据的,因为我们的所以数据都是放在session中,我们切换页面的时候没有关闭浏览器,还是一个session
注意:使用EL表达式获取javaBean对象的属性时,一定要保证该属性的getter方法存在且正确,要不然将无法获取
该功能的逻辑很简单,因为数据都保存在了session域中,直接在cart.jsp页面就可以获取到数据,只需要考虑有数据和没有数据的时候怎么样输出就可以了
具体代码见笔记
删除购物车的商品项
当我们点击删除的时候将会调用servlet中的deleteItem方法,同时需要把商品项的Id发送过去,deleteItem方法需要id
- 给删除绑定单击事件,提示用户是否删除商品
清空购物车的实现
- 给清空购物车绑定单击事件
添加单击事件的思路和前面的一样 - servlet里面的清空方法
//定义清空购物车的方法
protected void clear(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//调用Cart的clear方法清空购物车
Cart cart = (Cart) req.getSession().getAttribute("cart");
if(cart!=null){
cart.clear();
}
//重定向回原来的页面
resp.sendRedirect(req.getHeader("Referer"));
}
修改购物车商品数量
- 修改商品数量
- 思路分析:
我们有一个事件是Onchange事件,当输入框里面的内容改变将执行,这个功能就可以替代我们判断输入框里面的内容是否改变,如果我们不使用onchange事件,判断内容是否改变的功能需要我们自己编写
所以将上面的事件修改成onchange
- 给改变商品数量绑定事件
//给修改输入框绑定失去输入框内容改变事件(输入框中内容往改变将触发)
$(".updateCount").change(function (){
//获取商品名称
var name= $(this).parent().parent().find("td:first").text()
//获取商品id
var id = $(this).attr('bookId');
//获取商数量
var count = this.value;
if ( confirm("你确定要将【" + name + "】商品修改数量为:" + count + " 吗?") ) {
//发起请求。给服务器保存修改
location.href =
"http://localhost:8080/book/cartServlet?action=updateCount&count="+count+"&id="+id;
} else {
// defaultValue 属性是表单项 Dom 对象的属性。它表示默认的 value 属性值。
this.value = this.defaultValue;
}
});
- servlet中的处理方法
//定义修改商品数量的方法
protected void updateCount(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求的参数id 和商品数量count
int id = WebUtils.parseInt(req.getParameter("id"), 0);
int count = WebUtils.parseInt(req.getParameter("count"), 1);
//获取购物车对象
Cart cart = (Cart) req.getSession().getAttribute("cart");
if(cart!=null){
//修改商品数量
cart.updateCount(id,count);
}
//重定向回原来的页面
resp.sendRedirect(req.getHeader("Referer"));
}
首页购物车数据展示
-
显示最后一件放入购物车的物品的策略:
1.可以将最后一件放入购物车的商品的名称放到session域中,在首页调用2.可以在Cart类中专门创建一个属性用来存储最后一个放入购物车的商品名称 -
我们采取将最后一件的名称存放到session中的策略
-
存在一个问题(此处的显示策略取绝于产品经理)
-
我们使用判断购物车中是否为空,进行不同的显示来处理:
-
具体代码见笔记
第七阶段订单
订单模块的分析
[https://blog.csdn.net/weixin_44491423/article/details/121270438?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169952083816800180677258%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169952083816800180677258&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-121270438-null-null.142v96pc_search_result_base9&utm_term=%E4%B9%A6%E5%9F%8E%E9%A1%B9%E7%9B%AE%E8%AE%A2%E5%8D%95&spm=1018.2226.3001.4187](参考 万分感想)
发货是修改status记录的订单状态
查询订单详情其实是列出该订单对应的商品项
- 订单模块分析
编写订单模块的数据库表
- sql
# 创建订单模块所需要的表
#订单表
CREATE TABLE t_order(
`order_id` VARCHAR(50) PRIMARY KEY,
`create_time` DATETIME,
`price` DECIMAL(11,2),
`status` INT,
`user_id` INT,
FOREIGN KEY(`user_id`) REFERENCES t_user(`id`)
);
#订单项表
CREATE TABLE t_order_item(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100),
`count` INT,
`price` DECIMAL(11,2),
`total_price` DECIMAL(11,2),
`order_id` VARCHAR(50),
FOREIGN KEY(`order_id`) REFERENCES t_order(`order_id`)
);
编写订单模块的2个数据模型Order和OrderItem
- order
package com.atguigu.pojo;
import java.math.BigDecimal;
import java.util.Date;
//订单的javaBean
public class Order {
private String orderId;//订单编号
private Date createTime;//订单创建时间
private BigDecimal price;//订单金额
private Integer state=0;//订单状态
//0表示未发货,1表示已发货,2表示已签收
private Integer userId;//用户id
public Order() {
}
public Order(String orderId, Date createTime, BigDecimal price, Integer state, Integer userId) {
this.orderId = orderId;
this.createTime = createTime;
this.price = price;
this.state = state;
this.userId = userId;
}
/**
* 获取
* @return orderId
*/
public String getOrderId() {
return orderId;
}
/**
* 设置
* @param orderId
*/
public void setOrderId(String orderId) {
this.orderId = orderId;
}
/**
* 获取
* @return createTime
*/
public Date getCreateTime() {
return createTime;
}
/**
* 设置
* @param createTime
*/
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
/**
* 获取
* @return price
*/
public BigDecimal getPrice() {
return price;
}
/**
* 设置
* @param price
*/
public void setPrice(BigDecimal price) {
this.price = price;
}
/**
* 获取
* @return state
*/
public Integer getState() {
return state;
}
/**
* 设置
* @param state
*/
public void setState(Integer state) {
this.state = state;
}
/**
* 获取
* @return userId
*/
public Integer getUserId() {
return userId;
}
/**
* 设置
* @param userId
*/
public void setUserId(Integer userId) {
this.userId = userId;
}
public String toString() {
return "Order{orderId = " + orderId + ", createTime = " + createTime + ", price = " + price + ", state = " + state + ", userId = " + userId + "}";
}
}
- orderItem
package com.atguigu.pojo;
import java.math.BigDecimal;
//订单项的javaBean
public class orderItem {
private Integer id;//订单项编号
private String name;//订单项名称
private Integer count;//订单项数量
private BigDecimal totalPrice;//订单项总价
private BigDecimal price;//订单项单价
private String orderId;//订单项的订单编号
public orderItem() {
}
public orderItem(Integer id, String name, Integer count, BigDecimal totalPrice, BigDecimal price, String orderId) {
this.id = id;
this.name = name;
this.count = count;
this.totalPrice = totalPrice;
this.price = price;
this.orderId = orderId;
}
/**
* 获取
* @return id
*/
public Integer getId() {
return id;
}
/**
* 设置
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return count
*/
public Integer getCount() {
return count;
}
/**
* 设置
* @param count
*/
public void setCount(Integer count) {
this.count = count;
}
/**
* 获取
* @return totalPrice
*/
public BigDecimal getTotalPrice() {
return totalPrice;
}
/**
* 设置
* @param totalPrice
*/
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
/**
* 获取
* @return price
*/
public BigDecimal getPrice() {
return price;
}
/**
* 设置
* @param price
*/
public void setPrice(BigDecimal price) {
this.price = price;
}
/**
* 获取
* @return orderId
*/
public String getOrderId() {
return orderId;
}
/**
* 设置
* @param orderId
*/
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String toString() {
return "orderItem{id = " + id + ", name = " + name + ", count = " + count + ", totalPrice = " + totalPrice + ", price = " + price + ", orderId = " + orderId + "}";
}
}
编写订单模块的dao和测试
注意仅仅是演示订单的生成这个功能的实现,其他的功能由自己实现
- 我们这个模块需要定义2个接口分别操作订单和订单项
前面还有OrderDao和OrderItemDao2个抽象接口,这里省略
- OrderDaoImpl
package com.atguigu.dao.impl;
import com.atguigu.dao.OrderDao;
import com.atguigu.pojo.Order;
public class OrderDaoImpl extends BaseDao<Order> implements OrderDao {
//加将订单保存的数据库中
@Override
public int saveOrder(Order order) {
String sql = "insert into t_order(`order_id`,`create_time`,`price`,`status`,`user_id`)"+
"values(?,?,?,?,?)";
super.update(sql,order.getOrderId(),order.getCreateTime(),order.getPrice(),order.getState(),order.getUserId());
return 0;
}
}
- OrderItemDaoImpl.
package com.atguigu.dao.impl;
import com.atguigu.dao.OrderItemDao;
import com.atguigu.pojo.OrderItem;
public class OrderItemDaoImpl extends BaseDao<OrderItem> implements OrderItemDao {
//加将订单项保存的数据库中
@Override
public int saveOrderItem(OrderItem orderItem) {
String sql = "insert into t_order_item(`name`,`count`,`price`,`total_price`,`order_id`)"+
"values(?,?,?,?,?)";
super.update(sql,orderItem.getName(),orderItem.getCount(),orderItem.getPrice(),orderItem.getTotalPrice(),orderItem.getOrderId());
return 0;
}
}
- 测试
package com.atguigu.test;
import com.atguigu.dao.OrderDao;
import com.atguigu.dao.impl.OrderDaoImpl;
import com.atguigu.pojo.Order;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.Date;
import static org.junit.Assert.*;
public class OrderDaoTest {
OrderDao orderDao = new OrderDaoImpl();
@Test
public void saveOrder() {
int a = orderDao.saveOrder(new Order("1234566",new Date(),new BigDecimal(100),0,1));
System.out.println(a);
}
}
编写订单模块的Service和测试
- OrderServiceImpl
package com.atguigu.service.impl;
import com.atguigu.dao.OrderDao;
import com.atguigu.dao.OrderItemDao;
import com.atguigu.dao.impl.OrderDaoImpl;
import com.atguigu.dao.impl.OrderItemDaoImpl;
import com.atguigu.pojo.Cart;
import com.atguigu.pojo.CartItem;
import com.atguigu.pojo.Order;
import com.atguigu.pojo.OrderItem;
import com.atguigu.service.OrderService;
import java.util.Date;
import java.util.Map;
//实现生成订单
public class OrderServiceImpl implements OrderService {
private OrderDao orderDao = new OrderDaoImpl();
private OrderItemDao orderItemDao = new OrderItemDaoImpl();
@Override
public String createOrder(Cart cart, Integer userId) {
/*
生成订单需要保存订单和保存订单项。
1.需要首先保存订单,然后保存订单下
因为订单表和订单项表有外键约束,不然订单项表将保存不下去
*/
//1.保存订单
//获取订单编号(要求唯一性)
String orderId=System.currentTimeMillis()+""+userId;
Order order = new Order(orderId,new Date(),cart.getTotalPrice(),0,userId);
orderDao.saveOrder(order);//保存订单到数据库
//2.保存订单项
//遍历购物车里面的每件商品,将他们封装成商品项
for (Map.Entry<Integer, CartItem>entry : cart.getItems().entrySet()){
// 获取每一个购物车中的商品项
CartItem cartItem = entry.getValue();
// 转换为每一个订单项
OrderItem orderItem = new
OrderItem(null,cartItem.getName(),cartItem.getCount(),cartItem.getPrice(),cartItem.getTotalPrice(),
orderId);
// 保存订单项到数据库
orderItemDao.saveOrderItem(orderItem);
}
//清空购物车(生成订单按照常理就是打算购买了)
cart.clear();
return orderId;//返回订单号
}
}
- 测试
package com.atguigu.test;
import com.atguigu.pojo.Cart;
import com.atguigu.pojo.CartItem;
import com.atguigu.service.OrderService;
import com.atguigu.service.impl.OrderServiceImpl;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import static org.junit.Assert.*;
public class OrderServiceTest {
private OrderService orderService = new OrderServiceImpl();
@Test
public void createOrder() {
Cart cart = new Cart();
cart.addItem(new CartItem(1,"java从入门到放弃",3,new BigDecimal(123),new BigDecimal(123)));
cart.addItem(new CartItem(2,"java从入门到放弃",4,new BigDecimal(123),new BigDecimal(123)));
cart.addItem(new CartItem(3,"java从入门到放弃",5,new BigDecimal(123),new BigDecimal(123)));
String orderId= orderService.createOrder(cart,1);
System.out.println(orderId);
}
}
实现该功能的web层(结账功能的实现生成订单)
注意:当我们没有登录就进行去结账的操作时,将会出现空指针异常
- OrderServlet程序
package com.atguigu.web;
import com.atguigu.pojo.Cart;
import com.atguigu.pojo.User;
import com.atguigu.service.OrderService;
import com.atguigu.service.impl.OrderServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class OrderServlet extends BaseServlet{
private OrderService orderService = new OrderServiceImpl();
//生成订单
protected void createOrder(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cart cart =(Cart) req.getSession().getAttribute("cart");//获取存储到session中的购物车
//获取存储在session中的用户编号
User loginUser = (User) req.getSession().getAttribute("loginUser");
//当用户没有登录时,不允许其进行结账
if(loginUser==null){
//没有登录就跳转到登录页面
req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
return;//不允许其执行后面的代码
}
final Integer userId = loginUser.getId();
final String orderId = orderService.createOrder(cart, userId);//获取订单编号
req.setAttribute("orderId", orderId);//将订单编号保存到request域中
//请求转发到结账页面
req.getRequestDispatcher("/pages/cart/checkout.jsp").forward(req, resp);
}
}
解决生成订单的bug
- bug1.更新库存和销量
- bug2.防止表单重复提交
**我们在转发的checkout页面时,如果使用请求转发,按f5将会找出表单重复提交
功能完善--查询我的订单
1.关于日期Date类型的输出存在问题:日期直接就无法输出(未完成)
经验:其实在前面讲过:我们的表封装成javaBean对象是通过调用他们的getter方法实现的,所有我们的列名一定要和javaBean的列名保持一致,但是大小写没有影响(估计底层使用了忽略大小写的比较)但是好像对于Date对象例外,反正无法输出
思路分析
发货
BeanUtils无法直接将String转为Date
为了方便我们将表中的create_Time属性给删除了,java中的代码也相应做出了调整
查看订单详情
书城项目第八阶段
修改优化1
- 过滤器
有一个小问题:
我们应该也要将这个servlet程序也进行拦截
ThreadLocal 的使用
只要是当前线程关联的数据,不管代码层级调用有多深,不影响数据传递去获取
换成ThreadLocal后,key 就是当前线程,就不要存储了,只需要存储value就可以了
对于ThreadLocal类的理解
- 文心一言:ThreadLocal的作用
- ThreadLocal和线程安全有什么关系
当多个线程并发执行时,我们将数据关联ThreadLocal,就相当于每一个线程都有自己独立的该数据的副本,相互不影响。这样就避免了竞争
ThreadLocal不是用来解决对象共享访问问题
在前面的描述中好像ThreadLocal可以将数据隔离,然后解决线程竞争的问题,但是ThreadLocal并不能解决共享数据的访问问题。而主要是提供了保存对象的方法和避免参数传递的方便的对象访问方式(作用) - key=当前线程名 value=保存进去的变量。本质上也是通过ThreadLocal内部的一个ThreadLocalMap实现map集合
ThreadLocal保存数据成map集合原理参见
ThreadLocal的原理实现
package com.threadlocal;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Random;
class OrderService {
public void createOrder(){
String name = Thread.currentThread().getName();
System.out.println("OrderService 当前线程[" + name + "]中保存的数据是:" +
ThreadLocalTest.data.get(name));
new OrderDao().saveOrder();
}
}
class OrderDao {
public void saveOrder(){
String name = Thread.currentThread().getName();
System.out.println("OrderDao 当前线程[" + name + "]中保存的数据是:" +
ThreadLocalTest.data.get(name));
}
}
public class ThreadLocalTest {
public static Map<String,Object> data = new Hashtable<>();
// public static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
private static Random random = new Random();
public static class Task implements Runnable {
@Override
public void run() {
// 在 Run 方法中,随机生成一个变量(线程要关联的数据),然后以当前线程名为 key 保存到 map 中
Integer i = random.nextInt(1000);
// 获取当前线程名
String name = Thread.currentThread().getName();
System.out.println("线程["+name+"]生成的随机数是:" + i);
data.put(name,i);
//threadLocal.set(i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new OrderService().createOrder();
// 在 Run 方法结束之前,以当前线程名获取出数据并打印。查看是否可以取出操作
Object o = data.get(name);
//Object o = threadLocal.get();
System.out.println("在线程["+name+"]快结束时取出关联的数据是:" + o);
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++){
new Thread(new Task()).start();
}
}
}
1.我们在使用HashTable保存数据的时候,HashTable是线程安全的,时候HashTable来保存和取出数据的时候都是安全的(使用的是在HashTable部分方法中加锁来实现的)
2.当我们将容器改为ThreadLocal,我们实现安全的方式就变成了每个线程都有自己独立的ThreadLocal,进而有自己独立的变量,他们相互之间是隔离的。当我们存储和取出数据的时候每个线程都是操作自己对于的变量,相互之间没有影响
使用 Filter 和 ThreadLocal 组合管理事务
之前项目所存在的问题:
我们在点击去结账(付款)后,将会生成对应的订单和订单项,生成订单和订单项应该是原子操作,中间是不能中断的。如果下单之后生成了订单却没有点单项将带来了很大的麻烦
此时我们的程序按照正常的设想,抛出了异常,但是我们的订单被数据库保存了下来,但是订单项因为异常程序中断,订单项没有被数据库保存下来
此时其实就和我们在sql里面学习的事务管理有关了,事务管理正好可以解决这个问题
-
实现原理
-
因为提交事务和回滚事务都需要关闭连接,所以将关闭连接和提交事务和回滚事务绑定在一个方法中书写
在我们的dao中,保存订单和保存订单项都可能出现异常。为了保证异常可以被上层捕获进行回滚,我们将异常向上抛出到service。我们在createorder()这个方法的时候会进行异常捕获,而createOrder方法在servlet中进行调用所以我们在servlet中进行异常捕获和进行提交和回滚操作
- 全新的jdbcUtils工具类
package com.atguigu.utils;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
//JDBC工具类:用于管理获取获取和关闭连接
public class JdbcUtils {
private static DataSource dataSource; // 定义一个连接池
private static ThreadLocal<Connection> conns = new ThreadLocal<>();//用于管理事务操作
// 准备初始化连接池
static {
try {
Properties properties = new Properties();
// 读取 jdbc.properties 属性配置文件
InputStream inputStream =
JdbcUtils.class.getClassLoader().getResourceAsStream("deruid.properties");
// 从流中加载数据
properties.load(inputStream);
// 创建 数据库连接 池
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
// 关闭资源
/*
* 注意:在数据库连接池技术中,close不是真的断开连接
* 而是把使用的Connection对象放回连接池
* ResultSet和PreparedStatement资源不用我们关闭
* 由DBUtils提供的query和update方法的底层进行关闭
*
*/
/*public static void close(Connection connection) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}*/
/*
* 获取数据库连接池的连接
* 如果返回null则说明获取连接失败,否则获取连接成功
*/
public static Connection getConnection() {
Connection con = conns.get();//从ThreadLocal中获取连接
if(con==null){
try {
con = dataSource.getConnection();
conns.set(con);//将连接放入ThreadLocal中,供后面的jdbc操作使用
con.setAutoCommit(false);//设置为手动管理事务
} catch (SQLException e) {
e.printStackTrace();
}
}
return con;
}
/*
*提交事务并释放连接
* */
public static void commitAndClose(){
final Connection connection = conns.get();//获取ThreadLocal中的连接
if(connection!=null){
try {
connection.commit();//提交事务
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
try {
connection.close();//释放连接
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
//一定要执行remove操作,否则会报错(因为Tomcat服务器底层使用了线程池技术)
conns.remove();
}
/*
* 回滚事务,并释放连接
* */
public static void rollbackAndClose(){
final Connection connection = conns.get();//获取ThreadLocal中的连接
if(connection!=null){
try {
connection.rollback();//回滚事务
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
try {
connection.close();//释放连接
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
//一定要执行remove操作,否则会报错(因为Tomcat服务器底层使用了线程池技术)
conns.remove();
}
}
//~ Formatted by Jindent --- http://www.jindent.com
- baseDao类(取消了close关闭线程方法,将捕获异常改为向上抛异常)
package com.atguigu.dao.impl;
/*
1.Dao负责和数据库进行交互
2.BaseDao作为所有DAO的公共父类,里面定义对数据库增删改查的通用方法
3.使用DBUtils操作数据库
*/
import com.atguigu.utils.JdbcUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
//开发BasicDAO,是其他DAO的父类
public abstract class BaseDao<T> {//使用泛型类表示
//定义属性
private QueryRunner queryRunner = new QueryRunner();//用于执行sql
//通用的DML方法(返回所影响的行数)
//适用于任意表
//parames:表示传入的?的具体值
public int update(String sql, Object... parames) {//这里连接的获取放在在函数里面比较好
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
final int affectedRow = queryRunner.update(connection, sql, parames);//可变参数本质是数组
return affectedRow;
//执行sql
} catch (SQLException e) {
new RuntimeException(e);//往上层抛
}
//在update方法的内部,connection resultSet preparedStatement都已经被关闭
return -1;//返回-1说明执行失败
}
//查询的结果返回多个对象(即返回多行结果,使用List包装)
//适用于任意表
/*
参数解释:
clazz:表示传入一个类的Class对象,比如Actor.class
params:传入?的具体值
返回对应的ArrayList集合
*/
public List<T> queryMulti(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
final List<T> list = queryRunner.query(connection, sql, new BeanListHandler<T>(clazz), params);
return list;//返回一个List集合
} catch (SQLException e) {
new RuntimeException(e);//往上层抛
}
return null;
}
//返回单行结果的通用方法(返回一个javaBean对象 )
public T querySingle(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
T t = queryRunner.query(connection, sql, new BeanHandler<T>(clazz), params);
return t;//返回一个javaBean对象
} catch (SQLException e) {
new RuntimeException(e);//往上层抛
}
return null;
}
//返回单行单列,返回一个单值
//因为是返回一个单值不需要封装,所有不用传入Class对象
public Object queryScalar(String sql,Object...parames){
Connection connection = null;
try {
connection = JdbcUtils.getConnection();
Object object = queryRunner.query(connection, sql, new ScalarHandler (), parames);
return object;//返回任意一个单值
} catch (SQLException e) {
new RuntimeException(e);//往上层抛
}
return null;
}
}
- 在OrderServlet中捕获异常并进行回滚事务
//生成订单
protected void createOrder(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cart cart = (Cart) req.getSession().getAttribute("cart");//获取存储到session中的购物车
//获取存储在session中的用户编号
User loginUser = (User) req.getSession().getAttribute("loginUser");
//当用户没有登录时,不允许其进行结账
if (loginUser == null) {
//没有登录就跳转到登录页面
req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
return;//不允许其执行后面的代码
}
final Integer userId = loginUser.getId();
String orderId=null;//获取订单编号
try {
orderId = orderService.createOrder(cart, userId);
JdbcUtils.commitAndClose();//提交事务
} catch (Exception e) {
JdbcUtils.rollbackAndClose();//回滚事务
e.printStackTrace();
}
//req.setAttribute("orderId", orderId);//将订单编号保存到request域中
req.getSession().setAttribute("orderId", orderId);//重定向到session中
//重定向到结账页面
// req.getRequestDispatcher("/pages/cart/checkout.jsp").forward(req, resp);(过去的)
resp.sendRedirect(req.getContextPath() + "/pages/cart/checkout.jsp");
}
根据我们之前的了解德鲁伊数据库连接池在完成操作后可以实现自动释放连接。所以我们可以不用担心因为只有事务操作才能释放连接从而导致大量获取连接而导致连接没有被释放的问题了
问题分析:仅仅给createOrder()加上事务管理,对其他的操作会产生什么影响(如sendOrder发货)
使用 Filter 过滤器统一给所有的 Service 方法都加上 try-catch。来进行实现事务的管理
在实际的开发中我们有很多的service,有很多的方法需要用到事务管理,如果在servlet中给每一个service方法加上事务管理这太麻烦了,我们可以使用filter给service方法统一加上事务管理
Filster管理事务的原理:
- 事务管理过滤器
package com.atguigu.filter;
import com.atguigu.utils.JdbcUtils;
import javax.servlet.*;
import java.io.IOException;
//事务过滤器:用于事务的管理
public class TransactionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(servletRequest, servletResponse);//调用目标资源(主要是servlet)
JdbcUtils.commitAndClose();//提交事务
} catch (Exception e) {
JdbcUtils.rollbackAndClose();//回滚事务
e.printStackTrace();
}
}
@Override
public void destroy() {
}
}
- 事务管理过滤器
<!--配置事务过滤器-->
<filter>
<filter-name>TransactionFilter</filter-name>
<filter-class>com.atguigu.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<!--对web工程下所有请求进行过滤-->
<url-pattern>/*</url-pattern>
</filter-mapping>
将所有异常都统一交给 Tomcat,让 Tomcat 展示友好的错误
- 存在的问题描述
而且事务回滚成功了,我们的数据库被没有添加数据
- 异常配置
<!--tomcat异常处理-->
<!--error-page 标签配置,服务器出错之后,自动跳转的页面-->
<error-page>
<!--error-code 是错误类型-->
<error-code>500</error-code>
<!--location 标签表示。要跳转去的页面路径-->
<location>/pages/error/500Error.jpg</location>
</error-page>
<!--error-page 标签配置,服务器出错之后,自动跳转的页面-->
<error-page>
<!--error-code 是错误类型-->
<error-code>404</error-code>
<!--location 标签表示。要跳转去的页面路径-->
<location>/pages/error/404Error.jpg</location>
</error-page>
- 这里我的异常图片用中文命名,浏览器无法展示,不清楚原因,可能是因为编码
书城项目第九阶段
- 前端代码
$("#username").blur(function () {
//1 获取用户名
var username = this.value;
$.getJSON("http://localhost:8080/book/userServlet","action=ajaxExistsUsername&username=" +
username,function (data) {
if (data.existsUsername) {
$("span.errorMsg").text("用户名已存在!");
} else {
$("span.errorMsg").text("用户名可用!");
}
});
});
- servlet代码
//ajax请求验证用户名是否正确
public void ajaxExistsUsername(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("调用哦了ajaxExistsUsername");
//1.获取请求参数
final String username = req.getParameter("username");
//2.调用service方法
final boolean existUsername = userService.existUsername(username);
//3.将结果封装成map对象回传给客户端
Map<String,Object> resultMap= new HashMap<>();
resultMap.put("existUsername",existUsername);
//4.将map对象转换成json格式
Gson gson = new Gson();
final String json = gson.toJson(resultMap);
//5.将json数据回传给客户端
resp.getWriter().write(json);
}
使用 AJAX 修改把商品添加到购物车
- 原理分析
- servelt端代码
//ajax处理添加商品
protected void ajaxAddItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
System.out.println("刚刚调用了 ajaxAddItem");
// 获取请求的参数 商品编号
int id = WebUtils.parseInt(req.getParameter("id"), 0);
// 调用 bookService.queryBookById(id):Book 得到图书的信息
Book book = bookService.queryBookById(id);
// 把图书信息,转换成为 CartItem 商品项
CartItem cartItem = new CartItem(book.getId(), book.getName(), 1, book.getPrice(), book.getPrice());
// 调用 Cart.addItem(CartItem);添加商品项
Cart cart = (Cart) req.getSession().getAttribute("cart");
if (cart == null) {
cart = new Cart();
// req.getSession().setAttribute("cart", cart);不用将数据存储到session中了
}
cart.addItem(cartItem);
System.out.println(cart);
// 最后一个添加的商品名称
// req.getSession().setAttribute("lastName", cartItem.getName());(不用将数据存储到session中了
//6、返回购物车总的商品数量和最后一个添加的商品名称
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("totalCount", cart.getTotalCount());
resultMap.put("lastName", cartItem.getName());
Gson gson = new Gson();
String resultMapJsonString = gson.toJson(resultMap);
resp.getWriter().write(resultMapJsonString);
}
- html代码(不用获取session中的值作为标签值了)
<div style="text-align: center">
<c:if test="${empty sessionScope.cart.items}">
<%--购物车为空的输出--%>
<span id="cartTotalCount"> </span>
<div>
<span style="color: red" id="cartLastName">当前购物车为空</span>
</div>
</c:if>
<c:if test="${not empty sessionScope.cart.items}">
<%--购物车非空的输出--%>
<span id="cartTotalCount"></span>
<div>
<span style="color: red" id="cartLastName"></span>加入到了购
物车中
</div>
</c:if>
</div>
- JS代码(发送ajax请求而不是普通请求)
//给加入购物车按钮绑定单击事件
$("button.addToCart").click(function (){
let bookId = $(this).attr("bookId");/*获取按钮标签的,bookId的属性值*/
// location.href="cartServlet?action=addItem&id="+bookId;(更新为使用ajax请求)
$.getJSON("http://localhost:8080//book/cartServlet","action=ajaxAddItem&id="+bookId,function (data){
$("#cartTotalCount").text("您的购物车中有 " + data.totalCount + " 件商品");
$("#cartLastName").text(data.lastName);
});
});
通过使用jaax请求,将返回的数据以json的形式传递给前端,而不是将数据存储到session域中,形成了局部更新页面