[JavaWeb]文件上传下载和书城项目

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[JavaWeb]文件上传下载和书城项目相关的知识,希望对你有一定的参考价值。

一、文件上传

1. 文件上传介绍

  1. 必须使用表单进行文件的上传,且 method=post
  2. form 标签的 encType 属性值必须是 mutipart/form-data
  3. 在 from 标签中使用 input type=file 表单项添加需要上传的文件
  4. 表单中不可用 来确定 Servlet 调用的方法,因为 type=“hidden” 会被转换为 mutipart/form-data 类型的数据进行传输,以至于 Servlet 中通过方法找不到的方法,可在 action 后指定响应的方法

2. 文件上传时 Htpp 协议内容说明

  1. encType=multipart/form-data 表示提交的数据,以多段(每一个表单项一个数据段)的形式进行拼接,然后以二进制流的形式发送给服务器
  2. 服务器从流中获取浏览器上传的文件数据
ServletInputStream inputStream = req.getInputStream();
byte[] buffer = new byte[1024];
int read = inputStream.read(buffer);
System.out.println(new String(buffer, 0, read));

3. 文件上传常用 API

commons-fileupload-1.2.1.jar 的 ServletFileUpload 类,用于解析上传的数据

方法名功能
boolean ServletFileUpload.isMultipartContent(HttpServletRequest)判断当前上传的数据格式是否是多段格式
List<FileItem> parseRequest(HttpServletRequest)解析上传的数据,FileItem 表示每一个表单项
boolean FileItem.isFormField()判断当前表单项是否是普通表单项
String FileItem.getFieldName()获取表单项的 name 属性值
String FileItem.getString()获取表单项的 value 属性值
String FileItem.getName()获取上传的文件名
void FileItem.write(file)将上传的文件写到 file 所指定的磁盘

使用 fileupload 解析上传的数据

    // 判断上传的文件是否为多段格式
    if (ServletFileUpload.isMultipartContent(req)) 
        FileItemFactory fileItemFactory = new DiskFileItemFactory();
        // 创建用于解析上传数据的工具类
        ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
  
        try 
            // 逐条解析文件数据
            List<FileItem> list = servletFileUpload.parseRequest(req);
            for (FileItem fileItem : list) 
                // 普通表单项,输出 name - value 信息
                if (fileItem.isFormField()) 
                    System.out.println("name:" + fileItem.getFieldName());
                    System.out.println("value:" + fileItem.getString("UTF-8"));
                 else 
                    // 上传的文件名
              		String uploadFileName = fileItem.getName();
                    // 文件表单项的 name 属性值
                    String fieldName = fileItem.getFieldName();
                    // 将文件写入到指定磁盘目录
                    fileItem.write(new File("d/" + fileName));
                
            
         catch (Exception e) 
            e.printStackTrace();
        
    

二、文件下载

1. 文件下载的实现

服务器处理流程:获取客户端想要下载的文件名、在回传前通过响应头告诉客户端返回的数据类型、在回传前通过响应头告知客户端收到的数据是用于文件下载、从磁盘读取需要下载的文件内容、把下载的文件内容回传给客户端

2. 使用 URLEncoder 解决 Chrome、IE 浏览器中文下载名乱码问题

/**
 * @author Spring-_-Bear
 * @datetime 2022/3/1 11:49
 */
public class DownloadServlet extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException 
        // 获取客户端想要下载的文件名
        String downloadFileName = "BeFree.jpg";
        ServletContext servletContext = getServletContext();
        // 获取要下载的文件类型
        String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
        System.out.println(mimeType);
        // 回传数据前通过响应头告诉客户端返回的数据类型
        resp.setContentType(mimeType);
        // 回传数据前通过响应头告知客户端数据是用于下载
        // Content-Disposition 响应头告知客户端收到的数据如何处理
        resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("李春雄.jpg", "UTF-8"));
        // 从磁盘读取想要下载的文件内容, 斜杠 / 被服务器解析为 http://ip:port/projectName/ 映射到 IDEA 工程的 webapp 目录
        InputStream resourceAsStream = servletContext.getResourceAsStream("/file/" + downloadFileName);
        // 获取相应输出流
        ServletOutputStream outputStream = resp.getOutputStream();
        // 将输入流中二进制数据复制给输出流
        IOUtils.copy(resourceAsStream, outputStream);
    

3. Base64 编解码操作

String china = "中华人民共和国";
BASE64Encoder base64Encoder = new BASE64Encoder();
String encodeChina = base64Encoder.encode(china.getBytes(StandardCharsets.UTF_8));
System.out.println(encodeChina);

BASE64Decoder base64Decoder = new BASE64Decoder();
byte[] bytes = base64Decoder.decodeBuffer(encodeChina);
china = new String(bytes, StandardCharsets.UTF_8);
System.out.println(china);

// 使用 Base64 编解码解决火狐浏览器附件中文乱码问题
/*
 * =?charset?B?xxx?= 说明:
 * =? 表示编码内容的开始
 * charset 字符集
 * B 表示 BASE64 编码
 * xxx 是中文文件名经过 BASE64 编码后的内容
 * ?= 编码内容的结束
 */
