文章目录
- 1. Servlet 介绍
- 1.1 什么是 Servlet
- 1.2 Servlet 的主要工作
- 2. Servlet 程序创建步骤
- 2.1 创建项目
- 2.2 引入依赖
- 2.3 创建目录
- 2.4 编写代码
- 2.5 打包程序
- 2.6 部署程序
- 2.7 验证程序
- 3. 使用 Smart Tomcat 进行部署
- 3.1 安装 Smart Tomcat
- 3.2 配置 Smart Tomcat
- 3.3 使用 Smart Tomcat
- 4. 访问出错解决方案
- 4.1 出现 404
- 4.2 出现 405
- 4.3 出现 500
- 4.4 出现“空白页面”
- 4.5 出现“无法访问此网站”
- 4.6 出现中文乱码问题
- 5. Servlet 运行原理
- 5.1 Servlet 的架构
- 5.2 Tomcat 的伪代码
- 6. Servlet API 详解
- 6.1 HttpServlet
- 6.2 HttpServletRequest
- 6.3 HttpServletResponse
- 7. 实现服务器版表白墙程序
- 7.1 基本介绍
- 7.2 准备操作
- 7.3 代码实现
- 7.4 持久化存储
- 8. Cookie 和 Session
- 8.1 Cookie 介绍
- 8.2 Session 会话机制介绍
- 8.3 Cookie 和 Session 的区别
- 8.4 Servlet 中 Cookie 和 Session 的核心方法
- 8.5 实现用户登录功能
- 9. 上传文件操作
- 9.1 Servlet 中上传文件的核心方法
- 9.2 上传文件操作实现
1. Servlet 介绍
1.1 什么是 Servlet
-
Servlet(Server Applet 的缩写,全称 Java Servlet): 是用 Java 编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类,一般情况下,人们将 Servlet 理解为后者。
-
Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。
-
Servlet 是一种实现动态页面的技术,是一组由 Tomcat 提供给程序员的 API,帮助程序员简单高效的开发一个 web app
1.2 Servlet 的主要工作
- 允许程序员注册一个类,在 Tomcat 收到的某个特定的 HTTP 请求的时候,执行这个类中的一些代码
- 帮助程序员解析 HTTP 请求,把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
- 帮助程序员构造 HTTP 响应,程序员只要给指定的 HttpResponse 对象填写一些属性字段,Servlet 就会自动的按照 HTTP 协议的方式构造出一个 HTTP 响应字符串,并通过 Socket 编写返回给客户端
2. Servlet 程序创建步骤
2.1 创建项目
以下使用 IDEA 带大家编写一个简单的 Servlet 程序,主要是让大家了解一个大致的流程
-
首先使用 IDEA 创建一个 Maven 项目
-
创建好的项目如下
-
通过上图我们可以看到创建好的项目中有一些目录结构,这是 Maven 项目的标准结构,其中
src
: 用于存放源代码和测试代码的根目录main
: 用于存放源代码的目录test
: 用于存放测试代码的目录java
: 用于存放 Java 代码的目录resources
: 用于存放依赖的资源文件pom.xml
: 是 Maven 项目的核心配置文件,关于这个 Maven 项目的相关属性,都是在这个 xml 中进行配置
2.2 引入依赖
Maven 项目创建完成后,会自动生成一个 pom.xml
文件,我们需要在这个文件中引入 Servlet API 依赖的 jar 包
-
打开中央仓库,搜索 Servlet,点击
Java Servlet API
-
选择对应 Tomcat 版本的 Servlet(由于我当前使用的是 Tomcat 8 系列,所以选择 Servlet 3.1.0 即可)
-
将中央仓库提供的该版本的 xml 复制到项目的
pom.xml
中 -
修改后的
pom.xml
文件如下一个项目中可以有多个依赖,每个依赖都是一个
<dependency>
标签。引入的依赖都要放在一个<dependencies>
的标签中,该标签用于放置项目依赖的 jar 包,Maven 会自动下载该依赖到本地 -
在拷贝的依赖中有几个参数,分别具有如下含义:
groupId
: 表示组织或者公司的 IDartifactId
: 表示项目或者产品的 IDversion
: 表示版本号scope
: 用于指定依赖的作用范围,包含所在项目的测试、编译、运行、打包等声明周期。
-
如果你想找到刚刚 Maven 下载到本地的第三方库,路径如下
2.3 创建目录
Web 项目对于目录结构还有自己的要求,只有 Maven 的标准目录是不够的,需要再创建以下目录并进行配置
-
在 main 目录下,创建一个
webapp
目录webapp 目录就是用于部署到 Tomcat 中的一个重要目录,里面可以存放一些静态资源
-
在 webapp 目录下,创建一个
WEB-INF
目录 -
在 WEB-INF 目录下,创建一个
web.xml
文件Tomcat 通过找到这个 web.xml 文件才能够正确处理 webapp 中的动态资源
-
编写
web.xml
Servlet 中 web.xml 中的内容不能是空的,里面的写法是固定的(这里的写法专属于 Servlet),用到的时候可以直接拷贝下面的代码
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
2.4 编写代码
以下编写一个让响应返回一个自定义字符换的简单代码
-
创建一个 TestServlet 类,并且让它继承于
HttpServlet
HttpServlet 这个类来自于 pom.xml 中引入的 Servlet API 依赖的 jar 包
-
在 TestServlet 类中重写
doGet
方法doGet 是 HttpServlet 类中的方法,此处是在子类中重写了父类的 doGet
-
为了了解 doGet 方法的作用,我们可以看看它的源码
HttpServletRequest
: 表示 HTTP 请求,Tomcat 按照 HTTP 请求的的格式把字符串格式的请求转换成了一个 HttpServletRequest 对象,通过这个对象就可以获取请求中的信息HttpServletResponse
: 表示 HTTP 响应,通过代码可以把响应的对象构造好,然后 Tomcat 将响应返回给浏览器- 通过 doGet 的源码我们可以大致了解,它的作用是根据收到的请求通过响应返回一个 405 或者 400,那么我们可以重写这个方法,根据收到的请求执行自己的业务逻辑,把结果构造成响应对象
-
在 doGet 方法中,通过
HttpServletResponse
类的getWriter()
方法往响应的 body 中写入文本格式数据resp.getWriter()
会获取到一个流对象,通过这个流对象就可以写入一些数据,写入的数会被构造成一个 HTTP 响应的 body 部分,Tomcat 会把整个响应转成字符串,通过 Socket 写回给浏览器 -
需要给 TestServlet 加上一个特定的注解
@WebServlet("/test")
上述助解表示 Tomcat 收到的请求中,URL 的 Servlet Path 路径为
/test
的请求才会调用 TestServlet 这个类的代码,注解中的字符串表示着 URL 的 Servlet Path -
到这里程序的编写已经完成了!但是你可能会疑惑上述代码不是通过 main 方法作为入口的,这是因为 main 方法已经被包含在 Tomcat 中了,我们写的程序并不能单独执行,而是需要搭配 Tomcat 才能执行起来(在 Tomcat 的伪代码中我们具体分析了这个问题)
2.5 打包程序
在程序编写好之后,就可以使用 Maven 进行打包
-
首先修改 pom.xml,加入一些必要的配置(打包的类型和打包后的包名)
- packaging 标签中用于设置打包的类型(如果不修改打包类型则默认为 jar 包,jar 包是普通 Java 程序打包的结果,里面包含了一些
.class
文件;而部署在 Tomcat 中的压缩包一般为 war 包,war 包里面是 Java Web 程序,里面除了 .class 文件之外,还包含 HTML、CSS、JavaScript、图片等等) - finalName 标签中用于设置打包后的名字(包名很重要,它对应着请求中 URL 的 Context Path)
- packaging 标签中用于设置打包的类型(如果不修改打包类型则默认为 jar 包,jar 包是普通 Java 程序打包的结果,里面包含了一些
-
执行打包操作(打开 Maven 窗口,展开 Lifecycle,双击 package 进行打包)
-
打包成功后,可以发现多了个 target 目录,该目录下有一个 testServlet.war 的压缩包
2.6 部署程序
接下来我们就可以进行程序的部署
-
首先将打好的 war 包拷贝到 Tomcat 的 webapps 目录下
-
启动 Tomcat(在 Tomcat 的 bin 目录中点击
startup.bat
)
2.7 验证程序
此时通过浏览器访问 http://127.0.0.1:8080/testServlet/test 就可以看到程序实现的结果了
注意:URL 中的路径分成了两个部分 Context Path 和 Servlet Path
- Context Path 这个路径表示一个 webapp,来源于打包的包名
- Servlet Path 这个路径表示一个 webapp 中的一个页面,来源于对应的 Servlet 类
@WebServlet
注解中的内容
3. 使用 Smart Tomcat 进行部署
为了简化上述操作流程,其实是有一些更简单的方式
- 对于创建项目、引入依赖、创建目录这三个步骤,其实可以使用项目模板来快速生成,但是由于项目模板加载速度很慢,因此这里并不推荐
- 对于打包程序和部署程序这两个步骤,其实可以使用 Smart Tomcat 插件来快速实现,以下将介绍它的使用方式
3.1 安装 Smart Tomcat
-
点击 File → Settings
-
点击 Plugins,在搜索栏搜索 Smart Tomcat,然后进行安装即可
3.2 配置 Smart Tomcat
-
点击 Add Configuration
-
点击左上角的+号,并选择 Smart Tomcat
-
主要修改这三个参数
- Name:这一栏其实可以随便填
- Tomcat Server:表示 Tomcat 所在的目录
- Deployment Directory:表示项目发布目录
- Context Path:表示项目路径,默认值是项目名称
- Servlet Port:表示服务端口
- Admin Port:表示管理端口
- VM options:表示 JVM 参数
-
配置好 Smart Tomcat 之后,Add Configuration 就会显示成 Name 的名字,并且右边多了个三角形运行的符号
3.3 使用 Smart Tomcat
-
点击三角形运行 Smart Tomcat,出现如下信息表示程序启动成功
-
点击蓝色的连接,跳转到项目路径,再增加 Servlet Path 就可以显示出该程序的结果
4. 访问出错解决方案
4.1 出现 404
出现 404 原因: 用户访问的资源不存在,大概率是 URL 的路径写的不正确
错误实例1: 少写了 Context Path 或者 Context Path 写错了
错误实例2: 少写了 Servlet Path 或者 Servlet Path 写错了
错误实例3: web.xml 写错了(如清空 web.xml 中的内容)
4.2 出现 405
出现 405 原因: 访问的服务器不能支持请求中的方法或者不能使用该请求中的方法
错误实例1: 没有重写 doGet 方法
错误实例2: 重写了 doGet 方法,但是没有删除父类的 doGet 方法
4.3 出现 500
出现 500 原因: 服务器出现内部错误,往往是 Servlet 代码中抛出异常导致的
错误实例: 代码中出现空指针异常
4.4 出现“空白页面”
出现空白页原因: 响应的 body 中并没有内容
错误实例: 将 resp.getWriter().write()
操作删除
4.5 出现“无法访问此网站”
出现“无法访问此网站”原因: 一般是不能正确访问到 Tomcat(可能是 Tomcat 没启动,也可能是 IP/端口号写错了)
错误实例: 注解 @WebServlet
中少写了 /
4.6 出现中文乱码问题
响应出现中文乱码问题原因: 使用的编译器的编码方式(一般是 utf-8)和浏览器的编码方式不同,浏览器默认跟随系统编码方式,win10 系统默认是 GBK 编码
解决方式: 通过响应对象的 setContentType()
方法来修改浏览器对于响应正文的编码格式
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("吞吞吐吐大魔王");
}
}
5. Servlet 运行原理
在 Servlet 的代码中,我们并没有写 main 方法,那么对应的 doGet 代码是如何被调用呢?响应又是如何返回给浏览器的呢?
5.1 Servlet 的架构
我们自己实现的 Servlet 是在 Tomcat 基础上运行的,下图显示了 Servlet 在 Web 应用程序中的位置
当浏览器给服务器发送请求时,Tomcat 作为 HTTP 服务器,就可以接收到这个请求。Tomcat 的工作就是解析 HTTP 请求,并把请求交给 Servlet 的代码来进行进一步的处理。Servlet 的代码根据请求计算生成响应对象,Tomcat 再把这个响应对象构造成 HTTP 响应,返回给浏览器。并且 Servlet 的代码也经常会和数据库进行数据的传递。
5.2 Tomcat 的伪代码
下面通过 Tomcat 的伪代码的形式来描述 Tomcat 初始化和处理请求两部分核心逻辑
-
Tomcat 的初始化流程
class Tomcat { // 用来存储所有的 Servlet 对象 private List<Servlet> instanceList = new ArrayList<>(); public void start() { // 根据约定,读取 WEB-INF/web.xml 配置文件 // 并解析被 @WebServlet 注解修饰的类 // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类. Class<Servlet>[] allServletClasses = ...; // 这里要做的的是实例化出所有的 Servlet 对象出来; for (Class<Servlet> cls : allServletClasses) { // 这里是利用 java 中的反射特性做的 // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的 // 方式全部在 WEB-INF/classes 文件夹下存放的,所以 tomcat 内部是 // 实现了一个自定义的类加载器(ClassLoader),用来负责这部分工作。 Servlet ins = cls.newInstance(); instanceList.add(ins); } // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次 for (Servlet ins : instanceList) { ins.init(); } // 启动一个 HTTP 服务器,并用线程池的方式分别处理每一个 Request ServerSocket serverSocket = new ServerSocket(8080); // 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况 ExecuteService pool = Executors.newFixedThreadPool(100); while (true) { Socket socket = ServerSocket.accept(); // 每个请求都是用一个线程独立支持,这里体现了 Servlet 是运行在多线程环境下的 pool.execute(new Runnable() { doHttpRequest(socket); }); } // 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次 for (Servlet ins : instanceList) { ins.destroy(); } } public static void main(String[] args) { new Tomcat().start(); } }
- Tomcat 的代码内置了 main 方法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main 方法开始执行的
- 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理
- Tomcat 通过反射这样的语法机制来创建被 @WebServlet 注解修饰的类的实例
- 这些实例被创建完之后,就会调用其中的 init 方法进行初始化
- 这些实例被销毁之前,就会调用其中的 destory 方法进行收尾工作
- Tomcat 内部也是通过 Socket API 进行网络通信
- Tomcat 为了能够同时处理多个 HTTP 请求,采取了多线程的方式实现,因此 Servlet 是运行在多线程环境下的
-
Tomcat 处理请求流程
class Tomcat { void doHttpRequest(Socket socket) { // 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析和响应构建 HttpServletRequest req = HttpServletRequest.parse(socket); HttpServletRequest resp = HttpServletRequest.build(socket); // 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容 // 直接使用 IO 进行内容输出 if (file.exists()) { // 返回静态内容 return; } // 走到这里的逻辑都是动态内容了 // 找到要处理本次请求的 Servlet 对象 Servlet ins = findInstance(req.getURL()); // 调用 Servlet 对象的 service 方法 // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了 try { ins.service(req, resp); } catch (Exception e) { // 返回 500 页面,表示服务器内部错误 } } }
- Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串,然后 Tomcat 会按照 HTTP 协议的格式解析成一个 HttpServletRequest 对象
- Tomcat 会根据 URL 中的 Path 判定这个请求是请求一个静态资源还是动态资源。如果是静态资源,直接找到对应的文件,把文件的内容通过 Socket 返回;如果是动态资源,才会执行到 Servlet 的相关逻辑
- Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service 方法
- 通过 service 方法,就会进一步调用我们重写的 doGet 或者 doPost 方法等等
-
Servlet 的 service 方法的实现
class Servlet { public void service(HttpServletRequest req, HttpServletResponse resp) { String method = req.getMethod(); if (method.equals("GET")) { doGet(req, resp); } else if (method.equals("POST")) { doPost(req, resp); } else if (method.equals("PUT")) { doPut(req, resp); } else if (method.equals("DELETE")) { doDelete(req, resp); } ...... } }
- Servlet 的 service 方法内部会根据当前请求的方式,决定调用其中的某个 doXXX 方法
- 在调用 doXXX 方法的时候,会触发多态机制,从而执行到我们自己写的子类的 doXXX 方法
6. Servlet API 详解
对于 Servlet 主要介绍三个类,分别是 HttpServlet、HttpServletRequest 和 HttpServletResponse。
其中 HttpServletRequest 和 HttpServletResponse 是 Servlet 规范中规定的两个接口,HttpServlet 中并没有实现这两个接口的成员变量,它们只是 HttpServlet 的 service 和 doXXX 等方法的参数。这两个接口类的实例化是在 Servlet 容器中实现的。
6.1 HttpServlet
核心方法
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/... | 收到其它对应请求的时候调用(由 service 方法调用) |
Servlet 的生命周期: Servlet 的生命周期就是 Servlet 对象从创建到销毁的过程,下面来介绍其生命周期的过程
- Servlet 对象是由 Tomcat 来进行实例化的,并且在实例化完毕之后调用 init 方法(只调用一次)
- Tomcat 对于收到的请求,都会通过对应的 Servlet 的 service 方法来进行处理(可以调用多次)
- Tomcat 在结束之前,会调用 Servlet 的 destory 方法来进行回收资源(最多调用一次)
注意: init 和 service 能够保证在各自的合适时机被 Tomcat 调用,但是 destory 不一定,它是否能够被调用取决于 Tomcat 是如何结束的
- 如果直接杀死进程,那么就来不及调用 destory
- 如果通过 Tomcat 的管理端口(默认 8005)进行关闭,就能够调用 destory
处理 GET 请求示例:
直接通过浏览器 URL 发送一个 GET 方法的请求,来对这个请求进行处理
@WebServlet("/get")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("get");
}
}
处理 POST 请求示例:
由于通过浏览器 URL 发送的请求是 GET 方法的请求,因此我们需要通过其它方式来发送一个 POST 请求然后用于处理。发送 POST 请求的方式有通过 Ajax、form 表单或者 socket api 进行构造,如果单纯的用于测试就比较麻烦,这里推荐使用软件 postman,这是一个很强大的 API 调试、Http 请求的工具。
@WebServlet("/post")
public class TestServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("post");
}
}
6.2 HttpServletRequest
核心方法
方法 | 描述 |
---|---|
String getProtocol() | 返回协议的名称和版本号 |
String getMethod() | 返回请求的 HTTP 方法的名称 |
String getRequestURL() | 返回请求的 URL,不带查询字符串 |
String getRequestURI() | 返回该请求的 URL 的一部分,不带协议名、端口号、查询字符串 |
String getContextPath() | 返回指示请求 URL 中 Context Path 部分 |
String getServletPath() | 返回指示请求 URL 中 ServletPath 部分 |
String getQueryString() | 返回请求首行中 URL 后面的查询字符串 |
Enumeration getParameterNames() | 返回一个 String 对象的枚举,包括在该请求中的参数的名称 |
String getParameter(String name) | 以字符串形式返回请求参数的值,如果参数不存在则返回 null |
String[] getParameterValues(String name) | 返回一个字符串对象的数组,包括所有给定的请求的参数,如果参数不存在则返回 null |
Enumeration getHeaderNames() | 返回一个枚举,包括该请求中所有的头名 |
String getHeader(String name) | 以字符串形式返回指定的请求头的值 |
String getCharacterEncoding() | 返回请求正文中使用的字符编码的名称 |
String getContentType() | 返回请求正文的 MIME 类型,如果不知道类型则返回 null |
int getContentLength() | 以字节为单位返回请求正文的长度,并提供输入流,如果长度未知则返回-1 |
InputStream getInputStream() | 用于读取请求的正文内容,返回一个 InputStream 对象 |
示例1: 通过上述方法返回一个页面是该请求的具体 HTTP 请求格式
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 此处返回一个 HTML,在 HTML 中显示 HttpRequestServlet 类中的一些核心方法
// 把这些 API 的返回结果通过 StringBuilder 进行拼接
resp.setContentType("text/html;charset=utf-8");
StringBuilder html = new StringBuilder();
html.append(req.getMethod());
html.append(" ");
html.append(req.getRequestURL());
html.append("?");
html.append(req.getQueryString());
html.append(" ");
html.append(req.getProtocol());
html.append("</br>");
Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()){
String headName = headerNames.nextElement();
String header = req.getHeader(headName);
html.append(headName);
html.append(": ");
html.append(header);
html.append("</br>");
}
html.append("</br>");
//InputStream body = req.getInputStream();
resp.getWriter().write(html.toString());
}
}
示例2: 处理 HTTP 请求的 body 中的数据格式
-
如果 body 的内容格式是
x-www-form-urlencoded
,使用getParameter
进行处理- 此处是要获取 body 的数据,由于 GET 方法一般没有 body,这里使用 POST 方法演示
- 约定 body 的数据格式为:x-www-form-urlencoded
- 约定 body 的数据内容为:username=123&passwd=456
@WebServlet("/postParameter") public class PostParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); String username = req.getParameter("username"); String passwd = req.getParameter("passwd"); resp.getWriter().write("username=" + username + "</br>" +"passwd=" + passwd); } }
-
如果 body 的内容格式是
json
,首先将整个 body 都读取出来,再借助第三方库的方法按照 json 的格式来进行解析,Java 标准库没有内置对于 json 解析的方法)- 此处是要获取 body 的数据,由于 GET 方法一般没有 body,这里使用 POST 方法演示
- 约定 body 的数据格式为:json
- 约定 body 的数据内容为:
{
username=123,
passwd=456
} - 此处使用 jackson 第三方库,使用之前需要去 Maven 的中央仓库将 jackson 的依赖引入 pom.xml 中
- jackson 中的核心类是
ObjectMapper
,通过这个类的readValue(String content, Class<T> valueType)
方法,就可以将 json 字符串转化为一个类的对象(第一个参数是 json 字符串,第二个参数是类对象),ObjectMapper 会遍历定义的类中的每个成员的名称,去 json 字符串的 key 中查找,如果找到了就将对应的值返回给该成员
// 自定义的将 json 字符串转化的类 class UserInfo { public String username; public String passwd; } @WebServlet("/jsonParameter") public class JsonParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); // 1. 先将整个 body 读取出来 String body = readBody(req); // 2. 按照 json 格式进行解析 ObjectMapper objectMapper = new ObjectMapper(); UserInfo userInfo = objectMapper.readValue(body, UserInfo.class); resp.getWriter().write("username=" + userInfo.username + "</br>" + "passwd=" + userInfo.passwd); } // 定义一个方法来读取请求中的全部 body private String readBody(HttpServletRequest req) throws IOException { // 1. 先拿到 body 的长度,单位是字节 int contentLength = req.getContentLength(); // 2. 准备一个字节数组,来存放 body 内容 byte[] buffer = new byte[contentLength]; // 3. 获取到 InputStream 对象 InputStream inputStream = req.getInputStream(); // 4. 从 InputStream 对象中读取到数据,将数据放到字节数组中 inputStream.read(buffer); // 5. 将存放 body 内容的字节数组转换成字符串 return new String(buffer, "utf-8"); } }
6.3 HttpServletResponse
核心方法
方法 | 描述 |
---|---|
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name, String value) | 设置一个带有给定的名称和值的 header,如果 name 已经存在,则覆盖旧的值 |
void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header,如果 name 已经存在,不覆盖旧的值,而是添加新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码,例如 utf-8 |
void sendRedirect(String location) | 设置 Location 字段,实现重定向 |
PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据 |
示例1: 通过代码,构造出不同的响应状态码
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int status = 404;
resp.setStatus(status);
resp.getWriter().write("status=" + status);
}
}
示例2: 在响应报头设置一个 Refresh 字段,实现字段刷新程序
Refresh 的值表示每秒刷新的时间,当程序是毫秒级刷新的时候,可能存在误差
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 给响应设置一个 Refresh 的 header,每隔 1s 钟刷新一次
resp.setHeader("Refresh", "1");
// 返回一个当前的时间,用来显示刷新的效果
resp.getWriter().write("timestamp=" + System.currentTimeMillis());
}
}
示例3: 实现重定向操作
-
方法一:在响应报头设置状态码和 Location 来实现重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 将状态码设置为 3XX resp.setStatus(302); // 设置一个 Location,重定向到 CSDN 博客主页 resp.setHeader("Location", "https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
-
方法二:直接使用 sendRedirect() 方法来实现重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.sendRedirect("https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
7. 实现服务器版表白墙程序
7.1 基本介绍
在之前的文章《【Web 三件套】 JavaScript WebAPI》中实现过了一个纯前端的表白墙代码,实现后的效果如下。这次将会结合上述的知识,实现一个服务器版的表白墙程序
7.2 准备操作
-
创建好一个 Servlet 项目
-
将之前写好的纯前端的表白墙代码拷贝到 webapp 目录下
-
约定好前后端交互的接口,该程序只需约定两个接口
-
从服务器获取全部留言
-
约定请求:方法为 GET,请求路径为 /message
-
约定响应:版本号为 HTTP/1.1,状态码为 200 OK,采用 JSON 数据格式
-
JSON 具体格式为:
[ { from: "", to: "", message: "" } ]
-
-
通过客户端给服务器新增一个留言
- 约定请求:方法为 POST,请求路径为 /message
- 约定响应:版本号为 HTTP/1.1,状态码为 200 OK,提交成功后响应页面显示“提交成功”
-
-
创建一个
MessageServlet
类,@WebServlet
注解为/message
,对应着约定的请求路径,通过上方的约定完成服务器段的代码 -
更改前端的代码
7.3 代码实现
后端代码实现:
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.HttpRetryException;
import java.util.ArrayList;
import java.util.List;
// 这个类表示一条消息的详细情况
class Message{
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 通过这个数组来表示所有的消息
private List<Message> messages= new ArrayList<>();
// 通过这个代码来完成获取服务器所有消息的操作
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
// 获取到消息列表
// 此处要做的就是把当前的 messages 数组转成 json 格式返回给浏览器
ObjectMapper objectMapper = new ObjectMapper();
// 通过 ObjectMapper 的 writeValuesAsString() 方法就可以将一个对象转换成 json 字符串
// 由于这里的 message 是一个 List,那么得到的结果是一个 json 数组
String jsonString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(jsonString);
}
// 通过这个代码来完成新增消息的操作
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
messages.add(message);
resp.getWriter().write("提交成功!");
}
}
前端代码实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表白墙</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 600px;
margin: 0 auto;
}
h1 {
text-align: center;
padding: 20px 0;
color: pink;
}
p {
text-align: center;
font-size: 15px;
color: grey;
padding: 5px 0;
}
.row {
display: flex;
height: 40px;
justify-content: center;
align-items: center;
}
.row span {
width: 80px;
}
.row .edit {
width: 250px;
height: 35px;
}
.row .submit {
width: 330px;
height: 40px;
background-color: orange;
color: #fff;
border: none;
}
.row .submit:active {
background-color: grey;
}
</style>
</head>
<body>
<div class="container">
<h1>表白墙</h1>
<p>输入后点击提交,将会把消息显示在在墙上</p>
<div class="row">
<span>谁:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>对谁:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>说什么:</span>
<input type="text" class="edit">
</div>
<div class="row">
<input type="button" value="提交" class="submit">
</div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
let submitButton = document.querySelector('.submit');
submitButton.onclick = function() {
// 1. 获取到输入框里的内容
let edits = document.querySelectorAll('.edit');
let from = edits[0].value;
let to = edits[1].value;
let message = edits[2].value;
// 2. 根据输入框的内容,构造 HTML 元素,添加到页面中
if(from == '' || to == '' || message == '') {
return;
}
let div = document.createElement('div');
div.innerHTML = from + '对' + to + '说:' + message;
div.className = 'row';
let container = document.querySelector('.container');
container.appendChild(div);
// 3. 把上次输入的内容清空
for(let i = 0; i < edits.length; i++){
edits[i].value = '';
}
// 4. 把当前新增的消息发送给服务器
let body = {
from: from,
to: to,
message: message
};
$.ajax ({
url: "message",
method: "post",
contentType: "application/json;charset=utf8",
// 通过 JSON.stringify 将对象转成字符串
data: JSON.stringify(body),
success: function(data, status){
console.log(data);
}
})
}
// 服务器版本
// 1. 在页面加载的时候,从服务器获取到消息列表,并显示在网页上
function load() {
$.ajax({
method: "get",
url: "message",
success: function(data, status) {
// 此处得到的响应 data 其实已经被 jquery 转成了一个对象数组
// 但是这里的自动转换有个前提,服务器响应的 header 中 ContentType 是 json
let container = document.querySelector('.container');
for(let message of data){
// 遍历每个元素,针对每个元素拆功能键一个 div 标签
let div = document.createElement('div');
div.className = 'row';
div.innerHTML = message.from + "对" + message.to + " 说:" + message.message;
container.append(div);
}
}
})
}
load();
</script>
</body>
</html>
7.4 持久化存储
通过上述修改,原本的纯前端代码就加上了服务器,只要服务器开启后,即使刷新网页,已经添加的数据也不会消失。但是如果重启服务器的话,原本的数据就会丢失,为了解决这个问题,就需要让数据能够持久化存储。
持久化存储: 是把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘),是一种将程序数据在持久状态和瞬时状态间转换的机制。
持久化存储机制包括: JDBC 和 文件 IO
接下来将通过增加一个数据库来让上述表白墙程序可以持久化存储
-
先建库建表(可以先创建一个文件,将要建的数据库和表都写好)
drop database if exits messagewall; create database messagewall; use messagewall; drop table if exits message; create table message ( `from` varchar(50), `to` varchar(50), `message` varchar(1024) );
-
在 pom.xml 文件中引入 mysql 的 jar 包
-
连接数据库,创建一个 DBUtil 类,用于封装数据库的建立连接和资源释放操作
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; // 通过这个类来封装数据库的建立连接操作 public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/messagewall?characterEncoding=utf8&setSSL=false"; private static final String USERNAME = "root"; private static final String PASSWORD = "1234"; private static DataSource dataSource = new MysqlDataSource(); static { ((MysqlDataSource)dataSource).setURL(URL); ((MysqlDataSource)dataSource).setUser(USERNAME); ((MysqlDataSource)dataSource).setPassword(PASSWORD); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){ if(resultSet != null){ try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(statement != null){ try { statement.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(connection != null){ try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } }
-
修改 MessageWall 类的代码,主要修改的地方有两处,将原本的 messages 数组删除
-
在获取消息时,可以增加一个
getMessages()
方法,用于拿到数据库中的所有消息// 从数据库获取到所有消息 private List<Message> getMessages() { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; List<Message> messages = new ArrayList<>(); try { // 1. 和数据库建立连接 connection = DBUtil.getConnection(); // 2. 构造 sql String sql = "select * from message"; statement = connection.prepareStatement(sql); // 3. 执行 sql resultSet = statement.executeQuery(); // 4. 遍历结果集合 while(resultSet.next()){ Message message = new Message(); message.from = resultSet.getString("from"); message.to = resultSet.getString("to"); message.message = resultSet.getString("message"); messages.add(message); } } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, resultSet); } return messages; }
-
在新增消息是,可以新增一个
addMessage()
方法,用于往数据库存储一条新消息// 往数据库新增一条消息 private void addMessage(Message message) { Connection connection = null; PreparedStatement statement = null; try { // 1. 和数据库建立连接 connection = DBUtil.getConnection(); // 2. 构造 sql String sql = "insert into message values(?, ?, ?)"; statement = connection.prepareStatement(sql); statement.setString(1, message.from); statement.setString(2, message.to); statement.setString(3, message.message); // 3. 执行 sql statement.executeUpdate(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, null); } }
到这里为止,一个完整的服务器表白程序就写好啦!在我自己撸上面的代码时,由于连接 MySQL 的 URL 中的端口号写错了,导致自己找了很久的 bug,所以大家如果尝试上述代码时遇到问题,一定要看清是不是自己哪个地方打错了!
-
8. Cookie 和 Session
8.1 Cookie 介绍
在之前的文章《HTTP 协议详解》中,就介绍过了 Cookie,可以结合本文的内容来搭配理解。
-
Cookie 是什么?
Cookie 是浏览器提供的在客户端存储数据的一种机制(由于浏览器禁止了网页中的代码直接访问本地磁盘的文件,因此想要在网页中实现持久化存储,就可以通过 Cookie 这样的机制)
-
Cookie 里面存什么?
Cookie 存储的数据都是程序员自定义的,存储的数据是一个字符串,是键值对结构的,键值对之间使用
;
分割,键和值之间使用=
分割 -
Cookie 从哪里来?
服务器返回响应的时候,可以把要在客户端保存的数据以
Set-Cookie
这个 header 的方式返回给浏览器 -
Cookie 到哪里去?
客户端下次访问服务器的时候,就会把之前保存好的 Cookie 再发送给服务器
-
Cookie 的典型应用场景:
可以使用 Cookie 来保存用户的登录信息。比如我们登录过某个网站后,下次登录时就不需要重新输入用户和密码了
-
Cookie 的缺陷:
每次请求都要把该域名下所有的 Cookie 通过 HTTP 请求传给服务器,因此 Cookie 的存储容量是有限的。
在了解 Cookie 以后,我们发现 Cookie 是不能够用于存储和用户相关的直接信息的,一是 Cookie 的存储容量有限,二是发送请求时占用带宽很多,三是不太安全。即这些数据不适合保存在客户端,保存在服务器是更合适的,通过会话(Session)的方式就能够保存这些数据。
8.2 Session 会话机制介绍
基本介绍:
在计算机中,尤其是在网络应用中,Session 称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中。注意会话状态仅在支持 Cookie 的浏览器中保留。
会话的本质:
- 会话的本质就是一个哈希表,其中存储了一些键值对结构,key 叫做 sessionId,是一个不随机的、不重复的、唯一的字符串,value 就是要保存的身份信息,通过 HttpSession 对象来保存。key 和 value 都是 Servlet 自动创建的。
- 每个用户登录都会生成一个会话,服务器会以哈希表的方式将这些会话管理起来
- 一个会话的详细数据通过一个 HttpSession 对象来存储,并且 HttpSession 对象中存储的数据也是键值对结构,key 和 value 都是程序员自定义的
接着 Cooike 不适合用于存储用户相关的直接信息来讲,由于客户端不适合存储这些数据,服务器这边可以通过 Session 会话的方式来进行保存。下面将会以用户登录的流程来介绍 Session 会话机制
- 当用户成功登录之后,服务器在 Session 中会生成一个新的记录,并把 sessionId 返回给客户端(例如 HTTP 响应中可以通过 Set-Cookie 字段返回,其中 Cookie 的 key 为 “JSESSION”,value 为服务器生成的 sessionId 的具体的值)
- 然后客户端只需要保存这个 sessionId ,当后续再给服务器发送请求时,请求中就会带上 sessionId(例如 HTTP 请求中会带上 Cookie 字段用于传递 Session)
- 服务器收到请求后,就会根据请求中的 sessionId 在 Session 中查询对应用户的身份信息,在进行后续操作
Session 会话机制的好处:
- 使得客户端很轻量,不用保存太多数据
- 客户端和服务器之间传输的数据量小,节省带宽
- 数据都在服务器存储,即使客户端出现问题,数据也不会丢失
注意: Servlet 的 Session 默认是保存在内存中的,如果重启服务器 Session 数据将会丢失
8.3 Cookie 和 Session 的区别
-
Cookie 是客户端存储数据的一种机制,可以存储身份信息,也可以存储其它信息,是键值对结构的
-
Session 是服务器存储数据的一种机制,主要用于存储身份相关的信息,是键值对结构的
-
Cookie 和 Session 经常配合使用,但是不是必须的。
- Cookie 也完全可以保存一些数据在客户端,这些数据不一定是用户身份信息,不一定是 sessionId
- Session 中的 sessionId 也不需要非得通过 Cookie 和 Set-Cookie 来传递
8.4 Servlet 中 Cookie 和 Session 的核心方法
HttpServletRequest 类中的相关方法
方法 | 描述 |
---|---|
HttpSession getSession(参数) | 在服务器中获取会话,参数如果为 true,当不存在会话时会新建会话(包括生成一个新的 sessionId 和 HttpSession 对象),并通过 Set-Cookies 将 sessionId 返回给客户端;参数如果为 false,当不存在会话时会返回 null。如果存在 sessionId 且合法,就会根据这个 sessionId 找到对应的 HttpSession 对象并返回 |
Cookie[] getCookies() | 返回一个数组,包含客户端发送请求时的所有 Cookie 对象,会自动把 Cookie 中的格式解析成键值对 |
HttpServletResponse 类中的相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) | 把指定的 cookie 添加到响应中 |
HttpSession 类中的相关方法
- HttpSession是 Java平台对 session 机制的实现规范,因为它仅仅是个接口,具体实现为每个 web 应用服务器的提供商。
- 服务器会为每一个用户创建一个独立的 HttpSession,表示为一个会话,并且一个 HttpSession 对象里包含了多个键值对,可以往 HttpSession 中存储需要的数据
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在 Session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null |
void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 Session 会话中 |
boolean isNew() | 判定当前的会话是否是新创建的 |
Cookie 类中的相关方法
- 这个类描述了一个 Cookie,通过 Cookie 类创建的对象,每个对象就是一个键值对
- HTTP 的 Cookie 字段中实际上存储的是多个键值对,每个键值对在 Servlet 中都对应一个 Cookie 对象
方法 | 描述 |
---|---|
String getName() | 该方法返回 cookie 的名称(这个值是 Set-Cookie 字段设置给浏览器的,创建之后不能改变) |
String getValue() | 该方法获取与 Cookie 关联的值 |
void setValue(String newValue) | 该方法设置与 Cookie 关联的值 |
8.5 实现用户登录功能
接下来将使用上述的 Session 和 Cookie 的相关方法来实现一个用户登录功能,并且可以记录访问页面的次数
登录功能实现思路:
- 读取用户提交的用户和密码
- 对用户密码进行校验
- 判定是否登录成功
- 创建会话,保存自定义信息
- 重定向到指定页面
登录功能实现流程:
-
先实现一个登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录页面</title> </head> <body> <form action="login" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="登录"> </form> </body> </html>
-
实现一个 Servlet 来处理上面的登录请求
由于这里是通过 form 表单来构造的 post 请求,那么通过 HttpServletRequest 类中的
getParameter()
方法就能够获取请求正文中参数的值import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 1. 从请求中获取到用户名和密码 String username = req.getParameter("username"); String password = req.getParameter("password"); // 2. 对用户密码进行校验 if(username == null || "".equals(username) || password == null || "".equals(password)){ resp.getWriter().write("<h3>账号或密码不能为空!</h3>"); return; } // 3. 判断是否登录成功(假设用户名为 admin,密码为 1234。不过账号密码应该用数据库存储,这里只是用来测试) if(!username.equals("admin") || !password.equals("1234")){ resp.getWriter().write("<h3>账号或密码错误!</h3>"); return; } // 4. 登录成功,创建一个会话,用来记录当前用户的信息 HttpSession session = req.getSession(true); // 通过这个操作,就给会话中新增了一个程序员自定义的信息,访问次数 session.setAttribute("visitCount", 0); // 5. 把登录成功的结果反馈给客户端(这里的反馈不是简单的提示“登录成功”,而是直接跳转到指定页面) resp.sendRedirect("index"); } }
-
通过实现一个 Servlet 来表示登录成功后重定向的页面
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/index") public class IndexServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 只有登录成功参数才能是 true,这里是拿参数,所以要填 false HttpSession session = req.getSession(false); // 判断当前用户是否登录 if(session == null){ // 可以提示未登录,也可以重定向到登录页面 // resp.getWriter().write("<h3>登录为空!</h3>"); resp.sendRedirect("login2.html"); return; } // 表示用户登录过,获取会话中的访问次数 Integer visitCount = (Integer) session.getAttribute("visitCount"); visitCount += 1; session.setAttribute("visitCount", visitCount); resp.getWriter().write("<h3>visitCount = " + visitCount + "</h3>"); } }
-
到这里为止,一个简单的用户登录功能就实现成功了。效果如下
9. 上传文件操作
上传文件是日常开发中的一类常见需求,在 Servlet 中也进行了支持
9.1 Servlet 中上传文件的核心方法
HttpServletRequest 类中的相关方法
方法 | 描述 |
---|---|
Part getPart(String name) | 获取请求中给定 name 的文件 |
Collection<Part> getParts() | 获取所有的文件 |
Part 类中的相关方法
方法 | 描述 |
---|---|
String getSubmittedFileName() | 获取提交的文件名 |
String getContentType() | 获取提交的文件类型 |
long getSize() | 获取文件的大小,单位为字节 |
void write(String path) | 把提交的文件数据写入磁盘文件 |
9.2 上传文件操作实现
-
先写一个前端页面,用于上传文件
- 上传文件一般使用 post 请求的表单实现
- 通过 form 表单构造上传文件,要加上一个 enctype 字段,它表示 body 中的数据格式,它的默认值为:
x-www-form-urlencoded
,这里要修改成:multipart/form-data
,它是上传文件或者图片的数据格式 - 第一个 input 中 type 的值为 file,它表示第一个输入框为文件选择框,name 的值与后端中通过 getPart 获取指定文件的操作有关
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>上传文件</title> </head> <body> <form action="upload" method="post" enctype="multipart/form-data"> <input type="file" name="MyFile"> <input type="submit" value="上传"> </form> </body> </html>
-
写一个 Servlet 用于处理上传的文件
- 上传文件操作还需要给 Servlet 加上一个
@MultipartConfig
注解,否则服务器代码无法使用getPart()
方法 getPart()
方法中的参数和 form 表单input="file"
标签的 name 属性对应- 客户端一次可以提交多个文件,getPart() 方法根据 name 属性来获取不同的文件
- 写入磁盘文件操作的路径之间可以使用两个反斜杠
\\
,也可以使用一个正斜杠/
- 写入磁盘文件操作的路径最后为保存后的文件名,包括文件后缀
import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.IOException; @MultipartConfig @WebServlet("/upload") public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 通过 getPart 方法获取到前端传来的文件 Part part = req.getPart("MyFile"); // 获取文件名 String fileName = part.getSubmittedFileName(); System.out.println("文件名为: " + fileName); // 获取提交的文件类型 String fileType = part.getContentType(); System.out.println("文件类型为: " + fileType); // 获取文件的大小 long fileSize = part.getSize(); System.out.println("文件大小为: " + fileSize); // 把文件数据写入磁盘文件 part.write("C:\\Users\\bbbbbge\\Desktop\\upload.jpg"); resp.getWriter().write("上传成功!"); } }
- 上传文件操作还需要给 Servlet 加上一个
到这里为止,一个简单的文件上传操作就实现好了,我们可以通过抓包来观察下文件上传操作的请求是怎样的?
通过抓包操作我们会发现几点问题:
- 正文的大小和我们上传文件的大小不同,正文的比上传的文件的字节数略大
- 数据类型是
multipart/form-data
没有问题,但是后面多了一串boundary=----WebKitFormBoundaryAl26z0nbP6JzAUGL
,这个 boundary 在 body 中表示一个分隔线,第一条分割线下面是上传的文件的属性和文件的内容,当文件的内容结束时还有第二条分割线 - 由于有这个分割线和文件的一些属性,因此使得请求中正文的大小比上传的文件的内容略大