尚硅谷JavaWeb笔记——书城项目(第五阶段:图书模块(课程精华!!!!))

第五阶段-图书模块

MVC说明

MVC全称:Model模型、View试图、Controller控制器

MVC最早出现在JavaEE三层中的Web层,它可以有效的指导Web层的代码如何有效分离,单独工作。

  • View试图:只负责数据和页面的显示,不接受任何与现实无关的代码,便于程序员和美工分工合作
  • Controller控制器:只负责接受请求,调用业务层的代码处理请求,然后派发页面,是“调度者”的角色——Servlet程序
  • Model模型:将与业务逻辑相关的数据封装为具体的JavaBean对象,其中不掺杂任何与数据处理相关的代码——JavaBean/domain/entity/pojo

⭐️MVC是一种思想:理念是将软件代码拆分成为组件,单独开发,组合使用(解耦合)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBnTv5Es-1614146871617)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210216141124286.png)]

开发流程

Step1:编写图书模块的数据库表

首先需要根据需求创建对应的sql表以及插入数据

create table t_book(
                       `id` int primary key auto_increment,    # 注意这里是` 而不是 '
                       `name` varchar(100),
                       `price` decimal(11,2),
                       `author` varchar(100),
                       `sales` int,
                       `stock` int,
                       `img_path` varchar(200)
);

# 修改当前表的字符集
alter table t_book convert to charset utf8;

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,'水浒传','华仔',33.05,22,88,'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,'操作系统原理','刘优',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,'Unitx高级环境编程','乐天',99.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;

于是数据库中存入如下信息

Step2:编写图书模块的JavaBean对象

根据对应的book对象

public class Book {
    private Integer id;
    private String name;
    private String author;
    private BigDecimal price;
    private Integer sales;
    private Integer stock;
    private String imgPath = "static/img/default.jpg";

    public Book() {
    }