resp.setHeader("Content-Disposition", "attachment;filename=?UTF-8?B?" + new BASE64Encoder().encode("李春雄.jpg".getBytes(StandardCharsets.UTF_8)) + "?=");


// 使用 User-Agent 请求头判断,动态切换不同的方案解决所有浏览器附件中文乱码问题
if (req.getHeader("User-Agent").contains("Firefox")) 
    resp.setHeader("Content-Disposition", "attachment;filename=?UTF-8?B?" + new BASE64Encoder().encode("李春雄.jpg".getBytes(StandardCharsets.UTF_8)) + "?=");
 else 
    resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("李春雄.jpg", "UTF-8"));

三、书城项目

1. 动态获取 base 标签的值

<%@ page contentType="text/html;charset=UTF-8" %>
<% String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/"; %>
<base href="<%=basePath%>">

2. BaseServlet

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
    String action = req.getParameter("action");

    try 
        // 通过反射机制获取对应的方法对象,优化大量 if else 代码
        Method declaredMethod = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
        // 调用方法
        declaredMethod.invoke(this, req, resp);
     catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) 
        e.printStackTrace();
    

3. BeanUtils

BeanUtils 工具类,它可以一次性把所有的请求参数注入到 JavaBean 中

/**
 * @author Spring-_-Bear
 * @datetime 2022/3/2 8:41
 */
public class BeanUtils 
    /**
     * Let the form request parameters into a java bean object
     *
     * @param bean  java bean
     * @param value map
     * @return java bean
     */
    public static <T> T copyParamsToBean(T bean, Map<String, String[]> value) 
        try 
            org.apache.commons.beanutils.BeanUtils.populate(bean, value);
         catch (IllegalAccessException | InvocationTargetException e) 
            e.printStackTrace();
        
        return bean;
    

4. MVC

  1. MVC:Model(模型)、View(视图)、Controller(控制器),MVC 最早出现在 JavaEE 三层中的 web 层,它可以有效指导 web 层的代码如何有效地分离,独立工作
  2. view 视图层:只负责数据和界面的显示,不接受任何与显示无关的代码,便于程序员和美工的分工合作 - JSP/HTML
  3. Controller 控制器层:只负责接收请求,调用业务层的代码请求,然后派发页面,是一个“调度者”的角色 - Servlet
  4. Model 模型层:将与业务逻辑相关的数据封装为具体的 JavaBean 类,其中不掺杂任何与数据处理有关的代码 - JavaBean、domain、entity、pojo
  5. MVC 是一种思想,理念是将软件拆分为组件,单独开发,组合使用,降低系统的耦合度

5. 表单重复提交

表单重复提交:当用户提交完请求,浏览器会记录下最后一次请求的全部信息,当用户刷新页面时,浏览器就会发起记录的最后一次请求。由于请求转发属于一次请求,所以当用户添加图书再次刷新时会重复提交添加图书表单,所以此处应选择请求重定向(两次请求)以避免表单重复提交

6. 数据分页对象 POJO 类

分页模型对象 Page

public class Page<T> 
    /**
     * 每页显示数量常量
     */
    public static final Integer PAGE_SIZE = 4;
    /**
     * 当前页码
     */
    private Integer pageNo;
    /**
     * 总页数
     */
    private Integer pageTotal;
    /**
     * 每页显示的数量
     */
    private Integer pageSize = PAGE_SIZE;
    /**
     * 总记录数
     */
    private Integer recordTotalCount;
    /**
     * 当前页数据
     */
    private List<T> items;

分页方法的实现

@Override
public Page<Book> page(int pageNo, int pageSize) 
    Page<Book> page = new Page<>();
    page.setPageNo(pageNo);
    page.setPageSize(pageSize);
    // 查询并设置所有图书记录的总数
    int recordTotalCount = bookDao.queryBooksTotalCount();
    page.setRecordTotalCount(recordTotalCount);
    // 求解并设置显示的总页码
    int pageTotal = recordTotalCount / pageSize;
    if (recordTotalCount % pageSize != 0) 
        pageTotal++;
    
    page.setPageTotal(pageTotal);
    // 获取当前页的数据
    int begin = (pageNo - 1) * pageSize;
    List<Book> items = bookDao.queryPageItems(begin, pageSize);
    page.setItems(items);
    return page;

7. 分页页码显示实现

  1. 当前页码为 1 - 3 页,页码显示范围是 1 - 5
  2. 当前页码为末尾 3 页,页码显示范围是最后 5 页,即:总页码减 4 - 总页码
  3. 其余情况页码显示范围是:当前页码 - 2 到 当前页码 + 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:当前页码为前 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 页,则页码显示范围为末 5 页 --%>
            <c:when test="$requestScope.page.pageNo >= requestScope.page.pageTotal - 2">
                <c:set var="begin" value="$requestScope.page.pageTotal - 4"/>
                <c:set var="end" value="$requestScope.page.pageTotal"/>
            </c:when>
            <%-- 其余情况:页码显示范围为 pageTotal-2 到 pageTotal+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>
