前言
加油加油!!
一丶Servlet是什么
这样说,一个web服务器提供了web程序,用来处理服务端对请求的解析,以及响应的封装,这样就不需要我们自己写程序来完成对应的功能了。但是这样就会有一个问题。
假如当我们自己写一个web应用,然后把这个webapp部署到web服务器tomcat的时候,就会调用tomcat的API,也就是使用tomcat提供的类和接口中的方法或者属性。但是如果我们不使用tomcat,那么这个产品就无法使用了。
这个时候,就是我们Servlet存在的意义了。
Servlet为不同的JavaWeb服务器,规定了响应的编程规范(官方规范)。也就是说它会屏蔽web服务器实现的细节(不同的web服务器,可以是不同的对请求解析和响应封装的实现)。因为它规定了统一的编程规范(统一的类,接口,方法等),所以在我们换一个web服务器的时候,web应用程序还是可以用的。
所以Servlet可以做什么呢?
1.html是静态的资源文件,如果我想返回动态的网页内容,那么就可以使用html。
2.文件下载也可以使用Servlet,把静态文件放在tomcat/webapps下,访问就可以
下载。
3.用户登录也可以使用Servlet,在客户端浏览器输入账号密码,然后在服务端写程
序来校验账号密码,如果成功,就跳转到另一个页面,如果效验失败,那么就还是
在登录页面。
二丶Servlet的项目部署
<1>创建第一个Servlet程序
首先我们创建一个maven项目,这里我们就不演示怎么创建了,直接进入主题。
当我们创建成功之后,我们引入Servlet的依赖包,首先进入如下网址
引入Servlet的依赖包
之后在pom.xml文件添加如下语句引入Servlet的依赖包
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<!-- Servlet依赖包:官方提供的servlet标准-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--开发编译时候需要这个依赖包,运行时候不需要-->
<scope>provided</scope>
</dependency>
编写完之后刷完maven面板,不然不会生效
接着我们创建web需要的目录和web.xml(描述/配置web的应用信息)
接着引入配置信息
<!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>
然后我们就可以正式进行servlet的开发了。
<2>关于servlet程序的解释
在src路径的java包下创建一个类。这里我把类的名称设置为HelloServlet
第一步
类名称上面的内容添加
@WebServlet("")
webServlet表示当前类是一个servlet类,需要处理请求和响应。
那么我们要怎样知道哪一个请求是需要这个Servlet类来处理的?
那么就需要定义访问路径
第二步
@WebServlet("/hello")
注意,这里访问路径是"/"开头,不然就会报错。当然,这个虚拟路径,可能没有对应的文件。
第三步
接着我们就要处理HTTP协议啦,也就是继承HttpServlet类
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
这里注意啦
HttpServletRequest:http请求封装的类型,使用这个对象就可以获取http请求报文的内容
HttpServletResponse:http响应封装的类型,使用这个对象就可以设置http响应报文的内容
第四步
然后我们重写父类的doXXX方法(XXX就是提供的服务方法,请求的时候,也要使用相同的方法)
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp); //这段代码一定要删掉,不然会报错
resp.getWriter().print("hello World");
}
}
<3>项目在tomcat的部署
然后我们在porn.xml中加入如下语句,使得我们的压缩格式是war格式
<groupId>org.example</groupId>
<artifactId>servlet-study</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- web应用需要打包为war格式,普通Java应用打包为jar -->
<packaging>war</packaging>
然后把这个压缩文件复制到webapps目录下,也就是我们的tomcat文件目录下,接着运行tomcat,然后就会解压缩并且生成一个相同名称的文件夹
至此,我们的Servlet程序运行完毕。
<4>步骤的简化
不得不说,上述程序是有点繁琐了,所以有没有可以简化步骤的呢?
当然是有的,如下所示安装tomcat插件。
然后安装完之后进行配置
然后我们就可以直接在IDEA里面运行Tomcat了
三丶访问出错
<1>404
出现404表示用户访问的资源不存在,大概率是URL的路径不正确。
就像这里,我们如果我们不给访问路径加hello的话,那么url访问路径就是错误的,就会出现404.
<2>405
405方法不支持,什么意思呢?看如下代码
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp); //这段代码一定要删掉,不然会报错
resp.getWriter().print("hello World");
}
}
这里我们在浏览器输入url的时候,就会发送一个http的GET请求,所以当我们把这个doGet改为doPost的时候,因为没有实现doGet方法,就会出现如下情况。
<3>500
往往是 Servlet 代码中抛出异常导致的,这里指的是服务器内部出错。如果出现了这种情况,就一定要检查异常堆栈信息(页面上可能有,idea控制台,tomcat/logs当中的日志文件)
就比如这样举一个例子,看如下代码:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int i = 0;
int j = 5 / 0;
resp.getWriter().print(j);
}
}
因为除数不能为0,这里就出现了500错误
<4>空白页面
这里咋说呢,这样演示吧
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("看这里");
}
}
当我们运行这段代码的时候,可以发现没有报错,但是页面就是空白的,抓包也是空数据,这个就是空白页面。
<5>出现无法访问此页面
这里的话可能就是Servlet Path写错了,总的来说出现这种错误,在Tomcat启动的日志里找关键信息就好啦。
<6>总结
1.4xx 的状态码表示路径不存在, 往往需要检查 URL 是否正确, 和代码中设定的
Context Path 以及Servlet Path 是否一致.
2.5xx 的状态码表示服务器出现错误, 往往需要观察页面提示的内容和 Tomcat 自身
的日志, 观察是否存在报错.
3.出现连接失败往往意味着 Tomcat 没有正确启动, 也需要观察 Tomcat 的自身日
志是否有错误提示.
4.空白页面这种情况则需要我们使用抓包工具来分析 HTTP 请求响应的具体交互
过程.
四丶Servlet运行原理
1>接收请求
1.用户在浏览器输入一个URL此时客户端浏览器就会构造一个HTTP请求,这个请求会被网络协议栈逐层进行封装,然后变为二进制的bit流,最后通过物理层的硬件设备转成光信号/电信号传输出去
2.网络主机接收到这些光信号/电信号又会通过网络协议栈逐层解析,最后还原成HTTP请求交给Tomcat进程(根据端口号确定对应进程)处理。
3.Tomcat通过Socket读取到这个请求(一个字符串),并且按照HTTP请求的格式来解析这个请求,根据Content Path来确定一个webapp,再根据Servlet Path确定一个具体的类,再根据当前请求的方法(Post/Get),决定调用这个类的doGet或者doPost方法,此时我们代码当中的第一个参数HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
具体图示如下:
2>根据请求计算响应
在我们的doGet/doPost方法当中,就执行到了我们自己的代码,我们自己的代码会根据请求当中的一些信息,来给HttpServletResponse对象设置一些属性,比如状态码,header,body等等。
3>返回响应
- doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
2.响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去
3.主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
4.浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上
五丶Servlet API详解(上)
<1>HttpServletRequest–获取queryString
Tomcat通过Socket API读取HTTP请求(字符串),并且按照HTTP协议的格式,把字符串解析成为HttpServletRequest的对象。
这里就着重介绍一些重点的API,介绍方式还是一边写一遍介绍。
因为IEDA有些版本写前端代码时候没有高亮显示,所以这里我使用VSCode进行前端代码的书写。
首先在webapp文件目录下创建request.html文件
然后书写
<h3>get with query string</h3>
<!--当前页面访问路径:/[contextPath应用上下文路径]/request.html,需要跳转到/[contextPath]/request-->
<!--也可以在url输入链接跳转后相同的url绝对路径来访问-->
<a href="request?username=abc&password=123">get</a>
这里通过url路径来进行访问的时候,就可以看到如下内容
点击get,就可以将这个queryString发送给我们的服务端。
然后我们在IDEA中书写我们的后端代码,来获取queryString当中包含的信息
这里就着重介绍一些我们常用的方法
1>getProtocol()请求协议名
这里是返回一个String,也就是返回请求协议的名称和版本
System.out.println("请求协议名:" + req.getProtocol());
2>getMethod()请求方法
返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT
System.out.println("请求方法:" + req.getMethod());
3>getContextPath()上下文路径
返回指示请求上下文的请求 URI 部分。
System.out.println("上下文路径/应用名:" + req.getContextPath());
4>getServletPath()资源路径/servletPath
返回资源路径
System.out.println("资源路径/servletPath:" + req.getServletPath());
5>getHeader(String name)
以字符串形式返回指定的请求头的值。
System.out.println("请求头中键为Host的值" + req.getHeader("Host"));
当我们在重写的doGet方法中完成一下内容之后,点击提交按钮,就可以获取queryString当中的信息。
具体代码如下:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("请求协议名:" + req.getProtocol());
System.out.println("请求方法:" + req.getMethod());
System.out.println("上下文路径/应用名:" + req.getContextPath());
System.out.println("资源路径/servletPath:" + req.getServletPath());
System.out.println("请求头中键为Host的值" + req.getHeader("Host"));
req.setCharacterEncoding("utf-8");//设置body解析的编码格式(表单提交的时候这里会有乱码)
System.out.println("username:" + req.getParameter("username"));
System.out.println("password:" + req.getParameter("password"));
//响应给客户端的内容
resp.getWriter().println("请求信息已获取,在idea控制台查看");
}
这里要着重注意设置"utf-8",以防止乱码。提交完成后在控制台就能看到如下显示。
<2>HttpServletRequest–获取body
Get请求的正文内容是在queryString当中,但是Post是在body当中,所以如果我们要获取body当中的数据格式,我们要怎么办呢?
其实也就和上面的获取queryString的代码是一模一样的,但是在这里,我们就必须要重写doPost方法,然后在doPost方法内部发起对doGet方法的调用,这样就可以获取body的内容
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);//逻辑一样,就调用doGet就好
}
<3>获取body–form表单格式提交
form表单提交在不进行特别声明的时候,默认是Get方法。
<form action="request">
<input type="text" name="username" placeholder="输入用户名">
<br>
<input type="password" name="password" placeholder="输入密码">
<br>
<input type="submit" value="提交">
</form>
POST的请求一般是通过body传递给服务器,但是他这个数据类型就有待推敲。如果说是表单格式,也就是form表单形式,那么我们还是可以通过上文里的getParameter 获取参数的值。
但是这里我们肯定是要设置一下数据的格式的!和上面不同的就是需要特别声明方法的类型
<h3>表单格式:/request</h3>
<form action="request" method="post">
<input type="text" name="username" placeholder="输入用户名">
<br>
<input type="password" name="password" placeholder="输入密码">
<br>
<input type="submit" value="提交">
</form>
所以这里对应显示如下:
<4>获取body-- form-data格式提交
<1>获取简单数据
form-data格式提交的时候需要特别注明请求数据的格式
enctype="multipart/form-data"
具体代码如下:
<h3>form-data格式:/form-data-servlet</h3>
<form action="form-data-servlet" enctype="multipart/form-data" method="post"><!--enctype是请求数据的格式-->
<input type="text" name="username" placeholder="输入用户名">
<br>
<input type="password" name="password" placeholder="输入密码">
<br>
<input type="submit" value="提交">
</form>
然后在IDEA中对body内容的获取,其实在这里也还是用getParameter()这个方法。
req.setCharacterEncoding("utf-8");//设置body解析的编码格式
System.out.println("username:" + req.getParameter("username"));
System.out.println("password:" + req.getParameter("password"));
<2>获取文件
当form-data格式上传文件的时候,我们要如何获取呢?这里我们就不能用getParameter()这个方法啦。
那么具体怎么做呢?
方 法 一 \color{red}{方法一} 方法一
//获取上传文件的二进制数据,转换为字符串打印
InputStream is = head.getInputStream();//获取输入流(里面包含了数据)
//了解:输入流.available返回包含的数据长度
byte[] bytes = new byte[is.available()];//获取长度
//从输入流中,把数据读取到byte[]中,字节数组对象就有了这些数据
is.read(bytes);
System.out.println(new String(bytes,"utf-8"));
之后我们就能够接收文件啦。
方 法 二 \color{red}{方法二} 方法二
第二种方法,我们可以直接获取头信息,然后通过头信息把客户端上传的文件保存在本地,在这个过程中,我们要使用getPart(String name)这个方法
Part head = req.getPart("head");
head.write("D://" + head.getSubmittedFileName());
这里要注意,如果说路径上已经有这个文件,那么再上传就会报错。
<5>获取body–json格式提交
如果说body是json格式提交,那么我们就要通过getInputStream获取请求正文的数据。当然这里不仅仅是json格式,不管Content-Type是什么类型的,都是可以获取到的。但是其他的一般都比较复杂,所以不推荐。这里json的使用使用这个方法比较典型的场景
<1>方法一 + 方法二
这里为什么直接两个方法放在一起呢?是因为这两种方法都不太推荐,获取整体信息还好说,那么如果说我们要单独获取username和password呢?
这就不太方便,所以这里这两种方法我们单单做一个了解就好。
InputStream is = req.getInputStream();//输入流
InputStreamReader isr = new InputStreamReader(is,"utf-8");
BufferedReader br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String str;
while((str= br.readLine()) != null){
sb.append(str);
}
<2>方法三
这里我们要引入一个第三方库
import com.fasterxml.jackson.databind.ObjectMapper;
然后通过这个第三方库实现json字符串和java对象的相互转化。
ObjectMapper mapper = new ObjectMapper();
Map json = mapper.readValue(is, Map.class);
System.out.println("获取的json字符串转换的map:" + json);
这种方式简单暴力,就直接转换为一个map对象,转换为Map对象是一个万金油的方法,任何json格式都可以转换为map。
但是!!!map使用也不方便,它使用指定的键来获取值,那么我们就需要再对这个方法进行改良,也就是加入一个静态内部类
static class User{
private String username;
private String password;
//再提供getter 和 setter方法
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPssword() {
return password;
}
public void setPssword(String pssword) {
this.password = pssword;
}
}
然后之后在主类我们就不需要这么麻烦了
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(is,User.class);
System.out.println("获取的json字符串转换的user对象:" + user);