韩顺平教育-家居网购
本项目是一个web项目,技术框架是javaee经典三层架构
项目落地:https://download.csdn.net/download/2401_83418369/90487973
MySQL数据库的数据表:https://download.csdn.net/download/2401_83418369/90488414
收获心得:
1、增删改查不能混用(这里不能混用的意思是在查询select中写insert方法),一条一条分清楚,只有在连表查询时才在Service层根据业务结合使用
2、思路分析一定是根据需求走的,在需求中设计如何实现增删改查四条路线,这四条路是独立的,只是根据不同的业务需求在调用时可能会交叉调用的情况,在较为复杂的情况一般是在Service层交叉调用DAO层的api
3、每个模块的作用要分清楚,Service层是业务层主要是处理业务的逻辑,DAO层是对数据库的增删改查,不要将业务层的思路用到DAO层,或则是业务层的代码写到control层
4、分析思路先完成主线后完成细节,比如从前端点击某个按钮会跳转到哪个页面,这条主线先打通,后面再加条件,思路清晰
5、处理业务层的数据比较复杂时采用数据模块来传输数据
使用分层模式目的: 目的是为了解耦,降低代码的耦合度,有利项目的维护和升级
一、开发环境搭建
Tomcat的配置在另一期刊,这里不在叙说
tomcat的安装与配置-CSDN博客
1、项目具体分层(不同包)方案:
自己创建的包名可以不同,满足规范即可
2、把Tomcat里的jar包、下载的jdbc的jar包和工具类dbutils的jar包拷贝到指定目录(具体在下面的文章里下载)
c3p0、Druid连接池+工具类 Apache-DbUtils-CSDN博客
jdbc连接mysql-CSDN博客
3、把前端人员给的静态页拷贝到Web运用即可
下载地址:https://download.csdn.net/download/2401_83418369/90464023
4.将工具类和BasicDAO拷贝到对应位置
下载位置:https://download.csdn.net/download/2401_83418369/90464061
拷贝之后需要自己在测试类里面测试,看是否能连接成功,工具类主要是连接数据库,BasicDAO将连接数据库和apache工具类的方法进行再次封装得到 增删改查方法
二、实现功能1:会员注册
整体思路:
1、前端JS 校验
1. 验证用户名:必须字母,数字下划线组成,并且长度为 3 到 6 位 =》 正则表达式
2. 验证密码:必须由字母,数字下划线组成,并且长度为 3 到 6 位
3. 邮箱格式验证:常规验证即可
4. 验证码:后面实现
这里我使用js代码来验证的,当然还可以使用jQuery
修改的文件是index.html:
<script type="text/javascript">
window.onload=function (){
var btn = document.getElementById("sub-btn");
btn.onclick=function (){
var user = document.getElementById("username").value;
var Patten=/^\w{3,6}$/
if (!Patten.test(user)){
alert("请输入用户名3-6位")
return false;
}
var password = document.getElementById("password").value;
if (!Patten.test(password)){
alert("请输入密码3-6位")
return false;
}
var repWd = document.getElementById("repwd").value;
if (repWd!==password){
alert("两次密码不正确,请重新输入!!!")
return false;
}
var email = document.getElementById("email").value;
var Patten2=/^(\w)+@([a-zA-Z]+\.)+[a-zA-Z]+$/
if (!Patten2.test(email)){
alert("邮件格式有误!!!")
return false;
}
}
}
</script>
2、创建数据库和数据表
CREATE DATABASE home_furnishing;
USE home_furnishing;
CREATE TABLE member(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL UNIQUE,
`password` VARCHAR(32) NOT NULL,
`email` VARCHAR(64)
);
INSERT INTO member(`username`,`password`,`email`)
VALUES('admin','admin','hsp@hanshunping.net');
SELECT * FROM member;
3、创建javaBean(注意该字段名和数据库的字段名一一对应)
package com.study.furns.javabean;
public class Member {
private Integer id;
private String username;
private String password;
private String email;
public Member(Integer id, String username, String password, String email) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
}
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Member() {
}
}
4、创建MemberDAO的接口和实现类
DAO层主要是实现数据库的增删改查理解该DAO的作用,从一个消费者的角度思考:你需要什么得到什么? 例如:查询数据,用户输入一个名字就要得到该名字的完整信息
MemberDAO接口:
package com.study.furns.dao;
import com.study.furns.javabean.Member;
import java.sql.SQLException;
public interface MemberInter {
public Member queryMember(String name) throws SQLException;
public int saveMember(Member member);
}
MemberDAO实现类
package com.study.furns.dao.dao_impl;
import com.study.furns.dao.BasicDAO;
import com.study.furns.dao.MemberInter;
import com.study.furns.javabean.Member;
import java.sql.SQLException;
public class MemberDao extends BasicDAO<Member> implements MemberInter {
/**
* 理解该DAO的作用,从一个消费者的角度思考:你需要什么得到什么?
* 例如:查询数据,用户输入一个名字就要得到该名字的完整信息
*/
@Override
public Member queryMember(String name) throws SQLException {
String sql="select * from member where username=?";
return querySingle(sql, Member.class, name);
}
@Override
public int saveMember(Member member) {
String sql="insert into member values(?,?,MD5(?),?)";
return update(sql, member.getId(),member.getUsername(), member.getPassword(), member.getEmail());
}
}
5、测试MemberDAO
主要的目的是保证MemberDAO的正确运行
package com.study.furns.test;
import com.study.furns.dao.dao_impl.MemberDao;
import com.study.furns.javabean.Member;
import org.junit.Test;
import java.sql.SQLException;
public class MemberDaoTest {
MemberDao memberDao = new MemberDao();
@Test
public void query() throws SQLException {
Member member = memberDao.queryMember("admin");
System.out.println(member);
}
@Test
public void save(){
Member member = new Member(null, "admin", "hsp", "hsp@qq.com");
int i = memberDao.saveMember(member);
if (i>=1){
System.out.println("成功");
}
else {
System.out.println("失败");
}
}
}
6、创建MemberService的接口和实现类
Service主要是用于将MemberDAO进行再封装,根据实际业务情况编写方法调用DAO层的api
例如:注册用户,相当于增加数据,还有查询数据,先判断用户是否存在,然后再增加数据
MemberService的接口:
package com.study.furns.service;
import com.study.furns.javabean.Member;
import java.sql.SQLException;
public interface MemberServiceInter {
public boolean register(Member member);
public boolean isExist(String name) throws SQLException;
}
MemberService的实现类:
package com.study.furns.service.service_impl;
import com.study.furns.dao.MemberInter;
import com.study.furns.dao.dao_impl.MemberDao;
import com.study.furns.javabean.Member;
import com.study.furns.service.MemberServiceInter;
import java.sql.SQLException;
/**
* 再次封装DAO,service层主要是调用DAO层里面的方法,将方法再次封装
*/
public class MemberService implements MemberServiceInter {
MemberInter memberDao=new MemberDao();
@Override
public boolean register(Member member) {
return memberDao.saveMember(member)==1;
}
@Override
public boolean isExist(String string) throws SQLException {
return memberDao.queryMember(string) != null;
}
}
7、测试 MemberService
package com.study.furns.test;
import com.study.furns.javabean.Member;
import com.study.furns.service.MemberServiceInter;
import com.study.furns.service.service_impl.MemberService;
import org.junit.Test;
import java.sql.SQLException;
public class MemberServiceTest {
MemberServiceInter memberServiceInter =new MemberService();
@Test
public void exist() throws SQLException {
boolean admin = memberServiceInter.isExist("admin");
System.out.println(admin);
}
@Test
public void add(){
Member member = new Member(null, "jack", "jack", "jack@qq.com");
boolean register = memberServiceInter.register(member);
System.out.println(register);
}
}
8、创建RegisterServlet(controller层)对静态页面的请你进行解析和调度Service层
package com.study.furns.web;
import com.study.furns.javabean.Member;
import com.study.furns.service.MemberServiceInter;
import com.study.furns.service.service_impl.MemberService;
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.sql.SQLException;
public class RegisterServlet extends HttpServlet {
private MemberServiceInter memberService = new MemberService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("user-name");
String password = req.getParameter("user-password");
String email = req.getParameter("user-email");
try {
boolean exist = memberService.isExist(username);
resp.setContentType("text/html;charset=utf-8");
if (!exist){
Member member = new Member(null, username, password, email);
memberService.register(member);
req.getRequestDispatcher("/views/member/register_ok.html").forward(req,resp);
}else {
req.getRequestDispatcher("/views/member/register_fail.html").forward(req,resp);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}
注意细节:
1、配置druid文件的路径为工作路径,不然会抛出异常
properties.load(JDBCUtilsByDruid.class.getClassLoader().getResourceAsStream("druid.properties"));
2、有两个静态页面的增加需要添加到/views/member/ 注意更改base路径、端口...
下载地址:https://download.csdn.net/download/2401_83418369/90464232
三、实现功能2:完成会员登录功能
整体思路:
需求分析:
1. 输入用户名、密码后提交
2. 判断会员是否存在
3. 会员存在(数据库),显示登录成功页面
4. 否则,返回登录页面,重新登录
思路分析:
从消费者的角度思考:用户需要提交用户名和密码,得到结果是登入成功或失败,那么Service层就是实现login方法,那么DAO层对应实现查询数据库并返回数据
实现步骤:
1、在MemberDAO的接口和类中增加方法queryByNameAndPassword并实现
2、在Test包中测试queryByNameAndPassword
3、在MemberService的接口和类中增加方法login并实现
4、在Test包中测试login
5、增加LoginServlet实现接收请求并调用Service的login方法,最后发送响应
四、实现功能3:登录错误提示,表单回显
需求分析:
1. 输入用户名、密码后提交
2. 如果输入有误,则给出提示
3. 在登录表单回显用户名
思路分析:
消费者的角度分析:用户输入姓名和密码,得到的不只是登入成功或失败,得到的失败结果在页面中显示出来,因为html是静态页面,无法和servlet进行交互,所以需要将html改为jsp
实现步骤:
1、在LoginServlet类中在判断login方法为false的条件下,将request域对象设置属性和值,并请求转发到login.jsp页面
2、将login.html文件改成login.jsp文件,并使用request域对象得到对应属性的值
3、将属性值在对应的标签中显示即可
注意细节:
1、将html修改为jsp文件时一定要加上下面的声明标签,否则会出现乱码!!!
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
五、实现功能4:web层 servlet 减肥
需求分析:(如图)
思路分析:
方法1:
在项目开发中,同一个业务(模块),一般对应一个 Servlet 即可,比如 LoginServlet 和
RegisterServlet 都是在处理和会员相关的业务,应当合并,可以通过隐藏的输入框携带不同的value值,那么在Web层的servlet中使用if-else区分不同的value值来调用不同的方法
方法2:
在方法1的基础上进行再次优化,方法1的确解决了使用单一请求,又根据请求携带的参数值不同而调用不同的方法,但是又产生了其他问题:在memberServlet类中获取参数值时需要大量的if-else if 来判断,这样的也很麻烦,那么怎样才能根据不同的参数值自动调用对应的方法呢?方法1已经将register方法和login方法封装在memberServlet里面, 这里主要运用反射的技术,通过对象实例获取Class类再调用getDeclaredMethod获取方法对象,根据不同的方法名来调用不同的方法,那么为什么要用模板设计模式?主要是用于消除重复代码。
模板设计模式的核心优势:
- 消除重复代码:公共逻辑抽取到父类,子类仅实现差异部分。
- 流程标准化:确保算法步骤的一致性,如订单处理中的验证、计算、保存等步骤
反射允许在运行时动态获取类信息并创建对象,与模板模式结合可实现以下功能:
- 动态加载子类:根据配置或输入参数选择具体的实现类,避免硬编码。
- 自动化策略选择:通过注解或配置文件标记子类,反射自动识别并实例化。
- 消除条件分支:替代大量的
if-else
或switch
语句,提升可维护性
实现步骤:
方法1:
1、在Web层新建一个memberServlet类,完成配置文件,通过request域对象获取名为action的属性值并在doGet方法中使用if-else来判断不同的属性值来调用不同的方法
2、在login.jsp文件中,找到提交登录和注册的表单,并多添加一个input标签,类型是hidden,根据不同的业务写不同的value值
<input type="hidden" name="action" value="register">
方法2:
1、创建一个BasicServlet该抽象类需要继承HttpServlet但是不需要配置映射url,因为具体运行类型是子类memberServlet 根据请求中携带不一样的参数值就对应不同的方法,在doPost方法中使用实例对象或类名创建Class对象,再调用getDeclaredMethod获得子类的不同方法,最后调用invoke方法调用子类的方法
2、在方法1的基础上修改memberServlet类,可以将doPost方法注销,继承BasicServlet类即可
注意:方法2中action的值必须和方法名一样,不然反射创建不了方法对象!!!
六、实现功能5:后台管理 显示家居
需求分析:
1. 给后台管理提供独立登录页面,不对外公开
2. 管理员登录成功后,显示管理菜单页面
3. 管理员点击家居管理,显示所有家居信息
思路分析:
根据消费者角度:先看到的是管理员页面,然后管理员输入自己的姓名和密码就可以成功登入或登入失败,这里先实现登入的功能,登入信息进过Service层主要是login方法,DAO层主要是对姓名和密码的查询操作,这里和普通用户的创建过程基本一样,登入成功后,主要是ManagerServlet响应一个请求转发到管理员的网址(manage_menu.jsp),管理员根据不同的操作例如点击家居管理这个链接:那么就可以看到所有的家具显示在页面(furn_manage.jsp)在DAO层相当于查询某个数据返回一个集合的数据
实现步骤:(第一阶段先实现管理员登录流程)
1、创建一个新表用于存储管理员的信息
2、创建javaBean对象
3、创建静态页面如管理员登入页面,管理员管理家具页面等
4、创建ManagerDAO层的接口和实现类实现查询姓名和密码返回一个对象还有插入方法
5、测试ManagerDAO的查询方法和插入方法
6、创建ManagerService层的实现类和接口封装ManagerDAO层的方法得到布尔值
7、测试ManagerService层封装的方法
8、创建ManagerServlet解析请求的参数调用Service层的查询方法,如果管理员不存在则登入失败,存在则登录页面
9、登入管理员如果成功则进入管理页面否则请求转发到当前登入页面
第二阶段:实现家居显示
1、拷贝前端的管理员登录页面、管理页面、显示页面
2、创建数据表,根据显示页面的家居信息来设计字段
3、创建javabean对象
4、创建furn_Dao接口和实现类,实现对数据库的查询操作例如查询所有商品信息返回一个集合
5、创建furn_DaoTest进行测试
6、创建furn_Service接口和实现类,调用furn_Dao的方法,不需要传入什么参数只需要返回一个集合
7、测试furn_Service
8、创建furn_servlet调用furn_Service里面的方法,得到list集合通过request域对象设置集合中的数据再请求转发给前端页面
9、在前端页面接收request请求的属性并加载到页面
注意细节:
1、web.xml配置FurnServlet的URL可以写成/manage/furn,多加一个manage用于后面调用filter过滤器增加安全性
2、a 标签默认是get请求如果想携带参数就在后面加上 "?参数名=参数值"
3、调用jsp的jstl需要声明
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
七、实现功能6:后台管理 添加家居
需求分析:
1. 管理员进入到家居管理页面
2. 点击添加家居,进入到 furn_add.jsp 页面,可以添加家居(如图)
思路分析:
根据管理员角度:点击添加家具链接进入添加家具的页面,将添加家具的信息写上后点击添加家居,就会得到添加成功或失败的信息,如果成功那么就会跳转到显示家具的页面,如果失败就会重新回到添加页面,从Service层分析就是添加成功或失败,输入的是一个家具对象,在DAO层分析就是传入一个Furn对象,然后将对象存储返回的是一个Boolean值
实现步骤:
1、在FurnDAO层添加一个方法add并实现
2、测试FurnDAO的add方法
3、在FurnService 的接口和实现类中添加add方法
4、测试FurnService 的add方法
5、在FurnServlet类中添加add方法接收添加页面传来的属性值并且封装成一个Furn对象,调用FurnService 的add方法返回的结果再判断,如果成功则请求转发到furn_manage.jsp页面,失败则请求转发到当前页面
注意细节:
1、中文乱码问题需要在FurnServlet父类的doPost方法中设置setCharacterEncoding方法
2、正确添加数据后相当于再次调用FurnServlet的query方法路径名加上?action=query即可
3、为了解决浏览器刷新后会发出最后一次Servlet请求,导致重复数据的问题,可以使用重定向代替请求转发
4、为了解决添加数据时类型转换异常的问题,这里需要在前端校验或者在后端使用try catch来捕获异常
补充:自动封装javaBean的工具
下载地址:https://download.csdn.net/download/2401_83418369/90467290
注意事项:
该工具类相当于将所有name=字段名的属性值封装到javaBean中,必须要name属性的值和javaBean的字段名保持一致!!!底层用了反射加类型转换
Furn furn = new Furn();
try {
BeanUtils.populate(furn,req.getParameterMap());
} catch (Exception e) {
throw new RuntimeException(e);
}
boolean add = furnServiceInter.add(furn);
可以二次封装:
package com.study.furns.utils;
import org.apache.commons.beanutils.BeanUtils;
import java.util.Map;
public class JavaBeanUtils {
public static <T> T populate(T bean , Map value){
try {
BeanUtils.populate(bean,value);
} catch (Exception e) {
throw new RuntimeException(e);
}
return bean;
}
}
八、实现功能7:后台管理 删除家居
需求分析:
1. 管理员进入到家居管理页面
2. 点击删除家居链接,弹出确认窗口,确认删除,取消放弃
思路分析:
从管理员的角度出发,管理员点击小插件就会弹出一个确认框,最后根据确认框来确定是否真的删除数据,在点击确认按钮后,前端会获取商品的id,因为id是数据库的唯一标识(主键)所以使用request.id就可以获取到商品的id,这个id会发送到FurnServlet被request捕获,然后调用删除的方法,在Service层就是实现了删除方法根据传入的id得到删除是否成功,在DAO就是对数据库进行删除数据操作
关于如何获取商品的id呢?FurnServlet又如何获取呢?这里当我看见前端的小插件是一个a标签时我就想是否可以将id信息通过a标签携带过去,所以通过jsp的循环中得到的Furn对象直接就可以拿到id,然后通过a标签携带多个参数即可,通过&分隔开
URL路径?参数名1=值1&参数名2=值2
实现步骤:
1、在DAO实现删除方法根据传入的id返回Boolean值
2、测试DAO的删除方法
3、在Service层调用DAO层的方法
4、测试Service层的删除方法
5、在FurnServlet增加删除方法并调用Service层的方法,如果成功则重定向到furn_manage.jsp页面
注意细节:
1、get请求方式例如a标签可以携带多个参数不仅可以携带方法名还可以携带自己想要的字段,使用&分隔即可
2、静态页面中需要再次验证是否真正删除该家居,应该使用Class标签而不是id标签,因为在循环体里面id标签不能表示唯一,这样会导致绑定事件失败
3、前端校验需要获取商品的名字,这时需要获取父类的节点再获取子类的节点
4、使用confirm要确认是否要删除,确认返回true,取消返回false
5、document.getElementsByClassName
返回的是类数组(HTMLCollection
),需遍历每个元素并绑定事件。若未正确循环,部分元素可能漏绑
window.onload=function (){
var deleteClass = document.getElementsByClassName("delete");
for (let i = 0; i < deleteClass.length; i++) {
deleteClass.item(i).onclick=function (){
var name= deleteClass[i].parentNode.parentElement.children[1].children[0].innerHTML;
return confirm("你确定要删除["+name+"]这个家居吗?");
}
}
}
九、实现功能8:后台管理 修改家居
需求分析:
1. 管理员进入到家居管理页面
2. 点击修改家居链接,回显该家居信息
3. 填写新的信息,点击修改家居按钮
4. 修改成功后,重新刷新显示家居列表
思路分析:
从管理员的角度分析:首先需要点击修改图标,进入一个修改家居的页面,这里会返回一个家居信息,这里应该是查询方法返回的对应id的商品信息,Service层应该是根据id返回一个javaBean对象,DAO层也是查询根据id来返回一个javaBean对象。
在修改页面中修改家居的信息,当提交时就会将指定id的信息进行修改,在Service层就会调用update方法,在DAO层修改指定的id的商品信息返回的结果是Boolean值
实现步骤:
1、在DAO层增加查询方法,查询根据id返回一个javaBean对象
2、测试DAO的查询方法
3、在Service层调用DAO层的方法
4、测试Service层的方法
5、在FurnServlet中增加查询的方法,如果成功就跳转到修改的页面
6、在DAO层再增加修改的方法,根据id和修改的字段的值,需要传入一个javaBean对象然后返回Boolean值
7、测试修改的方法
8、在Service层调用修改的方法
9、测试修改的方法
10、在FurnServlet中增加修改方法,将修改页面的属性值返回到该方法中,如果修改成功就重定向到管理页面,如果失败就重新请求转发到该页面
注意细节:
1、请求转发的对象或集合直接使用req.setAttribute方法发送到请求域中,在jsp页面使用${requestScope.furn.name}拿到数据
2、post请求一般不再网址上携带参数,需要隐藏的输入框,如果要携带多个参数就使用多个输入框
3、写代码要思路清晰,循序渐进,走一步测一步,不要乱,不然多不知道错在哪
十、实现功能9:实现功能 后台分页(分页显示家居)
需求分析:
1. 管理员进入到家居管理后台页面
2. 点击家居管理,可以按分页规则显示家居
思路分析:
从消费者角度分析:当用户点击分页导航,那么后端根据页码数来发送信息和数量返回给用户,用户点击上一页就会返回上一页的信息,在Service层主要是分页查询内容,有内容就返回没有就不返回在DAO层是使用limit来限制返回内容。
这里主要涉及后端发送数据给前端需要创建一个javaBean对象,将Furn集合,第几页的,数量发送给前端,前端直接接收一个javaBean对象,然后再用for循环解析
实现步骤:
1、增加一个javaBean对象,属性有数量,页码数,Furn集合等等,后端将这些字段信息封装为一个javaBean对象发送给前端,而前端发送页码数和数量给后端作为查询的条件
2、在DAO层增加getItems方法和getTotalRows方法,因为这两个数据是需要从数据库中获取
3、这里需要区分哪些数据是从MySQL中取出来的,哪些数据不需要从数据库得到,由自己控制的?所以DAO层获取依靠MySQL的数据,Service层获取其他数据并将DAO层的其他数据封装在Page方法
语法:SELECT * FROM 表名 LIMIT 偏移量, 每页记录数
公式:偏移量 = (页码 - 1) * 每页记录数
第1页(每页10条):
SELECT * FROM employee LIMIT 0, 10; -- 或简写为 LIMIT 10
第2页:
SELECT * FROM employee LIMIT 10, 10;
这个很容易找到规律的,随便举几个例子就可以找出规律
3、测试DAO层的两个方法
4、在Service层增加page方法创建无参构造对象然后使用set方法给Page对象设值并将DAO层的两个方法得到属性并使用set方法封装在Page对象,最后返回Page对象
5、测试Service层的page方法
6、在FurnServlet增加page方法,使用request请求域对象接收数据并调用Service层的page方法返回给furn_manage.jsp
注意细节:
1、创建的javaBean对象名为Page,这个类是一个泛型类,因为不止是用来分页显示Furn数据还有可能显示其他的数据,这里可以看作是一个模型!
2、在获取数据库中所有行数时在DAO层调用的queryScalar方法返回的是一个Long类型不能直接转换成Integer(因为包装类不能直接转换)需要先转成Number(他们的父类)再强转为子类Integer
3、并不是只有有参构造器可以创建对象,使用无参构造器也可以创建对象再使用set方法设置属性即可
4、显示页数是总数据量除以每页显示的数据,如果有余数就加一
十一、实现功能10:后台分页(分页导航)
需求分析:
1. 管理员进入到家居管理后台页面
2. 可以通过分页导航条来进行分页显示
3. 完成上页
4. 下页
5. 显示共多少页
6. 点击分页导航, 可以显示对应页的家居信息
7. 在管理员进行修改, 删除, 添加 家居后,能够回显原来操作所在页面数据
思路分析:
前面已经实现了查询分页的功能,并且将数据通过request请求域对象的方式发送到furn_manage.jsp文件中,那么只需要在该静态页面接收数据即可,注意字段名必须要写正确!!!否则接收不到
下面是导航栏的展示,显示所有页数
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up">
<ul>
<li><a href="manage/furn?action=page&pageId=1">首页</a></li>
<c:if test="${requestScope.page.pageId>1}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-1}">上页</a></li>
</c:if>
<c:set var="begin" value="1"/>
<c:set var="end" value="${requestScope.page.pageTotalCount}"/>
<c:forEach begin="${begin}" end="${end}" var="i">
<c:if test="${i==requestScope.page.pageId}">
<li><a class="active" href="manage/furn?action=page&pageId=${i}">${i}</a></li>
</c:if>
<c:if test="${i!=requestScope.page.pageId}">
<li><a href="manage/furn?action=page&pageId=${i}">${i}</a></li>
</c:if>
</c:forEach>
<c:if test="${requestScope.page.pageId<requestScope.page.pageTotalCount}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId+1}">下页</a></li>
</c:if>
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageTotalCount}">末页</a></li>
<li><a>共${requestScope.page.pageTotalCount}页</a></li>
<li><a>共${requestScope.page.totalRow}记录</a></li>
</ul>
</div>
下面这个是自己写的展示一部分的导航栏,就是规定只展示3页,但是所在页面没有点亮还需改进
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up">
<ul>
<li><a href="manage/furn?action=page&pageId=1">首页</a></li>
<c:if test="${requestScope.page.pageId>1}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-1}">上页</a></li>
</c:if>
<c:set var="begin" value="1"/>
<c:set var="end" value="3"/><%--限制显示的页数最多是三页,也可以自己修改--%>
<c:forEach begin="${begin}" end="${end}" var="i">
<c:if test="${requestScope.page.pageId<=3}"><%--在三页之前是展示前三页的选择--%>
<li><a href="manage/furn?action=page&pageId=${i}">${i}</a></li>
</c:if><%--到第四页开始是展示后三页的选择,这里还是要列出来,找规律!!!动手!!!!一定要写出来才能找到规律!!!--%>
<c:if test="${requestScope.page.pageId>3}"><%--4--%><%--5--%>
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-3+i}">${requestScope.page.pageId-3+i}</a></li>
<%--2,3,4--%><%--3,4,5--%>
</c:if>
</c:forEach>
<c:if test="${requestScope.page.pageId<requestScope.page.pageTotalCount}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId+1}">下页</a></li>
</c:if>
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageTotalCount}">末页</a></li>
<li><a>共${requestScope.page.pageTotalCount}页</a></li>
<li><a>共${requestScope.page.totalRow}记录</a></li>
</ul>
</div>
最后也是成功解决了,通过前面的案例结合自己写的,多加一个if判断,即if里面嵌套if
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up">
<ul>
<li><a href="manage/furn?action=page&pageId=1">首页</a></li>
<c:if test="${requestScope.page.pageId>1}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-1}">上页</a></li>
</c:if>
<c:set var="begin" value="1"/>
<c:set var="end" value="4"/><%--限制显示的页数最多是三页,也可以自己修改--%>
<c:forEach begin="${begin}" end="${end}" var="i">
<c:if test="${requestScope.page.pageId<=end}"><%--在三页之前是展示前三页的选择--%>
<%-- <li><a href="manage/furn?action=page&pageId=${i}">${i}</a></li>--%>
<c:if test="${i==requestScope.page.pageId}">
<li><a class="active" href="manage/furn?action=page&pageId=${i}">${i}</a></li>
</c:if>
<c:if test="${i!=requestScope.page.pageId}">
<li><a href="manage/furn?action=page&pageId=${i}">${i}</a></li>
</c:if>
</c:if><%--到第四页开始是展示后三页的选择,这里还是要列出来,找规律!!!动手!!!!一定要写出来才能找到规律!!!--%>
<c:if test="${requestScope.page.pageId>end}"><%--4--%><%--5--%>
<%-- <li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-end+i}">${requestScope.page.pageId-end+i}</a></li>--%>
<%--2,3,4--%><%--3,4,5--%>
<c:if test="${requestScope.page.pageId-end+i==requestScope.page.pageId}">
<li><a class="active" href="manage/furn?action=page&pageId=${requestScope.page.pageId-end+i}">${requestScope.page.pageId-end+i}</a></li>
</c:if>
<c:if test="${requestScope.page.pageId-end+i!=requestScope.page.pageId}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-end+i}">${requestScope.page.pageId-end+i}</a></li>
</c:if>
</c:if>
</c:forEach>
<c:if test="${requestScope.page.pageId<requestScope.page.pageTotalCount}">
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId+1}">下页</a></li>
</c:if>
<li><a href="manage/furn?action=page&pageId=${requestScope.page.pageTotalCount}">末页</a></li>
<li><a>共${requestScope.page.pageTotalCount}页</a></li>
<li><a>共${requestScope.page.totalRow}记录</a></li>
</ul>
</div>
继续优化,因为在首页查询根据用户输入的名字返回数据的数量不同,所以导航栏展示的页面应该也不同,所以我使用if来判断,如果返回的数据的总页数小于或等于3,那么就循环遍历产生对应的页数,如果返回的数据的总页数大于3,那么就规定导航栏的显示页数为三
<c:set var="begin" value="1"/>
<c:if test="${requestScope.page.pageTotalCount>3}">
<c:set var="end" value="3"/><%--限制显示的页数最多是三页,也可以自己修改--%>
</c:if>
<c:if test="${requestScope.page.pageTotalCount<=3}">
<c:set var="end" value="${requestScope.page.pageTotalCount}"/><%--限制显示的页数最多是三页,也可以自己修改--%>
</c:if>
但是如果用户查询到没有数据,那么导航栏应该不能出现首页和末页这两个选择,所以又增加个if条件判断
<c:if test="${requestScope.page.pageTotalCount!=0}">
<li><a href="consumer?action=page&pageId=1&name=${requestScope.page.name}">首页</a></li>
</c:if>
<c:if test="${requestScope.page.pageTotalCount!=0}">
<li><a href="consumer?action=page&pageId=${requestScope.page.pageTotalCount}&name=${requestScope.page.name}">末页</a></li>
</c:if>
最后的整体代码:
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up">
<ul>
<c:if test="${requestScope.page.pageTotalCount!=0}">
<li><a href="consumer?action=page&pageId=1&name=${requestScope.page.name}">首页</a></li>
</c:if>
<c:if test="${requestScope.page.pageId>1}">
<li><a href="consumer?action=page&pageId=${requestScope.page.pageId-1}&name=${requestScope.page.name}">上页</a></li>
</c:if>
<c:set var="begin" value="1"/>
<c:if test="${requestScope.page.pageTotalCount>3}">
<c:set var="end" value="3"/><%--限制显示的页数最多是三页,也可以自己修改--%>
</c:if>
<c:if test="${requestScope.page.pageTotalCount<=3}">
<c:set var="end" value="${requestScope.page.pageTotalCount}"/><%--限制显示的页数最多是三页,也可以自己修改--%>
</c:if>
<c:forEach begin="${begin}" end="${end}" var="i">
<c:if test="${requestScope.page.pageId<=end}"><%--在三页之前是展示前三页的选择--%>
<c:if test="${i==requestScope.page.pageId}">
<li><a class="active" href="consumer?action=page&pageId=${i}&name=${requestScope.page.name}">${i}</a></li>
</c:if>
<c:if test="${i!=requestScope.page.pageId}">
<li><a href="consumer?action=page&pageId=${i}&name=${requestScope.page.name}">${i}</a></li>
</c:if>
</c:if><%--到第四页开始是展示后三页的选择,这里还是要列出来,找规律!!!动手!!!!一定要写出来才能找到规律!!!--%>
<c:if test="${requestScope.page.pageId>end}"><%--4--%><%--5--%>
<%-- <li><a href="manage/furn?action=page&pageId=${requestScope.page.pageId-end+i}">${requestScope.page.pageId-end+i}</a></li>--%>
<%--2,3,4--%><%--3,4,5--%>
<c:if test="${requestScope.page.pageId-end+i==requestScope.page.pageId}">
<li><a class="active" href="consumer?action=page&pageId=${requestScope.page.pageId-end+i}&name=${requestScope.page.name}">${requestScope.page.pageId-end+i}</a></li>
</c:if>
<c:if test="${requestScope.page.pageId-end+i!=requestScope.page.pageId}">
<li><a href="consumer?action=page&pageId=${requestScope.page.pageId-end+i}&name=${requestScope.page.name}">${requestScope.page.pageId-end+i}</a></li>
</c:if>
</c:if>
</c:forEach>
<c:if test="${requestScope.page.pageId<requestScope.page.pageTotalCount}">
<li><a href="consumer?action=page&pageId=${requestScope.page.pageId+1}&name=${requestScope.page.name}">下页</a></li>
</c:if>
<c:if test="${requestScope.page.pageTotalCount!=0}">
<li><a href="consumer?action=page&pageId=${requestScope.page.pageTotalCount}&name=${requestScope.page.name}">末页</a></li>
</c:if>
<li><a>共${requestScope.page.pageTotalCount}页</a></li>
<li><a>共${requestScope.page.totalRow}记录</a></li>
</ul>
</div>
查询数据只有两页<3所以展示两个分页
查询数据有6页时,只显示三个分页
没有查到数据时显示这两个即可
注意细节:
1、当用户点到第一页时应该不能继续点上一页所以应该使用<c:if></c:if>来判断,在最后一页也是如此
2、如果是同一次请求转发,那么在静态页面可以使用 param.属性名 来获取从Servlet设置的对象Page,就不用通过requestScope.page.pageId来获取值,直接param.pageId,注意不要写错字段名
3、对于家具进行增删改查后, 仍然会回到当前页,这里就是多携带了一个参数,通过传递pageId参数(页面参数)到最后请求转发或重定向时使用page方法携带页面参数即可
4、多个参数一定要用&分隔!!!
十二、实现功能11:首页分页
需求如图:
1. 顾客进入首页页面
2. 分页显示家居
3. 正确显示分页导航条
思路分析:
这里用户看到的数据是写死的,现在目的是将这些数据从数据库中取出来并且完成分页展示,那么这里只需要增加一个Servlet即可,Service层,DAO层和数据库的表都不需要变动,在用户请求的Servlet调用page方法即可,并且请求转发到index.jsp页面并调用page方法,这里直接将原来index里重复的模块删除,使用foreach循环拿出page方法传来的数据即可,然后将导航栏从原来的地方复制过来,更改调用方法的Servlet地址即可
注意细节:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<jsp:forward page="/consumer?action=page"></jsp:forward>
在启动Tomcat时默认会访问index.jsp文件,这时就需要调用forward方法请求转发到consumerServlet
十三、实现功能12:首页搜索
需求分析/ 图解:
1. 顾客进入首页页面
2. 点击搜索按钮,可以输入家居名
3. 正确显示分页导航条,并且要求在分页时,保留上次搜索条件
思路分析:
因为items和pageTotalCount需要从数据库中获取,所以在DAO层增加这两个方法,Service层将这两个方法还有其他的字段例如通过前端传入的或自己设置的字段进行封装为pageByName方法,传入的参数是一个名字返回的是一个page对象,那么最后的难题其实在于分页时如何得到搜素名字的数据呢?
因为导航栏的调用方法是page方法,他会将所有的家居信息返回,点击某一页时返回的是所有家具的分页,但是用户需要的是查询出来的分页,那么怎么解决这个问题?这里我又不能修改调用的方法,这样当重新进入主页面时又会因为没有输入名字而查询不到结果,那么修改不了请求地址只能增加一些属性,我觉得可以增加一个name属性,name的属性值是page对象传到前端的,通过request请求域对象传过去,那么Page这个javaBean可以增加一个属性name,用来判断用户是通过什么查询数据的,如果name的值为空那么就是在主页面上随意点击查看,如果name不为空,那么就是搜索查询信息,那么返回的就是查询出来的数据,分页也是调用pageByName这个方法,在前端获取pageId,pageSize,这里会调用Service层的pageByName方法接收三个参数pageId,pageSize,name,返回一个page对象,在DAO层就是增加两个方法。
实现步骤:
1、在Page里增加一个属性name,用于将接收到的家居名字通过Page对象传给前端,主要是用于区分到底是调用page方法还是调用pageByName方法,这里老师是将url用起来传递给那个index的导航栏的每个分页,整体思路是一样的
2、在DAO层增加getItemsByName方法,传pageId,pageSize和name,返回list集合,还有getTotalRowsByName方法传递一个name,返回查询数据的数量
3、测试两个方法
4、在Service层调用DAO层的两个方法,根据page对象的属性,将属性一一封装,即传递三个参数返回一个page对象
5、测试Service层的方法
6、在consumerServlet增加一个pageByName方法,通过index.jsp传来的name,pageId,pageSize调用Service层的pageByName方法
7、测试前端传来的数据是否能够成功调用pageByName方法并输出到控制台
8、在前端接收后端发来的数据并显示
9、根据点击分页链接增加一个name属性用于保存后端传来的name值
10、在page方法中判断name是否为空,如果不是空,就重定向到pageByName方法
注意细节:
1、重定向和请求转发一样,请求转发之后和重定向之后会继续执行下面的内容 若需调用后终止代码,务必显式添加 return 语句 。
2、这里必须要用重定向因为请求转发后搜索框一直保持使用状态,那么请求转发需要注意URL地址的参数值如果是中文的话需要使用URLEncoding方法,这里需要区分一下res.setcontenttype那个是解析响应内容HTML或json如果含有中文的话需要这个方法来规定编码和解码方式
String encodedName = URLEncoder.encode(name, "UTF-8");
3、用户搜索是模糊查询
select COUNT(*) from furn where name like ?
4、long包装类不能直接转成integer因为没有继承关系
((Number)queryScalar(sql,strName)).intValue();
补充:这里有一个奇怪的现象,当点击家居管理时会发出一个访问首页面的请求,这很不合理,因为用户请求的是http://localhost:8080/jiaju/manage/furn?action=page&pageId=1 为啥会请求到主页面?主页面是通过/consumer?action=page请求转发的,所以会调用/consumerServlet,这里就会调用两个Servlet,最后找到原因是请求的图片地址是#,这个#会默认访问当前页面的地址,当前页面用了base标签,所以就是base标签的地址
十四、实现功能13:显示登录名
需求如图:
1. 顾客登录成功
2. login_ok.jsp 显示欢迎信息
3. 返回首页,显示登录相关菜单
思路分析:
当用户点击登入选项会跳转到登入页面,登入页面输入的信息回通过Service层的验证,主要是通过姓名和密码是否能查出用户,登入成功就会携带一个session对象,session对象使用setAttribute存储用户信息,将这个session对象在index.jsp接收
注意事项:
使用request传递参数时,要么就在请求中全程使用req.setAttribute然后在前端页面使用${requestScope.user}接收,要么你不用全程使用request.setAttribute只要是同一个request请求,就直接使用${param.user}接收
十五、实现功能14:注销登录
需求如图:
1. 顾客登录成功后
2. login_ok.jsp 点击安全退出,注销登录
3. 返回首页,也可点击安全退出, 注销登录
思路分析:
当用户点击安全退出时会向Servlet发送一个请求请求中会调用方法,在Servlet中该方法会销毁session,从而安全退出
十六、实现功能15:注册验证码
需求分析:
表单重复提交情况:
1. 提交完表单。服务器使用请求转发进行页面跳转。用户刷新(F5),会发起最后一
次的请求, 造成表单重复提交问题。解决:用重定向
2. 用户正常提交,由于网络延迟等原因,未收到服务器的响应,这时,用户着急多
点了几次提交操作,也会造成表单重复提交。解决: 验证码
3. 用户正常提交,服务器也没有延迟,但是提交完成后,用户回退浏览器。重新提
交。也会造成表单重复提交,解决:验证码
这里直接调用Google的api,在web.xml文件中配置好基本信息即可,在前端可以先验证用户填的验证码是否为空,避免用户不填验证码而重复提交表单信息,在memberServlet中在register方法中使用String attribute =(String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY); 获取session中属性(KAPTCHA_SESSION_KEY)的验证码,并且和String code = req.getParameter("code");用户提交的验证码进行比较,如果相同则进行下面的注册验证,失败则返回到注册页面,并且可以将注册信息返回
下载地址:https://download.csdn.net/download/2401_83418369/90473682
注意事项:
1、验证码使用之后立即销毁,在后端接收到验证码后立即销毁
String attribute =(String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY);
req.getSession().removeAttribute(KAPTCHA_SESSION_KEY);
2、进行验证码判断时先判断验证码是否为空 ,如果用户重复提交表单,第二次校验时attribute
已为null,
直接调用attribute.equalsIgnoreCase(code)
会抛出空指针异常,导致程序崩溃。
if (attribute!=null&&attribute.equalsIgnoreCase(code)) {
为什么验证码不能重复使用:
1、 若验证码可重复使用,攻击者可利用同一验证码多次尝试登录或注册,绕过验证机制直接暴力破解账户密码。例如,帝国CMS曾因未清除Session中的验证码,导致攻击者通过重放数据包实现无限次登录尝试
2、重复使用验证码可能被用于批量注册虚假账户或自动刷帖。例如,某些论坛系统未及时刷新验证码时,攻击者可利用同一验证码注册大量垃圾账号
3、 攻击者截获验证码后重复提交,模拟合法用户操作。短信验证码若未设置时效性或一次性机制,可能被用于盗取账户或支付劫持
十七、实现功能 16: 添加家居到购物车
需求分析:
1. 会员登录后,可以添加家居到购物车
2. 完成购物车的设计和实现
3. 每添加一个家居, 购物车的数量+1,并显示
思路分析:
这里有两种实现方式:
1、通过session存储数据模型,这里有两个数据模型,一个是将数据存储在一个javaBean对象中(一件商品的信息),而每一个javaBean都在购物车模型里面(购物车)
这里需要理解的是Furn对象必须是同一个,增删改查还是原来Furn存储在数据库中的数据,只是进行增删改查后存储的位置不同而已,但是还是要根据id将Furn对象找出来才能进行增加的操作,至于改查删可以直接在购物对象里进行操作
首先当用户点击add选项时,前端会动态绑定事件,将商品对应的ID发给后端cartservlet并调用additem方法,在方法中首先会通过获取ID找到对应的Furn对象,在根据Furn对象进行添加数据到cartItem
注意细节:
1、当用户首次点击添加商品时,session的属性cart并没有创建,这里需要判断cart属性值是否为空
2、当第一次将cartItem对象添加到购物车对象cart时,首先要判断cart对象是否为空,如果为空首先应创建一个cartItem,在count属性中设置为1,商品总价为销售价格,当cartItem不为空时这时其实是同一个商品对象,直接将count数量加一并且商品总价为售价加一即可,因为这里默认点击一次添加一次商品
如图所示:
没有session属性值(这里属性值是一个购物车对象cart)就先创建一个空购物车对象,没有商品对象就先创建一个商品对象,当第二次买同一个商品时就不需要再创建同一个对象只需要在同一个对象中修改count属性和总价属性值即可
3、如果想回先到原来的页数直接使用request获取refer即跳转的位置就是页数的地址
十八、实现功能17:显示购物商品的总数量
需求如图:
每添加一个商品就在购物车那显示数量+1
思路分析:
通过遍历的方式将每个cartItem对象的count属性值进行相加,并将这个属性值显示在前端页面即可
注意细节:
1、这里的${sessionScope.cart.totalCount}其实调用的是cart对象里面的getTotalCount 方法
2、totalCount属性一定要在调用时清零,不然会重复相加之前的数据!!!,相当于要重新查询总共的数量
十九、实现功能18:显示购物车
需求如图:
1. 查看购物车,可以显示如下信息
2. 选中了哪些家居,名称,数量,金额
3. 统计购物车共多少商品,总价多少
思路分析:
因为cart购物车对象存储在session的属性cart里面,所以不用与web交互,直接在session里面取出cart对象然后通过遍历对象就可以获取cartItem对象
注意细节:
1、从session中获取的cart属性值是cart对象,还需要获取cartItemMap,所以需要实现cartItemMap的get方法,所以在遍历集合中使用Item.value才能获取cartItem对象,然后才能再得到属性值
<c:forEach items="${sessionScope.cart.cartItemMap}" var="item">
${item.value.name}
2、在获取cartItem里的属性值之前先要判断是否存在这个集合
<c:if test="${not empty sessionScope.cart.cartItemMap}">
二十、实现功能19:修改购物车
需求分析:
1. 进入购物车,可以修改购买数量
2. 更新该商品项的金额
3. 更新购物车商品数量和总金额
思路分析:
用户点击修改商品数量时,就会调用cartServlet里面的修改方法,然后得到要修改商品的id然后根据所给的id找到对应的cartItem对象,再进行更改商品数量调用修改方法
二十一、实现功能20:删除购物车里的商品
需求如图:
1. 进入购物车,可以删除某商品
2. 可以清空购物车
3. 要求给出适当的确认信息
思路分析:
用户点击删除按钮,先从前端进行校验,给出提示是否真正删除数据,然后将商品的id传给后端,根据id从hashmap中找到并删除即可,如果要清空购物车的数据,就可以将商品数据从hashmap中全部删除。最后根据跳转页面进行返回
二十二、实现功能 21:生成订单
需求如图:
1. 进入购物车,点击购物车结账
2. 生成订单和订单项
3. 如会员没有登录,先进入登录页面,完成登录后再结账
思路分析:
首先用户点击生成订单选项时就会先后端发出请求,得到的结果是否已经成功登入,如果没有将跳转登入页面,如果登入了,就可以进入订单页面,得到的是订单已经生成的页面,然后当用户点击详情就可以得到一个orderItem对象集合
这两个表就是将session中的cartItem字段信息存储位置转移到MySQL中,这里还要考虑不同用户的订单不同,应该在查询时根据不同用户的id来返回数据
这里的增删改查不能混弄,一条一条分清楚,只有在连表查询时才在Service层根据业务结合使用
思路分析一定是根据需求走的,在需求中设计如何实现增删改查四条路线,这四条路是独立的,只是根据不同的业务需求在调用时可能会交叉调用一般是在Service层交叉调用DAO层的api
实现步骤:
1、新建两个数据表order和orderItem字段信息根据需求可得
2、新建两个javaBean对象和数据表的字段一一对应
3、在orderItemDAO创建save方法并实现传一个hashmap不用返回信息,在orderDAO创建order对象,根据传入的order对象不返回信息,这里应该还有两个查询方法,在orderItemDAO中根据order_id返回对应的orderItem集合,在orderDAO中根据order_id查询对应的order对象,这里的order_id是order表的订单号,orderItem的订单号,改为id也行
4、测试两个方法
5、在Service层,根据用户id返回订单对象,根据用户id查询出order_id再根据order_id查询返回一个orderItem集合
6、测试两个查询方法
7、在orderservlet先得到userid即用户id,如果没有登入用户就提示先登入用户,点击订单管理就会根据用户id查询得到一个order集合,点击详情,先查询用户id对应的订单号再根据订单号查询出一个orderItem集合
注意细节:
1、如果从后端传送的数据过多或复杂应该采用数据模块的方式传输数据到前端
二十三、实现功能22:过滤器权限验证
需求如图:
1. 当前系统 , 用户/客 顾客 在没有登录 , 是可以添加商品到购物车的 , 还可以查看购物
车 车, 我们使用过滤器加入权限验证,要求如下:
2. 用户在进入后台管理,添加购物车前,需要先登录
3. 如果没有登录,则进入会员登录页面
思路分析:
当用户点击某些资源时需要进行过滤验证,验证用户是否已经登录,如果没有登录就请求转发到登录页面,如果已登录就继续请求后面的资源,这里主要是设置需要过滤的URL,还有一些排除的URL(即不需要验证的URL资源)
实现步骤:
编写一个类实现filter接口
package com.study.furns.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class AuthFilter implements Filter {
private List<String> list;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//这里获取排除验证的URL
String excluededUrl = filterConfig.getInitParameter("excluededUrl");
String[] arrays = excluededUrl.split(",");
list = Arrays.asList(arrays);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
//这里根据请求的URL进行判断是否需要进行验证
if (!list.contains(httpServletRequest.getServletPath())) {
HttpSession session = httpServletRequest.getSession();
Object member = session.getAttribute("member");
if (member == null) {
httpServletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
@Override
public void destroy() {
System.out.println("过滤器被销毁。。。");
}
}
在web.xml文件配置filter信息
<filter>
<filter-name>authfilter</filter-name>
<filter-class>com.study.furns.filter.AuthFilter</filter-class>
<init-param>
<param-name>excluededUrl</param-name>
<param-value>/views/manager/manage_login.jsp,/views/manager/manage_menu.html</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>authfilter</filter-name>
<url-pattern>/views/cart/*</url-pattern>
<url-pattern>/views/manager/*</url-pattern>
<url-pattern>/views/order/*</url-pattern>
<url-pattern>/cart</url-pattern>
<url-pattern>/manage/furn</url-pattern>
<url-pattern>/manager</url-pattern>
<url-pattern>/order</url-pattern>
</filter-mapping>
注意细节:
1、配置filter文件要写在web文件的最上面,因为filter也可以当作是一个Servlet,如果有多个filter配置文件优先从文件上面访问
2、<init-param>配置文件的信息只是不同的初始化信息,具体排除不需要过滤的URL需要自己实现业务代码
3、不满足过滤条件的应进行请求转发,因为请求转发是在服务器内跳转的不需要再次进行过滤(直接请求转发到指定的资源,就是打回到指定的地方),不能使用重定向,因为重定向是从浏览器发出请求到服务器,如果重定向的访问满足过滤条件就会再次被捕获进行过滤条件判断,又因为不满足所以再次打回,这样就会造成循环重定向的操作
二十四、实现功能23:事务管理
下订单问题思考
1.将 FurnDAOImpl.java 的 updateFurn 的 SQL 故意写错
2.在 OrderServiceImpl 的 savaOrder() 捕获一下异常, 目的是保证程序继续执行
3.看看会有什么结果=》数据不一致.
思路分析
1. 用 使用 Filter + ThreadLocal 进行事务管理
2. 说明: 在一次 http 请求, ,servlet-service-dao 的调用过程 , 始终是一个线程,这是使用 ThreadLocal 的前提
实现步骤:
1、修改工具类JDBCUtilsByDruid.java将获取到的connection对象存入ThreadLocal。
ThreadLocal<Connection>为每个线程存储独立的Connection副本。首次调用getConnection()时,当前线程的ThreadLocal中没有连接(get()返回null),此时从连接池(ds)获取新连接,存入ThreadLocal。后续调用时直接返回已存储的连接,保证同一线程使用同一连接。
private static ThreadLocal<Connection> threadLocalConnection =new ThreadLocal<>();
/**
* 这里将connection放入threadLocal里面,保证进行操作数据库时从连接池取出的是同一个connection对象
* @return
* threadLocalConnection.get()并非直接从连接池获取连接,
* 而是通过ThreadLocal维护线程专属的连接。首次获取时从池中借出,后续复用同一连接
*/
public static Connection getConnection() {
Connection connection = threadLocalConnection.get();
if (connection==null){
try {
connection = ds.getConnection();
//设置手动提交事务才能保证不是自动提交,如果自动提交就不能进行回滚
connection.setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
threadLocalConnection.set(connection);
}
return connection;
}
2、在工具类JDBCUtilsByDruid.java中增加两个方法:
public static void commit(){
Connection connection = threadLocalConnection.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);
}
}
}
threadLocalConnection.remove();
}
/**
* 这里是保证事务处理失败时进行回滚操作
*/
public static void rollback(){
Connection connection = threadLocalConnection.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);
}
}
}
threadLocalConnection.remove();
}
3、在BasicDAO中修改代码,因为关闭连接的操作在commit和rollback方法中实现了,所以BasicDAO中进行增删改查后不需要关闭连接,将finally的代码删除
4、创建一个Filter过滤器对象用于管理所有操作MySQL的事务
package com.study.furns.filter;
import com.study.furns.utils.JDBCUtilsByDruid;
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);
JDBCUtilsByDruid.commit();
} catch (Exception e) {
JDBCUtilsByDruid.rollback();
throw new RuntimeException(e);
}
}
@Override
public void destroy() {
}
}
配置Filter文件
<filter>
<filter-name>TransactionFilter</filter-name>
<filter-class>com.study.furns.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
5、注意将BasicServlet中的异常处理往上抛,如果提前捕获异常并做出了处理,那么在过滤器中将不能捕获异常并回滚
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String action = req.getParameter("action");
try {
Method declaredMethod = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
declaredMethod.invoke(this,req,resp);
} catch (Exception e) {
throw new RuntimeException(e);//重点
}
}
注意细节:
1、一定要设置手动提交不然操作数据库是自动提交不能再回滚
2、一定要关闭connection连接和threadLocal连接避免造成资源浪费,如果连接会一直被占用,会导致连接池耗尽
3、一定要检查异常捕获是否往上抛出,避免在过滤器中捕获不到异常而不能回滚
4、ThreadLocal<Connection>
为每个线程存储独立的Connection
副本。首次调用getConnection()
时,当前线程的ThreadLocal
中没有连接(get()
返回null
),此时从连接池(ds
)获取新连接,存入ThreadLocal
。后续调用时直接返回已存储的连接,保证同一线程使用同一连接。
二十五、实现功能24:统一错误提示页面
需求如图
1. 如果在访问/示 操作网站,出现了内部错误,统一显示 500.jsp
2. 如果访问/ 操作不存在的页面/servlet 示 时,统一显示 404.jsp
思路分析:
1. 发生错误/ 异常时,将错误/ 异常 抛给 tomcat
2. 在 web.xml 配置不同错误显示页面即可
实现步骤:
1、配置web.xml文件
<error-page>
<error-code>404</error-code>
<location>/views/error/404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/views/error/500.jsp</location>
</error-page>
2、在过滤器中处理异常是抛出给Tomcat
@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();
throw new RuntimeException(e);
}
}
二十六、实现功能25:使用ajax 提示注册会员名是否存
需求如图
1. 注册会员时,如果名字已经注册过,当光标离开输入框,提示会员名已经存在,
否则提示不存在
2. 要求使用 ajax 完成
思路分析:
在后端根据Ajax请求发送的name参数就行获取,再调用已写好的根据验证名字返回布尔值的方法进行验证,创建一个hashmap,将返回的结果存放到集合中,再将集合转成JSON数据,然后返回即可,因为处理阶段在前端,所以在后端只需返回结果无需处理,前端通过接收JSON数据并解析,再根据布尔值来处理
在memberServlet中添加验证方法:
protected void check(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("name");
try {
boolean exist = memberService.isExist(username);
//创建一个hashmap集合用于存放数据
Map<String, Object> hashMap = new HashMap<>();
hashMap.put("exist",exist);
Gson gson = new Gson();
//将hashmap转成JSON格式的字符串
String json = gson.toJson(hashMap);
//如果用户存在那么就将该用户以JSON的形式发送到前端
resp.getWriter().write(json);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
在登录页面监听事件并发出Ajax请求
// 这里编写一个Ajax请求,当用户注册会员信息时,
// 光标离开输入框时会根据是否已存在该用户来返回用户是否已存在
var username = document.getElementById("username");
var error = document.getElementById("errorMsg");
username.onblur=function (){
var name =username.value;
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("GET","member?action=check&name="+name,true);
xmlHttpRequest.onreadystatechange=function (){
if (xmlHttpRequest.readyState==4&&xmlHttpRequest.status==200){
//使用JSON内置函数解析后端发来的JSON数据
var parse = JSON.parse(xmlHttpRequest.response);
//通过取出键:exist的值:布尔类型,来返回数据到浏览器
if (parse.exist){
error.innerText="会员名已存在,直接登录即可"
}else {
error.innerText="会员名不存在,可以注册"
}
}
}
xmlHttpRequest.send();
}
二十七、实现功能26:使用 ajax 添加购物车
需求如图
使用 Ajax 请求, 进行优化 , 刷新购物车数量即可
思路分析:
修改addItem方法
protected void addItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id = req.getParameter("id");
int i = JavaBeanUtils.parseInt(id, 0);
Furn furn = furnServiceInter.find(i);
if (furn!=null) {
HttpSession session = req.getSession();
Cart attribute = (Cart) session.getAttribute("cart");
if (attribute == null) {
attribute = new Cart();
req.getSession().setAttribute("cart", attribute);
}
attribute.addItem(furn);
//返回数据是一个商品数量
int totalCount = attribute.getTotalCount();
Map<String, Object> hashMap = new HashMap<>();
hashMap.put("totalCount",totalCount);
String json = new Gson().toJson(hashMap);
resp.getWriter().write(json);
}
}
在前端进行Ajax请求
<script type="text/javascript">
window.onload=function (){
var xmlHttpRequest = new XMLHttpRequest();
var element = document.getElementsByClassName("add-to-cart");
var cartCount= document.getElementById("cartCount");
for (let i = 0; i < element.length; i++) {
element[i].onclick=function (){
if (element[i].id==0){
return false;
}
// window.location.href="cart?action=addItem&id="+element[i].id;
xmlHttpRequest.open("get","cart?action=addItem&id="+element[i].id)
xmlHttpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xmlHttpRequest.onreadystatechange=function (){
if (xmlHttpRequest.readyState==4&&xmlHttpRequest.status==200){
var parse = JSON.parse(xmlHttpRequest.response);
if (parse["url"]==undefined){
//这里表示已经登录过,不用返回URL
cartCount.innerText=parse["totalCount"]
}else {
location.href=parse["url"]
}
}
}
xmlHttpRequest.send()
}
}
}
</script>
根据URL的情况进行分析,如果URL没有定义那么说明用户已成功登录,parse["url"]==undefined直接返回总数量,如果接收到URL就进行请求转发
过滤器的代码:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
//这里根据请求的URL进行判断是否需要进行验证
if (!list.contains(httpServletRequest.getServletPath())) {
HttpSession session = httpServletRequest.getSession();
Object member = session.getAttribute("member");
if (member == null) {
if (!WebUtils.isAjaxRequest(httpServletRequest)){
httpServletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);
}else {
Map<String, Object> hashMap = new HashMap<>();
hashMap.put("url","views/member/login.jsp");
String json = new Gson().toJson(hashMap);
httpServletResponse.getWriter().write(json);
}
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
注意细节
1、Ajax 请求本质是 JavaScript 通过 XMLHttpRequest
对象与服务器通信,期望接收纯数据(如 JSON)而非完整的页面内容。若过滤器尝试通过 request.getRequestDispatcher().forward()
转发到登录页或其他页面,返回的 HTML 内容会被 Ajax 视为“数据”,无法触发浏览器自动跳转或页面刷新
Ajax 请求的响应数据由 JavaScript 代码处理,而非浏览器直接渲染。当过滤器返回 HTML 页面时,Ajax 客户端(如 XMLHttpRequest
或 fetch
)会将其视为普通字符串或 JSON 解析失败的异常数据,不会触发浏览器的自动页面跳转
2、初始化请求(必须调用 open()
后设置请求头) 必须在调用 open()
方法之后、send()
方法之前设置请求头,否则会抛出错误
3、设置请求头 使用 setRequestHeader(header, value)
方法添加自定义或标准请求头
二十八、实现功能27:上传/更新家居图片
需求如图
1. 后台修改家居,可以点击图片,选择新的图片
2. 这里的会使用到文件上传功能
前端:
function prev(event) {
//获取展示图片的区域
var img = document.getElementById("prevView");
//获取文件对象
var file = event.files[0];
//获取文件阅读器: Js的一个类,直接使用即可
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
//给img的src设置图片url
img.setAttribute("src", this.result);
}
}
<style type="text/css">
#pic{
position: relative;
}
input[type="file"] {
position: absolute;
left: 0;
top: 0;
height: 200px;
opacity: 0;
cursor: pointer;
}
</style>
<form action="manage/furn?action=update&id=${requestScope.furn.id}&pageId=${requestScope.pageId}" method="post" enctype="multipart/form-data">
<td class="product-thumbnail">
<div id="pic">
<img id="prevView" class="img-responsive ml-3" src="${requestScope.furn.img_path}"
alt=""/>
<input type="file" name="img_path" id="" value="${requestScope.furn.img_path}" onchange="prev(this)"/>
</div>
</td>
后端:FurnServlet修改update方法
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int id = JavaBeanUtils.parseInt(req.getParameter("id"), 0);
Furn furn = furnServiceInter.find(id);
if (ServletFileUpload.isMultipartContent(req)) {
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
servletFileUpload.setHeaderEncoding("utf-8");
try {
List<FileItem> list = servletFileUpload.parseRequest(req);
for (FileItem fileItem : list) {
if (fileItem.isFormField()) {//普通表单
if ("name".equals(fileItem.getFieldName())){
furn.setName(fileItem.getString("utf-8"));
} else if ("maker".equals(fileItem.getFieldName())) {
furn.setMaker(fileItem.getString("utf-8"));
} else if ("price".equals(fileItem.getFieldName())) {
furn.setPrice(new Double(fileItem.getString()));
} else if("stock".equals(fileItem.getFieldName())) {
furn.setStock(new Integer(fileItem.getString()));
}else if ("sales".equals(fileItem.getFieldName())){
furn.setSales(new Integer(fileItem.getString()));
}
} else {//是一个文件
//获取上传的文件的名字
String name = fileItem.getName();
//如果用户没有选择新的图片, name = ""
if (!"".equals(name)) {
//1.指定一个目录 , 就是我们网站工作目录下
String filePath = "/" + WebUtils.FURN_IMG_PATH;
//2. 获取到完整目录 [io/servlet基础]
String fileRealPath =
req.getServletContext().getRealPath(filePath+"/"+ TimeUtility.get());
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();//关闭流
//删除旧的图片
String imgPath = furn.getImg_path();
//必须要获取绝对物理路径才能找到正确的位置
//因为从MySQL中取出的数据是相对路径所以找不到图片的位置
String realPath = req.getServletContext().getRealPath(imgPath);
File file = new File(realPath);
if (file.exists()){
file.delete();
}
//更新家居的图片路径
furn.setImg_path(WebUtils.FURN_IMG_PATH+"/"+TimeUtility.get()+"/"+name);
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
furnServiceInter.update(furn);
req.getRequestDispatcher("/views/manager/update_ok.jsp").forward(req,resp);
}
获取时间的工具类
package com.study.furns.utils;
import java.time.LocalDateTime;
public class TimeUtility {
public static String get(){
LocalDateTime now = LocalDateTime.now();
int year = now.getYear();
int monthValue = now.getMonthValue();
int dayOfMonth = now.getDayOfMonth();
String total=year+"/"+monthValue+"/"+dayOfMonth;
return total;
}
}
整个项目还有很多细节优化。。。。 (花费2周)
补充:什么是MVC架构 ?
MVC 全称∶ Model 模型、View 视图、 Controller 控制器。
MVC 最早出现在 JavaEE 三层中的 Web 层,它可以有效的指导 WEB 层的代码如何有效
分离,单独工作。
View 视图∶只负责数据和界面的显示,不接受任何与显示数据无关的代码,便于程序员和美工的分工合(Vue/JSP/Thymeleaf/HTML)
Controller 控制器∶只负责接收请求,调用业务层的代码处理请求,然后派发页面,是一个"调度者"的角色(Servlet), 这个概念会贯穿 javaee
Model 模型∶将与业务逻辑相关的数据封装为具体的 JavaBean 类,其中不掺杂任何与数据处理相关的代码(JavaBean/Domain/Pojo)