<%-- 页码显示结束 --%>

8. 表单重复提交的三种常见情况

  1. 服务器使用请求转发来进行页面跳转。这个时候,当用户按下功能键 F5,就会发起最后一次请求,由于请求转发是一次请求,就会造成表单重复提交的问题。解决方法:使用请求重定向来进行页面跳转(两次不同的请求)
  2. 用户正常提交表单至服务器,但由于网络延迟等原因,迟迟未收到服务器的响应,此时用户以为提交失败就会重复提交表单
  3. 用户成功提交表单后回退页面,再次重复提交表单

9. 谷歌验证码的使用

当用户第一次访问表单的时候就要给表单随机生成一个验证码字符串,然后把验证码保存到 Session 域中,并将验证码生成为图片显示在表单中

  1. 导入谷歌验证码第三方 jar 包 kaptcha-2.3.2.jar

  2. 在 web.xml 中配置生成谷歌验证码的 Servlet 类访问地址

    <servlet>
        <servlet-name>KaptchaServlet</servlet-name>
        <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>KaptchaServlet</servlet-name>
        <url-pattern>/verifyCode.jpg</url-pattern>
    </servlet-mapping>
    
  3. 在表单中使用 img 标签去显示验证码图片并使用它

    <img alt="" src="verifyCode.jpg" style=" float: right; margin-right: 20px; width: 100px; height: 35px">
    
  4. 在服务器 UserServlet 中获取谷歌生成的验证码和客户端请求发送的验证码

    HttpSession session = req.getSession();
    Object token = session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
    // 移除验证码
    session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
    
    String verifyCode = req.getParameter("verifyCode");
    
    if (token != null && token.toString().equalsIgnoreCase(verifyCode)) 
        System.out.println("验证码正确");
     else 
        System.out.println("验证码错误");
    
    
  5. 浏览器点击图片刷新验证码

    // 给验证码图片绑定单击事件
    $("#code_img").click(function () 
        // 在事件响应的 function 函数中有一个 this 对象。这个 this 对象是当前正在响应事件的 dom 对象
        // 加上时间作为参数以解决火狐和 IE 浏览器缓存问题
        this.src = "$basePathverifyCode.jpg?date=" + new Date();
    );
    

10. 购物车模型 POJO 类

package com.bear.book.pojo;

import java.math.BigDecimal;
import java.util.LinkedHashMap;

/**
 * 购物车模型
 *
 * @author Spring-_-Bear
 * @datetime 2022/3/4 16:06
 */
public class Cart 
    private final LinkedHashMap<Integer, CartItem> items = new LinkedHashMap<>();

    /**
     * 添加商品项到购物车
     *
     * @param cartItem 商品项
     */
    public void addItem(CartItem cartItem) 
        // 先判断购物车中是否存在此商品项,存在则数量 + 1,修改金额,不存在则添加到购物车
        CartItem item = items.get(cartItem.getId());
        if (item == null) 
            // 购物车中不存在对应的商品项
            items.put(cartItem.getId(), cartItem);
         else 
            // 数量自增一
            item.setCount(item.getCount() + cartItem.getCount());
            // 总价 = 原总价 + 单价
            item.setTotalPrice(item.getPrice().multiply(new BigDecimal(item.getCount())));
        
    

    /**
     * 删除商品项
     *
     * @param id 商品项 id
     */
    public void deleteItem(Integer id) 
        items.remove(id);
    

    /**
     * 清空购物车
     */
    public void clear() 
        items.clear();
    

    /**
     * 修改商品项的数量
     *
     * @param id    商品项 id
     * @param count 新商品项数量
     */
    public void updateCount(Integer id, Integer count) 
        CartItem item = items.get(id);
        if (item != null) 
            // 修改商品数量
            item.setCount(count);
            // 修改商品总价
            item.setTotalPrice(item.getPrice().multiply(new BigDecimal(count)));
        
    

    public Integer getTotalCount() 
        Integer totalCount = 0;
        for (CartItem value : items.values()) 
            totalCount += value.getCount();
        
        return totalCount;
    

    public BigDecimal getTotalPrice() 
        BigDecimal totalPrice = new BigDecimal(0);
        for (CartItem value : items.values()) 
            totalPrice = totalPrice.add(new BigDecimal(value.getTotalPrice().toString()

以上是关于[JavaWeb]文件上传下载和书城项目的主要内容,如果未能解决你的问题,请参考以下文章

JavaWeb项目(书城idea)---注册

JavaWeb书城项目遇到的一些问题

JavaWeb SpringBoot 电商书城平台系统(已调试《精品毕设》) 实现了书城网站的浏览加入购物车操作订单操作支付操作分类查看搜索以及后台上传图书信息以及订单管理和一些基本功能

网上书城+源码

基于 JavaWeb 的尚书房网上书城项目

基于 JavaWeb 的尚书房网上书城项目