文章目录
- 🐀Java后端经典三层架构
- 🐇MVC模型
- 🐇开发环境搭建
- 🐇会员注册
- 🌳前端验证用户注册信息
- 🌳思路分析
- 🍉创建表
- 🍉创建实体类
- 🍉DAO
- 🍌MemberDAOImpl
- 🍉Service
- 🍌MemberServiceImpl
- 🌳接通web层
- 🐇会员登陆
- 🌳登陆错误_信息回显
- 🐇servlet合并
- 🍎反射+模板设计模式+动态代理
- 🌳显示家居
- 🌳添加家居
- 🍉解决重复添加
- 🍉后端数据校验说明
- 🍉BeanUtils自动封装Bean
- 🌳删除家居
- 🌳修改家具
- 🍃后台分页
- 🍒新建Page类
- 🍒DAO
- 🍒Service
- 🍒web层获取page对象
- 🍒前端页面
- 🍅后台分页导航
- 🍅修改后返回原页面
- 🍅删除后返回原页面
- 🍅添加后返回原页面
- 🍃首页分页
- 🍅首页搜索
- 🍅两个奇怪的问题
- 🌳会员显示登录名
- 🍅注销登录
- 🍅验证码
- 🌳购物车
- 🍆显示购物车
- 🍆修改购物车
- 🍆删除购物车
- 🌳生产订单
- 🍉创建表
- 🍉实体类
- 🍉DAO
- 🍉service
- 🍉servlet
- 🍉前端
- 🌳显示订单[订单管理]
- 🌈过滤器权限验证
- 🌈事务管理
- 1. 数据不一致问题
- 2. 程序框架图
- 🌈Transaction过滤器
- 🌈统一错误页面
- 🌈Ajax检验注册名
- 🌈Ajax添加购物车
- 🌈上传与更新家具图片
- 🌈作业布置
- 🍍会员登陆后不能访问后台管理
- 🍍解决图片冗余问题
- 🍍分页导航完善
🐀Java后端经典三层架构
分层 | 对应包 | 说明 |
---|---|---|
web层 | com.zzw.furns.web/servlet/controller/handler | 接受用户请求, 调用service |
service层 | com.zzw.furns.service | Service接口包 |
com.zzw.furns.service.impl | Service接口实现类 | |
dao持久层 | com.zzw.furns.dao | Dao接口包 |
com.zzw.furns.dao.impl | Dao接口实现类 | |
实体bean对象 | com.zzw.furns.pojo/entity/domain/bean | JavaBean类 |
工具类 | com.zzw.furns.utils | 工具类 |
测试包 | com.zzw.furns.test | 完成对dao/service测试 |
🐇MVC模型
MVC全称: Model模型, View试图, Controller控制器
MVC最早出现在JavaEE三层中的Web层, 它可以有效地指导WEB层代码如何有效地分离, 单独工作
- View试图: 只负责数据和界面的显示, 不接受任何与显示数据无关的代码, 便于程序员和美工的分工与合作(Vue/Jsp/Thymeleaf/Html)
- Controller控制器: 只负责接收请求, 调用业务层的代码处理请求, 然后派发给页面, 是一个"调度者"的角色
- Model模型: 将与业务逻辑相关的数据封装为具体的JavaBean类, 其中不掺杂任何与数据处理相关的代码(JavaBean/Domain/Pojo)
解读
- model 最早期就是javabean, 就是早期的jsp+servlet+javabean
- 后面业务复杂度越来越高, model逐渐分层化/组件化(service+dao)
- 后面又出现了持久化技术(service+dao+持久化技术(hibernate / mybatis / mybatis-plus))
- MVC依然是原来的mvc, 只是变得更加强大
🐇开发环境搭建
详情请参考👉
- 新建Java项目, 导入web框架
- 导入jar包
- 项目的结构
- 拷贝到web路径下
- 配置Tomcat
Rebuild project, 让项目识别到这些资源, 然后再启动Tomcat
- 对于复杂的前端页面, 要学会打开当前页面的结构, 提高工作效率
🐇会员注册
🌳前端验证用户注册信息
script引文件是src属性
<script type="text/javascript" src="../../script/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
$(function () {//页面加载完毕后执行 function
$("#sub-btn").click(function () {
//采用过关斩将法
//正则表达式验证用户名
var usernameValue = $("#username").val();
var usernamePattern = /^\w{6,10}$/;
if (!usernamePattern.test(usernameValue)) {
$("span[class='errorMsg']").text("用户名格式不对, 需要6-10个字符(大小写字母,数字,下划线)");
return false;
}
//验证密码
var passwordValue = $("#password").val();
var passwordPattern = /^\w{6,10}$/;
if (!passwordPattern.test(passwordValue)) {
$("span.errorMsg").text("密码格式不对, 需要6-10个字符(大小写字母,数字,下划线)");
return false;
}
//两次密码要相同
var rePwdValue = $("#repwd").val();
if (passwordValue != rePwdValue) {
$("span.errorMsg").text("两次密码不相同");
return false;
}
//这里仍然采用过关斩将法
//验证邮件
var emailVal = $("#email").val();
//在java中, 正则表达式的转义是\\; 在js中, 正则表达式转义是\
var emailPattern = /^[\w-]+@([a-zA-Z]+\.)+[a-zA-Z]+$/;
if (!emailPattern.test(emailVal)) {
$("span.errorMsg").text("电子邮件的格式不正确, 请重新输入");
return false;
}
//这里暂时不提交=>显示验证通过
$("span.errorMsg").text("验证通过");
return false;
});
})
</script>
🌳思路分析
创建表->javabean->DAO->service
分层 | 对应包 | 说明 |
---|---|---|
web层 | RegisterServlet.java | 接受浏览器发送数据; 调用相关的service;根据执行结果,返回页面数据 |
service层 | MemberService.java | Service接口包 |
MemberServiceImpl.java | Service接口实现类 | |
dao持久层 | MemberDAO.java | Dao接口包 |
MemberDAOImpl | Dao接口实现类 | |
实体bean对象 | Member.java | JavaBean类 |
工具类 | JdbcUtilsByDruid.java | 工具类 |
🍉创建表
🍉创建实体类
满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器
- 从满汉楼项目引入BasicDAO.java, JdbcUtilsByDruid.java, Druid.properties
- 修改Druid配置文件要连接的数据库名, 确保用户名密码正确. ?后面是做批处理用的
- 修改JdbcUtilsByDruid的路径
配置快捷键
- 测试
🍉DAO
🍌MemberDAOImpl
public class MemberDAOImpl extends BasicDAO<Member> implements MemberDAO { /** * 通过用户名返回对应的Member * @param username 用户名 * @return 对应的Member, 如果没有该Member返回null */ @Override public Member queryMemberByUsername(String username) { //现在sqlyog测试, 然后再拿到程序中, 这样可以提高我们的开发效率, 减少不必要的bug String sql = "SELECT id, username, `password`, email FROM member WHERE username = ?"; Member member = querySingle(sql, Member.class, username); return member; } /** * 保存一个会员 * @param member 传入一个Member对象 * @return 如果返回-1, 就是失败; 返回其它的数字, 就是受影响的行数 */ @Override public int saveMember(Member member) { //连同单引号一并换成 ? , 它会自动加上单引号 String sql = "INSERT INTO member(id, username, `password`, email) " + "VALUES(NULL, ?, MD5(?), ?)"; int updateRows = update(sql, member.getUsername(), member.getPassword(), member.getEmail()); return updateRows; } }
测试
🍉Service
🍌MemberServiceImpl
public class MemberServiceImpl implements MemberService { //定义MemberDAO属性 private MemberDAO memberDAO = new MemberDAOImpl(); /** * 判断用户名是否存在 * * @param username 用户名 * @return 如果存在返回true, 否则返回false */ @Override public boolean isExistsByUsername(String username) { //小技巧: 如果看某个方法: // (1)ctrl+b 定位到memberDAO的编译类型中的方法 // (2)如果使用ctrl+alt+b 会定位到实现类的方法 //如果有多个类实现了该方法, 会让你选择 return memberDAO.queryMemberByUsername(username) == null ? false : true; } @Override public boolean registerMember(Member member) { return memberDAO.saveMember(member) == 1 ? true : false; } }
测试
🌳接通web层
将所有路径修改成相对路径
配置RegisterServlet, 请求RegisterServlet
🐇会员登陆
MemberDAO
MemberDAOImpl
测试(不要忘了测试)
快捷键
MemberService
测试(不要忘了测试)
web层
- 新建LoginServlet
login.html请求
login_ok.html
快捷键
效果
🌳登陆错误_信息回显
将login.html重命名为login.jsp, 修改base标签
LoginServlet
login.jsp
添加span标签
🐇servlet合并
方法一: 增加隐藏域
合并到MemberServlet
🍎反射+模板设计模式+动态代理
🌳显示家居
需求分析
- 给后台管理提供独立登陆页面 manage_login.jsp(已提供)
- 管理员(admin表)登陆成功后, 显示管理菜单页面
- 管理员点击家具管理, 显示所有家居信息
程序框架图
- 页面准备
- 新建admin表 👉 参考member表
新建furn表
- 新建Admin实体类
新建Furn实体类
- 书写AdminDAO, AdminDAOImpl, 并测试; 书写AdminService, AdminServiceImpl, 并测试 👉 参考Member
书写FurnDAO, FurnDAOImpl 👉 并测试
- 书写FurnService, FurnServiceImpl 👉 并测试
- 接通web层
配置web.xml, 书写AdminServlet
配置web.xml, 书写FurnServlet
将doGet()方法移到BasicServlet中
- 前端页面
manage_login.jsp 登录验证, 请求AdminServlet
manage_menu.jsp 请求FurnServlet
furn_manage.jsp 显示家居信息
🌳添加家居
思路分析
- 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
- 调用FurnService.add(Furn furn)方法
- 跳转到显示家居的页面
- FurnDAO
- FurnService
- web层
FurnServlet
解决中文乱码问题
- 前端: 添加furn_add.jsp
🍉解决重复添加
请求转发, 当用户刷新页面时, 会重新发出第一次的请求, 造成数据重复提交
解决方案: 使用重定向
🍉后端数据校验说明
后端方案一
后端方案二
前端方案三
🍉BeanUtils自动封装Bean
引入: commons-logging-1.1.1.jar, commons-beanutils-1.8.0.jar
- 使用BeanUtils自动封装javabean
debug小技巧👉
- 报错
原因: 由于前端没有传imagePath的字段, 所有后端在构建的furn对象的时候, imagePath传了个null,
解决方案👇
- 将 把数据自动封装成JavaBean的功能封装到工具类
public class DataUtils { //将方法, 封装到静态方法, 方便使用 public static <T> T copyParamToBean(Map value, T bean) { try { BeanUtils.populate(bean, value); } catch (Exception e) { throw new RuntimeException(e); } return bean; } }
调用
🌳删除家居
需求分析
- 管理员进入到家居管理页面
- 点击删除家居链接, 弹出确认窗口, 确认-删除, 取消-放弃
程序框架图
- FurnDAO
- FurnService
- web层
FurnServlet
- furn_add.jsp页面
jQuery操作父元素, 兄弟元素, 子元素, 请移步👉
js弹框请移步👉
🌳修改家具
思路分析
- 管理员进入家居管理页面furn_manage.jsp
- 点击修改家居链接, 回显该家居信息 [furn_update.jsp]
- 填写新的信息, 点击修改家居按钮
- 修改成功后, 显示刷新后的家居列表
程序框架图
- FurnDAO
- FurnService
- web层
FurnServlet
- 前端
furn_manage.jsp 点击修改,发出请求
furn_update.jsp 修改数据,点击提交
🍃后台分页
shortcuts: ctrl+alt+u👉在局部打开类图
程序框架图
🍒新建Page类
🍒DAO
思路
实现
🍒Service
🍒web层获取page对象
🍒前端页面
取缔list方法
furn_manage.jsp
🍅后台分页导航
程序框架图
<!-- Pagination Area Start --> <div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"> <ul> <%--如果当前页 > 1, 就显示首页和上一页--%> <li><a style="pointer-events:${requestScope.page.pageNo == 1 ? "none" : "auto"};" href="manage/furnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a> </li> <li><a style="pointer-events: ${requestScope.page.pageNo == 1 ? "none" : "auto"}" href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a> </li> <%--显示所有的分页数 先确定开始的页数 begin 1; 再确定结束的页数 end=>pageTotal--%> <%--最多显示10页, 这里涉及算法--%> <c:set scope="page" var="begin" value="1"></c:set> <c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set> <%--循环显示--%> <c:forEach begin="${pageScope.begin}" end="${pageScope.end}" var="i"><%--总的页数--%> <%--如果i是当前页, 就使用class="active"来修饰--%> <li><a class="${i eq requestScope.page.pageNo ? "active" : ""}" href="manage/furnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a> </li> </c:forEach> <%--如果当前页 < 总的页数, 就显示末页和下一页--%> <li> <a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};" href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a> </li> <li> <a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};" href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a> </li> <li><a>共${requestScope.page.pageTotal}页</a></li> <li><a>共${requestScope.page.totalRow}记录</a></li> </ul> </div> <!-- Pagination Area End -->
🍅修改后返回原页面
🍅删除后返回原页面
🍅添加后返回原页面
🍃首页分页
需求分析
- 顾客进入首页页面
- 分页显示家居
- 正确显示分页导航条, 即功能完善, 可以使用
程序框架图
实现>1. 新建CustomerFurnServlet
2. 前端页面
直接请求CustomerFurnServlet, 获取网站首页要显示的分页数据
类似我们网站的入口页面👉jsp请求转发标签
index.jsp
3. 显示数据<c:forEach items="${requestScope.page.items}" var="furn"> <div class="col-lg-3 col-md-6 col-sm-6 col-xs-6 mb-6" data-aos="fade-up" data-aos-delay="200"> <!-- Single Product --> <div class="product"> <div class="thumb"> <a href="shop-left-sidebar.html" class="image"> <img src="${furn.imagePath}" alt="Product"/> <img class="hover-image" src="assets/images/product-image/5.jpg" alt="Product"/> </a> <span class="badges"> <span class="sale">-10%</span> <span class="new">New</span> </span> <div class="actions"> <a href="#" class="action wishlist" data-link-action="quickview" title="Quick view" data-bs-toggle="modal" data-bs-target="#exampleModal"><i class="icon-size-fullscreen"></i></a> </div> <button title="Add To Cart" class=" add-to-cart">Add To Cart </button> </div> <div class="content"> <h5 class="title"> <a href="shop-left-sidebar.html">Simple ${furn.name} </a></h5> <span class="price"> <span class="new">家居: ${furn.name}</span> </span> <span class="price"> <span class="new">厂商: ${furn.business}</span> </span> <span class="price"> <span class="new">价格: ${furn.price}</span> </span> <span class="price"> <span class="new">销量: ${furn.saleNum}</span> </span> <span class="price"> <span class="new">库存: ${furn.inventory}</span> </span> </div> </div> </div> </c:forEach>
分页导航
<!-- Pagination Area Start --> <div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"> <ul> <li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}" href="customerFurnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a> </li> <li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}" href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a> </li> <c:set scope="page" var="begin" value="1"></c:set> <c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set> <c:forEach begin="${begin}" end="${end}" var="i"> <li><a class="${i == requestScope.page.pageNo ? "active" : ""}" href="customerFurnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a> </li> </c:forEach> <li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}" href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a> </li> <li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}" href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a> </li> <li><a>共${requestScope.page.pageTotal} 页</a></li> <li><a>共${requestScope.page.totalRow}记录</a></li> </ul> </div> <!-- Pagination Area End -->
🍅首页搜索
需求分析
- 顾客进入首页页面
- 点击搜索按钮, 可以输入家居名
- 正确显示分页导航条, 并且要求在分页时, 保留上次搜索条件
程序框架图
- DAO
模糊查询👉
- service
- web层 CustomerFurnServlet
page方法就被抛弃了
- 前端 index.jsp
🍅两个奇怪的问题
- 点击家居管理, 发出两个请求
抓包
原因
请求首页面即进入到indx.jsp, index.jsp又请求转发到CustomerFurnServlet
问题解决- 首页分页出现问题
原因
🌳会员显示登录名
需求分析
- 会员登陆成功, login_ok.jsp显示登录信息
- 如果登陆成功后返回首页面, 显示订单管理和安全退出
- 如果用户没有登陆过, 首页就显示登录和注册超链接
程序框架图
实现
重命名时, 会联动修改
将login_ok.jsp中的index.html改成index.jsp. 注意, 不要改成views/customer/index.jsp
🍅注销登录
思路分析
- 用户登录成功后
- login_ok.jsp, 点击安全退出, 注销登录
- 返回首页, 也可点击安全退出, 注销登录
程序框架图
实现
🍅验证码
程序框架图
- web层
KaptchaServlet -> 引入kaptcha-2.3.2.jar包, 在web.xml中配置KaptchaServlet
MemberServlet
- 前端页面
login.jsp
验证码不能为空
点击图片更换验证码
将register_ok.html, register_fail.html改造成jsp页面
login.jsp页面总是默认停留在会员登录的div内, 修改
注册回显信息
🌳购物车
程序框架图
cartItem模型
Cart数据模型
测试
实现
- 创建CartServlet
- 首页获取id请求后台
- 首页购买的商品总数量
🍆显示购物车
需求分析
- 查看购物车, 可以显示如下信息
- 选中了哪些家居, 名称, 数量, 金额
- 统计购物车共多少商品, 总价多少
程序框架图
- 走通购物车
cart.jsp
index.jsp跳转
排错
定位
页面源代码
- 显示家居项
<tbody> <%--找到显示购物车项, 进行循环的items--%> <c:if test="${not empty sessionScope.cart.items}"> <%-- 1.sessionScope.cart.items => 取出的是HashMap<Integer, CartItem> 2.所以通过foreach标签取出的每一个对象, 即entry是 HashMap<Integer, CartItem>的 k-v 3.var其实就是 entry 4.所以要取出cartItem对象, 是通过 entry.value取出 --%> <c:forEach items="${sessionScope.cart.items}" var="entry"> <tr> <td class="product-thumbnail"> <a href="#"><img class="img-responsive ml-3" src="assets/images/product-image/1.jpg" alt=""/></a> </td> <td hidden="hidden" class="product-name"><a href="#">${entry.key}</a></td> <%--隐藏域--%> <td class="product-name"><a href="#">${entry.value.name}</a></td> <td class="product-price-cart"><span class="amount">$${entry.value.price}</span> </td> <td class="product-quantity"> <div class="cart-plus-minus" onclick="change1(this)"> <input class="cart-plus-minus-box" type="text" name="qtyButton" value="${entry.value.count}"/> </div> </td> <td class="product-subtotal">$${entry.value.totalPrice}</td> <td class="product-remove"> <a href="cartServlet?action=del&key=${entry.key}"><i class="icon-close"></i></a> </td> </tr> </c:forEach> </c:if> </tbody>
- 计算总价
🍆修改购物车
需求分析
- 进入购物车, 可以修改购买数量
- 更新该商品项的金额
- 跟新购物车商品数量和总金额
程序框架图
- Cart增加方法
- CartServlet
- 前端
cart.jsp
🍆删除购物车
需求分析
- 进入购物车, 可以删除某商品
- 可以清空购物车
- 要求给出适当的确认信息
程序框架图
- 删除购物车
- 清空购物车
🌳生产订单
需求分析
- 进入购物车, 点击购物车结账
- 生成订单和订单项
- 如果会员没有登陆, 先进入登陆页面, 完成登陆后再结账
程序框架图
🍉创建表
order表
-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;
-- 删除表
DROP TABLE `order`;
-- 创建数据库
CREATE DATABASE home_furnishing;
-- 切换
USE home_furnishing;
-- 创建订单表
-- 每个字段应当使用 not null 来约束
-- 字段类型的设计, 应当和相关联表的字段类型相对应
-- 是否需要使用外键?
-- 1.需要[可以从db层保证数据的一致性(早期hibernate框架要求必须使用外键)]
-- 2.不需要[外键对效率有影响, 应当从程序的业务层保证数据的一致性(推荐)]
CREATE TABLE `order` (
id VARCHAR(60) PRIMARY KEY, -- 订单编号
create_time DATETIME NOT NULL,-- 年月日 时分秒
price DECIMAL(10,2) NOT NULL,-- 订单价格
`status` TINYINT NOT NULL, -- 订单状态(1未发货 2已发货 3已结账)
member_id INT NOT NULL -- 谁的订单
)CHARSET utf8 ENGINE INNODB;
order_item表
-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;
-- 删除表
DROP TABLE order_item;
-- 创建数据库
CREATE DATABASE home_furnishing;
-- 切换
USE home_furnishing;
-- 创建订单明细表
CREATE TABLE order_item (
id INT PRIMARY KEY AUTO_INCREMENT, -- 订单明细id
`name` VARCHAR(32) NOT NULL,-- 家居名
`count` INT UNSIGNED NOT NULL,-- 数量
price DECIMAL(10, 2) NOT NULL,-- 价格
total_price DECIMAL(10, 2) NOT NULL,-- 订单项的总价格
order_id VARCHAR(60) NOT NULL -- 订单编号
)CHARSET utf8 ENGINE INNODB;
🍉实体类
订单表
public class Order {
private String id;
private Date createTime;
private BigDecimal price;
private Integer status;
private Integer memberId;
public Order() {
}
//有参构造器
//getter方法, setter方法
}
订单明细表
public class OrderItem {
private Integer id;
private String name;
private Integer count;
private BigDecimal price;
private BigDecimal totalPrice;
private String orderId;
public OrderItem() {
}
//有参构造器
//getter方法, setter方法
}
🍉DAO
OrderDAO
OrderItemDAO
🍉service
public class OrderServiceImpl implements OrderService {
private OrderDAO orderDAO = new OrderDAOImpl();
private OrderItemDAO orderItemDAO = new OrderItemDAOImpl();
private FurnDAO furnDAO = new FurnDAOImpl();
//在这里可以感受到javaee分层的好处. 在service层, 通过组合多个dao的方法,
// 完成某个业务 慢慢体会好处
@Override
public String saveOrder(Cart cart, int memberId) {
//将cart购物车的数据以order和orderItem的形式保存到DB中
//因为生成订单会操作多张表, 因此会涉及到多表事务的问题, ThreadLocal+Mysql事务机制+过滤器
//1.通过cart对象, 构建一个对应的order对象
// 先生成一个UUID, 表示当前的订单号, UUID是唯一的
String orderId = UUID.randomUUID().toString();//订单id
Order order = new Order(orderId, new Date(), cart.getCartTotalPrice(), 0, memberId);
//保存order到数据表
orderDAO.saveOrder(order);//订单生成成功
//通过cart对象, 遍历CartItem, 构建OrderItem对象, 并保存到对应的order_item表
Map<Integer, CartItem> cartItems = cart.getItems();
String orderItemId = "";
for (CartItem cartItem : cartItems.values()) {
//通过cartItem对象构建了orderItem对象
OrderItem orderItem = new OrderItem(null, cartItem.getName(), cartItem.getCount(),
cartItem.getPrice(), cartItem.getTotalPrice(), orderId);
//保存
orderItemDAO.saveOrderItem(orderItem);
//更新furn表 saleNum销量 - inventory库存
//(1) 获取furn对象
Furn furn = furnDAO.queryFurnById(cartItem.getId());
//(2) 更新furn对象的 saleNum销量 - inventory库存
furn.setInventory(furn.getInventory() - cartItem.getCount());
furn.setSaleNum(furn.getSaleNum() + cartItem.getCount());
//(3) 更新到数据表
furnDAO.updateFurn(furn);
}
//清空购物车
cart.clear();
return orderId;
}
}
🍉servlet
public class OrderServlet extends BasicServlet {
//定义属性
private OrderService orderService = new OrderServiceImpl();
/**
* 生成订单
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
protected void saveOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
//获取购物车
Cart cart = (Cart) session.getAttribute("cart");
//如果cart为空, 说明会员没有购买任何家居, 转发到首页
if (cart == null) {
request.getRequestDispatcher("/index.jsp").forward(request, response);
return;
}
//获取到登陆的member对象
Member member = (Member) session.getAttribute("member");
if (member == null) {//说明用户没有登录, 转发到登陆页面
//重定向到登陆页面
request.getRequestDispatcher("/views/member/login.jsp")
.forward(request, response);
return;//直接返回
}
//可以生成订单
String orderId = orderService.saveOrder(cart, member.getId());//订单, 订单明细已生成
session.setAttribute("orderId", orderId);//订单id
//使用重定向放入到checkout.jsp
response.sendRedirect(request.getContextPath() + "/views/order/checkout.jsp");
}
}
- 防止生成空订单
hashMap.clear之后, 是置空还是size置为0
🍉前端
checkout.html修改为checkout.jsp
🌳显示订单[订单管理]
- 添加购物车按钮动态处理
需求分析
- 如果某家居库存为0, 前台的"Add to Cart" 按钮显示为"暂时缺货"
- 后台也加上校验. 只有在 库存>0时, 才能添加到购物车
思路分析
- 首页添加家居到购物车时, 加以限制
- 购物车里, 更新家居数量时,加以限制
- 管理订单
需求分析
- 完成订单管理-查看
- 具体流程参考显示家居
- 静态页面order.html 和 order_detail.html 已提供
程序框架图
- DAO
- service
- web层
- 前端
index.jsp, cart.jsp, login_ok.jsp, checkout.jsp, order.jsp均可跳转到订单管理
- 管理订单项
程序框架图
- DAO
- service
- web层
在Order实体类中新增count属性, 在生成订单时, 将totalCount赋给count
- 前端
order.jsp跳转到OrderItemServlet
order_detail.jsp
order表增加count列 - ALTER TABLEorder
ADDCOUNT
INT UNSIGNED NOT NULL AFTER price;
🌈过滤器权限验证
需求分析
- 加入过滤器权限验证
- 如果没有登陆, 查看购物车和添加到购物车, 就会自动转到会员登陆页面
- 配置拦截url
<?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"> <!--过滤器一般我们配置在上面--> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.zzw.furns.filter.AuthFilter</filter-class> <init-param> <!--这里配置了后, 还需要在过滤器中处理--> <param-name>excludedUrls</param-name> <param-value>/views/manage/manage_login.jsp,/views/member/login.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <!--这里配置要验证的url 1.在filter-mapping中的url-pattern配置 要拦截/验证的url 2.对于我们不去拦截的url, 就不配置 3.对于要拦截的目录中的某些要放行的资源, 再通过配置指定 --> <url-pattern>/views/cart/*</url-pattern> <url-pattern>/views/manage/*</url-pattern> <url-pattern>/views/member/*</url-pattern> <url-pattern>/views/order/*</url-pattern> <url-pattern>/cartServlet</url-pattern> <url-pattern>/manage/furnServlet</url-pattern> <url-pattern>/orderServlet</url-pattern> <url-pattern>/orderItemServlet</url-pattern> </filter-mapping> </web-app>
2.过滤器逻辑判断
/** * 这是用于权限验证的过滤器, 对指定的url进行验证 * 如果登陆过, 就放行; 如果没有登陆, 就回到登陆页面 * @author 赵志伟 * @version 1.0 */ @SuppressWarnings({"all"}) public class AuthFilter implements Filter { private List<String> excludedUrls; @Override public void init(FilterConfig filterConfig) throws ServletException { //获取到配置的excludedUrls String strExcludedUrls = filterConfig.getInitParameter("excludedUrls"); String[] split = strExcludedUrls.split(","); //将 splitUrl 转成 list excludedUrls = Arrays.asList(split); System.out.println("excludedUrls= " + excludedUrls); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("请求/cartServlet 被拦截..."); HttpServletRequest request = (HttpServletRequest) servletRequest; //得到请求的url String url = request.getServletPath(); System.out.println("url= " + url); //判断是否要验证 if (!excludedUrls.contains(url)) { //获取到登陆的member对象 Member member = (Member) request.getSession().getAttribute("member"); if (member == null) {//说明用户没有登录 //转发到登陆页面, 转发不走过滤器 servletRequest.getRequestDispatcher("/views/member/login.jsp") .forward(servletRequest, servletResponse); 重定向-拦截-重定向-拦截-重定向-拦截 //((HttpServletResponse) servletResponse) // .sendRedirect(request.getContextPath() + "/views/member/login.jsp"); return;//直接返回 } } //验证通过, 放行 filterChain.doFilter(servletRequest, servletResponse); System.out.println("请求/cartServlet验证通过, 放行"); } @Override public void destroy() { } }
- 处理管理员登陆
🌈事务管理
1. 数据不一致问题
- 将FurnDAOImpl.java的updateFurn方法的sql故意写错
[furnDAO.updateFurn(furn);
由ctrl+alt+b定位到updateFurn的实现方法]- 在OrderServiceImpl的saveOrder()方法内捕获一下异常, 目的是保证程序能够继续执行
- 查看数据库里的数据会有什么结果. 会出现数据不一致的问题.
我在首页购买了一个小台灯, 数据库中生成了对应的订单和订单项, 但家居表里该小台灯的销量和库存没有变化, 纹丝不动. 相当于客户下单了, 但没有给人家发货.
2. 程序框架图
思路分析
- 使用 Filter + ThreadLocal 来进行事务管理
- 说明: 在一次http请求中, servlet-service-dao 的调用过程, 始终是一个线程, 这是使用ThreadLocal的前提
- 使用ThreadLocal来确保所有dao操作都在同一个Connection内
程序框架图
- 修改JdbcUtilsByDruid工具类
public class JdbcUtilsByDruid { private static DataSource dataSource; //定义属性ThreadLocal, 这里存放一个Connection private static ThreadLocal<Connection> threadlocalConn = new ThreadLocal<>(); /** * 从ThreadLocal获取connection, 从而保证在一个线程中 * 获取的是同一个Connection */ public static Connection getConnection() { Connection connection = threadlocalConn.get(); if (connection == null) {//说明当前的threadlocal没有这个连接 try { //就从数据库连接池中取出连接放入threadlocal connection = dataSource.getConnection(); //将连接设置为手动提交, 既不要让它自动提交 connection.setAutoCommit(false); threadlocalConn.set(connection); } catch (SQLException e) { throw new RuntimeException(e); } } return connection; } /** * 提交事务 */ public static void commit() { Connection connection = threadlocalConn.get(); if (connection != null) { try { connection.commit(); } catch (SQLException e) { throw new RuntimeException(e); } finally { try { connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } //1.当提交后, 需要把connection从threadlocalConn中清除掉 //2.不然会造成threadlocalConn长时间持有该连接, 会影响效率 //3.也因为Tomcat底层使用的是线程池技术 threadlocalConn.remove(); } } /** * 说明: 所谓回滚是 回滚/撤销 和connection管理的操作 删除/修改/添加 */ public static void rollback() { Connection connection = threadlocalConn.get(); if (connection != null) { try { connection.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } finally { try { connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } threadlocalConn.remove(); } }
- 修改BasicDao
删掉各个方法finally代码块里的close方法. 只有在事务结束后才实施关闭连接的操作. 一是提交事务后关闭连接; 二是增删改出错后, 回滚关闭连接.public List<T> queryMany(String sql, Class<T> clazz, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); List<T> tList = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), objects); return tList; } catch (SQLException e) { throw new RuntimeException(e);//编译异常->运行异常抛出 } } //查询单行, 返回的是一个对象 public T querySingle(String sql, Class<T> clazz, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); T object = queryRunner.query(connection, sql, new BeanHandler<>(clazz), objects); return object; } catch (Exception e) { throw new RuntimeException(e); } } //查询某一字段 public Object queryScalar(String sql, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); Object query = queryRunner.query(connection, sql, new ScalarHandler(), objects); return query; } catch (Exception e) { throw new RuntimeException(e); } } public int update(String sql, Object... objects) { Connection connection = null; try { //这里是从数据库连接池获取connection //注意:每次从连接池中取出connection, 不能保证是同一个 //1.我们目前已经是从和当前线程关联的ThreadLocal获取的connection //2.所以可以保证是同一个连接[在同一个线程中/在同一个请求中 => 因为一个请求对应一个线程] connection = JdbcUtilsByDruid.getConnection(); return queryRunner.update(connection, sql, objects); } catch (Exception e) { throw new RuntimeException(e); } }
- 控制层进行事务管理
前提OrderServiceImpl里报错的代码取消try-catch, 在OrderServlet控制层捕获//1.如果我们只是希望对orderService.saveOrder()方法进行事务控制 //2.那么我们可以不使用过滤器,直接在这个位置进行提交和回滚即可 //可以生成订单 String orderId = null;//订单, 订单明细已生成 try { orderId = orderService.saveOrder(cart, member.getId()); JdbcUtilsByDruid.commit();//提交 } catch (Exception e) { JdbcUtilsByDruid.rollback(); e.printStackTrace(); }
🌈Transaction过滤器
程序框架图
体会: 异常机制是可以参与业务逻辑的
- 在web.xml中配置
<filter> <filter-name>TransactionFilter</filter-name> <filter-class>com.zzw.furns.filter.TransactionFilter</filter-class> </filter> <filter-mapping> <filter-name>TransactionFilter</filter-name> <!--这里我们对请求都进行事务管理 --> <url-pattern>/*</url-pattern> </filter-mapping>
- 在OrderService控制层里取消捕获异常, 将代码重新改回下述模样
String orderId = orderService.saveOrder(cart, member.getId());
同时BasicServlet模板里也取消异常捕获, 或者将异常抛出, 代码如下try { Method declaredMethod = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class); System.out.println("this = " + this);//com.zzw.furns.web.MemberServlet@38f54ed7 declaredMethod.invoke(this, req, resp); System.out.println("this.getClass() = " + this.getClass()); } catch (Exception e) { //将发生的异常, 继续throw throw new RuntimeException(e); }
- 在代码执行完毕后, 会运行到Transaction过滤器的后置代码, 在这里进行异常捕获, 如果发生异常, 则回滚.
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { //放行 filterChain.doFilter(servletRequest, servletResponse); JdbcUtilsByDruid.commit();//统一提交 } catch (Exception e) {//出现了异常 JdbcUtilsByDruid.rollback();//回滚 e.printStackTrace(); } }
🌈统一错误页面
需求分析
- 如果在访问/操作网站时, 出现了内部错误, 统一显示 500.jsp
- 如果访问/操作的页面/servlet不存在时, 统一显示 404.jsp
思路分析
- 发生错误/异常时, 将错误/异常抛给tomcat
- 在web.xml中配置不同错误显示的页面即可
- 引入404.html, 500.html, 修改成jsp文件
页面首行<%@ page contentType="text/html;charset=UTF-8" language="java" %>
base标签<base href="<%=request.getContextPath()%>/">
将跳转链接改成index.jsp
<a class="active" href="index.jsp"> <h4 style="color: darkblue">您访问的页面不存在 返回首页</h4> </a>
- web.xml配置
<!--错误提示的配置一般写在web.xml的下面--> <!--500 错误提示页面--> <error-page> <error-code>500</error-code> <location>/views/error/500.jsp</location> </error-page> <!--404 错误提示页面--> <error-page> <error-code>404</error-code> <location>/views/error/404.jsp</location> </error-page>
- 修改事务过滤器, 将异常抛给tomcat
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { //放行 filterChain.doFilter(servletRequest, servletResponse); JdbcUtilsByDruid.commit();//统一提交 } catch (Exception e) {//出现了异常 //只有在try{}中出现了异常, 才会进行catch{} //才会进行回滚 JdbcUtilsByDruid.rollback();//回滚 //抛出异常, 给tomcat. tomcat会根据error-page来显示对应页面 throw new RuntimeException(e); //e.printStackTrace(); } }
🌈Ajax检验注册名
需求分析
- 注册会员时, 如果名字已经注册过, 当光标离开输入框, 提示会员名已经存在, 否则提示不存在
- 要求使用ajax完成
程序框架图
- MemberServlet - 返回json格式的字符串 - 方式一
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.获取用户名 String username = req.getParameter("username"); //2.调用service boolean existsByUsername = memberService.isExistsByUsername(username); //3.思路 //(1)如果返回json格式[不要乱写, 要根据前端的需求来写] //(2)因为前后端都是我们自己写的, 格式我们自己定义 //(3){"isExist": true}; //(4)先用最简单的方法拼接 => 一会改进[扩展] String resultJson = "{\"isExist\": " + existsByUsername + "}"; //4.返回 resp.getWriter().print(resultJson); }
返回json格式的字符串 - 方式二
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.获取用户名 String username = req.getParameter("username"); //2.调用service boolean existsByUsername = memberService.isExistsByUsername(username); //3.思路 //(1)如果返回json格式[不要乱写, 要根据前端的需求来写] //(2)因为前后端都是我们自己写的, 格式我们自己定义 //(3){"isExist": true}; //(4)先用最简单的方法拼接 => 一会改进[扩展] //String resultJson = "{\"isExist\": " + existsByUsername + "}";字符串就不需要再转 //(5)将要返回的数据封装成map => json格式 Map<Object, Object> map = new HashMap<>(); map.put("isExist", existsByUsername); //map.put("email", "978964140@qq.com"); //map.put("phone", "13031748275"); //4.返回json格式的数据 Gson gson = new Gson(); String resultJson = gson.toJson(map); resp.getWriter().print(resultJson); }
- 前端
$("#username").mouseleave(function () {//鼠标离开事件[无需点击, 即可触发] var usernameValue = $(this).val(); $.getJSON( //这里尽量准确, 一把确定[复制粘贴] "memberServlet", "action=isExistByName&username=" + usernameValue, function (data) { alert(data.isExist); console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log() } /*========================================================================================*/ "memberServlet?action=isExistByName&username=" + usernameValue, function (data) { alert(data.isExist); console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log() } /*========================================================================================*/ "memberServlet", { action: "isExistByName", username: usernameValue }, function (data) { alert(data.isExist); console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log() } /*========================================================================================*/ "memberServlet", { "action": "isExistByName", "username": usernameValue }, function (data) { alert(data.isExist); //前端人员只能通过console.log()来查看你的数据, 然后才知道怎么获取你的数据 console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log() if (data.isExist) { $("span[class='errorMsg']").text("用户名 " + usernameValue + " 不可用"); } else { $("span[class='errorMsg']").text("用户名 " + usernameValue + " 可用"); } ) }
- Ajax检验验证码
- MemberServlet
protected void verifyCaptcha(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //获取用户提交的验证码 String captcha = req.getParameter("captcha"); //从session中获取 生成的验证码 HttpSession session = req.getSession(); String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY); //立即删除session中的验证码, 防止该验证码被重复使用 session.removeAttribute(KAPTCHA_SESSION_KEY); //如果token不为空, 并且和用户提交的验证码保持一致, 就继续 if (token != null) { Map<Object, Object> map = new HashMap<>(); boolean verifyCaptcha = token.equalsIgnoreCase(captcha); map.put("verifyCaptcha", verifyCaptcha); //返回json格式的数据 Gson gson = new Gson(); String resultJson = gson.toJson(map); resp.getWriter().print(resultJson); } }
- 前端
$("#code").blur(function () {//光标焦点离开事件[点击后离开, 才可以触发] var captchaValue = this.value; $.getJSON( "memberServlet?action=verifyCaptcha&captcha="+captchaValue, function (data) { console.log("data= ", data); if (data.verifyCaptcha) { $("span.errorMsg2").text("验证码正确"); } else { $("span.errorMsg2").text("验证码错误"); } } ); })
在验证码标签旁补充一个span标签
<span class="errorMsg2" style="float: right; font-weight: bold; font-size: 15pt; margin-left: 10px; color: lightgray;"></span>
🌈Ajax添加购物车
- CartServlet添加addItemByAjax方法
//添加一个添加家居到购物车的方法 [Ajax]
protected void addItemByAjax(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int id = DataUtils.parseInt(request.getParameter("id"), 1);//家居id
//根据id获取对应的家居信息
Furn furn = furnService.queryFurnById(id);
//先把正常的逻辑走完, 再处理异常的情况
//如果某家居的库存为0, 就不要添加到购物车, 直接请求转发到首页面
//if (furn.getInventory() <= 0) {
// request.getRequestDispatcher("/index.jsp").forward(request, response);
// return;
//}
HttpSession session = request.getSession();
Cart cart = (Cart) session.getAttribute("cart");
//得到购物车 有可能是空的,也有可能是上次的
if (cart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
//构建一条家居明细: id,家居名,数量, 单价, 总价
//count类型为Integer, 不赋值默认值为null
CartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());
//将家居明细加入到购物车中. 如果家居id相同,数量+1;如果是一条新的商品,那么就新增
cart.addItem(cartItem, furn.getInventory());
System.out.println("cart= " + cart);
//规定格式 {"cartTotalCount": 3}
//方式一:
//String resultJson = "{\"cartTotalCount\": " + cart.getTotalCount() + "}";
//response.getWriter().print(resultJson);
//方式二: 创建map,可扩展性强
Map<Object, Object> map = new HashMap<>();
map.put("cartTotalCount", cart.getTotalCount());
//转成json
Gson gson = new Gson();
String resultJson = gson.toJson(map);
//返回
response.getWriter().print(resultJson);
//String referer = request.getHeader("referer");
//response.sendRedirect(referer);
}
- 前端
//给所有选定的button都赋上点击事件
$("button.add-to-cart").click(function () {
var id = $(this).attr("furnId");
//location.href = "cartServlet?action=addItem&id=" + id;
//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题
$.getJSON(
"cartServlet?action=addItemByAjax&id=" + id, function (data) {
console.log("data=", data);
//刷新局部 <span class="header-action-num"></span>
$("span.header-action-num").text(data.cartTotalCount);
}
)
});
- 解决Ajax请求转发失败
测试, 会发现针对ajax的重定向和请求转发会失败, 也就是AuthFilter.java的权限拦截不生效, 也就是点击Add to Cart, 后台服务没有响应
使用ajax向后台发送请求跳转页面无效的原因
- 主要是服务器得到的是ajax发送过来的request, 也就是说这个请求不是浏览器请求的, 而是ajax请求的. 所以servlet根据request进行请求转发或重定向都不能影响浏览器的跳转
- 解决方案: 如果想要实现跳转, 可以返回url给ajax, 在浏览器执行window.location(url);
工具类添加方法 - 判断请求是不是一个ajax请求
/** * 判断请求是不是一个ajax请求 * @param request * @return */ public static boolean isAjaxRequest(HttpServletRequest request) { //X-Requested-With: XMLHttpRequest return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); }
修改AuthFilter.java
if (member == null) {//说明用户没有登录 if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求 //转发到登陆页面, 转发不走过滤器 servletRequest.getRequestDispatcher("/views/member/login.jsp") .forward(servletRequest, servletResponse); } else {//如果是ajax请求 //返回ajax请求, 按照json格式返回 {"url": url} //1.构建map Map<Object, Object> map = new HashMap<>(); map.put("url", "views/member/login.jsp"); //2.转成json字符串 String resultJson = new Gson().toJson(map); //3.返回 servletResponse.getWriter().print(resultJson); } 重定向-拦截-重定向-拦截-重定向-拦截 //((HttpServletResponse) servletResponse) // .0sendRedirect(request.getContextPath() + "/views/member/login.jsp"); return;//直接返回 }
修改getJson
//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题 $.getJSON( "cartServlet?action=addItemByAjax&id=" + id, function (data) { console.log("data=", data); if (data.url == undefined) { //刷新局部 <span class="header-action-num"></span> $("span.header-action-num").text(data.cartTotalCount); } else { location.href = data.url; } } )
🌈上传与更新家具图片
引入文件上传下载的包: commons-io-1.4.jar, commons-fileupload-1.2.1.jar
FurnDAOImpl的查询语句加上图片字段 image_path as imagePath
需求分析
- 后台修改家居, 可以点击图片, 选择新的图片
- 这里会用到文件上传功能
思路分析-程序框架图
- furn_update.jsp
<style type="text/css"> #pic { position: relative; } input[type="file"] { position: absolute; left: 0; top: 0; height: 180px; opacity: 0; cursor: pointer; } </style>
去掉a标签
<div id="pic"> <img class="img-responsive ml-3" src="${requestScope.furn.imagePath}" alt="" id="preView"> <input type="file" name="imagePath" id="" >value="${requestScope.furn.imagePath}" οnchange="prev(this)"/> </div>
- 分析空指针异常
将form表单改成文件表单
<form action="manage/furnServlet" method="post" enctype="multipart/form-data"></form>
点击修改家居
报错
将web.xml中500的错误提示配置注销掉, 将异常信息暴露出来
再次点击修改家居信息, 报错信息显示出来, BasicServlet空指针异常
所以有时候报错信息显示出来很重要
分析: 如果表单是enctype=“multipart/form-data”, 那么req.getParameter(“action”) 的方法得不到action值, 所以BasicServlet会报错
具体原因: req.getParameter(“action”)取不到form-data里的数据
- 解决空指针异常
解决方案: 将参数action, id, pageNo以url拼接的方式传参, BasicServlet便不会出错
注意: post请求可以人为主动在地址中拼接参数,拼接的参数可以直接像get那样接收
<form action="manage/furnServlet?action=update&id=${requestScope.furn.id}&pageNo=${param.pageNo}" method="post" enctype="multipart/form-data">
- FurnServlet update方法
处理普通字段if (fileItem.isFormField()) {//文本表单字段 将提交的家居信息, 封装成Furn对象 switch (fileItem.getFieldName()) { case "name": furn.setName(fileItem.getString("utf-8")); break; case "business": furn.setBusiness(fileItem.getString("utf-8")); break; case "price": furn.setPrice(new BigDecimal(fileItem.getString())); break; case "saleNum": furn.setSaleNum(Integer.parseInt(fileItem.getString())); break; case "inventory": furn.setInventory(Integer.parseInt(fileItem.getString())); break; } }
处理文件字段
将文件上传路径保存成一个常量public class WebUtils { public static final String FURN_IMG_DIRECTORY = "assets/images/product-image/"; }
//文件表单字段 => 获取上传的文件的名字 String name = fileItem.getName(); //如果用户没有选择新的图片, name = "" if (!"".equals(name)) { //1.把上传到到服务器 temp目录下的文件保存到指定的目录 String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY; //2.获取完整的目录 String fileRealPath = req.getServletContext().getRealPath(filePath); System.out.println("fileRealPath= " + fileRealPath); //3.创建这个上传的目录 File fileRealPathDirectory = new File(fileRealPath); if (!fileRealPathDirectory.exists()) { fileRealPathDirectory.mkdirs(); } //4.将文件拷贝到fileRealPathDirectory目录下 //对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖 //构建了一个上传的文件的完整路径[目录+文件名] name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name; String fileFullPath = fileRealPathDirectory + "\\" + name; //保存 fileItem.write(new File(fileFullPath)); //关闭流 fileItem.getOutputStream().close(); //更新家居图的图片 furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name); }
全部代码
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //将提交修改的家居信息,封装成Furn对象 //如果你的表单是enctype="multipart/form-data", req.getParameter("id") 得不到id int id = DataUtils.parseInt(req.getParameter("id"), 0); //获取到对应furn对象[从db中获取] Furn furn = furnService.queryFurnById(id); //todo 如果furn为null, 则return //1.判断是不是文件表单 if (ServletFileUpload.isMultipartContent(req)) { //2.创建DiskFileItemFactory对象, 用于构建一个解析上传数据的工具对象 DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory(); //3.构建一个解析上传数据的工具对象 ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory); //解决中文乱码问题 servletFileUpload.setHeaderEncoding("utf-8"); //4.servletFileUpload对象可以把表单提交的数据[文本/文件], 封装到FileItem文件项中 try { List<FileItem> list = servletFileUpload.parseRequest(req); for (FileItem fileItem : list) { //判断是不是一个文件 => 文本表单字段 if (fileItem.isFormField()) { 将提交的家居信息, 封装成Furn对象 switch (fileItem.getFieldName()) { case "name"://家居名 furn.setName(fileItem.getString("utf-8")); break; case "business"://制造商 furn.setBusiness(fileItem.getString("utf-8")); break; case "price"://价格 furn.setPrice(new BigDecimal(fileItem.getString())); break; case "saleNum"://销量 furn.setSaleNum(Integer.parseInt(fileItem.getString())); break; case "inventory"://库存 furn.setInventory(Integer.parseInt(fileItem.getString())); break; } } else { //文件表单字段 => 获取上传的文件的名字 String name = fileItem.getName(); //如果用户没有选择新的图片, name = "" if (!"".equals(name)) { //1.把上传到到服务器 temp目录下的文件保存到指定的目录 String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY; //2.获取完整的目录 String fileRealPath = req.getServletContext().getRealPath(filePath); System.out.println("fileRealPath= " + fileRealPath); //3.创建这个上传的目录 File fileRealPathDirectory = new File(fileRealPath); if (!fileRealPathDirectory.exists()) { fileRealPathDirectory.mkdirs(); } //4.将文件拷贝到fileRealPathDirectory目录下 //对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖 //构建了一个上传的文件的完整路径[目录+文件名] name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name; String fileFullPath = fileRealPathDirectory + "\\" + name; //保存 fileItem.write(new File(fileFullPath)); //关闭流 fileItem.getOutputStream().close(); //更新家居图的图片 furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name); } } } //跟新furn对象->DB furnService.updateFurn(furn); System.out.println("更新成功..."); //请求转发到 update_ok.jsp req.getRequestDispatcher("/views/manage/update_ok.jsp") .forward(req, resp); } catch (Exception e) { throw new RuntimeException(e); } } }
将checkout.jsp复制成update_ok.jsp
<a class="active" href="manage/furnServlet?action=page&pageNo=${param.pageNo}"> <h4>家居修改成功, 点击返回家居管理页面</h4> </a>
🌈作业布置
🍍会员登陆后不能访问后台管理
需求分析
- 管理员admin登陆后, 可访问所有页面
- 会员登陆后, 不能访问后台管理相关页面, 其他页面可以访问
- 假定管理员名字就是admin, 其它会员名就是普通会员
AuthFilter - 代码
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("请求/cartServlet 被拦截...");
HttpServletRequest request = (HttpServletRequest) servletRequest;
//得到请求的url
String url = request.getServletPath();
System.out.println("url= " + url);
//判断是否要验证
if (!excludedUrls.contains(url)) {
//获取到登陆的member对象
Member member = (Member) request.getSession().getAttribute("member");
if (member == null) {//说明用户没有登录
if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求
//转发到登陆页面, 转发不走过滤器
servletRequest.getRequestDispatcher("/views/member/login.jsp")
.forward(servletRequest, servletResponse);
} else {//如果是ajax请求
//返回ajax请求, 按照json格式返回 {"url": url}
//1.构建map
Map<Object, Object> map = new HashMap<>();
map.put("url", "views/member/login.jsp");
//2.转成json字符串
String resultJson = new Gson().toJson(map);
//3.返回
servletResponse.getWriter().print(resultJson);
}
return;//直接返回
}
//如果member不为空
if ("admin".equals(member.getUsername())) {//管理员登陆
//全部放行
} else {//普通用户登录, 部分页面不能放行
//如果该用户不是admin, 但是它访问了后台, 就转到管理员登录页面
//if ("/manage/furnServlet".equals(url) || url.contains("/views/manage/")) {
//.* 匹配任意个字符
if ("/manage/furnServlet".equals(url) || url.matches("^/views/manage/.*")) {
request.getRequestDispatcher("/views/manage/manage_login.jsp")
.forward(servletRequest, servletResponse);
}
}
}
//如果请求的是登录页面, 那么就放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("请求/cartServlet验证通过, 放行");
}
🍍解决图片冗余问题
需求分析
- 家居图片都放在一个文件夹, 会越来越多
- 请尝试在assets/images/product-image/目录下, 自动创建年月日目录, 比如20230612. 以天为单位来存放上传图片
- 当上传新家居的图片, 原来的图片就没有用了, 应当删除原来的家居图片
工具类添加方法 - 返回当前日期
public static String getYearMonthDay() { //第三代日期类 LocalDateTime now = LocalDateTime.now(); int year = now.getYear(); int month = now.getMonthValue(); int day = now.getDayOfMonth(); String date = year + "/" + month + "/" + day + "/"; return date; }
🍍分页导航完善
需求分析
- 如果总页数<=5, 就全部显示
- 如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)
2.1 如果当前页是前3页, 就显示1-5
2.2 如果当前页是后3页, 就显示最后5页
2.3 如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页
代码实现
<c:choose>
<%--如果总页数<=5, 就全部显示--%>
<c:when test="${requestScope.page.pageTotal <= 5}">
<c:set scope="page" var="begin" value="1"></c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set>
</c:when>
<%--如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)--%>
<c:when test="${requestScope.page.pageTotal > 5}">
<c:choose>
<%--如果当前页是前3页, 就显示1-5--%>
<c:when test="${requestScope.page.pageNo <= 3}">
<c:set scope="page" var="begin" value="1"></c:set>
<c:set scope="page" var="end" value="5"></c:set>
</c:when>
<%--如果当前页是后3页, 就显示最后5页--%>
<c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal - 3}">
<c:set scope="page" var="begin" value="${requestScope.page.pageTotal - 4}"></c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set>
</c:when>
<%--如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页--%>
<c:otherwise>
<c:set scope="page" var="begin" value="${requestScope.page.pageNo - 2}"></c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageNo + 2}"></c:set>
</c:otherwise>
</c:choose>
</c:when>
</c:choose>
🐀🐂🐅🐇🐉🐍🐎🐏