    public Book(Integer id, String name, String author, BigDecimal price, Integer sales, Integer stock, String imgPath) {
        this.id = id;
        this.name = name;
        this.author = author;
        this.price = price;
        this.sales = sales;
        this.stock = stock;
        if (imgPath != null && "".equals(imgPath)) {
            this.imgPath = imgPath;
        }
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getSales() {
        return sales;
    }

    public void setSales(Integer sales) {
        this.sales = sales;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public String getImgPath() {
        return imgPath;
    }

    public void setImgPath(String imgPath) {
        if (imgPath != null && "".equals(imgPath)) {
            this.imgPath = imgPath;
        }
    }

    @Override
    public String toString() {
        return "book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", sales=" + sales +
                ", stock=" + stock +
                ", imgPath='" + imgPath + '\'' +
                '}';
    }
}

Step3:编写图书模块Dao和测试Dao

根据业务需求设置对应的Dao接口(规定有哪些操作需要执行)

无非就是增删改+各种查询

public interface BookDao {
    // 设置增删改查的方法
    public int addBook(Book book);
		// 删除
    public int deleteBookById(Integer id);
		// 修改
    public int updateBook(Book book);
		
  	// 各种花式查询	
  	// 通过id查书
    public Book queryBookById(Integer id);
		// 查询所有书
    public List<Book> queryBooks();
}

对应的Dao实现类代码如下:

public class BookDaoImpl extends BaseDao implements BookDao {
  @Override
  // 添加一本书
  public int addBook(Book book) {
    // id自增,这里不需要添加
    String sql = "insert into t_book(`name` , `author` , `price` , `sales` , `stock` , `img_path`) values(?,?,?,?,?,?)";
    int i = update(sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
    return i;
  }

  // 通过ID删除书
  @Override
  public int deleteBookById(Integer id) {
    String sql = "DELETE FROM t_book WHERE id = ?";
    int i = update(sql, id);
    System.out.println("已经删除 " + i + " 本书····");
    return i;

  }
	// 修改某本书
  @Override
  public int updateBook(Book book) {
    System.out.println("BookDaoImpl updateBook 程序在[" +Thread.currentThread().getName() + "]中");

    String sql = "UPDATE t_book SET `name`=? , `author`=? , `price`=? , `sales`=? , `stock`=? , `img_path` = ? WHERE id = ?";
    int i = update(sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath(), book.getId());
    return i;
  }

  // 通过ID查询书
  @Override
  public Book queryBookById(Integer id) {
    // 这里由于需要返回查询的对象,需要使用别名来查询,不然无法查询到结果
    String sql = "SELECT `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath FROM t_book where id = ?";
    Book book = queryForOne(Book.class, sql, id);
    return book;
  }
  
	// 查询所有书
  @Override
  public List<Book> queryBooks() {
    String sql = "SELECT `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath FROM t_book";
    List<Book> books = queryForList(Book.class, sql);
    return books;
  }
出现的问题:数据无法插入

趁着记忆鲜活,记录一下刚刚出现的问题:数据无法在idea中通过对应java语句插入,经过debug后也没有发现问题出在哪,进入JDBC.Utils工具类中发现获取连接的代码如下:

public static Connection getConnection3Druid() throws SQLException{

		Connection conn = conns.get();

		if (conn == null) {
			conn = source1.getConnection();//从数据库连接池中获取连接
			conns.set(conn); 保存到ThreadLocal对象中,供后面的jdbc操作使用 相对最后一次保存有效!
			conn.setAutoCommit(false);// 设置为手动管理事务
		}

		return conn;
	}

同时又发现虽然无法读取到数据,但通过mysql控制台插入的数据id不按照正确的顺序增加,也就是说先前在java中确实成功进入了数据库,并执行了对应的增加操作,但由于某些原因数据没能正确的插入进去。

同时又在网上看到,当设置手动提交数据后,需要记得commit提交数据,同时结合上述代码成功发现问题所在:

应当将conn.setAutoCommit(false);语句注释调,让其自动提交才能实现真正将数据插入进去。

同时修改properties中的配置信息,使得访问数据库时采用UTF-8的格式来编码数据。

Step4:编写图书模块的Service和测试Service

书模块Service层的接口规范

public interface BookService {
  	// 添加一本书
    public void addBook(Book book);
		// 删除一本书
    public void deleteBookById(Integer id);
		// 更新书的信息
    public void updateBook(Book book);
		// 通过ID查书
    public Book queryBookById(Integer id);
		// 查询所有书
    public List<Book> queryBooks();
}

实现类:

public class BookServiceimpl implements BookService {
    BookDao bookDao = new BookDaoImpl();
  // 添加一本书
    @Override
    public void addBook(Book book) {
        bookDao.addBook(book);
    }
	// 通过ID删除一本书
    @Override
    public void deleteBookById(Integer id) {
        bookDao.deleteBookById(id);
    }
	// 更新书的信息
    @Override
    public void updateBook(Book book) {
        bookDao.updateBook(book);
    }
	// 通过ID查询一本书
    @Override
    public Book queryBookById(Integer id) {
        return bookDao.queryBookById(id);
    }
	// 查询所有书
    @Override
    public List<Book> queryBooks() {
        return bookDao.queryBooks();
    }
}

Step5:编写图书模块的Web层,和页面联调测试

web层的请求响应流程如下:在web层中,用户请求的数据内容并不能直接在jsp中获取,而是需要先访问对应的Servlet程序,Servlet从数据库中得到数据后,将数据保存在request域(同次请求有效),将获取到的数据联通请求一起请求响应给对应jsp页面,jsp页面再从request域中获取Servlet查询到的数据,将数据动态响应至页面上。

整个过程的流程示意图以及对应模块所做的具体内容如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZfIhNQwq-1614146871620)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210216164904744.png)]

先前讲过每一个模块都可以继承BaseServlet完成请求的分发响应,BookServlet也不例外。在BookServlet中同样只需要定义对应请求的操作即可,代码如下:

public class BookServlet extends BaseServlet {
	// 调用Service层的对象实例
  private BookService bookService = new BookServiceimpl();
  // 增图书操作
  protected void add(HttpServletRequest request, HttpServletResponse response){}
  // 删图书操作
  protected void delete(HttpServletRequest request, HttpServletResponse response) {}
  // 改图书操作
  protected void update(HttpServletRequest request, HttpServletResponse response) {}
  // 查询一本图书操作
  protected void getBook(HttpServletRequest request, HttpServletResponse response) {}
  // 查询所有图书操作
  protected void list(HttpServletRequest request, HttpServletResponse response)}
}
功能一:列出当前数据库中的全部图书信息
protected void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //	1、通过BookService查询全部图书
    List<Book> books = bookService.queryBooks();
    //	2、把全部图书保存到Request域中
    request.setAttribute("books", books);
    //	3、请求转发到/pages/manager/book_manager.jsp页面
    request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request,response);
  }

