目录
编辑一、Servlet 是什么
二、第一个 Servlet 程序
1、创建项目
2、引入依赖
3、创建目录
4、编写代码
5、打包程序
6、部署操作
7、验证
小结:
三、更方便的部署方式
四、访问出错怎么办
五、Servlet 常用 API
1、HttpServlet
(1)init
(2)destroy
(3)service
2、HttpServletRequest
3、HttpServletResponse
六、前端给后端传参
1、GET,query string
2、POST ,form
3、POST ,json
七、、Cookie 和 Session
八、模拟登录的代码
Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序员的 API, 帮助程序员简单高效的开发一个 web app.
网页分为两种:
静态页面:页面的内容始终是固定不变的
动态页面:页面的内容随着输入参数的不同而改变
静态页面,就只是单纯的 html ,而动态页面则是 html + 数据
老规矩,先写一个 hello world
预期写一个 servlet 程序,部署到 tomcat 上,通过浏览器访问,得到 hello world 字符串
一共七个步骤:
1、创建项目
2、引入依赖
3、创建目录结构
4、编写代码
5、打包程序
6、部署程序
7、验证
此处,我们要创建一个 maven 项目
maven :是一个 “工程管理” 工具
1、规范目录结构
2、管理依赖
3、构建
4、打包
5、测试
........
我们现在,主要使用 管理依赖 和 打包 功能
选择 maven ,然后创建新的项目即可
如果是首次使用 maven ,项目创建好了之后,会在下面读条,从中央仓库加载一些 maven 依赖
(需要联网,由于 maven 仓库在国外,网络不是很稳定,所以这里的读条会比较久)
注意此处的 目录结构:
这里我们所需要引入的依赖就是 servlet 对应的 jar 包
在 maven 的官网中,搜索 servlet ,然后选择所需要的对应的版本
这里我选择的是 3.1.0的版本(和 tomcat 8 对应)
注意:粘贴的时候,要注意加一个 dependencies 标签
dependencies 这个标签是 project 顶级标签的子标签,位置不能放错,如果有多个依赖,都往标签里粘贴就行
如果是首次使用,此时这里的字可能是红色的,红色说明还没下载完,一般只要一粘贴,idea 的 mave 就会自动触发依赖的下载,下载完毕就不红了(下载只需要一次)
如果过了很久还没下载完,可以点一下刷新
虽然 maven 已经帮我们自动的创建了一些目录,但是还不够
因为此处是需要 maven 去开发一个 web 程序,还需要别的目录
创建步骤:
(1)在 main 目录下(和 java ,resources 并列),创建一个 webapp 目录
(2)在 webapp 下创建 WEB-INF 目录
(3)在 WEB-INF 目录下,创建一个 web.xml 文件
(4)给 web.xml 写一点东西进去
注意:此处目录名字和结构,一点都不能出错
web.xml 内容:
Archetype Created Web Application
当前写的 servlet 程序和以往写的别的代码相比,有一个非常大的区别:没有 main 方法!!!
main 方法可以视作是汽车的发动机,有了发动机才能跑
如果现在有一辆车,没有发动机,怎么跑呢?
挂一个车头,让车头拽着他跑就行了!!!
我们写的 servlet 程序就是车厢,tomcat 就是车头
把我们写好的 servlet 程序扔到 webapps 目录下,就相当于把车厢给挂在车头后面了
tomcat 如何识别 webapps目录下,哪些是需要拉着跑的车厢呢?
就是靠目录下有一个 WEB-INF / web.xml,这个文件的作用就是让 tomcat 能够识别当前代码为 webapp,并进行加载
我们先创建一个类,继承自 HttpServlet
HttpServlet 是 servlet api 里提供的现成的类,写 servlet 代码一般都是继承这个类
这里继承的主要目的是为了针对 HttpServlet 进行功能的扩展
接下来,重写 doGet 方法
我们写的这个 doGet 方法,不需要我们自己手动调用,而是交给 tomcat 来调用,tomcat 收到一个 get 请求,就会触发 doGet 请求
Tomcat 就会构造好两个参数 req 和 resp
req 是 TCP socket 中读出来的字符串,按照 HTTP 协议解析得到的对象
这个对象里的属性是什么?
就是 和 HTTP 请求报文格式相对应的
resp 就是一个空的对象(不是 null,只是 new 了一个对象,但是没有设置属性),程序员就需要在 doGet 里面,根据 req ,结合自己的业务逻辑,构造出一个 resp 对象
doGet 做的工作就是根据请求,计算响应,其中 resp 这个参数,本质上就是一个 ”输出型参数“
此处的 Writer 对象是属于 resp 对象的,此时进行的 write 操作其实是往 resp 的 body 部分进行写入,等 resp 对象构造好了,tomcat 会统一的转成 HTTP 响应的格式,再写 socket
流对象,不一定是非得写入网卡,也不一定非得写入硬盘,也可以写到内存缓冲区里面(关键是看你代码实现的细节)
最后,再加上注解
注解是 java 中特殊的类,java 专门定义了一种 “语法糖” 来实现注解
注解的作用:针对一个 类 / 方法 ,进行额外的 “解释说明” ,赋予这个 类 / 方法 额外的功能 / 含义
此处的 @WebServlet 注解,作用是把当前的 类 和一个 HTTP 请求的路径关联起来
doGet 是 Tomcat 收到 GET 请求的时候就会调用,但是具体是不是要调用 doGet ,还得看当前 GET 请求的路径是什么,不同的路径可以触发不同的代码(关联到不同的类上面)
一个 Servlet 程序中,可以有很多的 Servlet 类,每个 Servlet 类,都可以关联到不同的路径(对应到不同的资源),因此此处多个 Servlet 就实现了不同的功能
@WebServlet ("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//这个是在服务器的控制台里打印
System.out.println("hello world!");
//要想把 hello world 返回到客户端,需要使用下面代码:
//getWriter 会得到一个 Writer 对象
resp.getWriter().write("hello world1");
}
}
这里所谓的打包程序,就是把程序编译好,得到一些 .class 文件
再把这些 ,class 打成一个压缩包
jar 包就是一种 .class 构成的压缩包
但是此处我们要打的是 war 包
jar 包只是一个普通的 java 程序,war 则是 tomcat 专属的用来描述 webapp 的程序
一个 war 包,就是一个 webapp
借助 maven ,直接点击即可
打开 maven 面板,然后选中 package,直接双击或者右键运行都可
默认情况下,maven 打的是 jar 包,此处我们需要打 war 包,因此需要微调一下 pom.xml
这个就描述了 打的包是 jar 包还是 war 包
这个则描述了打的 war 包的名字(可以不写)
调整完毕之后,我们再重新进行打包
此时,war 包就打成功了
把刚才打包好的 war 拷贝到 tomcat 的 webapps 目录中即可
无论 tomcat 是再同一个还是不同的电脑上,都是这样进行拷贝的
然后,启动 tomcat
如果 tomcat 正在运行,直接拷贝,tomcat 也能识别,但是这个识别操作可能存在 bug
当启动成功后,就说明当前的部署操作成功了
打开浏览器,输入 url,访问写好的这个代码
注意这个 URL 路径的写法:
第一级路径,也叫做 context path / application path(这个目录就代表一个 webapp 网站)
对应到我们刚才所打的 war 包的名字
第二级路径,也叫做 Servlet path ,就对应到代码中的注解
刚才在 浏览器地址栏中输入 URL 之后,浏览器就构造了一个对应的 HTTP GET 请求,发给了 tomcat
tomcat 就根据 第一级路径 去确定了具体的 webapp ,再根据 第二级路径 确定调用了哪个类
再然后通过 GET / POST 方法确定调用了 HelloServlet 的哪个方法(doGet / doPost)
此时,tomcat 就执行对应的代码完成对应的工作
上述步骤,是使用 Servlet 最朴素的步骤,我们也可以通过一些操作来简化步骤
打包和部署程序这两个操作,我们可以使用 IDEA 的 Tomcat 插件,把 Tomcat 集成到 IDEA 中,这样就省去了手动打包,手动部署的部分,只需要按一下运行,就可以自动打包部署
基于 Tomcat 插件自动打包部署,适用于开发阶段,频繁修改代码频繁验证
手动打包手动部署,适用于上线阶段,发布程序
IDEA 功能虽然很强,但是也不是面面俱到的
IDEA 提供了一些 API ,可以让程序员开发插件,对 IDEA 的现有功能进行扩展
这个就是我们所需要用到的插件
首次使用 Tomcat 插件,需要进行配置:
1、新增运行配置
2、点击加号,新增配置
3、设置一下 Tomcat 所在的路径
4、运行 Tomcat
正常情况下,点击之后,IDEA 就会调用 tomcat 来运行程序了
smart tomcat 工作原理:
并不是自动把 war 包拷贝了(webapps 里面是不变的),而是通过另一种方式来启动 tomcat 的
tomcat 支持启动的时候,显示指定一个特定的 webapp 目录,相当于是让 tomcat 加载单个 webapp 运行
idea 直接调用 tomcat ,tomcat 直接加载当前项目中的目录
这个过程其实没有打 war 包,也没有 拷贝,也没有解压缩的过程
此时,程序是可以正常运行的,但是像之前 webapps 下已有的一些内容,比如说像欢迎页面,是没有的
出现 404:
1、路径写错了
1、比如 浏览器发了一个 GET 请求,但是你的代码里面没有写 toGet
2、super.doGet 没有干掉
出现 500:
代码抛异常了
出现 “空白页面”
代码里面没有写 resp.getWriter.write();
出现 “无法访问此网站”:
tomcat 没启动
HttpServlet 实例化指的是 tomcat 首次收到了和该类相关联的请求的时候
tomcat 收到了 /hello 这样的路径的请求,就会调用到 HelloServlet,于是就需要先对 HelloServlet 进行实例化(实例化只进行一次)
后续再收到 /hello ,就不必再实例化了,直接复用之前的 HelloServlet 实例即可
@WebServlet ("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
//可以在这里重写 init 方法
//插入一些我们自己 "初始化" 相关的逻辑
System.out.println("init");
}
此时就会发现,Init 代码只会在实例化的时候,首次被调用,后续不管怎么触发请求,都不会再被调用了
Servlet 是服务器上的代码,只要服务器不重新启动,init 就不会再执行到
只要服务器在运行,都可能在使用,当服务器终止的时候,就不使用了
注意:这里的 destroy 能否被执行到,是不确定的
1、如果是直接通过 smart tomcat 的停止按钮,这个操作本质上是通过 tomcat 的 8005 端口主动停止,能够触发 destroy
2、如果是直接杀进程,可能就来不及执行 destroy ,就没了
因此,我们不太推荐使用 destroy ,因为它不是很靠谱
每次收到 http 请求,就会触发(路径匹配的请求)
doGet 就是在 service 中调用的
这三个方法,就是 HttpServlet 中给最关键的三个方法
Servlet 的生命周期是怎么回事?
1、开始的时候,执行 init
2、每次收到请求,执行 service
3、销毁之前,执行 destroy
接下来,我们写了这么一段代码:
@WebServlet("/method")
public class methodServlet 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");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doDelete");
resp.getWriter().write("doDelete");
}
}
当我们在浏览器中运行的时候,会发现,此时发送的是一个 doGet 请求
那么如何构造其它不同种类的请求呢?
1、通过 ajax
2、通过 postman
我们现在来看一下通过 postman 构造请求:
也可以选择通过 ajax 来构造请求
注意:这个 html 文件要放在 webapp 目录下
Document
当然,也可以用绝对路径的写法,在这里,绝对路径是这样写的:
HttpServletRequest 表示的是 HTTP 请求
这个对象是 Tomcat 自动构造的,Tomcat 其实是会实现监听端口,接受链接,读取请求,解析请求,构造请求对象等一系列工作
query string 是键值对结构,此处就可以通过 getParameter 方法来根据 key 获取到 value
代码示例:
@WebServlet("/showRequest")
public class showRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//这里是设置响应的 content-type,告诉浏览器,响应的 body 里的数据格式是什么样的
resp.setContentType("text/html");
//弄一个 StringBuilder ,把这些 api 的结果给拼起来,统一写回到响应中
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getProtocol());
stringBuilder.append("
");
stringBuilder.append(req.getMethod());
stringBuilder.append("
");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("
");
stringBuilder.append(req.getContextPath());
stringBuilder.append("
");
stringBuilder.append(req.getQueryString());
stringBuilder.append("
");
//获取到 header 中所有的键值对
Enumeration headerNames= req.getHeaderNames();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
stringBuilder.append(headerName + " " + req.getHeader(headerName));
stringBuilder.append("
");
}
resp.getWriter().write(stringBuilder.toString());
}
}
一个 HTTP 响应中,报头的 key 是可以存在多个重复的
设置的 ContentType 和 字符集,务必要在 write 上面
这是构造重定向响应(302)
重定向:浏览器会自动跳转到指定的新地址
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("https://www.sogou.com");
}
}
或者这样写,效果和上面的代码效果是相同的
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//resp.sendRedirect("https://www.sogou.com");
resp.setStatus(302);
resp.setHeader("Location","https://www.sogou.com");
}
}
keep-alive:建议 浏览器 和 服务器 之间保持长连接
TCP 是有连接的
短连接:每个连接至进行一次请求和响应的传输
长连接:每个连接可以进行多次请求和响应
1、GET ,通过 query string 传递给后端
2、POST ,通过 form 表单的格式
3、POST,通过 json 的格式
在前端给后端传两个数字,一个是 studentId,一个是 classId
@WebServlet("/getParameter")
public class getParameterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//预期浏览器会发一个形如 /getParameter?studentId=10&classId=20 的请求
//借助 req 里面的 getparameter 方法,就能拿到 query string 中的键值对内容了
//getparameter得到的是 string 类型的结果
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html");
resp.getWriter().write("studentId = " + studentId + "classId = " + classId);
}
}
这里的 query string 键值对,会自动被 tomcat 处理成形如 Map 这样的结构,后续就可以随时通过 key 获取 value 了
如果 key 在 query string 中不存在,此时返回值就是 null
对于前端是 form 表单这样的数据格式,后端还是使用 getParameter 来获取
form 表单也是键值对,与 query string 的格式一样,只不过这部分内容在 body 中
@WebServlet("/postParameter")
public class PostParameterServlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html");
resp.getWriter().write("studentId = " + studentId + "classId = " + classId);
}
}
使用 getParameter 既可以获取到 query String 里面的键值对,也可以获取到 form 表单构造的 body 中的键值对
json 是一种非常主流的数据格式,也是键值对结构
我们可以把 body 按照这个格式来 组织,前端可以通过 ajax 的方式来构造出这个内容,更简单的办法是使用 postman
@WebServlet("/postParameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//通过这个方法来处理 body 为 json 格式的数据
//直接把 req 对象里 body 给完整读取出来
//getInputStream
//在 流对象 中,读多少个字节,取决于 content-length
int length = req.getContentLength();
byte[] buffer = new byte[length];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
//把这个字节数组构造成 string
String body = new String(buffer);
System.out.println("body = " + body);
resp.getWriter().write(body);
}
}
此处打印的结果,就是从 body 中读到的内容
postman 构造出一个指定的 POST 请求,body 就是 json 数据
请求到达 tomcat ,tomcat 解析成 req 对象
在 servlet 代码中, req.getInputStream 读取 body 的内容
又把 body 的内容构造成响应结果,返回给浏览器(postman)
这个代码的执行流程,和上面通过 form 表单传参,流程是类似的,只不过传输的数据格式不同
当前通过 json 传递数据,但是服务器这边只是把整个 body 读出,没有按照键值对的方式来处理(还不能根据 key 获取 value)
所以,我们可以使用第三方库 jackson
通过 maven 来引入第三方库
选择好对应得版本之后,将 xml 片段粘贴到 pom.xml 中
class Student{
public int studentId;
public int classId;
}
@WebServlet("/postParameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//使用 jackson 所涉及到得核心对象
ObjectMapper objectMapper = new ObjectMapper();
//readvalue 就是把一个 json 格式得字符串,转换成 java 对象
Student student = objectMapper.readValue(req.getInputStream(),Student.class);
System.out.println(student.studentId + "," + student.classId);
}
}
1、这个代码会从 body 中,读出 json 格式的字符串
2、然后根据第二个参数的类对象,创建出以恶搞 Student 实例
3、再解析上述 json 格式的字符串,处理成 map 键值对结构
4、再遍历所有键值对,看 键 的名字 和 Student 实例的哪个属性名字匹配,就把对应的 value 设置到该属性中
5、返回该 Student 实例
Cookie 是什么?
浏览器提供的持久化存储数据的机制
Cookie 从哪里来?
Cookie 从服务器返回给浏览器
服务器代码中,由程序员决定要把什么样的信息保存到客户端这边
通过 HTTP 响应的 Set-Cookie 字段,把键值对写回去即可
Cookie 到哪里去?
Cookie 会在后续浏览器访问服务器的时候带到请求的 header 中,发给服务器
为什么要这么搞?(从服务器来,回到服务器)
服务器不是只给一个客户端提供服务的,同一时刻要处理多个客户端
此时服务器就可以通过 Cookie 中给的值,来识别当前客户端是谁,当前客户端的服务提供到哪个环节了(服务器借助 Cookie 自报家门)
Cookie 保存在哪里?
Cookie 存储在浏览器(客户端)所在主机的硬盘中
浏览器会根据域名来分别存储
Cookie 有一个最典型的应用——标识用户的身份信息
Cookie 和 Session 之间的 关联 和 区别
关联:
在网站的登录功能中,需要配合使用
区别:
Cookie 是客户端的存储机制, Session 是服务器的存储机制
Cookie 里面可以存各种键值对,Session 则专门用来保存用户的身份信息
Cookie 完全可以单独使用,不搭配 Session (实现 非登录 场景下)
Session 也可以不搭配 Cookie 使用(手机 app 登录服务器,服务器也需要 Session,此时就没有 Cookie 的概念),Cookie 是和浏览器强相关的
Cookie 是属于 HTTP 协议中的一部分,Session 则可以和 Cookie 无关
这个例子中涉及到两个页面:
1、登录页面
2、主页面
涉及到两个 servlet :
1、处理登录的 Loginservlet ,判定用户名密码
2、构造主页面的 Indexservlet
1、编写登录页面
2、编写 LoginServlet 处理登录请求
所谓的会话,是一个键值对
key 是 sessionId ,value 是一个 HttpSession 对象
每个客户端登录的时候,都有这样的一个键值对(会话),服务器要管理多个这样的对话
服务器可以弄一个哈希表,把这些对话组织起来
getSession(true) 就是判定当前请求,是否已经有对应的会话了(拿着请求中的 Cookie 里的 SessionId 查一下 哈希表)
如果 SessionId 不存在,或者没查到,就创建新的会话并插入到哈希表中,如果查到了,就返回查到的结果
创建会话过程:
1、构造一个 HttpSession 对象
2、构造唯一的 sessionId
3、把这个键值对插入到哈希表
4、把 SessionId 设置到 响应报文中 Set-Cookie 字段中
HttpSession 对象,自己也是一个键值对
可以通过 setAttribute 和 getAttribute 来存取键值对
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
//验证用户名密码是否正确
//正常情况下,用户名密码使用数据库保存
//此处直接写死
//此处,用户名: zhangsan 和 lisi
//密码:123
if (!username.equals("zhangsan") && !username.equals("lisi")) {
//登录失败
//直接重定向到登录页面
System.out.println("登录失败,用户名错误");
resp.sendRedirect("login.html");
return;
}
if (!password.equals("123")){
//登录失败
System.out.println("登录失败,密码错误");
resp.sendRedirect("login.html");
return;
}
//登录成功
//1、创建一个会话
HttpSession session = req.getSession(true);
//2、把当前的用户名保存到对话中,此处的 HttpSession 可以当成一个 Map 使用
session.setAttribute("username",username);
//3、重定向到主页
resp.sendRedirect("index");
}
}
会话这里,服务器是如何组织的?
3、编写 IndexServlet 生成主页
此处可以这样取,是因为前面的登录操作已经存过了
HttpSession value 的类型是 Object ,需要手动强转为 String(设定 Object 意思就是你存各种类型都行)
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
//通过重定向,浏览器发送的是 Get 请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//先判定用户的登录状态
//如果用户还没有登录,要求先登录
//已经登陆了,则根据会话中的用户名,来显示到页面上
//这个操作不会触发会话的创建
HttpSession session = req.getSession(false);
if (session == null){
//未登录状态
System.out.println("用户未登录");
resp.sendRedirect("login.html");
return;
}
//已经登陆了
String username =(String) session.getAttribute("username");
//构造页面
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write(" 欢迎 " + username + " 回来!");
}
}