目录
一、Hello World
1.创建项目
2.引入依赖
3.创建目录
4.编写代码
4.1 继承 HttpServlet 父类,重写 doGet 方法
4.2 在 doGet 中编写代码,打印 hello world
4.3 给 HelloServlet 加上注解
4.4 完整代码
5.打包代码
6.部署
7.验证程序
二、简化部署方式
1.安装 Smart Tomcat 插件
2.配置 Smart Tomcat 插件
3.常见错误
4.简化的本质区别
三、servlet 中常见的问题
1. 404
2.405
3.500
4.出现“空白页面”
5.出现“无法访问此网站”
四、Servlet API 详解
1. HttpServlet
1.1 HttpServlet 方法
1.2 Servlet 的生命周期(面试题)
Tomcat 的基本使用是比较容易的:
1️⃣启动2️⃣把内容拷贝到 webapps3️⃣通过浏览器访问4️⃣使用 netstat 查看端口
我们要学习的重点是基于 Tomcat 进行编程!!
现在要写网站后端(HTTP 服务器),虽然可以重头写一个 HTTP 服务器,但是比较麻烦,Tomcat 已经完成这部分工作,并且 Tomcat 给我们提供了一系列 API,可以让我们在程序中直接调用;此时就可以省去一部分工作(HTTP 服务器肯定要根据 HTTP 协议解析请求报文,还要根据 HTTP 协议,构造响应报文,Tomcat 已经弄好了),更专注于业务逻辑了(写的程序要解决什么问题,是怎么解决的)
接下来我们将学习Tomcat 给提供了一系列 API 也叫 Servlet
一、Hello World
在 java 中使用 Servlet,先从一个 hello world 着手❗❗注意,接下来见到的是咱们整个学习生涯中,最复杂的 hello world;需要经历七个步骤(对初学者非常不友好),但是这些步骤都是一个固定套路
1.创建项目
此处需要创建一个 maven 项目:maven 是一个构建工具,能帮助我们去构建、测试、打包一个项目;
首次使用 maven 项目,IDEA 会从互联网上加载很多的依赖,需要花一定的时间,同时需要保证网络通畅
一个 maven 项目,首先会有一个 pom.xml 配置文件;这个文件描述了maven 项目的各个方面的内容;
<?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>
</project>
一个 maven 创建好之后,IDEA 会帮助我们自动创建出一些目录:
2.引入依赖
Servlet 是 tomcat 提供的 api(不是标准库);标准库例如:String、Thread、List/Map、Scanner...只要装了 jdk,这些都是内置的;因此 servlet 是需要额外下载安装的(Tomcat 安装好了,是 Tomcat 运行时使用的,现在阶段是开发阶段,需要额外安装 Servlet 的 jar 包)
从中央仓库下载安装:Maven Repository: Search/Browse/Explore (mvnrepository.com)
在 pom.xlm 中添加 dependenceies 标签,把上边代码复制粘贴到这个标签中
<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>provided</scope>
</dependency>
</dependencies>
此时依赖就下载成功了;jar 包是被下载到本地的一个隐藏目录了,c 盘的用户里 .m2 文件中,找到 resposity打开,再找到 javax 寻找 servlet,里边有我们下载的 3.1.0 版本,只要下载好以后,后续使用就不必重新下载
3.创建目录
1️⃣创建 webapp 目录:在 main 目录下, 和 java 目录并列, 创建一个 webapp 目录 (注意, 不是 webapps).
2️⃣创建 WEB-INF 目录:然后在 webapp 目录内部创建一个 WEB-INF 目录
3️⃣创建一个 web.xml 文件
这里的目录结构、目录位置、目录名字务必保证一字不差❗❗
web.xml 是给 tomcat 看的:tomcat 从 webapps 目录中加载 webapp,就是以 web.xml 为依据的
4️⃣编写 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>
4.编写代码
在 java 项目下创建一个类 HelloServlet:
import javax.servlet.http.HttpServlet;
public class HelloServlet extends HttpServlet {
}
创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确,尝试刷新
4.1 继承 HttpServlet 父类,重写 doGet 方法
public class HelloServlet extends HttpServlet {
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
- HttpServletRequest 表示 HTTP 请求:Tomcat 收到请求把这个请求按照 HTTP 协议的格式,解析成了对象(这个对象里的属性就是 HTTP 中的各个信息)也就是说 Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body 等) 都是通过这个对象来获取.
- HttpServletResponse 表示 HTTP 响应:此处响应对象是一个空的对象,需要在 doGet 中设置响应的一些数据(例如响应的 body、header和状态码等...,只要把属性设置到这个 resp 对象中,Tomcat 就会自动根据响应对象,构造一个 HTTP 响应字符串,通过 socket 返回给客户端
- 重写 doGet 方法:这个方法不是手动调用,而是 Tocmat 在合适的时机自动调用的
这种代码的编写方式,就算“框架”(framework)
1️⃣注释掉 super.doGet(req, resp);
点进父类的 doGet 可以看到这里直接返回一个错误页面,如果步干掉就返回了一个 405
4.2 在 doGet 中编写代码,打印 hello world
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
resp.getWriter().write("hello wprld");
}
}
- resp.getWriter() :得到了 resp 内部持有的 Writer 对象(字符流),既然是字符流就可以使用 write 来写,此处写的数据就是写到 http 响应的body中 Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览器
4.3 给 HelloServlet 加上注解
@WebServlet("/hello")
4.4 完整代码
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")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
resp.getWriter().write("hello world");
}
}
上述代码就已经写完了,不需要 main 方法;上述代码并非独立运行,而是把这个代码插入到 Tomcat 中,由 Tomcat去调用的
5.打包代码
我们的程序不能独立运行,而是必须放到 Tomcat 上运行(部署),部署的前提是打包
- 对于一个规模比较大的项目,里边就会包含很多的 .java 文件,进一步就会产生很多的 .class,所以把这些 .class 答案宝成一个压缩包再进行拷贝,是比较科学的
- 在 java 中使用的压缩包为 jar(普通的 java 程序)、war(部署给 tomcat 的程序)
- war 和 jar 本质上没有区别,都是把一堆 .class 文件给打包进去,但是 war 包是属于 tomcat 的专属格式,里边会有一些特定的目录结构和文件,比如 web.xml,后续 tomcat 就要识别这些内容来加载 webapp
如何使用 maven 进行打包操作❓❓打开 maven 窗口 (般在 IDEA 右侧就可以看到 Maven 窗口, 如果看不到的话可以通过 菜单 -> View -> Tool Window -> Maven 打开),然后展开 Lifecycle , 双击 package 即可进行打包
打包的操作:
- 检查代码中是否存在一些依赖,依赖是否下载好(这个事情都是 maven 负责的,之前引入了 serlvet 的依赖)
- 把代码进行编译,生成一堆 .class 文件
- 把这些 .class 文件,以及 web.xml 按照一定的格式进行打包
为了打出来的是 war 包,需要调整 pom.xml,描述打包生成的包格式:pom.xml 中,在 project 顶级标签下方,写一个 <packing> 标签,描述打包的类型是 war;此处也可以修改打包的文件名
此时重新打包,就看到了一个 war 包
打好的 war 包,就是一个普通的压缩包,收可以使用解压工具(WINRAR)打开,看到里边的内容,但是并不需要手动解压缩,直接把整个 war 交给 tomcat,tomcat能够自动的解压缩
6.部署
把打好的 war 包 ,拷贝到 tomcat 的 webapps 目录中;重新打开 tomcat 的 bin 文件 的 startup.bat ,此时就已经部署完成
这个乱码表示 hello_servlet.war 已经部署完成,只不过日志乱码;乱码是因为拜尼马方式不一样,tomcat 使用的编码是 utf8,而 windows 的 cmd 编码是 gdk
7.验证程序
doGet : 遇到 GET 请求,就可以执行 doGet,前提是 请求的 URL 的路径要匹配
浏览器 url 中输入(hello_servlet表示一级路径、hello表示二级路径):127.0.0.1/8080/hello_servlet/hello
此处的路径是分两级:
- hello_servlet:称为 Context Path / Application Path,标识了一个 webapp(也就是 webapp 的目录名 / war 包名)一个 Tomcat 上可以多个webapp
- hello:称为 Servlet Path,标识当前请求要调用哪个 Servlet 类的 doGet(一个 webapp 中可以有多个 Servlet,自然就有多个 doGet),此处的 hello 是根据注解来的
二、简化部署方式
上述 hello word 的程序也可以简化:把 5(打包代码) 和 6(部署) 简化成一键式完成,我们使用 IDEA 中的 Smart Tomcat 插件来完成这个过程
IDEA 功能非常多,非常强大,凡是及时如此,IDEA也无法做到“面面俱到”,为了支持这些特定的、小众的功能就引入了“插件体系”,插件可以视为对IDEA原有功能的扩充,程序猿可以按需使用;同理很多这样的程序都引入了插件体系,例如 VSCode
1.安装 Smart Tomcat 插件
1️⃣打开 file,继续打开 Settings
2️⃣选择 Plugins, 选择 Marketplace, 搜索 "tomcat", 点击 "Install"
使用 Smart Tomcat 插件,可以简化打包部署工作(社区版使用的方法);IDEA 专业版来说,内置了 Tomcat Server(这个东西用起来更复杂,还是建议使用 smart tomcat)
2.配置 Smart Tomcat 插件
首次使用,需要配置插件:
1️⃣点击右上角的 "Add Configuration";选择左侧的 "Smart Tomcat"
2️⃣在这里 Name 可以改也可以不改,我改为了 hello servlet;并且把 Tomcat server 的路径改为 原本 Tomcat 安装的路径
3️⃣修改 Content path:访问程序的两级路径中的第一级 (我的第一级目录是 hello_servlet)
- 特殊规则:
- 如果我们的程序是拷贝 war 包到 webapps 中运行,此时 Context Path 是 war 包名字
- 如果我们的程序是使用 Smart Tomcat 运行,Context Path 是在上述配置中,手动设置的,默认是项目名字
4️⃣运行代码
此时右上角的 "Add Configuration"旁边有个三角形,点击即可运行
此时 Tomcat 的日志就在 IDEA 中就显示了,不会再单独弹出 cmd,因此乱码问题就解决了
3.常见错误
初学者可能出现以下错误问题:
端口被占用:Tomcat 启动需要绑定两个端口,8080(业务端口)、8005(管理端口);一个端口号只能呗一个进程绑定;此时我们直接把 bin 文件打开的 startup.bat 关闭即可
此时就运行成功了
以上地址是提示 Tomcat 如何访问(localhost == 127.0.0.1),不要点,点了就是 404 ;因为这个路径只有 Context Path,没有 Servlet Path
4.简化的本质区别
smart tomcat 的运行方式和之前拷贝到 webapps 中,是存在本质的
- smart tomcat 使用了 Tomcat 另外一种运行方式;在运行 Tomcat 的时候,通过特定的参数来指定 Tomcat 加载某个特定目录中的 webapp
因此,上述过程既不会打包也不会拷贝;这是开发和调试阶段使用的方式,如果是部署到生产环境,还的是打 war 包拷贝
三、servlet 中常见的问题
1. 404
404 表示浏览器访问的资源在服务器上不存在
1️⃣请求的路径写错了
例如刚刚上述的地址( Tomcat 如何访问):路径只有 Context Path,没有 Servlet Path 或者路径只有 Servlet Path,没有 Context Path
再例如:Servlet Path 写的和 URL 不匹配,也会出现 404
2️⃣路径写对了,但是 war 包没有被正确加载
web.xml 写错了 或者 如果有两个 Servlet 的 Servlet Path 相同,会导致 war 包不能被正确加载(如果没正确加载,会在日志中有提示)
2.405
405 表示 对应 HTTP 请求方法没有实现
1️⃣发送请求的方法和代码不匹配:比如代码写的是 doPost,而发送的请求是 GET 请求
2️⃣ 方法和代码匹配,但是忘记消掉 super.doXXX
3.500
500 往往是 Servlet 代码中抛出的异常导致:需要观察异常调用栈
修改 HelloServlet 代码:
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
String s = null;
System.out.println(s.length());
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
resp.getWriter().write("hello world");
}
}
4.出现“空白页面”
修改代码:去掉 resp.getWritter().write() 操作
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
//resp.getWriter().write("hello world");
}
}
重启服务器,访问服务器可以看到一个空白页面
5.出现“无法访问此网站”
停止 Tomcat,然后访问服务器就可以看到“无法访问此网站”
四、Servlet API 详解
虽然 Servlet API 有很多,重点掌握三个类即可
- HttpServlet
- HttpServletRequest
- HttpServletResponse
1. HttpServlet
Servlet 程序都是要继承一个 HttpServlet 类
因此我们就需要知道哪些方法是能够被重写的,也就是 HttpServlet 中有什么方法,都是做什么的
1.1 HttpServlet 方法
方法名称 | 调用时机 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/...
|
收到其他请求的时候调用
(
由
service
方法调用
)
|
init |
在
HttpServlet
实例化之后被调用一次
|
destroy
|
在
HttpServlet
实例不再使用的时候调用一次
|
service
|
收到
HTTP
请求的时候调用
|
1️⃣init 方法:
HttpServlet 被实例化之后会调用一次(只调用一次)(首次匹配请求的时候,会被调用),使用这个方法来做一些初始化相关的工作
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("打印 init");
}
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
resp.getWriter().write("hello world");
}
}
启动服务器,此时并没有实例化对象,并没有执行到“打印 init”:
而是服务器收到一个匹配的请求的时候会被调用(能够调用 doGet 的请求):
此时就执行了 “打印 init”;上述 127.0.0.1:8080/hello_servlet/hello 这个请求会触发 HelloServlet 类的 doGet 的执行,就会调用 doGet 之前,先调用 init;❗❗注意,只会调用一次
因此这个方法来做一些初始化相关的工作
2️⃣destroy 方法:
这个方法是 webapp 被卸载(被销毁之前)执行一次;用来做一些收尾工作
destroy 是否能被执行,是不靠谱的❓❓❓
- 如果是通过 8005 管理端口来停止服务器,此时 destroy 能执行
- 如果是直接杀死进程的方式停止服务器,此时 destroy 执行不了
所以不建议使用 destroy
❓❓8005 管理端口是什么
Tomcat 启动会使用两个端口:8080业务端口(工作)、8005管理端口(生活)
3️⃣service 方法:
每次收到路径匹配的请求都会执行;doGet 和 doPost 其实是在 service 中被调用的,一般不会重写 service,只是重写 doXXX 就行了
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
//HttpServlet 被实例化之后会调用一次(只调用一次)(首次匹配请求的时候,会被调用),使用这个方法来做一些初始化相关的工作
System.out.println("打印 init");
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("执行 service");
}
//继承 HttpServlet 父类,重写 doGet 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);//调用父类的doGet,需要注释掉这个代码
//这里在服务器的控制台中,打印了字符串(服务器看到了,客户端没看到)
System.out.println("hello world");
//这个是给 resp 的 body 写入 hello world 字符串,这个内容就会被 HTTP 响应返回给浏览器,显示到浏览器页面上
resp.getWriter().write("hello world");
}
}
4️⃣doGet、doPost、doPut...方法:
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doGet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPut");
}
}
启动服务器,使用 Postman 构造请求:
❗❗注意:
@WebServlet("/hello")
//创建一个类 HelloServlet , 继承自 HttpServlet(来自于从 maven 中央仓库下载的 jar 包);如果提示不出来说明 jar 包没有加载正确
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("这是一个 doGet 请求");
}
}
???说明是乱码了
- 数据返回的时候,自身是一种编码方式(IDEA 里边写一个字符串,默认都是 UTF8)
- 浏览器在展示的时候,也有一种编码方式(根据系统的默认编码——windows 10 简体中文版,默认编码是 gdk)
如果上述两个方式对不上,就会乱码
解决方法:加入 resp.setContentType("text/html; charset=utf8"); 代码代表:告诉浏览器返回的数据是 utf8
1.2 Servlet 的生命周期(面试题)
生命周期:什么阶段在做什么事
- init 是初始情况下调用一次
- destroy 是结束之前调用一次
- service 是每次收到路径匹配的请求都调用一次
🌈这节课我们讲解了 Servlet 的使用,写了有史以来最复杂的 Hello World,并且解决了写代码出现的问题,而且介绍了三种重要的 Servlet API 中的 HttpServlet,下节课我们将介绍 HttpServletRequest 、 HttpServletResponse以及代码示例