Servlet
文章目录
1. Servlet是什么
Servlet是一种实现动态页面的技术,是一组Tomcat提供给程序猿的API,帮助程序猿简单高效的开发一个web app,主要是开发动态页面
静态页面也就是内容始终固定的页面,即使用户不同/时间不同/输入的参数不同,页面内容也不会发生变化。
动态页面就是用户不同/时间不同/输入的参数不同,页面内容会发生变化
构造动态页面的奇数有很多,每种语言都有意思额相关的库/框架来做这件事
Servlet就是Tomcat这个HTTP服务器提供给Java的一组API,来完成构建动态页面这个任务
Servlet主要做的工作
- 允许程序猿注册一个类,在Tomcat收到某个特定的HTTP请求的时候,执行这个类中的一些代码
- 帮助程序猿解析HTTP请求,把HTTP请求从一个字符串解析成一个HttpRequest对象
- 帮助程序猿构造HTTP响应,程序猿只要给指定的HttpResponse对象填写一些属性字段,Servlet就会自动的按照HTTP协议的方式构造出一个HTTP响应字符串,并通过Socket写回给客户端
2. 第一个Servlet程序
2.1 创建项目
使用IDEA创建一个Maven项目
2.2 引入依赖
Maven项目创建完毕后,会自动生成一个pom.xml文件,在这个文件中,引入Servlet API依赖的jar包
在中央仓库中搜索servlet,选择3.1.0版本,Servlet版本和Tomcat版本匹配,Tomcat选择8.5版本,Servlet就要使用3.1.0版本
把中央仓库中提供的xml复制到项目的pom.xml中
修改后的pom.xml:
<?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>servlet</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
<!-- scope这个标签表示当前这个jar包只是在开发阶段使用,不需要打包到最终的发布包中,因为Tomcat里面内置了Servlet-->
<scope>provided</scope>
</dependency>
</dependencies>
<!-- tomcat不能识别jar包,所以需要在这里设置打包格式,tomcat能识别war包,所以设置打包格式为war包-->
<packaging>war</packaging>
<!-- 为了方便测试访问,设置打包后的名字,-->
<build>
<finalName>helloservlet</finalName>
</build>
</project>
标签内部放置项目依赖的jar包,maven会自动下载赖以到本地,
2.3 创建目录
当项目创建好了之后,IDEA会自动帮我们创建一些目录:
- src:表示源代码所在的目录
- main/java:表示源代码的根目录,后续创建.java文件就放到这个目录里
- main/resources:表示项目的一些资源文件所在的目录
- test/java:表示测试代码的根目录
1、除了这些目录,还需要创建webapp目录,
在main目录下,和java目录并列,创建一个webapp目录
2、创建WEB-INF
再在webapp目录中创建一个WEB-INF目录
3、创建web.xml
在WEB-INF目录中创建一个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 Application</display-name>
</web-app>
webapp目录就是未来部署到Tomcat中的一个重要的目录,当前我们可以往webapp中放一些静态资源,比如html,css等
在这个目录中还有一个重要的文件web.xml,Tomcat找到这个文件才能正确处理webapp中的动态资源
2.4 编写代码
在java目录中创建一个HelloServlet类:
@WebServlet("/hello")
//这个/hello就是HTTP请求中URL的路径,只要是这个路径的HTTP请求,就会调用这个HttpServlet对应的方法,如果是GET请求,就调用doGET方法,如果是POST请求,就调用doPOST方法,这个路径没有包含(Content Path,就是当前webapp的名字)
//保证当前的类和一个特定的HTTP请求关联起来,
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello world");//这个操作就是往HTTP响应的body中写了一个字符串
//不管请求是什么 ,都返回一个hello world
}
}
- 首先创建一个类HelloServlet,继承自HttpServlet
- 在类上方加上注解,
- 重写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这个程序的一小部分逻辑,也就是根据请求处理响应的逻辑,其他部分都由Tomcat完成。
类要满足以下条件,Tomcat才会调用
- 创建的类需要继承自HttpServlet
- 这个类需要使用@WebServlet注解关联上一个HTTP的路径
- 这个类需要实现doXXX方法
当这三个条件都满足之后,Tomcat就可以找到这个类,并且在合适的时机进行调用
2.5 打包程序
使用maven进行打包,打开maven窗口,展开Lifecycle,双击package即可进行打包
打包成功后,可以看到在target目录下,生成了一个jar包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpTv2H6a-1655822589958)(C:\Users\Freedom\Desktop\CSDN\JAVA EE\Servlet图片\打包.jpg)]
但是这样的jar包并不是我们所需要的,Tomcat需要识别的是另一种war包格式,还有一点,打包生成的war包名字太复杂了,所以要在pom.xml中设置打包格式和生成的war包的名称
在pom.xml中新增一个packing标签,表示打包的方式是打一个war包,再新增一个build标签,内置一个finalName标签,表示打出的war包的名字是helloservlet
<packaging>war</packaging>
<build>
<finalName>helloservlet</finalName>
</build>
重新打包结果:
2.6 部署程序
把war包拷贝到Tomcat的webapps目录下,启动Tomcat(在解压后的Tomcat目录的bin目录中,找到startup.bat,双击即可启动Tomcat),Tomcat就会自动把war包解压缩
2.7 验证程序
通过浏览器访问http://127.0.0.1:8080/helloservlet/hello 就可以看到结果了
URL中的PTH分成两个部分,其中helloservlet为Context Path,hello为WebServlet注解中的路径
3. 更方便的部署方式(Smart Tomcat)
手动拷贝war包到Tomcat的过程比较麻烦,我们可以使用IDEA中的Smart Tomcat插件来完成这个工作
3.1 安装Smart Tomcat插件
File->Settings->plugins,搜索tomcat
3.2 配置Smart Tomcat插件
点击IDEA右上角的"ADD Configuration",选择左侧的"Smart Tomcat"
Name这一栏可以随便填写,在Tomcat Server这一栏选择Tomcat所在的目录
Context Path默认填写的值是项目的名称
配置完成后,启动程序,IDEA就会自动进行过编译,部署,启动Tomcat的过程
在浏览器中使用http://127.0.0.1:8080/servlet/hello 访问页面
路径的对应关系:
使用Smart Tomcat部署的时候,可以发现Tomcat的webapps内部并没有拷贝一个war包,也没有看到解压缩的内容,Smart Tomcat相当于是在Tomcat启动的时候直接引用了项目中的webapp和target目录
4. 常见的访问出错
4.1 出现404
404表示用户访问的资源不存在,大概率是URL的路径写的不正确,
- 例如少写了Context Path:127.0.0.1:8080/hello
- 少写了Servlet Path:127.0.0.1:8080/servlet
- Servlet Path写的和URL不匹配:代码中@WebServlet注解中路径为(“/helloServlet”),此时再用127.0.0.1:8080/servlet/hello 访问就访问不到了,两个路径要匹配才行
- web.xml写错了,也会出现404
4.2 出现405
405表示对象的HTTP请求方法没有实现。例如:没有实现doGet方法
在浏览器地址栏直接输入URL,会发送一个HTTP GET请求
此时就会根据/servlet/hello这个路径找到HelloServlet这个类,并且尝试调用类中的doGet方法,如果没有实现doGet方法,就会出现405
4.3 出现500
500状态码一般时Servlet代码中抛出异常导致的
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
String s = null;
resp.getWriter().write(s.length());
}
}
这里设置一个String类型的s,引用为null,尝试将s的长度写入响应时会发生空指针异常。此时页面就会出现500
4.4 出现"空白页面"
如果不往响应中写内容,就会出现空白页面,通过抓包可以看到响应的body为空
4.5 出现"无法访问此网站"
一般就是Tomcat启动失败了
例如:Servlet Path 写错了,将@WebServlet注解中写为(“hello”),没有加斜杠,
4.6 小结
- 4xx的状态码表示路径不存在,需要检查URL是否正确,和代码中设定的Context Path以及 Servlet Path是否一致
- 5xx的状态码表示服务器出现错误,需要观察页面的提示内容和Tomcat自身的日志,观察是否存在报错
- 出现连接失败一般是Tomcat没有正确启动,也需要观察Tomcat的自身日志是否有错误提示
- 空白页面这种情况需要我们使用抓包工具来分析HTTP请求响应的具体交互过程
5. Servlet运行原理
Tomcat的定位
当浏览器给服务器发送请求的时候,Tomcat作为HTTP服务器就可以接收到这个请求,HTTP协议作为一个应用层协议,需要底层协议来支持工作
接收请求:
- 用户在浏览器中输入一个URL,此时浏览器就会构造一个HTTP请求
- 这个HTTP请求会经过网络协议栈逐层进行封装成二进制bit流,最终通过物理层的硬件设备转换成光电信号传输出去
- 这个承载信息的光电信息通过互联网上的一系列网络设备,最终到达目的主机,这个过程也需要网络层和数据链路层的参与
- 服务器收到这些信号,通过网络协议栈的逐层分用,层层解析,最终还原成HTTP请求,交给Tomcat进程进行处理,根据端口号确定进程
- Tomcat通过Socket读取到这个请求,并按照HTTP请求的格式来解析这个请求,根据请求中的Context Path确定一个webapp,再通过Servlet Path确定一个具体的类,再根据当前请求的方法,决定调用这个类中的哪个方法,get对应doGet,post对应doPost…此时我们的代码中这些处理请求的方法中的第一个参数HttpServletRequest就包含了这个HTTP请求的详细信息
根据请求计算响应:
- 我们的代码会根据请求中的信息,给HttpServletResponse对象设置属性,例如状态码,header。body等
返回响应:
- 代码执行完毕后,Tomcat就会自动把HttpServletResponse这个对象转换成一个符合HTTP协议的字符串,通过Socket把这个响应发送出去
- 和接收请求一样,通过网络协议栈层层封装和分用,最终到达浏览器,浏览器通过Socket读到这个响应,按照HTTP响应的格式解析,并且把body中的数据按照一定的格式显示在浏览器的界面上
6. Tomcat执行流程
Tomcat初始化逻辑:
- Tomcat内置了main方法,当我们启动Tomcat的时候就是从main方法开始执行的
- 被@WebServlet注解修饰的类会在Tomcat启动的时候就被获取到,并集中管理
- Tomcat通过反射这样的语法机制来创建被@WebServlet注解修饰的类的实例
- 这些实例被创建完了之后,会调用其中的init方法进行初始化,这些实例销毁之前,会调用destory方法进行收尾工作,这两个方法我们自己写的类都可以重写,但是注意destory这个方法,如果我们重写了,但是直接结束掉Tomcat进行,那么destory就来不及执行,
- Tomcat内部也是通过Socket API进行网络通信的
- 为了能同时响应多个HTTP请求,采取多线程的方式实现,因此Servlet是运行在多线程环境下的
Tomcat处理请求逻辑:
- Tomcat从Socket中读取到的HTTP请求是一个字符串,然后会按照HTTP协议的格式解析成一个HttpServletRequest对象
- 根据URL中的path判定这个请求是要请求一个静态资源还是动态资源,如果是静态资源,直接找到对应的文件,把文件的内容通过socket返回,如果是动态资源,才会执行到Servlet的相关逻辑
- 根据URL中的Context Path 和Servlet Path确定要调用哪个Servlet实例的service方法
- 通过service方法,就会进一步调用到我们之前写的doGet或者doPost
Servlet的service方法内部会根据当前请求的方法,决定调用其中某个doxxx方法,在调用doxxx方法的时候,因为我们创建的类继承了HttpServlet类,HttpServlet又继承自Servlet类,并且重写了doxxx方法,所以就会触发多态机制,从而执行到我们自己写的子类中的doxxx方法
7. Servlet API
7.1 HttpServlet
我们写Servlet代码的时候,首先第一步就是创建类,继承自HttpServlet,并且重写其中的某些方法(例如doGet,doPost)
1.核心方法
方法名称 | 调用时机 |
---|---|
init | 在HttpServlet实例化之后被调用一次 |
destory | 在HttpServlet实例不再使用的时候调用一次 |
service | 收到HTTP请求时候调用 |
doGet | 收到GET请求的调用(由service方法调用) |
doPost | 收到POST请求的时候调用(由service方法调用) |
doPut/… | 收到其他请求的时候调用(由service方法调用) |
Servlet生命周期:
- Servlet在实例化之后被调用一次init
- Servlet每次收到HTTP请求,调用一次service
- Servlet在销毁之前,调用一次destory
HttpServlet的实例只是在程序启动时创建一次,而不是每次收到HTTP请求都重新创建实例
2. 代码实例:处理GET请求
创建MethodServlet.java 重写doGet方法,当接收到一个HTTP GET请求时,返回一个响应,内容为"GET 响应"
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
resp.getWriter().write("GET 响应");
}
}
创建testMethod.html,放到webapp目录中,
一个Servlet程序中可以同时部署静态文件,静态文件放在webapp目录中
通过一个按钮,为这个按钮创建一个回调函数,当按钮被点击时,向服务器发送一个HTTP GET请求,HTTP请求通过ajax的方式构造,type表示请求的方法,url表示请求的路径,回调函数中的data表示服务器返回的响应内容
<!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>Document</title>
</head>
<body>
<button onclick="sendGet()">发送GET请求</button>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function sendGet(){
$.ajax({
type: "GET",
url: "method",
success:function(data,status){
console.log(data);
}
})
}
</script>
</body>
</html>
重新部署程序,使用URL 127.0.0.1:8080/servlet/testMethod.html访问页面,点击"发送GET请求"按钮,可以在控制台中看到响应内容,通过Fiddler抓包可以看到,当浏览器中输入URL之后,浏览器先给服务器发送了一个HTTP GET请求,服务器返回一个HTML页面的响应,当点击"发送GET请求"按钮后,浏览器又通过ajax给服务器发送一个HTTP GET请求,响应的body中内容为"GET 响应"
3. 处理POST请求
在MethodServlet.java中,新增doPOST方法
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
resp.getWriter().write("POST 响应");
}
在testMethod.html中新增一个按钮,和对应的点击事件处理函数
<button onclick="sendPost()">发送POST请求</button>
<script>
function sendPost(){
$.ajax({
type: "POST",
url: "method",
// POST请求的body
data: "request body",
success:function(data,status){
console.log(data);
}
})
}
</script>
重新部署程序,用相同的方法访问testMethod.html页面,点击"发送POST请求",也可以在控制台中看到”POST 响应“
7.2 HttpServletRequest
Tomcat通过Socket API读取HTTP请求,并且按照HTTP协议的格式把字符串解析成HttpServletRequest对象
1. 核心方法
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本 |
String getMethod() | 返回请求的HTTP方法的名称,例如GET,POST |
String getRequestURI() | 从协议名称直到HTTP请求的第一行的查询字符串中,返回该请求的URL的一部分 |
String getContextPath() | 返回值是请求上下文的请求URI部分 |
String getQueryString() | 返回包含在路径后的请求URL中的查询字符串 |
Enumeration getParameterNames() | 返回一个String对象的枚举,相当于返回query string中所有键值对的key |
String getParameter(String name) | 以字符串形式返回指定key对应的value,如果参数不存在就返回null |
String [] getParameterValues(String name) | 返回一个字符串对象数组,包含指定key对应的所有value,针对key重复的情况,如果参数不存在则返回null |
Enumeration getHeaderNames() | 返回一个枚举,包含请求中header中所有的key |
String getHeader(String name) | 以字符串形式返回header中指定key的value |
String getCharacterEncoding() | 返回请求body中使用的字符编码的名称 |
String getContentType() | 返回请求body中数据格式类型,如果不知道类型则返回null |
int getContentLength() | 以字节为单位返回请求body的长度,并提供输入流。如果长度未知则返回-1 |
InputStream getInputStream() | 用于读取请求的body内容,返回一个InputStream对象 |
通过这些方法可以获取到一个请求中的各个方面的信息
2. 打印请求信息
创建ShowRequestServlet类
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置body格式
resp.setContentType("text/html; charset=utf-8");
//把生成的响应的body放入respBody
StringBuilder respBody = new StringBuilder();
respBody.append(req.getProtocol());//获取到请求的版本号
respBody.append("<br>");
respBody.append(req.getMethod());
respBody.append("<br>");
respBody.append(req.getRequestURI());
respBody.append("<br>");
respBody.append(req.getContextPath());
respBody.append("<br>");
respBody.append(req.getQueryString());
respBody.append("<br>");
respBody.append("<h3>headers</h3>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
respBody.append(headerName+": ");
respBody.append(req.getHeader(headerName));
respBody.append("<br>");
}
//respBody转为字符串写入响应
resp.getWriter().write(respBody.toString());
}
}
重新部署程序。在浏览器中输入127.0.0.1:8080/servlet/showRequest?a=10&b=20
3. 获取GET请求中的参数
GET请求中的参数一般是通过query string传递给服务器的,在服务器端可以通过getParameter来获取到参数的值,query string里面的内容是程序猿自定义的内容,每个键值对的键和值都是程序猿自己定义好的
约定query string 中键值对例如:userId=100&classId=200
创建GetParameterServlet类
@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//这是获取GET请求URL中的query string
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
//对query string 进行判定是否存在 key不存在 或者key存在value不存在 都视为query string不存在
if (userId == null || userId.equals("")){
//具体处理
}
resp.getWriter().write(String.format("userId:%s , classId: %s",userId,classId));
}
}
重新部署程序,在浏览器中输入127.0.0.1:8080/servlet/getParameter
可以看到当query string为空时,getParameter获取的值为null
当在浏览器中输入127.0.0.1:8080/servlet/getParameter/userId=123&classId=456 时可以看到:
此时说明服务器已经获取到客户端传过来的参数,注意:getParameter的返回值类型为String,如果需要对这个值进行算术运算,就需要手动将它转为int类型,Integer.parseInt
4. 获取POST请求中的参数(1)
POST请求的参数一般通过body传递给服务器,body中的数据格式有很多种,
如果采用form表单的形式,body中的数据格式为application/x-www-form-urlencoded,仍然可以通过getParameter获取参数的值
约定请求形如:
创建PostParameterServlet类,
@WebServlet("/postParameter")
public class PostParameterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//POST请求中的body类型是application/x-www-form-urlencoded时,和获取query string的方式一样
resp.setContentType("text/html;charset=utf-8");
String userId = req.getParameter("userId");
String classId = req.getParameter("classId");
resp.getWriter().write(String.format("userId:%s;classId:%s <br>",userId,classId));
}
}
创建testPost.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>testPost</title>
</head>
<body>
<form action="postParameter" method="POST">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
重新部署程序,在浏览器中输入 127.0.0.1:8080/servlet/testPost.html 可以看到HTML,
输入内容,点击提交,可以看到跳转到了新的页面,并显示出了刚刚提交的数据
通过抓包可以看到,form表单构造的body数据的格式为:
5. 获取POST请求中的参数(2)
如果POST请求中的body是按照json的格式来传递,可以先把整个body的字符串读取出来,再进行解析
约定请求形如:
/servlet/postParameterJson
{“userId”: 123,“classId”: 456} ,“key”: value格式
创建PostParameterJsonServlet类
@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//因为body类型是json,所以要先把POST请求的整个body读出来
String body = readBody(req);
//这里只是读出来了,并没有解析
resp.getWriter().write(body);
private String readBody(HttpServletRequest req) throws IOException {
//读取body需要根据req getInputStream得到一个流对象,从这个流对象中获取body
InputStream inputStream = req.getInputStream();
//具体读多少个字节,可以通过Content-Length 可以拿到请求body中的字节数
int contentlength = req.getContentLength();
byte [] buffer = new byte[contentlength];
inputStream.read(buffer);
return new String(buffer,"utf-8");
}
}
创建testPostJson.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>testPostJson</title>
</head>
<body>
<!-- 构造body为json格式的数据,就只能用ajax的方式来实现 -->
<button onclick="sendJson()">发送请求</button>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function sendJson(){
let body = {
userId: 123,
classId: 456
};
$.ajax({
url: "postParameterJson",
type: "POST",
//设置请求中body数据的格式类型
contentType: "application/json;charset=utf-8",
data: JSON.stringify(body),
success: function(body,status){
console.log(body);
}
})
}
</script>
</body>
</html>
通过浏览器可以看到响应的数据仍为一个整体的String类型,如果想要获取到userId和classId的具体值,还需要搭配json库进一步解析
6. 获取POST请求中的参数(3)
引入Jackson这个库,进行JSON解析
把中央仓库中的依赖配置添加到pom.xml中,
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
修改代码:
//通过这个类来表示解析后的结果
class JsonData{
public int userId;
public int classId;
}
@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//因为body类型是json,所以要先把POST请求的整个body读出来
String body = readBody(req);
//使用jackson解析,先创建一个jackson的核心对象,objectMapper
ObjectMapper objectMapper = new ObjectMapper();
JsonData jsonData = objectMapper.readValue(body,JsonData.class);
resp.getWriter().write(String.format("userId: %d;classId: %d <br>"
,jsonData.userId,jsonData.classId));
}
private String readBody(HttpServletRequest req) throws IOException {
//读取body需要根据req getInputStream得到一个流对象,从这个流对象中获取body
InputStream inputStream = req.getInputStream();
//具体读多少个字节,可以通过Content-Length 可以拿到请求body中的字节数
int contentlength = req.getContentLength();
byte [] buffer = new byte[contentlength];
inputStream.read(buffer);
return new String(buffer,"utf-8");
}
}
- JsonData这个类用来表示解析之后生成的json对象,这个类的属性的名字和类型要和json字符串的key对应
- jackson库的核心类为ObjectMapper,其中的readVaule方法把json字符串转成java对象,其中的writeVauleAsString方法把一个Java对象转成json格式的字符串
- readVaule的第二个参数为JsonData的类对象,通过这个类对象,在readVaule的内部就可以借助反射机制来构造出JsonData对象,并且根据json中key的名字,把对应的value赋值给JsonData的对应字段
7.3 HttpServletResponse
Servlet中的doxxx方法的目的就是根据请求计算得到响应,然后把响应的数据设置到HttpServletResponse对象中
然后Tomcat就会把这个HttpServletResponse对象按照HTTP协议的格式,转成一个字符串,并通过Socket写回给浏览器
1. 核心方法
方法 | 描述 |
---|---|
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name,String value) | 设置一个带有给定名称和值的header,如果name已经存在,则覆盖旧的值 |
void addHeader(String name,String value) | 添加一个带有给定名称和值的header,如果name已经存在,不覆盖旧的值,并列添加新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的body类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码,例如utf-8 |
void sendRedirect(String location) | 设置重定向位置URL,发送临时重定向响应到客户端 |
PrintWriter getWriter() | 用于往body中写入文本格式数据 |
OutputStream getOutputStream() | 用于往body中写入二进制格式数据 |
注意
- 响应对象是服务器要返回给浏览器的内容,这里的重要信息都是程序猿设置的,因此上面的方法都是写方法
- 对于状态码和响应头的设置要放到getWriter/getOutputStream之前,否则可能设置失效
2. 示例:设置状态码
实现一个程序,用户在浏览器通过参数指定要返回响应的状态码
创建StatusServlet类
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//让用户传入一个请求,在query string中带一个参数,代表响应的状态码。
//根据用户不同的输入,返回不同的状态码
resp.setContentType("text/html;charset=utf-8");
String statusString = req.getParameter("status");
if (statusString == null || statusString.equals("")){
resp.getWriter().write("当前的请求的参数status缺失");
return;
}
resp.setStatus(Integer.parseInt(statusString));//设置响应码为用户传入的参数,将字符串转成int
resp.getWriter().write("status: "+statusString);//响应body内容
}
}
部署程序,在浏览器中输入127.0.0.1:8080/servlet/status?status=200 可以看到
使用不同的status值,可以看到不同的响应结果,通过Fiddler抓包也可以看到响应结果
3. 示例:自动刷新
实现一个程序,让浏览器每秒钟自动刷新一次,并显示当前的时间戳
创建AutoRefreshServlet类
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.setHeader("Refresh","1");
long timeStamp = System.currentTimeMillis();//这个方法可以获取到当前的毫秒级时间戳
resp.getWriter().write("timestamp: "+ timeStamp);
}
}
通过设置HTTP响应报头中的Refresh字段,可以控制浏览器自动刷新的时间
部署程序,浏览器中输入 127.0.0.1:8080/servlet/autoRefresh 访问页面,可以看到浏览器每秒钟自动刷新一次
4. 示例:重定向
实现一个程序,返回一个重定向HTTP响应,自动跳转到另一个页面
创建RedirectServlet类
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// resp.setStatus(302);
// resp.setHeader("Location","https://www.sogou.com/");
resp.sendRedirect("https://www.sogou.com/");//直接设置url发送临时重定向响应到客户端
}
}
可以通过设置状态码为302,header中Location的值完成页面跳转
也可以直接设置url发送临时重定向响应到客户端
部署程序,浏览器中输入 127.0.0.1:8080/servlet/redirect 访问页面,可以看到页面自动跳转到搜狗主页了。通过抓包可以看到HTTP请求的具体流程
可以看到当浏览器中输入url时,服务器返回了一个状态码为302的响应,header中Location表示页面将跳转到www.sogou.com, 紧接着又发送了一个获取到搜狗主页的HTTP请求
8. Cookie和Session
HTTP协议自身是属于"无状态"协议,无状态指的是:默认情况下HTTP协议的客户端和服务器之间的这次通信,和下次通信之间没有直接的联系
但是实际开发中,我们需要知道请求之间的关联关系
8.1Cookie
Cookie是HTTP协议中的一个字段,是浏览器在客户端这边保存数据的方式,浏览器会根据域名/地址分别存储cookie。
Cookie是通过HTTP响应的set-Cookie字段来进行设置,是服务器返回给浏览器的。
Cookie会在下次请求中自动添加到请求中,发送给服务器,
Cookie中存储的是键值对结构的字符串,这些都是程序猿自定义的
8.2 会话机制(Session)
服务器同一时刻收到的请求是很多的,服务器需要清楚的区分每个请求是属于哪个用户,就需要服务器这边记录每个用户的信息的对应关系
会话的本质就是一个"哈希表",存储了一些键值对结构,key就是sessionId,vaule是用户信息,用户信息可以根据需求设计
Session是由服务器生成的一个"唯一性字符串",从session机制的角度来看,这个唯一性字符串称为"sessionId",站在整个登录流程的角度来看,也可以把这个唯一性字符串称为"token"
- 当用户登录的时候,服务器在Session中新增一个新记录,并且把sessionId/token返回给客户端,例如通过HTTP响应中的Set-Cookie字段返回
- 客户端后续再给服务器发送请求的时候,需要在请求中带上sessionId/token,例如通过HTTP请求中的Cookie字段带上
- 服务器收到请求之后,根据请求中的sessionId/tonke在Session信息中获取到对应的用户的信息,再进行后续操作
Servlet的Session默认是保存在内存中的,如果重启服务器则Session数据就会丢失
8.3 Cookie和Session的区别
- Cookie是客户端的机制,Session是服务器端的机制
- Cookie和Session经常在一起配合使用,但是不是必须配合
- 可以用Cookie来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是sessionId/token
- Session中的sessionId/token也不是非得通过Cookie/Set-Cookie传递
8.4 核心方法
HttpServletRequest类中的相关方法
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话,参数如果为true,则当不存在会话时新建会话;参数如果为false,则当不存在会话时返回null |
Cookie[] getCookie() | 返回一个数组,包含客户端发送该请求的所有的Cookie对象,会自动把Cookie中的格式解析成键值对 |
HttpServletResponse类中的相关方法
方法 | 描述 |
---|---|
void addCookid(Cookie cookie) | 把指定的cookie添加到响应中 |
HttpSession类中的相关方法
一个HttpSession对象里面包含多个键值对,我们可以往HttpSession中存任何我们需要的信息
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在该session会话中具有指定名称的对象,也就是返回指定key的value,如果没有指定名称的对象,则返回null |
void setAttribute(String name,Object value) | 该方法使用指定的名称绑定一个对象到该session会话 |
boolean isNew() | 判定当前是否是新创建出的会话 |
Cookie类中的相关方法
每个Cookie对象都是一个键值对
方法 | 描述 |
---|---|
String getName() | 该方法返回cookie的名称,名称在创建后不能改变,这个值是Set-Cookie字段设置给浏览器的 |
String getValue() | 该方法获取与cookie关联的值 |
void setVaule(String newVaule) | 该方法设置与cookie关联的值 |
HTTP的Cookie字段中存储的实际上是多组键值对,每个键值对在Servlet中都对应了一个Cookie对象
通过HttpServletRequest.getCookies()获取到请求中的一系列Cookie键值对
通过HttpServletResponse.addCookies()可以向响应中添加新的Cookie键值对
示例:实现用户登录
实现简单的用户登录逻辑,主要是通过HttpSession类完成,并不需要我们手动操作Cookie对象
先有一个html,包含用户名密码的输入框,登录按钮
LoginServlet类来处理登录请求
IndexServlet类,模拟登陆完成后,跳转到的主页,在这个主页里面就能够获取到用户的身份信息,比如用户的用户名和访问次数
login.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>Document</title>
</head>
<body>
<form action="login" method="POST">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
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.从请求的body中读取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
//2.判断用户名密码是否正确,此处不读数据库,固定用户名为zhangsan密码为123,
if(!"zhangsan".equals(username) || !"123".equals(password)){
//登录失败
resp.getWriter().write("用户名密码不正确");
return;
}
System.out.println("登陆成功");
//3.登录成功就创建一个会话信息,参数为true,则会话不存在就创建
//会话是根据请求中的sessionId查找的,sessionId在Cookie中,
//此处是首次登录,没有Cookie,Cookie是服务器返回的,所以就触发创建"会话",
//这里的创建"会话",就是创建一个HttpSession对象作为value
//再生成一个随机的字符串作为sessionId 也就是key
//把这个key value插入到表中。同时把这个生成的sessionId同构Cookie字段返回给浏览器
HttpSession httpSession = req.getSession(true);
//还可以存入程序猿自定义的数据,比如存入身份信息(用户名和登录次数)
httpSession.setAttribute("username","zhangsan");
httpSession.setAttribute("loginCount",0);
//因为这里的setAttribute的第两个参数是Object类型,不能直接写int0
// 但是此处的0会触发自动装箱,将int0构造成Integer 0
//4.让页面跳转到主页,使用重定向
resp.sendRedirect("index");
}
}
此处的getSession参数为true,表示查不到HttpSession时会创建新的HttpSession对象,并且生成一个sessionId,插入到哈希表中,并且把sessionId通过Set-Cookie返回给浏览器
IndexServlet
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//根据当前用户请求中的sessionId,获取用户信息,并显示到页面上
resp.setContentType("text/html;charset=utf-8");
//1.判定当前用户是否已经登录了.也就是请求中有没有sessionId,以及是否合法
//如果会话不存在,就不创建,这里只是查询是否存在。而不是不存在就创建,
HttpSession httpSession = req.getSession(false);
if (httpSession == null){
//当前没有合法会话,当前用户没有登录,就重定向到登陆页面,让用户进行登录
resp.sendRedirect("login.html");
return;
}
//2.用户已经登录,就从httpsession中获取到用户信息,
String username = (String) httpSession.getAttribute("username");
Integer loginCount = (Integer) httpSession.getAttribute("loginCount");
loginCount = loginCount + 1;//将访问次数加1,并写回
httpSession.setAttribute("loginCount",loginCount);
//3.返回一个HTML页面。显示获取到的信息,。
StringBuilder html = new StringBuilder();
html.append("<div>用户: "+username+"</div>");
html.append("<div>访问次数: "+ loginCount + "</div>");
resp.getWriter().write(html.toString());
}
}
在这个代码中看不到"哈希表",也看不到sessionId这样的概念,getSession操作内部提取到请求中的Cookie里的sessionId,然后查找哈希表,获取到对应的HttpSession对象
getSession参数为false,则获取不到HttpSession,不会创建新的HttpSession,而是返回null,此时说明用户没有登录
部署程序,通过127.0.0.1:8080/messagewall/index 访问
首次访问的时候可以看到,当前用户尚未登录,此时页面自动重定向到login.html
在login.html中输入用户名密码之后,会跳转到/login路径,此时服务器返回了一个sessionId,并在Session中记录用户信息,然后重定向到/index
- 第一次访问index时,用户还没有登录,直接重定向到login.html页面
- 输入用户名密码登陆成功后,跳转到login页面,并且生成了一个sessionId,再跳转到index主页
可以看到第一次登录成功时,响应Set-Cookie字段中有JESSIONID属性,随机生成了一个唯一性的字符串,后续跳转到主页面index时,请求中就会通过Cookie带着这个字段,
服务器返回的JESSIONID和后续用户请求的Cookie中所传递的JESSIONID是一样的,这就相当于是用户的唯一身份标识
9. 上传文件
在Servlet中也支持上传文件
9.1 核心方法
HttpServletRequest类方法
方法 | 描述 |
---|---|
Part getPart(String name) | 获取请求中给定name的文件 |
Collection getParts() | 获取所有的文件 |
Part类方法
方法 | 描述 |
---|---|
String getSubmittedFileName() | 获取提交的文件名 |
String getContentTypr() | 获取提交的文件类型 |
long getSize() | 获取文件的大小 |
void writy(String path) | 把提交的文件数据写入磁盘文件 |
9.2 示例
实现程序,通过网页提交一个图片到服务器上
创建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>上传图片</title>
</head>
<body>
<form action="upload" enctype="multipart/form-data" method="POST">
<input type="file" name="MyImage">
<input type="submit" value="上传图片">
</form>
</body>
</html>
- 上传文件一般通过POST请求的表单实现
- 在form中要加上multipart/form-data
创建UploadServlet类
@MultipartConfig//加上这个注解 servlet才能正确读取文件中的内容
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.从req对象中读取中part对象
Part part = req.getPart("MyImage");//参数是HTML中form表单input标签中的name属性值
//2.读取part对象中的一些参数
System.out.println(part.getSubmittedFileName());//读取出上传的文件的真实文件名
System.out.println(part.getContentType());//文件类型
System.out.println(part.getSize());//文件大小
//3.把文件写入到指定目录中
part.write("c:/MyImage.jpg");
//4.返回响应
resp.getWriter().write("upload ok");
}
}
- 需要给UploadServlet加上@MultipartConfig注解,否则服务器代码无法使用getiPart方法
- getPart的参数需要和form中input标签的name属性对应
- 客户端一次可以提交多个文件(使用多个input标签),此时服务器就可以通过getParts获取所有的Part对象
部署程序,在浏览器中通过 127.0.0.1:8080/messagewall/upload,html 访问
可以看到Content-Type为multipart/form-data这样的请求中带有一个boundary,值为随机生成的字符串,这个字符串在body这边作为一个"分割线",分割线下面是上传的文件的属性和文件的内容