说明:由于我们最先呈现出的是有所有图书信息的,故最先需要实现的功能是list功能,然后让所有其他功能嵌入其中。

功能二:添加图书
protected void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //	1、获取请求的参数==封装成为Book对象
    Book book = WebUtils.copyParamToBean(request.getParameterMap(), new Book());
    //	2、调用BookService.addBook()保存图书
    bookService.addBook(book);
    // 	3、跳到图书列表页面
    // 	/manager/bookServlet?action=list
    // 	request.getRequestDispatcher("/manager/bookServlet?action=list").forward(request, response);
    response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=list");
  }

关于添加图书的流程以及注意要点说明:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PahDztZr-1614146871623)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217112156054.png)]

流程说明:具体地,在07_book/manager/bookServlet?action=list页面中,点击添加图书按钮后会跳到07_book/pages/manager/book_edit.jsp页面,然后将需要添加的书的信息传给BookServlet程序中,并调用add方法将书存入数据库,存入完成后,再次返回07_book/manager/bookServlet?action=list,刷新显示添加了图书后的所有图书信息。

注意要点:BookServlet添加成功后返还的页面不能使用请求转发来传递信息,即下方代码

request.getRequestDispatcher("/manager/bookServlet?action=list").forward(request, response);

如果使用请求转发,会出现表单重复提交的bug

  • 当用户提交完请求,浏览器会记录下最后一次请求的全部信息,当用户按F5刷新页面时,就会发起浏览器记录的最后一次请求(这也是每次进入F12查看Network后点击刷新能看到上次请求的原因)。而最后一次请求内容就是add操作(因为请求转发始终都是一个请求add)

修改方法:将请求转发更改为请求重定向,重新发起进入显示页面的请求,这样当在对应页面再点击刷新时就只会有一个显示当前页面的请求

response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=list");
  • 此外,请求重定向需要当前工程的全部路径
功能三:删除图书

和添加图书模块的过程类似,删除图书的过程可由下图展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Cpr3Rus-1614146871624)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217131822309.png)]

用户在http://localhost:8080/07_book/manager/bookServlet?action=list页面选择要删除的图书后,在该页面就会通过标签通过get请求访问bookServlet页面,同时带着要删除的书的id信息,请求参数为action=delete。对应的jsp页面代码如下:

<td><a href="manager/bookServlet?action=delete&id=${book.id}">删除</a> </td>

bookServlet收到上述请求参数后就会调用在doget()方法中调用对应的delete()方法,从request域中获取id信息,再调用Service层中对应的方法完成对数据库的操作。最后再将请求重定向至初始页面。

