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以及代码示例
版权归原作者 奋斗小温 所有, 如有侵权,请联系我们删除。