上一篇介绍了Tomcat的作用和使用意义
Tomcat提供http服务器,可以在把项目直接放到webapps目录下即可,但是这样得到的是一个静态页面,如果要实现网上聊天这样的交互式的动态页面,是非常困难的,于是sun公司就提供Servlet来帮助实现动态页面.
构建动态页面的技术,不同的语言提供不同的实现方式.
而Servlet就是由Java提供的一组API,它是运行在web服务器如Tomcat,可以响应http协议的请求,通过用户自己的实现逻辑,完成不同的响应,然后把结果返回给客户端(浏览器)
总而言之,Servlet把Socket,http协议,多线程并发等技术封装好了,我们不必关心这些,从而降低web app的开发门槛,从而提高开发效率
下面就开始创建第一个Servlet项目啦
软件: IDEA
打开idea创建一个Maven项目,和创建Java项目相似
什么是Maven:
Maven是一种项目构建工具,我们之前写的代码都很简单,如果是面对复杂项目,会依赖很多外部第三方库,自身还有很多模块,模块之间也有依赖关系,此时的项目表示点击运行就可以了,而Maven可以将软件项目构建过程自动化,包括清理、编译、测试、报告、打包、部署项目。
在Maven项目创建好后,会生成一个pom.xml文件:
我们需要在该文件引入Servlet API依赖的jar包,可以手动下载+手动导入,但是Maven可以支持自动下载并导入依赖.
jar文件(Java归档 Java Archice): 该文件聚合大量的Java类文件,以zip格式创建,以.jar为文件拓展名,可以被编译器和JVM这要的工具直接使用
1)在中央仓库https://mvnrepository.com/ 中搜索servlet API
2)选择版本
Tomacat和Servlet的版本要匹配
比如使用Tomcat8.5就需要使用Servlet3.1.0,可以在http://tomcat.apache.org/whichversion.html 查询版本对应关系
下载3.1.0版本
3)把中央仓库提供的xml复制到项目的pom.xml中
修改后的pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>helloServletartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
dependencies>
project>
标签内部放置项目依赖的 jar 包. maven 会自动下载依赖到本地
groupId: 表示组织名称
artifactId: 表示项目名称
version: 表示版本号
这三个就是在中央仓库确定唯一包的依赖
从左到右,依次为groupId,artifactId,version
在项目创建好了,IDEA就已经自动为我们创建好了一些目录
- src: 源代码所在目录
- main/java: 源代码的根目录,后续我们写的.java文件就放在这个目录中
- main/resources: 项目的一些资源文件所在的目录
- test/java: 测试代码的根目录
上面的目录还不能构建出一个项目,还需要创建一些目录/文件,具体有什么作用后面会解释
1**)创建 webapp 目录**
2)创建 web.xml
先在webapp目录下创建一个WEB-INF
目录,然后再该目录下创建一个web.xml文件
3)编辑 web.xml
往 web.xml 中拷贝以下代码. 具体细节内容我们暂时不关注
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 Applicationdisplay-name>
web-app>
wabapp在部署到Tomcat中的一个重要目录,可以放入一些静态资源,如CSS,HTML等等,
web.xml是为了使Tomcat能够正确处理webapp中的动态资源
在main/java下创建HelloServlet
的.java文件,继承于HttpServlet类,并且重写其doGet
方法
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;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello");
resp.getWriter().write("hello");
}
}
解释说明:
- HttpServlet类是
javax.servlet.http
包下采用HTTP通信协议的类- @WebServlet(“/hello”)注解,表示Tomcat收到的请求中,路径为
/hello的请求才会调用HelloServlet类的代码
- 重写 doGet 方法. doGet 的参数有两个, 分别表示收到的 HTTP 请求 和要构造的 HTTP 响应. 这个方法会在 Tomcat 收到 GET 请求时触发
- HttpServletRequest 表示 HTTP 请求. Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body 等) 都是通过这个对象来获取
- HttpServletResponse 表示 HTTP 响应. 代码中把响应对象构造好(构造响应的状态码, header, body 等)
- resp.getWriter() 会获取到一个流对象, 通过这个流对象就可以写入一些数据, 写入的数据会被构造成一个 HTTP 响应的 body 部分, Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览器
注意:
上面的代码和平时写的代码有很大不同
- 当前代码不是通过main方法作为入口,main方法已经包含在Tomcat中,在合适的时机被Tomcat调用,所以当前的代码只是完整程序的一部分逻辑代码
- 对于可以被Tomcat调用的类所需要的条件主要有下面三个条件
- 创建的类需要继承HttpServlet
- 这个类需要使用@WebServlet注解来关联上一个HTTP路径
- 这个类需要实现doXXX方法
使用Maven打包,一般在右侧可以看见,如果没有.可以按照菜单 -> View -> Tool Window -> Maven 打开)
双击package,等待片刻后,就可以看见打包成功的信息
如果打包失败,可以根据错误消息解决
打包成功后,可以看见在target目录下生成了一个jar包
但是jar包不是我们需要的,Tomcat能够识别的是另一种war包格式
jar包和war包的区别:
- jar是普通java程序打包的结果,包含一些.class文件,而war包是java web的程序,除了.class文件,还包含了HTML,CSS,图片,以及其他jar包,打包成war包格式才可以被Tomcat识别
helloServlet-1.0-SNAPSHOT.jar
的由来由artifactId和version中的字符串拼接而成
1)把jar包转换成war包
在pom.xml中新增一个标签,表示打包成一个war包
<packaging>warpackaging>
2)修改生成war包的名称
<build>
<finalName>ServletHelloWorldfinalName>
build>
完整的pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>helloServletartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
dependencies>
<packaging>warpackaging>
<build>
<finalName>ServletHelloWorldfinalName>
build>
project>
重新使用Maven打包
如果没有打包成功没有看见target
目录生成,可能是没有勾选Show Excluded Files
把war包copy到Tomcat/webapps下
启动Tomcat它就会把war包解压
使用浏览器访问http://127.0.0.1:8080/ServletHelloWorld/hello
说明程序运行成功
注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
先总结一下是如何创建一个Servlet项目
- 创建项目(利用Maven对项目创建,打包,部署等等)
- 引入依赖(引入Servlet所需的jar包 - 直接使用Maven引入,不必下载)
- 创建目录(创建webapp -> WEB-INF ->web.xml,让Tomcat正确处理)
- 编写代码(继承自HttpServlet类,重写doGet方法,并且加上@WebServlet注解)
- 打包程序(先修改pom.xml中的packing标签中的打包格式为war同时修改finalName来设置包名,最后使用Maven 打包)
- 部署程序(把war包拷贝到Tomcat的wabapps目录中,然后重启Tomcat服务器)
- 验证程序(使用浏览器输入URL构建HTTP请求访问Tomcat服务器, URL = Context Path(war包的名字) + Servlet Path(注解中的名字))
我们可以发现1~3步是每次创建项目后就不必重复操作了,而4 和7每次修改代码后都要修改,5,6就是可以简化的操作,这里的简化可以使用插件来完成.
上面把war包拷贝到webapps中太过于复杂,可以使用IDEA提供的插件Smart Tomcat(后面还可以使用SpringBoot)
插件:对程序的一些特定场景,做出一些特定功能的扩展
Create configuration
创建配置点击绿色三角形,IDEA就会自动进行编译, 部署, 启动 Tomcat 的过程
此时在IDEA控制台中输出的Tomcat日志就没有乱码问题了
在浏览器中输入http://127.0.0.1:8080/helloServlet/hello
注意: URL 中的 PATH 也被分成两个部分, 和刚才不同的是, helloServlet 为 Context Path(在插件中配置的,默认为类名), hello 为 Servlet Path
在使用Smart Tomcat部署时并没有在Tomcat的webapps中拷贝war.
Smart Tomcat相当于在Tomcat启动时直接引用项目中的webapp和target目录
404 表示用户访问的资源不存在. 可能是 URL 的路径写的不正确 或者注解内容不匹配
错误示例1: 通过 /hello 访问服务器
错误示例2: 通过 /ServletHelloWorld 访问服务器
错误示例3: 注解内容不匹配
错误示例4: web.xml内容出错
405 表示对应的 HTTP 请求方法没有实现
错误实例: 没有实现 doGet 方法
往往是 Servlet 代码中抛出异常导致的
错误实例:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello");
String s = null;
resp.getWriter().write(s);
}
}
错误信息会表示出来
不填写响应
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello");
//resp.getWriter().write("hello");
}
}
错误示例1 : 注解内容忘记打/
错误示例2 : 未启动插件
在初学Servlet时,会遇见很多不同的问题,重点是学习排除错误和分析错误的思路
熟悉HTTP协议可以更加快速的寻错.
- 4xx的状态码表示路径出错,通常需要检查URL, 以及代码中的Context path,Servlet Path是否一致
- 5xx的状态码表示服务器出错,通常需要检查控制台的中打印的日志信息是否有报错
- 出现无法连接,通常需要检查Tomcat是否启动,如果启动了,就看看日志是否报错
- 空白页面需要使用抓包工具分析HTTP的请求响应过程
Servlet 提供了许多API,下面重点讲解HttpServlet,HttpServletRequset,HttpServletResponse
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
解释说明:
下面就演示如何使用这些方法,在使用之前要先构造请求,请求的构造有很多方法
- 利用浏览器的地址栏,通过url构造
- 利用前端的from表单构建(只能构建get和post)
- 利用前端的ajax构造
- 利用前端的src属性/herf属性构建
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>Documenttitle>
head>
<body>
<script src="https://code.jquery.com/jquery-3.6.1.min.js">script>
<script>
$.ajax({
type: 'post',
url: 'hello',
// 在控制台中查看结果
success: function(body) {
console.log(body);
}
});
script>
body>
html>
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPost");
}
}
http://127.0.0.1:8080/helloServlet/test.html
下面利用按键触发不同请求并把响应显示到页面上,同样利用ajax构建
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>Documenttitle>
<style>
.one {
font-size: 50px;
}
style>
head>
<body>
<div class="one">div>
<button id="doGet">getbutton>
<button id="doPost">postbutton>
<button id="doPut">putbutton>
<script src="https://code.jquery.com/jquery-3.6.1.min.js">script>
<script>
let doGetBtn = document.querySelector('#doGet');
doGetBtn.onclick = function() {
$.ajax({
type: 'get',
url: 'hello',
success: function(body) {
let div = document.querySelector('.one');
div.innerHTML = body;
}
})
};
let doPostBtn = document.querySelector('#doPost');
doPostBtn.onclick = function() {
$.ajax({
type: 'post',
url: 'hello',
success: function(body) {
let div = document.querySelector('.one');
div.innerHTML = body;
}
})
};
let doPutBtn = document.querySelector('#doPut');
doPutBtn.onclick = function() {
$.ajax({
type: 'put',
url: 'hello',
success: function(body) {
let div = document.querySelector('.one');
div.innerHTML = body;
}
})
}
script>
body>
html>
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet");
resp.getWriter().write("doGet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPost");
resp.getWriter().write("doPost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPut");
resp.getWriter().write("doPut");
}
}
使用第三方工具PostMan构建请求(原本时chrome的插件,现在已经独立成一个程序了),
下载网站: Postman API Platform | Sign Up for Free 根据自己的电脑下载对应版本
根据get请求
根据上图来分析其生命周期
- 当服务器收到HTTP请求后,会转发给Servlet容器,如果没有创建Servlet对象就会实例化该Servlet对象,随后调用init(),该方法只会调用一次,直到对象销毁.之后接受的请求都会被service()处理.
- 每次收到请求,都会调用service()方法,在其内部分析当前请求类型并调用对应doXXX方法
- 在对象销毁之前调用destory()一次实现资源的清理.
当Tomcat通过Scoket API 读取HTTP请求(字符串),然后把该请求按照HTTP协议的格式把字符串解析为一个HttpServletRequest对象.其对象中就包含了
- 请求的方法
- url
- 版本号
- body
- header
- …
下面介绍了如何通过该类提供的方法来获取这些内容.
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本。 |
String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该 请求的 URL 的一部分。 |
String getContextPath() | 返回指示请求上下文的请求 URI 部分。 |
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() | 用于读取请求的 body 内容. 返回一个 InputStream 对象 |
注意:
- URL(统一资源标识符),是一种抽象的概念,即不管用什么方法只要能够唯一标记某个资源,就是URI;URL(统一资源定位符),是通过网络地址标记资源的符号,综上:URL是URI的子集
- 请求对象是服务器接收到的信息,只能’读’,不能’写’.
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.util.Enumeration;
@WebServlet("/showRequest")
public class showRequest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置 Context 内容的格式,以及编码规范,防止出现乱码和浏览器不能识别html的标签(例如
标签)
resp.setContentType("text/html; charset = utf-8");
// 存放对象中读取的内容
StringBuffer respBody = new StringBuffer();
//返回请求协议的名称和版本
respBody.append(req.getProtocol()).append("
");
//返回请求的 HTTP 方法的名称
respBody.append(req.getMethod()).append("
");
//从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分
respBody.append(req.getRequestURI()).append("
");
//返回指示请求上下文的请求 URI 部分
respBody.append(req.getContextPath()).append("
");
//返回包含在路径后的请求 URL 中的查询字符串
respBody.append(req.getQueryString()).append("
");
respBody.append("headers: "
);
// 迭代获取header中的内容
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
respBody.append(headerName + " ");
respBody.append(req.getHeader(headerName));
respBody.append("
");
}
resp.getWriter().write(respBody.toString());
}
}
注意:
- 一定要设置编码规范,否则会出现乱码的情况,这是因为浏览器默认和windows的编码规范一样都是GBK,而服务器是UTF-8,不一致会出现乱码.
- header中的内容是以键值对的形式组织,所以可以根据键名,获取对应的值.
在URL中query string
中可以传递参数,而在服务器可以调用getParameter
这个API来获取参数
getParameter
类@WebServlet("/getParameter")
public class getParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset = utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId: " + userId + "classId: " + classId);
}
}
注意:
- getParameter的返回值默认为String,如果对参数类型有需要,可以利用对应包装类的装箱方法
- 后端获取
query string
中的key是事先约定好的,不会存在不含有key的情况
对于POST请求中的参数一般是通过body传递给服务器,而body中的数据格式有很多,下面是通过form表单的形式传递参数
PostParameter
@WebServlet("/getParameter")
public class getParameter extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset = utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write("userId: " + userId + "classId: " + classId);
}
}
testPost.html
提交页面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>Posttitle>
head>
<body>
<form action="postParameter" method="post">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
form>
body>
html>
点击提交后跳转
这里的参数类型为
Content-Type: application/x-www-form-urlencoded
利用postman构造POST请求
postParameterJson
类@WebServlet("/postParameterJson")
public class postParameterJson extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=utf-8");
String body = readBody(req);
resp.getWriter().write(body);
}
private String readBody(HttpServletRequest req) throws IOException {
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
return new String(buffer, "utf-8");
}
}
上面的JSON格式很简单,但是如果JSON格式变复杂了(例如嵌套JSON数据),此时如果由程序员自己写代码来解析并获取内部的键值对,就很艰难了.这里靠谱的解决方案是使用第三方库,本文讲解的是Jackson(同类的还有Gson, FastJson…).
引入Jackson步骤:
postParameterJson
类class JsonData {
public int userId;
public int classId;
}
@WebServlet("/postParameterJson")
public class postParameterJson extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=utf-8");
String body = readBody(req);
// 创建ObjectMapper 对象
ObjectMapper objectMapper = new ObjectMapper();
// 使用readValue方法把body字符串转换为JsonData对象
JsonData jsonData = objectMapper.readValue(body, JsonData.class);
resp.getWriter().write("userId: " + jsonData.userId + ", " + "classId: " + jsonData.classId);
System.out.println("userId: " + jsonData.userId + ", " + "classId: " + jsonData.classId);
System.out.println(objectMapper.writeValueAsString(jsonData));
}
private String readBody(HttpServletRequest req) throws IOException {
int contentLength = req.getContentLength();
byte[] buffer = new byte[contentLength];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
return new String(buffer, "utf-8");
}
}
注意:
引入依赖后别忘记下载依赖,要不然没有
ObjectMapper
类,可以在C:\Users\a\.m2
中查看,刚才下载的Jackson在Jackson的使用其实就是把json格式的字符串转换为java对象,然后把java对象转换为json字符串,而Jackson提供
ObjectMapper
类帮助我们完成相互转换过程创建的JsonData类中的属性的名字和类型要和body中字符串的key对应,访问权限是public,通过提供getter方法获取字段信息,
Jackson 库的核心类为 ObjectMapper. 其中的 readValue 方法把一个 JSON 字符串转成 Java 对象. 其中的 writeValueAsString 方法把一个 Java 对象转成 JSON 格式字符串.
readValue 的第二个参数为 JsonData 的 类对象. 通过这个类对象, 在 readValue 的内部就可以借助反射机制来构造出 JsonData 对象, 并且根据 JSON 中key 的名字, 把对应的 value 赋值给JsonData 的对应字段
如果json中key和类属性中名字不一致时,都会出现 500 状态码
Servlet中使用doXXX方法获取请求,然后根据请求计算出响应,随后把响应数据设置到HttpServletResponse 对象中
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过Socket 写回给浏览器
方法 | 描述 |
---|---|
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) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例 如,UTF-8。 |
void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端。 |
PrintWriter getWriter() | 用于往 body 中写入文本格式数据. |
OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据. |
注意:
- 对于请求是可读不可写的,对于响应是可写不可读
- 对响应的状态码/响应头/响应内容格式都是要在写操作(使用 getWriter() /getOutputStream())之前进行的
根据用户在浏览器中输入的参数指定要返回的状态码
@WebServlet("/statusServlet")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String status = req.getParameter("status");
if (status != null) {
resp.setStatus(Integer.parseInt(status));
}
resp.getWriter().write("status: " + status);
}
}
注意:
对于状态码404是默认服务器会显示:
但是有些服务器会个性化定制404
实现一个程序, 让浏览器每秒钟自动刷新一次. 并显示当前的时间戳
@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Refresh", "1");
long time = System.currentTimeMillis();
resp.getWriter().write("timeStamp: " + time);
}
}
应用场景: 文字直播(对现场直播转换为文字并对关键信息实时推送)
例如冬奥会中的文字直播
实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面
@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("http://www.sogou.com");
}
}
http://127.0.0.1:8080/helloServlet/redirectServlet
通过利用Servlet提供的API就可以把之前的表白墙修改为服务器版本,实现前后端分离.
创建Maven项目
按照之前的创建目录: webapp, WEB-INF,web.xml
<!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>
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>表白墙服务器版</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!-- 含义是只是在开发阶段才会需要这个依赖,而通过Tomcat部署后就不需要了 -->
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
</project>
把之前写好的表白墙前端代码放入webapp目录下
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>Documenttitle>
<style>
* {
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 600px;
margin: 0 auto;
background-color: pink;
border-radius: 20px;
}
h1 {
text-align: center;
padding: 20px 0;
}
p {
color: #666;
text-align: center;
font-size: 14px;
padding: 10px 0;
}
.row {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
span {
width: 100px;
line-height: 40px;
}
.edit {
width: 200px;
height: 30px;
}
.submit {
margin-top: 15px;
width: 300px;
height: 40px;
color: #fff;
background-color: rgb(77, 163, 202);
border: none;
}
.submit:active {
background-color: gray;
}
style>
head>
<body>
<div class="container">
<h1>表白墙h1>
<p>输入信息后提交,将会出现在表白墙上哦p>
<div class="row">
<span>谁:span>
<input class="edit" type="text">
div>
<div class="row">
<span>对谁:span>
<input class="edit" type="text">
div>
<div class="row">
<span>说什么:span>
<input class="edit" type="text">
div>
<div class="row">
<input type="button" value="提交" class="submit">
div>
div>
<script src="https://code.jquery.com/jquery-3.6.1.min.js">script>
<script>
var cut = 1;
var submit = document.querySelector('.submit');
submit.onclick = function () {
// 1.获取编辑框内容
var editor = document.querySelectorAll('.edit');
var from = editor[0].value;
var to = editor[1].value;
var message = editor[2].value;
if (from == '' || to == '' || message == '') {
return;
}
// 2.构造子元素
var row = document.createElement('div');
row.className = 'row';
row.innerHTML = '留言' + cut + ': ' + from + ' 对 ' + to + ' 说 ' + message;
row.style = 'font-size:10px font:宋体; height:35px'
cut++;
// 3.新增元素
var container = document.querySelector('.container');
container.appendChild(row);
// 4.清除编辑框内容
for (var i = 0; i < 3; i++) {
editor[i].value = '';
}
}
script>
body>
html>
就是浏览器向服务器发送哪些HTTP请求,服务器处理这些请求并计算出响应,在这个过程中,约定传输和解析数据的格式,请求和响应的格式等等.
在第一次访问页面时应该获取之前存在的全部留言并显示到页面,当新增留言时就应该把留言内容传输给服务器并由它保存.在获取留言和新增留言的过程就涉及到浏览器发送请求,服务器返回响应的过程.
1)约定获取全部留言交互格式
请求: GET
GET /message
响应: JSON格式
[
{
from: "黑猫",
to: "白猫",
message: "喵"
},
{
from: "黑狗",
to: "白狗",
message: "汪"
},
......
]
2)新增新留言
请求: JSON格式
POST /massage
{
from: "黑猫",
to: "白猫",
message: "喵"
}
响应: JSON格式
{
ok:1
}
ok为1说明新增成功,0说明新增失败
注意:
json格式传输数据时,采取的时键值对的格式, 这里的键应该是字符串类型的(这里为了省事,省略了),如果在PostMan中测试时请务必使用字符串.
这里的"约定"其实就是之前讲的应用层协议之自定义协议,这里的约定也可以采取其他方式,只要能够到达预期的效果就可以了
1)创建Message类
class Message {
public String from;
public String to;
public String message;
@Override
public String toString() {
return "Message{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", message='" + message + '\'' +
'}';
}
}
Message类的成员变量就是传递过程中JSON中键值对中的值,所以在类外要能够访问,一般设置访问权限为public,如果降低访问权限就需要提供getter()方法,总之,要提供获取成员变量的方法.
2)创建MessageServlet类
这个类应该实现前后端的交互功能,即浏览器向服务器获取数据和浏览器提交数据到服务器.根据上面的约定,服务器应该分别使用doGet()方法和doPost()方法.
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 保存所有的留言
private List<Message> messages = new ArrayList<>();
// 用于转换JSON格式的字符串
private ObjectMapper objectMapper = new ObjectMapper();
// 获取所有的留言
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 这里就是约定的JSON格式
resp.setContentType("application/json;charset=utf-8");
// 把list中的Message对象转换为JSON格式并写入响应返回给服务器
String respString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(respString);
}
//新增留言
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
messages.add(message);
resp.getWriter().write("{ \"ok\": 1 }");
}
}
添加下面的事件函数
<!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;
}
.container {
width: 600px;
height: 600px;
margin: 0 auto;
background-color: pink;
border-radius: 20px;
}
h1 {
text-align: center;
padding: 20px 0;
}
p {
color: #666;
text-align: center;
font-size: 14px;
padding: 10px 0;
}
.row {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
span {
width: 100px;
line-height: 40px;
}
.edit {
width: 200px;
height: 30px;
}
.submit {
margin-top: 15px;
width: 300px;
height: 40px;
color: #fff;
background-color: rgb(77, 163, 202);
border: none;
}
.submit:active {
background-color: gray;
}
</style>
</head>
<body>
<div class="container">
<h1>表白墙</h1>
<p>输入信息后提交,将会出现在表白墙上哦</p>
<div class="row">
<span>谁:</span>
<input class="edit" type="text">
</div>
<div class="row">
<span>对谁:</span>
<input class="edit" type="text">
</div>
<div class="row">
<span>说什么:</span>
<input class="edit" type="text">
</div>
<div class="row">
<input type="button" value="提交" class="submit">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
var cut = 1;
var submit = document.querySelector('.submit');
submit.onclick = function () {
// 1.获取编辑框内容
var editor = document.querySelectorAll('.edit');
var from = editor[0].value;
var to = editor[1].value;
var message = editor[2].value;
if (from == '' || to == '' || message == '') {
return;
}
// 2.构造子元素
var row = document.createElement('div');
row.className = 'row';
row.innerHTML = '留言' + cut + ': ' + from + ' 对 ' + to + ' 说 ' + message;
row.style = 'font-size:10px font:宋体; height:35px'
cut++;
// 3.新增元素
var container = document.querySelector('.container');
container.appendChild(row);
// 4.清除编辑框内容
for (var i = 0; i < 3; i++) {
editor[i].value = '';
}
//[新增内容]5. 需要把输入的内容通过ajax把内容转换为json格式构造成POST方法,交给后端服务器
let massageJson = {
"from": from,
"to": to,
"message":message
};
$.ajax({
type:'post',
url:'message',
contentType:'application/json;charset=utf-8',
data:JSON.stringify(massageJson),
success: function(body) {
alert("提交成功!");
},
error: function() {
// 会在服务器返回的状态码部署2XX时调用
alert("提交失败!");
}
});
}
//[新增内容]6. 在页面加载时调用,从服务器中获取所有的留言信息并显示到页面上
function load() {
$.ajax({
type:'get',
url:'message',
success: function(body) {
// 从服务器得到的body已经是一个js对象的数组
// 这是因为服务器本来返回的是JSON格式的字符串,但是ajax会根据Content-Type为application/json
// 对body自动解析为js对象的数组,然后遍历数组并把其内容填充到页面上
let container = document.querySelector('.container');
for (let message of body) {
let newDiv = document.createElement('div');
newDiv.className = 'row';
newDiv.innerHTML = message.from + " 对 " + message.to + " 说 " + message.message;
container.appendChild(newDiv);
}
}
})
}
// 在页面加载时自动调用
load();
</script>
</body>
</html>
配置好Smart Tomcat
再次理解Context Path和 Servlet Path
通常访问Servlet程序对应的路径是两级目录:
- 第一级路径是Context Path代表当前的webapp(网站),再一个Tomcat上可以同时部署多个webapp(网站),webapps目录下的每个目录都是一个单独的webapp
- 如果是通过startup.bat启动的Tomcat,那么webapps中对应的war包就是这个wabapp的context Path
- 如果是通过smart Tomcat 启动Tomcat,那么参数的Context Path就是 上面配置中指定的目录
- 第二级目录Servlet Path就是根据@WebServlet(“/message”)的内容确定,或者就是webapp下的静态文件.目录
实现效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OxjktIaY-1666595270341)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20221015215138436.png)]
下面使用Fiddler来查看其前后端交互过程
在第一次加载页面时就发生了两次HTTP请求
第一次:
响应返回类型为text/html,是为了获取html页面内容
第二次:
页面调用load()函数,从服务器中获取留言数据
由于开始没有数据返回的是一个空数组
请求类型为POST
请求中需要新增的JSON数据
服务器成功接受的响应内容
刷新页面浏览器会向服务器获取全部留言信息
这是因为浏览器发起的load()函数中的ajax会触发GET请求调用doXXX方法,在服务器返回响应(getWriter().write()中的respString字符串)就会触发浏览器的代码(ajax的success回调函数->响应字符串作为实参传递给回调函数中的body形参),从而把服务器中的留言重新布置
所以load()函数回车打开页面/刷新页面时调用
但是上面的代码还存在一个问题,那就是数据不可以持久化存储,当服务器关闭时,存储的数据会被清空,那么如何解决?
解决方案:
- 存文件.所以IO流来操作文件
- 数据库: 使用MySQL和JDBC
主流使用的是数据库,下面先提供文件存放的代码
数据存放在文件中要指定数据的格式,我们约定以行文本的方式存储,索引一行数据代表一组数据,每个字段之间用\t
来划分
class Message {
public String from;
public String to;
public String message;
@Override
public String toString() {
return "Message{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", message='" + message + '\'' +
'}';
}
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 保存所有的留言
//private List messages = new ArrayList<>();
// 用于转换JSON格式的字符串
private ObjectMapper objectMapper = new ObjectMapper();
// 获取存放和提取的文件路径
private String filePath = "d:/message.txt";
// 获取所有的留言
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Message> messages = load();
resp.setContentType("application/json;charset=utf-8");
String respString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(respString);
}
// 新增留言
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
save(message);
resp.getWriter().write("{\"ok\" : 1}");
}
// 把数据保存到文件中
private void save(Message message) {
System.out.println("向文件中写入数据");
try (FileWriter fileWriter = new FileWriter(filePath, true)){
fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
// 实时更新留言信息
private List<Message> load() {
List<Message> messages = new ArrayList<>();
System.out.println("从文件中读取数据");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))){
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
break;
}
String[] tokens = line.split("\t");
Message message = new Message();
message.from = tokens[0];
message.to = tokens[1];
message.message = tokens[2];
messages.add(message);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("共读取" + messages.size() + "条记录");
return messages;
}
}
虽然文件存储是解决重启服务器数据丢失的问题,但是使用数据库能够提供更多查找/新增/修改数据的方法.并且如果数据量大,数据复杂就需要更多代码来组织这些数据了.相比之下,还是数据库香.
1)引入依赖
之前我们使用MySQL是下载jar包,现在只需要引入依赖,版本用这个很好,我突发奇使用8.0.12,结果出现版本不兼容,出现异常
所以还是使用根据自己的MySQL版本选择合适的依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
2)创建数据库和message表
set character_set_database=utf8;
set character_set_server=utf8;
create database if not exists MessageWall;
use MessageWall;
drop table if exists messages;
create table messages(`from` varchar(100), `to` varchar(100), message varchar(100));
3)创建DBUtil类用于完成和数据库的连接过程
import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 此处建立连接需要使用DataSource,并且一个程序有一个DataSource示例即可,所以使用单例模式实现
public class DBUtil {
private static DataSource dataSource = null;
// 获取示例对象
private static DataSource getDataSource() {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/MessageWall?characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("123456");
}
return dataSource;
}
// 获取连接对象
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
// 推荐写成分开的 try catch.
// 保证及时一个地方 close 异常了, 不会影响到其他的 close 的执行.
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
4)修改MessageServlet 类
// 从数据库中查询数据
private List<Message> load() {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
// 存放查询的数据
List<Message> messageList = 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");
messageList.add(message);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
// 5. 释放资源
DBUtil.close(connection, statement, resultSet);
}
return messageList;
}
// 把新增的数据存放到数据库中
private void save(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 语句
int ret = statement.executeUpdate();
if (ret != 1) {
System.out.println("插入失败!");
} else {
System.out.println("插入成功!");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 4. 关闭连接
DBUtil.close(connection, statement, null);
}
}
因为HTTP协议是无状态的协议
对事物处理没有记忆能力,每次客户端和服务器会话完成后,下一次会话和这次没有直接联系
但是在有些场景下需要去保存请求之间的联系,
比如用户登录场景, 在用户登录第一次登录后,下一次就不必再次登录,这时候就可以使用Cookie来保存这些信息
图中的令牌就是存储在Cookie中.服务器就需要记录令牌信息和其对应的用户信息,这就是下面Session需要完成的
cookie特性:
- Cookie是存放在浏览器上,为了安全考虑不会让页面直接去访问文件,而是去访问浏览器中的Cookie
- **Cookie是不可跨域的,**每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,但是可以使用domain(Cookie属性)来访问一级域名和二级域名
Cookie属性:
属性 | 说明 |
---|---|
name=value | 按照键值对的方式来组织数据 |
domain | 指定当前cookie所在的域名,默认位当前域名 |
maxAge | cookie 失效的时间(s),如果是正数,代表在maxAge后该cookie失效,如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie .如果为 0,表示删除该 cookie .默认为 -1。 |
典型应用: 保存用户身份信息
利用Cookie和Session管理用户身份信息认证.
说明:
- 由服务器保存用户详细信息,在用户第一次登录由服务器生成唯一字段key,就是SessionID/Token,是按照键值对的形式存储,其value就可以根据需求存储用户信息,登录时间等等.
- 在后续用户在给服务器发送请求时,在HTTP请求的Cookie字段中带上Session/token,用户就不必重复登录
- 在服务器接受到HTTP请求后,根据请求中的Session/token在哈希表中查找用户对应信息.
Token的引入:
Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生.
Token的定义:
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码
使用Token的目的:
Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮
如何使用Token:
- 用设备号/设备mac地址作为Token
- 用session值作为Token
通常情况下使用的是方式2,因为Session是服务器和客户端报错通信的唯一识别信息,在同一用户的多次请求中,Session始终都是同一个对象,而不是多个对象,因为可以对它加锁,所以在处理并发场景下用户多次请求时,就可以使用Token验证是否是相同的,如果不一致就可以认为是重复的请求(因为对Token加锁,不是锁所有者访问就会得到一个不同的Token,所以被认定为重复提交),将会被拒绝.
总结: 由于SessioId和Token使用方式的相似,我们可以理解为同一东西的不同叫法
Cookie和Session是实现会话的技术手段
会话: 浏览器第一次向服务器发送请求时,会话建立,直到有一方断开连接为止.在这次会话中可以有多次请求和响应
区别:
- Cookie是客户端的机制,Session是服务器的机制
- 存储数据大小不同: Cookie保存的数据不能大于4kb(对同一个域名下的总Cookie数量限制为20),虽然Session没有大小限制,但是占用过多会提高服务器的负担
- 安全性不同: Session比Cookie安全
- 生命周期不同: Cookie可设置为长时间保存,比如默认登录功能;Session一般会在客户端关闭或者Session超时都会失效
HttpServletRequest 类中的相关方法
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果 为 false, 则当不存在会话时返回 null |
Cookie[] getCookies() | 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把 Cookie 中的格式解析成键值对 |
HttpServletResponse 类中的相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) | 把指定的 cookie 添加到响应中 |
HttpSession 类中的相关方法
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在该 session 会话中具有指定名称的对象,如果没 有指定名称的对象,则返回 null. |
void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 session 会话 |
boolean isNew() | 判定当前是否是新创建出的会话 |
Cookie 类中的相关方法
方法 | 描述 |
---|---|
String getName() | 该方法返回 cookie 的名称。名称在创建后不能改变。(这个值是 Set-Cookie 字段设置给浏览器的) |
String getValue() | 该方法获取与 cookie 关联的值 |
void setValue(String newValue) | 该方法设置与 cookie 关联的值 |
注:
- HTTP 的 Cookie 字段中存储的实际上是多组键值对. 每个键值对在 Servlet 中都对应了一个 Cookie对象.
- 通过
HttpServletRequest.getCookies()
获取到请求中的一系列 Cookie 键值对.- 通过
HttpServletResponse.addCookie()
向响应中添加新的 Cookie 键值对.
利用form表单提交用户信息,实现用户登录的监测登录次数
1)创建IndexServlet 类
用于登录成功后显示用户信息在页面上
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
// 1.判断用户是否已经登录过
HttpSession session = req.getSession(false);
// 如果用户没有登录,则要求用户重新登录
if (session == null) {
// 跳转到登录页面
resp.sendRedirect("login.html");
return;
}
// 2. 登录成功,获取 会话 中的数据
String username = (String)session.getAttribute("username");
Integer loginCount = (Integer)session.getAttribute("loginCount");
// 访问次数加一
loginCount += 1;
// 更新访问次数
session.setAttribute("loginCount",loginCount);
// 3. 返回数据给响应,并显示到页面
resp.getWriter().write("当前用户为: " + username + " 访问次数: " + loginCount);
}
}
2)创建 login.html
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>Documenttitle>
head>
<body>
<form action="login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
form>
body>
html>
- input中的name属性是按照post的格式,把用户名和密码通过body键值对的形式传入服务器
3)创建 LoginServlet 类
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
// 1. 获取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 2. 验证用户信息是否合法
if (username == null || username.equals("") || password == null || password.equals("")) {
// 返回一个提示.
resp.getWriter().write("用户名或者密码不完整! 登录失败!");
return;
}
if (!username.equals("admin") || !password.equals("123")){
resp.getWriter().write("用户名或者密码错误! 登录失败!");
return;
}
// 3. 登录成功,建立一个会话,然后把用户信息写入会话中
HttpSession session = req.getSession(true);
session.setAttribute("username", "admin");
Integer visitCount = (Integer)session.getAttribute("loginCount");
if (visitCount == null) {
session.setAttribute("loginCount", 0);
} else {
}
resp.sendRedirect("index");
}
}
- 此处的 getSession 参数为 true, 表示查找不到 HttpSession 时会创建新的 HttpSession 对象, 并生成一个 sessionId, 插入到 哈希表 中, 并且把 sessionId 通过 Set-Cookie 返回给浏览器
4)部署程序
5)流程详解
第一次请求: 获取login.html的页面
不存在Cookie
第一次响应:
也没有Set-cookie属性
第二次请求验证用户名和密码是否正确
第二次响应set-cookie属性已经含有
这里的
JSESSIONID
就是Servlet自动生成的key,value就是8A6EA685A2786F306ECD62ED0D21129F,也是服务器生成的SessionId
第三次请求携带刚才的Cookie访问服务器
在浏览器中也可以看见Cookie和Session
Cookie中存放一些信息
第三次响应中包含用户登录次数的信息
后续的请求都会带着这个Cookie去访问服务器
HttpServletRequest 类方法
方法 | 描述 |
---|---|
Part getPart(String name) | 获取请求中给定 name 的文件 |
Collection getParts() | 获取所有的文件 |
Part 类方法
方法 | 描述 |
---|---|
String getSubmittedFileName() | 获取提交的文件名 |
String getContentType() | 获取提交的文件类型 |
long getSize() | 获取文件的大小 |
void write(String path) | 把提交的文件数据写入磁盘文件 |
1)创建 upload.html, 放到 webapp 目录中
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>Documenttitle>
head>
<body>
<form action="upload" enctype="multipart/form-data" method="post">
<input type="file" name="MyFile">
<input type="submit" value="提交文件">
form>
body>
html>
- 一般是利用form表单的POST请求实现上传文件
- form表单中的
enctype="multipart/form-data"
指定上传的是文件- input标签中的name属性值对应的就是
Part getPart(String name)
中的name
2)创建 UploadServlet 类
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part part = req.getPart("MyFile");
// 文件名
System.out.println(part.getSubmittedFileName());
// 文件类型
System.out.println(part.getContentType());
// 文件大小
System.out.println(part.getSize());
// 上传文件到服务器
part.write("d:/MyImage.jpg");
resp.getWriter().write("upload ok");
}
}
- 需要给 UploadServlet 加上 @MultipartConfig 注解. 否则服务器代码无法使用 getPart 方法
- getPart 的 参数 需要和 form 中 input 标签的 name 属性对应
- 客户端一次可以提交多个文件. (使用多个 input 标签). 此时服务器可以通过 getParts 获取所有的Part 对象
3)部署程序,
点击上传文件时的post请求:
数据格式:
开头的和form表单中的对应,后面的决定上传文件的起始和结束边界,可以通过查看body部分观察到
文件开始标志
携带了文件的相关属性
文件结束标志