protected void delete(HttpServletRequest request, HttpServletResponse response) throws IOException {
  //	1、获取请求的参数id,图书编程
  int id = WebUtils.parseInt(request.getParameter("id"), 0);
  //	2、调用bookService.deleteBookById();删除图书
  bookService.deleteBookById(id);
  //	3、重定向回图书列表管理页面
  //   /book/manager/bookServlet?action=list
  response.sendRedirect(request.getContextPath() + "/book/manager/bookServlet?action=list";
}

注意:BookServlet类中的delete方法根据id删除对应数据时需要完成String类型向int类型的转换,转换函数在WebUtil类中定义

public static int parseInt(String strInt, int defaultValue) {
  try {
    return Integer.parseInt(strInt);
  } catch (NumberFormatException e) {
    // e.printStackTrace();
  }
  return defaultValue;
}

此外,为了防止用户误触碰到删除按钮,还需要给用户一个提示框,用于询问是否删除该对象,因此需要在book_manager.jsp中加入如下内容:

<script type="text/javascript">
  $(function () {
    // 给删除的a标签绑定单击事件,用于删除的确认提示操作
    $("a.deleteClass").click(function () {
      // 在事件的function函数中,有一个this对象。这个this对象,是当前正在响应事件的dom对象。
      /**
				 * confirm是确认提示框函数
				 * 参数是它的提示内容
				 * 它有两个按钮,一个确认,一个是取消。
				 * 返回true表示点击了,确认,返回false表示点击取消。
				 */
      // 取两次父元素,再找该父元素的第一个元素,就对应到书名来
      return  confirm("你确定删除出【" + $(this).parent().parent().find("td:first").text() + "】?");
      // return false// 阻止元素的默认行为===不提交请求
    })
  })
</script>

<!-- 对应的td标签如下。-->
<td><a class= "deleteClass" href="manager/bookServlet?action=delete&id=${book.id}">删除</a> </td>
功能四:修改图书信息

修改图书时分为两个步骤

  1. 把修改的图书信息回显到表单项
  2. 提交修改后的数据给服务器保存修改
第一步:数据回显

当我们在显示所有图书的页面07_book/pages/manager/manager.jsp点击修改时

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hjpYaMux-1614146871625)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217170120560.png)]

我们实际上时,我们会进入一个新的修改页面07_book/pages/manager/book_edit.jsp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtRwwKVM-1614146871626)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217170248475.png)]

但我们并非是直接进入,而是先在第一个页面将我们需要修改的请求数据发送给BookServlet程序,后者通过请求数据的id信息从数据库中获取数据后,将对应的数据回显至后面的页面中供我们修改,这个过程可用如下流程框图实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vdgswGFO-1614146871627)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217170604997.png)]

因此,在BookServlet类中应当定义一个getBook()方法,代码如下

  protected void getBook(HttpServletRequest request, HttpServletResponse response) {
    //	1、获取请求的参数图书编号
    int id = WebUtils.parseInt(request.getParameter("i"), 0);
    //	2、调用bookService.queryBookById查询图书
    Book book = bookService.queryBookById(id);
    //	3、保存到图书到Request域中
    request.setAttribute("book", book);
    //	4、请求转发到。pages/manager/book_edit.jsp页面
    request.getRequestDispatcher("/pages/manager/book_edit.jsp").foward(request,response);
  }

该方法通过id数据从数据库中获取对应的图书,并将请求响应转发给/pages/manager/book_edit.jsp,从而实现回显功能

第二步:提交修改

当在上述页面点击提交按钮后,会把表单信息再次通过Post请求发送给BookServlet程序,并调用update方法。后者会将请求发送的数据封装称为一个完整的JavaBean对象,执行bookService中的updateBook方法完成对数据库中数据的修改,最后将此次请求转发给"/manager/bookServlet?action=list"。对应的源代码如下:

protected void update(HttpServletRequest request, HttpServletResponse response) {
    //	1、获取请求的参数==封装成为Book对象
    Book book = WebUtils.copyParamToBean(request.getParameterMap(), new Book());
    //	2、调用BookService.updateBook( book );修改图书
    bookService.updateBook(book);
    //	3、重定向回图书列表管理页面
    //	地址:/工程名/manager/bookServlet?action=list
    request.getRequestDispatcher("/manager/bookServlet?action=list").forward(request,response);
  }

然后和添加删除图书一样,用户刷新页面只会保留最后一次请求——显示所有图书

⭐️上述过程会出现的小问题

问题一:页面复用:对于/pages/manager/book_edit.jsp页面,增加图书和修改图书都会访问该页面。因此需要根据两者访问该页面时的不同之处设计运行逻辑实现复用。

  • 方法一:通过方法名。可以分别给add请求和update增加一个属性值methos,分别对应不同的响应,对应的修改如下

    • 修改请求:

      <td><a href="client/bookServlet?action=getBook&id=${book.id}&method=update">修改</a></td>--%>
      
    • 添加请求:

      <td><a href="pages/manager/book_edit.jsp?method=update">添加图书</a></td>
      
    • book_edit.jsp页面的逻辑判断

  • 方法二:判断是否有book信息(以id值为key)

    • 如果当前请求域中包含有id信息,就说明时update方法,否则就是add方法
      <input type="hidden" name="action" value="${empty param.id ? "add" : "update"}" />
      

问题二:请求id缺失

注意到,如果是添加书,并不需要id信息,但如果是查询书,还需要将id信息发送给BookServlet。然而修改页面中并没有id信息,因此需要在表单的隐藏域中增加id信息,以供查询使用

<form action="manager/bookServlet" method="get">
  <input type="hidden" name="action" value="${empty param.id ? "add" : "update"}" />
  
  ⭐️<input type="hidden" name="id" value="${requestScope.book.id}" />⭐️

  <table>
    <tr>
      <td>名称</td>
      <td>价格</td>
      <td>作者</td>
      <td>销量</td>
      <td>库存</td>
      <td colspan="2">操作</td>
    </tr>		
    <tr>
      <td><input name="name" type="text" value="${requestScope.book.name}"/></td>
      <td><input name="price" type="text" value="${requestScope.book.price}"/></td>
      <td><input name="author" type="text" value="${requestScope.book.author}"/></td>
      <td><input name="sales" type="text" value="${requestScope.book.sales}"/></td>
      <td><input name="stock" type="text" value="${requestScope.book.stock}"/></td>
      <td><input type="submit" value="提交"/></td>
    </tr>	
  </table>
</form>

几个需要注意的问题:

问题一:通过标签进入的都是get请求,但如果仍然希望通过get得到post请求时应当将post请求嵌入get请求中,同时为了避免出现乱码的问题,需要在获得请求的时候就设置当前请求的编码格式(一定要在最开始设置)

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException  {
  // 解决post请求中文乱码问题
  // 一定要在获取请求参数之前调用才有效
  request.setCharacterEncoding("UTF-8");
  // 解决响应中文乱码
  response.setContentType("text/html; charset=UTF-8");
	...
}

问题二:为什么bookServlet网页的访问前要加上/manager/,内容如下:

<servlet>
  <servlet-name>BookServlet</servlet-name>
  <servlet-class>web.BookServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>BookServlet</servlet-name>
  <url-pattern>/manager/bookServlet</url-pattern>
</servlet-mapping>

这是因为在开发中有着前台和后台之分

  • 前台:给普通用户使用。一般不需要权限检查,就可以访问的资源,或者功能都属于前台功能。比如:淘宝不需要登陆就可以访问首页,其对应的地址是/cliednt/bookServlet
  • 后台:给管理员使用。一般都会有权限检查,才能访问的资源、页面或功能都是后台。对应的地址是manager/bookServlet
疑惑点

疑惑点一:在请求重转发中如果不加forward会怎么样

Step6:进阶修改——图书分页

图书分页的流程与说明如下图所示;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzlSZX8n-1614146871628)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210217195432229.png)]

思维过程:分页相关数据(构成一个类)—>>思考类中每个数据如何计算—>>>考虑相关类如何在三层架构中实现

构建Page对象
/**
 * Page是分页的模型对象
 * @param <T> 是具体的模块的javaBean类
 */
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> item;	// 这里为了该页面
  //这是分页条的请求地址
  private String url;

  ...对应的get set toString方法
		// 注意这里
    public void setPageNo(Integer pageNo) {
    //数据边界的有效检查
    if (pageNo < 1) {
      pageNo = 1;
    }
    if (pageNo > pageTotal) {
      pageNo = pageTotal;
    }
    this.pageNo = pageNo;
  }
}
  • 说明:为例提高复用性,这里最好使用泛型
修改BookServlet程序

首先需要注意到,之前在进入页面的请求是:/manager/bookServlet?action=list,但要求每次进入该页面都应当使用的page方法,故这里应当修改为action=page

根据先前建立的计算模型,当前page()方法接收到来自jsp的请求(包含PageNo以及PageSize);将这些数据作为参数,向下调用BookService层的page方法,请求获取当前参数下对应的Item数据;再将得到的数据信息装入request域中;最后再将该request域的数据转发给"/page/manager/book_manager.jsp"

对应源代码如下:

protected void page(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  //1 获取请求的参数 pageNo 和 pageSize
  int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 1);//如果没有传数值,默认第一页
  int pageSize = WebUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);//如果传值类就用所传数据,否则用默认值
  //2 调用BookService.page(pageNo,pageSize):Page对象
  Page<Book> page = bookService.page(pageNo, pageSize);
  page.setUrl("manager/book/Servlet?action=page");
  //3 保存Page对象到Request域中
  request.setAttribute("page", page);
  //4 请求转发到pages/manager/book_manager.jsp页面
  request.getRequestDispatcher("/page/manager/book_manager.jsp").forward(request, response);
}
修改BookService程序

根据上层的讨论,首先需要再BookService接口增加对应的方法规范

public Page<Book> page(int pageNo, int pageSize);

然后再在BookServiceImpl中实现该方法

@Override
public Page<Book> page(int pageNo, int pageSize) {
  Page<Book> page = new Page<>();

  // 设置每页显示的数量
  page.setPageSize(pageSize);
  // 求总记录数
  Integer pageTotalCount = bookDao.qureyForPageTotalCount();
  // 设置总记录数
  page.setPageTotal(pageTotalCount);

  // 求总页码
  int pageTotal = pageTotalCount / pageSize;
  if (pageTotalCount / pageSize > 0) {
    pageTotal++;
  }
  // 设置总页码
  page.setPageTotal(pageTotal);

  // 设置当前页码
  page.setPageNo(pageNo);
  // 求当前页数据的开始索引
  int begin = (page.getPageNo() - 1) * pageSize;
  // 求当前页数据
  List<Book> items = bookDao.qureyForPageItems(begin, pageSize);
  // 设置当前页数据
  page.setItem(items);
  return page;
}

说明:这里是对web层的进一步封装,只需要接受web层传来的两个参数pageNo以及pageSize,即可得到完整的页面信息类page类。在该方法中需要完成对Dao层的调用,重点需要获取的是PageTotalCount,以及pageItems的内容(调用两个sql语句)。

这里需要提前思考号如何规范地调用sql语句

select from t_book limit begin,size
修改BookDao程序

在BookDao接口中增加对应的方法规范

public Integer qureyForPageTotalCount();//定义查询页面总数的方法

List<Book> qureyForPageItems(int begin, int pageSize);	//定义查询从begin开始到begin+pageSize的数据项的内容

在BookDaoImpl实现类中增加该方法的具体实现方法

@Override
public Integer qureyForPageTotalCount() {
  String sql = "select count(*) from t_book";
  Number count = (Number) queryForSingleValue(sql);
  return count.intValue();
}

@Override
public List<Book> qureyForPageItems(int begin, int pageSize) {
  String sql = "SELECT `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath FROM t_book limit ?,?";
  return queryForList(Book.class, sql, begin, pageSize);
}
代码测试

从小单元向大单元测试,web层内容需要加断点联动web页面测试

因此在显示页面,将不再使用访问list的方式来获取数据,而是指请求当前request域中的page.item中的图书信息

<c:forEach items="${requestScope.page.item}" var="book">
  <tr>
    <td>${book.name}</td>
    <td>${book.price}</td>
    <td>${book.author}</td>
    <td>${book.sales}</td>
    <td>${book.stock}</td>
  </tr>
  <td><a href="manager/bookServlet?action=getBook&id=${book.id}">修改</a> </td>
  <td><a class= "deleteClass" href="manager/bookServlet?action=delete&id=${book.id}">删除</a> </td>
  </tr>
</c:forEach>
页面功能的填充
首页、上一页、下一页、末页功能的实现

如果当前页是不是在首页,则可以有首页与上一页

<c:if test="${requestScope.page.pageNo > 1}">
  <a href="${requestScope.page.url}&pageNO=1">首页</a>
  <a href="${requestScope.page.url}&pageNO=${requestScope.page.pageNo - 1}">上一页</a>
</c:if>

如果当前页不是在末页,则可以有末页与下一页

<c:if test="${requestScope.page.pageNo < requestScope.page.pageTotal}">
  <a href="${requestScope.page.url}&pageNO=${requestScope.page.pageNo + 1}">下一页</a>
  <a href="${requestScope.page.url}&pageNO=${requestScope.page.pageTotalCount}">末页</a>
</c:if>
跳转到指定页

跳到指定页码的功能实际上是通过location.href方法重写覆盖当前页面的url地址实现,因此只需要在绑定了的单击事件中根据参数

到第<input value="${param.pagNo}" name="pn" id="pn_input"/><input id="searchPageBtn" type="button" value="确定">

<script type="text/javascript">
  $(function () {
    $("#searchPageBtn").click(function () {
      //跳到指定的 页码 jsp
      var pageNo = $("#pn_input").val();
      var pageTotal = ${requestScope.page.pageTotal};
      if(pageNo>0&&pageNo<pageTotal)
        //js 提供了 一个location地址栏 对象
        //它有一个属性叫herf,可以获取浏览器地址栏中的地址
        //herf属性 可读 可写
        location.href = "${pageScope.basePath}${requestScope.page.url}&pageNo=" + pageNo;
      else{
        alert("输入页码不合法!");
        return false;
      }
    })
  })
</script>

注意,这里location.href使用的路径需要达到当前工程路径,同时为了防止登陆ip对ip造成影响,这里应该使用basePath动态获取

页码点击效果实现

该部分要完成的功能是点击页码时能够跳转到对应页面,同时也需要根据当前页码所在值显示一定范围的页码值域。需要考虑如下逻辑:

  • 情况一:当前页码总数<5(某个特定值,这里以5为例):显示全部页码,高亮当前所在页面并使其不可点击。
  • 情况二:当前页码总数>5
    • **子情况一:当前页码<3:**显示]前5个页码
    • 子情况二:当前页码>pageTotal-3:显示最后5个页码
    • 子情况三:3<当前页码<pageTotal-3:显示pageNo-2pageNo+2个页码

根据上述逻辑可以得到如下代码

<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 : 当前页码为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个的情况   页码的范围 总页码-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 : 当前页码为每5个后2个的情况   页码的范围 总页码-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="manager/bookServlet?action=page&pageNo=${i}">${i}</a>
        </c:if>
    </c:forEach>
增加页面后对其他功能的修改
添加模块

添加需求:当添加一条数据后,需要返回当前数据所在页面

需要在BookServlet中添加如下代码(提前设置了页面溢出检测

int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 0);
pageNo++;
// 让请求带着pageNo信息重定向至 page方法
response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=page&pageNo=" + pageNo);
删除模块

添加需求:当删除数据后,维持在删除数据的页面

//让请求带着pageNo信息 重定向至 page方法
response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=page&pageNo="+request.getParameter("pageNo"));
修改模块

添加需求:当修改数据后,维持在修改数据的页面

// 让请求带着pageNo信息 转发至 page方法
request.getRequestDispatcher("/manager/bookServlet?action=page&pageNo="+request.getParameter("pageNo")).forward(request,response);

此外,还需要在book_edit.jsp中添加一个隐藏于存储pageNo

<input type="hidden" name="pageNo" value="${param.pageNo}">

当把信息传递给add以及update方法时,同时需要把pageNo信息保存在request域中供对应方法调用。

出现的问题

忘记在/client/index.jsp中添加<%@include file="/pages/common/head.jsp" %>,造成相对路径错误。

Step7:根据价格搜索

需求:根据用户输入的价格区间,得到价格在区间内的全部图书信息

对应执行流程如下图所示,需求分析从web层反向进入Dao层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IPM7RfNz-1614146871629)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210218153144434.png)]

和实现分页功能时的思考方式相同,结合每层的供鞥综合分析各个层的输入输出

Web层——该层需要获取用户输入的minmaxpageNopageSize信息,得到当前页面应当得到的对应的物品信息(page)。

public void pageByPrice(){
1. 请求获取参数的`pageNo`,`pageSize`,`min`,`max`信息
2. 调用`bookService.pageByPrice(pageNo,pageSize,min,max):Page`方法
3. 保存分页对象`page`到`Request`域中
4. 将请求转发到`/page/client/index.jsp`页面
}

Sercive层——进一步抽取Web层的数据,将Web层需要的功能封装成BookService函数:构建一个Page类,存放了页码,等Page信息

public Page pageByPrice(pageNo,pageSize,min,max){
  主要求三个数据:总记录数,总页码,当前页面数据
    总记录数:调用	queryForPageTotalCount(min,max)	// 获取当前价格区间内页面的总数
    当前页面数据:调用 queryForPageItems(begin,size,min,max)	//获取当前价格区间的数据
}

DAO层——封装sql语句

public Integer queryForPageTotalCountByPrice(int min,int max){
  String sql = "select count(*) from t_book where where price between ? and ?";
  Number count = (Number)queryForSingleValue(sql,min,max);
  return count.intValue();
}

public List<Book> queryForPageItemsByPrice(int 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 queryForList(Book.class,sql,min,max,begin,pageSize);
}

Service层

public Page pageByPrice(pageNo,pageSize,min,max){
  Page page = new Page();
  // 设置当前页码
  page.setPageNo(pageNo);
  
  // 设置当前页面大小
  page.setPageNo(pageSize);

  // 设置总记录数
  page.setPageTotalCount(bookDao.queryForPageTotalCount(min,max));
  
  // 设置总页码数
  int pageTotal = page.PageTotalCount/pageSize;
  if(page.PageTotalCount%pageSize>0)
    pageTotal++;
	
  // 求当前页面数据开始的索引,如果当前页为1,则查询从0开始的4条数据,如果是2,则查从4开始的4条数据
  int begin = (page.getPageNo()-1)*page.Size;
  List<Book> = bookDao.queryForPageItems(begin,size,min,max)	//获取当前价格区间的数据
  
    //至此,Page中6项数据在这里完成5项的设置,最后一项path在Web层设置
    return page;
}

Web层

public void pageByPrice(){
  // 1. 请求获取参数的`pageNo`,`pageSize`,`min`,`max`信息
  int pageNo = request.getParameter("pageNo");
  int pageSize = request.getParameter("pageSize");
  int min = WebUtils.parseInt(request.getParameter("min"), 0);
  int max = WebUtils.parseInt(request.getParameter("max"), Integer.MAX_VALUE);
  // 2. 调用`bookService.pageByPrice(pageNo,pageSize,min,max):Page`方法
  Page<page> page = bookService.pageByPrice(pageNo,pageSize,min,max);

  //⭐️除了记录page数据,还需要将max和min数据回显给用户因此需要在这里设置url地址
  StringBuilder sb = new StringBuilder("client/bookServlet?action=pageByPrice");
	if(min!=null){
    sb.append("&min=").append(request.getParameter("min"));
  }
  if(max!=null){
    sb.append("&max=").append(request.getParameter("max"));
  }
  page.setUrl(sb);
  
  // 3. 保存分页对象`page`到`Request`域中
  request.setAttribute("page",page);
  
  // 4. 将请求转发到`/page/client/index.jsp`页面
    request.getRequestDispatcher("/pages/client/index.jsp").foward(request,response);

⚠️Web层中request.getParameter()方法是用于获取前台传入后台的数据,request.setAttribute()方法则是用于后台设置数据发送给前台

小结:对于某个需求如何实现,需要先从Web层一步步分析到Dao层。然后再反向一步步实现——测试,最后到达Web层。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值