JavaWeb-网络编程
网络编程必然离不开服务器,而tomcat
服务器则是用的比较广泛的一种
Tomcat
下载
- tomcat.org.apache 访问官网
tomcat
还有另外一个名字:catalina
(美国的一个岛屿,风景亮丽,据说作者是在这个岛上开发了一个轻量级的web服务器,体积小,运行速度快,因此tomcat
又被称为catalina
)tomcat
是java
语言写的tomcat
服务器想要运行,必须有jre
(java的运行时环境)tomcat
的logo是一只公猫,寓意表示Tomcat
服务器是轻巧的,小巧的)
在官网上下载对应版本和目标系统的安装包,这里我选择了Tomcat10
理论上安装完tomcat
后是需要配置CATALINA_HOME
环境变量的,但现在即使不配置也能启动,原因是脚本中有判断配,如果当前环境变量没有配置,会自动将tomcat
的应用目录配置为CATALINA_HOME
,这里就不再配置了
启动Tomcat
-
启动
Tomcat
服务器之前,请先安装JAVA
的JDK
(纯运行只需要JRE
),并配置JAVA_HOME
环境变量,这里不再赘述 -
进入解压后的
tomcat
目录下的bin
目录,里面存放着一些.bat
和.sh
后缀的文件,bat
是windows
系统的批处理文件,而sh
则是在Unix
系统中可以执行的shell
命令(这里以执行sh
文件演示)。将工作目录切换到服务器的bin
下,输入./startup.sh
后回车可以看到有输出Tomcat started
即为成功。(想在任意目录下都可执行,只需要将bin
目录添加到path
中) 实际上,执行startup.sh
后,shell
脚本也是去执行bin目录下的catalina.sh
文件 -
执行
catalina.sh
后,可以看到提供了一堆可执行参数,以及这些参数的作用 -
选择输入
start
参数,可以看到和执行startup
的输出一样
简单的了解Tomcat目录结构
由于默认的端口号配置的是8080
,当服务器启动后,可以通过http://localhost:8080
或者ip
去访问根应用,不出意外的话会看到下面这个网站,说明Tomcat
已经配置好且启动成功了(想关闭可以通过执行shutdown.sh
)
如果Windows机器跑起来后有乱码问题
可以将tomcat
目录下的conf
文件夹中的logging.properties
文件中,找到
将UTF-8
修改的GBK
,编码格式不对的问题即可解决
Servlet规范
Servlet
规范属于JavaEE
规范之一,只要是符合Servlet
规范的webapp
就可以放到遵循Servlet
规范的Web
服务器中执行,规范了哪些东西?
- 规范了
Java
接口和实现类 webapp
应该有的配置映射文件Java Servlet
和配置文件的指定存放位置等- 我们必须遵守规范,才可以让我们的应用按照规则进行运行
开发一个Servlet WebApp
- 现在
tomcat
服务器下的webapps
中,新建一个文件夹,取名为项目名称,这里示例取名为javaweb
- 在
javaweb
目录下新建一个文件夹叫WEB-INF
,这是规范的要求,不可以随意更改 - 在
WEB-INF
中新建classes
文件夹,存放的是我们最终编写的java
程序编译后的class
字节码文件 - 在
WEB-INF
中新建lib
文件夹,这里存放的是java
程序会依赖用到的第三方jar
包,也是规范之一,不可以随意更改 - 在
WEB-INF
中新建web.xml
文件,文件内容如下,格式可以从tomcat
自带的webapp
中的web.xml
抄过来,留下一个空内容的模板,后续的java
程序与访问地址需要通过这个web.xml
进行配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="true">
</web-app>
- 开始编写第一个
Servlet
程序,在这之前,需要知道的一些知识点:- JavaEE最高版本为
JavaEE8
- 目前已经归
Oracle
公司管,JavaEE9
后更改名字后不再叫JavaEE
而是叫jakarta EE
,所以对应的就是JakartaEE9
- 带来的问题就是以前的包名是
javax.servlet.Servlet
,现在改成了jakarta.servlet.Servlet
- 如果编写的
Java
程序使用的包名是javax.servlet.Servlet
,则无法放到Tomcat10+
的版本运行(这里装的是Tomcat10
),只能部署到Tomcat9-
到服务器上 - 这里先手撸,后续再使用
IDEA
进行开发,需要的只是Java
编译后的class
字节码文件,所以任意目录下开发编写即可
- JavaEE最高版本为
开发一个Servlet应用
需要实现Servlet
接口的规范,也就是lib
目录下的servlet-api.jar
中的类
这个接口中需要实现的方法,也可以在官方文档中找到
编写代码:
package cn.webapp;
import jakarta.servlet.*;
public class FirstApp implements Servlet {
@Override
public void init(ServletConfig servletConfig) {
System.out.println("First App run");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
System.out.println("执行了servic");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
此时如果直接使用javac
命令进行编译,百分百会报错,原因是我们使用了jakarta
包下的类,但没有告诉javac
去哪里找,所以需要使用-cp
指定jar
包的目录,顺便使用-d
指定编译后的产出位置,根据上面说的,我们需要放置到tomcat
目录下的webapps
下的【项目名文件夹】/classes
文件夹下,所以最终要执行的javac
命令如下
编译成功即可看到class
文件
接着修改上面定义过的WEB-INF
下的web.xml
,添加url
与Servlet
程序的关系
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="true">
<!-- 定义servlet -->
<servlet>
<servlet-name>first</servlet-name>
<servlet-class>cn.webapp.FirstApp</servlet-class>
</servlet>
<!-- 形成映射 -->
<servlet-mapping>
<!-- 这里的name需要和servlet定义的name一致 -->
<servlet-name>first</servlet-name>
<!-- 这里的url需要以/开头,其他随意 -->
<url-pattern>/first</url-pattern>
</servlet-mapping>
</web-app>
接着通过catalina.sh run
执行(我配置了path
,所以可以这么用,否则请使用相对或者绝对路径),可以看到tomcat
服务器没有报错,并且显示名为javaweb
的应用已部署
在浏览器地址栏中输入http://localhost:8080/javaweb/first
后按回车,回看到一片空白(没有处理响应的原因),但是回到shell
中,可以看到tomcat
服务器运行了我们编写的java
代码,输出了init
方法和service
方法中的内容,如此便搞懂了Tomcat
的执行机制和原理
想试错的朋友可以把Tomcat版本装个9的,一成不变的执行上述步骤,便会得到一个Servlet -- ClassNotFound的报错,原因就是因为包名不对,找不到jarkarta开头的包,改成javax后重新编译即可
理解了Tomcat如何运行的,使用IDEA开发就更简单
- 新建一个Java项目
- 右键项目选择
add Framework Support
,然后选择Web Application
,便会为我们生成WEB-INF
等一些列结构目录
照常编写代码后,需要处理IDEA
找不到Servlet
类的问题,此时需要添加项目依赖的jar
包,在Project Structure
中选择对应的模块,再选择Dependencies
,点击小➕号后添加jar
包,把Tomcat
目录下的lib
中的servlet-api.jar
添加进来,即可解决问题
接着在运行配置中,点击小➕号选择Tomcat
服务器,并选择好服务器所在的目录(CATALINA_HOME)
,Server
中的默认参数基本不用动,选择Deployment
进行部署应用
其中的Application context
这个名字不能随便写,需要匹配到Tomcat
目录中webapps
里的应用名字
配置完后点击运行或者debug
即可启动Tomcat
并部署编写好的Servlet
Servlet的生命周期(Lifecycle)
如果给我们自己编写的Servlet
添加一个无参构造函数并添加一些打印信息
- 在
Tomcat
启动后,可以看到并不会输出内容,说明默认情况下不会实例化。 - 当通过
url
访问后,便会对目标映射的对象进行实例化,先执行无参构造函数,再执行init
方法,然后再执行service
方法(由于是假单例模式,后续即使再访问1万次,也只会执行service
方法,而不会执行构造方法和init
方法) - 当服务器被关闭后,
Servlet
的destroy
方法便会被执行,由于是实例方法,在执行destroy
后对象才被销毁释放
构造方法与生命周期
- 在
web.xml
中,如果为servlet
标签添加了<load-on-startup>0</load-on-startup>
标签,便会在Tomcat
启动后马上进行实例化,其中的整数表示优先级,越小优先级越高 - 如果添加了一个非无参构造方法,但是又没有手动的添加无参构造,此时运行后就会出现服务器异常,
Tomcat
无法实例化Servlet
,所以一般不建议开发者手动处理构造函数 - 所以便有了
init
方法,若需要做一些执行一次的事情,如链接数据库等操作,便可以放在init
生命周期函数中 destroy
方法便是可以在销毁前去做一些释放资源或者存档等事情
引出新问题
现在的编写问题实际上非常的繁琐,每实现一个Servlet
都要实现所有接口的方法,而实际上我们只想处理service
方法业务,好在Tomcat
已经有写好的一个抽象类,后续我们就不用再implements Servlet
了,只需要extends GenericServlet
,并且只实现service
方法即可,因为其他方法在GenericServlet
都有默认实现了
这里可以思考一下GenericServlet
中的两个init
方法的逻辑
我们都知道init
方法有一个默认的ServletConfig
对象传入作为第一个参数,很明显是通过Tomcat
服务器传入的,那么GeneriServlet
中做了一点操作,它内部定义了一个私有变量config
,在Tomcat
调用Servlet
的init
方法时,把传入的Servlet
对象保存到了对象实例成员中,然后再实现一个方法getServletConfig
把这个私有成员返回出去,后续只需要调用这个方法就能获取到ServletConfig
对象
而之所以有两个init
,是因为如果开发者Override
重写了带参的init
后,父类的私有成员config
便不会再被赋值,后续调用getServletConfig
也只能获得null
,所以提供了一个空参的init
方法,如果确实需要在这个钩子处理一些事物,自己的实现类重写空参的init
方法即可,因为父类带参的init
会被tomcat
调用,而这个方法又调用了无参的init
,所以既保证了私有成员config
的存在,也可以让开发者运行自己的代码逻辑。
ServletConfig是什么?
在Servlet
中我们获取并输出到控制台即可看到,实际上是Tomcat
的一个实现类StandardWrapperFacade
实现了这个规范,不同的Web
服务器可能实现不同,但都肯定有这么一个实现类,在控制台输出查看
并且,不同的Servlet
对象会有不同的ServletConfig
实例对象,他们之间是属于1对1
等关系,伪代码可以通过下面的推算帮助理解
public class TomcatImpl{
public static void main(String[] args){
// 通过反射进行对象实例化
Class klass Class.forName("web.xml中某个servlet标签中servlet-class的全限定类名");
// 多态类型转换,实例化目标Servlet
Servlet servlet = (Servlet)klass.newInstance();
// 创建一对一等配置实例对象
ServletConfig servletConfig = new org.apache.catalina.core.StandardWrapperFacade();
// 将servletConfig传入到init方法中
servlet.init(servletConfig);
servlet.service();
// ....
}
}
Tomcat
会去解析web.xml
中servlet
标签内的信息,并注入到ServletConfig
对象中,使程序运行时可以获取到配置中的信息,如通过init-param
标签添加的servlet-name
和init-param
的值
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>cn.mgl.Test</servlet-class>
<init-param>
<param-name>driver</param-name>
<param-value>com.mysql.cj.driver.Driver</param-value>
</init-param>
<init-param>
<param-name>user</param-name>
<param-value>root</param-value>
</init-param>
</servlet>
可以通过ServletConfig
提供的方法getInitParameterNames
和getInitParameter
获取到配置中的值
ServletContext
ServletContext
也是规范中的一个接口,Tomcat
服务器也有其对应的实现类。在同一个webApp
中,创建了两个Servlet
,并且都在service
方法中输出了ServletContext
对象
可以观察到,实际上都是同一个对象
- 在同一个
webapp
中,多个servlet
共享一个servletContext
(俗称应用上下文,应用域) tomcat
服务器下的webapps
中有多少个webapp
,就会有多少个servletContext
- 一个
ServletContext
通常对应一个web.xml
文件 - 对象的创建由
Tomcat
完成,在启动webapp
时创建
和ServletConfig
类似,都可以进行一些初始化参数的配置,一般来说如果配置就是要配置一些所有Servlet
共享的,ServletConfig
的叫init-param
,而ServletContext
则叫context-param
,下面代码中定义了一个共享的配置分页页数为10条
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<context-param>
<param-name>page</param-name>
<param-value>10</param-value>
</context-param>
<!--servlet省略-->
</web-app>
下面代码包含了获取servlet
名字,servlet
配置和context
的配置信息,this
可以不加,父类的方法可以直接调用,加了纯粹为了示例区分下代码
public class MainServlet extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
System.out.println("MainServlet");
ServletConfig servletConfig = this.getServletConfig();
String servletName = servletConfig.getServletName();
System.out.println("servletName:" + servletName);
// 获取当前servlet的配置信息
Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
while (initParameterNames.hasMoreElements()) {
String key = initParameterNames.nextElement();
System.out.println(String.format("key:%s,value:%s", key, getInitParameter(key)));
}
System.out.println("------------------------");
// 获取servletContext的配置信息
ServletContext servletContext = this.getServletContext();
System.out.println("MainServlet获取到的servletContext:" + servletContext);
// 需要注意后续,servletContext和config的方法名都一样,书写上需要区分
Enumeration<String> initParameterNamesFormContext = servletContext.getInitParameterNames();
while (initParameterNamesFormContext.hasMoreElements()) {
String s = initParameterNamesFormContext.nextElement();
System.out.println(String.format("key:%s,value:%s", s, servletContext.getInitParameter(s)));
}
}
}
访问servlet
后输出的内容如下
servletContext.getContextPath
方法获取应用的根路径,重点在于动态,因为部署时webapp
取的名字没法确定,如webapp
名为xxx,则获取到的就是/xxx
servletContext.getContextRealPath("/index.html")
,入参为String
,需要传入一个从应用根路径开始算起的文件名,返回对应的磁盘真实路径,拿到这个地址就可以进行一些IO
流操作,路径中的/
可以不加
利用ServletContext写日志
在使用IDEA
启动项目时,可以留意控制台前几行输出,会发现有一个CATALINA_BASE
变量的目录输出,在配置中配置了tomcat
的实际目录,idea
将这个tomcat
拷贝了一份,并在运行时使用了拷贝的那一份,所以如果去配置的tomcat
目录是找不到对应信息的
// 其他代码省略
servletContext.log("调用context的log方法写入日志,欢迎访问");
servletContext.log("写一个可抛出的异常",new RuntimeException("认真的吗"));
执行后,会在CATALINA_BASE目录下/logs/localhost.[日期].log
中进行日志写入
logs
目录下有3
种类型的日志文件
catalina.[日期].log
用于记录服务器端java的控制台输出信息localhost.[日期].log
用于记录ServletContext
对象的log
方法的记录信息(开发者没有手动调,如报错等情况下Tomcat
容器也会去执行这个方法记录日志)localhost_access_log.[日期].txt
用于记录访问日志(地址 时间 资源 状态)
利用ServletContext进行应用域的缓存
所谓的缓存就是为了减少开销,所有服务公用同一份资源,缓存的东西应该满足以下条件
- 较少的被更改(若会被频繁的修改,会涉及线程安全问题)
- 且数据量不大(数据量过大会占用内存资源,从而影响服务器性能)
- 需要被所有服务共享(不需要共享的话放进干啥~)
ServletContext
提供了以下3个API
进行读写操作
public void setAttribute(String name,Object value);
新增和修改,修改直接覆盖即可public Obejct getAttribute(String name);
获取public void removeAttribute(String name);
删除
api
设计的好,其实一看就知道如何使用,只要在同一个应用域下,所有Servlet
都可以访问到这一份缓存数据(需要注意线程安全问题)
基于Http协议开发
由于开发的为B/S
结构应用,都是根据Http
协议规范进行数据传输,所以有一个专门的实现类叫做HttpServlet
,让开发者处理Http
协议更加方便,只需要让原本继承GenericServlet
类的改成继承HttpServlet
(它继承了GenericServlet
)
但是得先了解下什么是Http
协议
HTTP协议
超文本传输协议(
HTTP
)是一个用于传输超媒体文档(例如 HTML)的应用层协议。它是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的。HTTP 遵循经典的客户端 - 服务端模型,客户端打开一个连接以发出请求,然后等待直到收到服务器端响应。HTTP 是无状态协议,这意味着服务器不会在两个请求之间保留任何数据(状态)。尽管通常基于 TCP/IP 层,但它可以在任何可靠的传输层上使用,也就是说,该协议不会像 UDP 那样静默的丢失消息。RUDP——作为 UDP 的可靠化升级版本——是一种合适的替代选择。
- 超文本的意思是除了字符外,还可以传输流媒体如图片,音频视频等内容
- 浏览器与服务器之间通讯需要遵循HTTP规范
- 浏览器向服务器发送数据(称为请求
Request
) - 服务器向浏览器发送数据(称为响应
Response
) - 不限定浏览器(
Chrome
),不限定服务器(Tomcat
),只要遵循规则即可进行通讯
协议规定的请求格式(Request)
Get请求报文
GET /xmm/login.html HTTP/1.1(请求行)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Cookie: Idea-53d14c26=0270aae0-2b88-451d-8e75-71e5c8948756
Host: localhost:9999
Pragma: no-cache
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1(从Accept开始都是请求头)
(空白行)
(请求体)
最后有两行空格,一行是空白行,一行是请求体
Post请求报文
POST /xmm/test/post HTTP/1.1 (请求行)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 26
Content-Type: application/x-www-form-urlencoded
Host: localhost:9999
Origin: http://localhost:9999
Referer: http://localhost:9999/xmm/login.html
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1 (从Accept开始都是请求头)
(空白行)
username=jack&password=123 (请求体)
- 请求行,有
3
部分- 请求方式(7种):
get,post,delete,put,head,options,trace
URI
,统一资源标识符,代表网络中的某个资源名称,但是无法通过URI
定位,URL
才是统一资源定位符,可以准确的访问。可以简单理解为不带主机端口的/xmm/login.html
是URI
,http://localhost:9999/xmm/login.html
是URL
HTTP
协议版本号 普遍为1.1
- 请求方式(7种):
- 请求头,主要形式为键值对
- 空白行,可以理解为分割线
- 请求体,客户端需要向服务器发送的数据
协议规定的响应格式(Response)
HTTP/1.1 200 ok(响应行)
Content-Type: application/json;charset=utf-8
Content-Length: 33
Date: Fri, 09 Sep 2022 01:25:47 GMT
Keep-Alive: timeout=20
Connection: keep-alive(从Content-type开始都是响应头)
{"status":"success","code":200} (响应体)
- 状态行有三部分组成
- 协议版本号
- http协议状态码,列举一些常见的
200
表示成功401
表示为进行权限校验(可以理解为未登录)403
表示无权限(可以理解为登录了,但是确实没有资格访问)404
表示资源不存在,常规情况下是客户端uri写错(一般4开头的都是客户端问题)500
表示服务端错误,一般5开头的都是服务器问题- 有一个用猫来表示状态的网站,感兴趣可以看看 http猫
- 状态的描述
ok
表示成功结束not found
表示找不到资源
- 响应头,和请求报文的请求头类似,都是键值对的形式
- 空白行
- 响应体,其实就是一堆字符,会被浏览器解析渲染输出执行等,需要根据具体内容确定
Post和Get有什么区别
这是一个老生常谈的问题,从传输协议层上get
和post
没有任何本质的区别(以get
举例,get
请求也能带请求体,post
请求也能用查询参数传参),都是一堆字节流在B/S
传输,但由于浏览器或者服务器有一些规则筛选拦截,造成了即使客户端发了一个get
请求带上请求体,但服务端却没有拿到
get
请求一般发送数据放到URI
后面,如/xmm/login?pageSize=10&page=1
,以?
号文分割线,后面的都是键值对形式的数据,俗称查询参数,这里想表达的就是分页查第一页,每页十条(也就是get
请求普遍在请求行发数据)post
请求一般在请求体中发送,数据不会出现在浏览器的地址栏中get
请求只能发送普通的字符串,且有长度限制,但不同浏览器限制不同,所以无法发送大量数据post
请求可以发任意类型数据,字符,流媒体,且可以发大数据,理论上没有长度限制- 一般来说使用
get
来获取数据(查),post
发送数据(增删改) - 安全性
get
是绝对安全的,因为不会修改服务器上的数据post
请求由于会修改服务器上的数据,需要谨慎操作
- 缓存性
get
支持缓存- 浏览器会有自己的缓存实现,如果在浏览器的缓存找不到才会再次从服务器获取
get
请求不想被浏览器缓存的话,可以在uri
上加特定的标识,比如在get
请求的查询参数上加上时间戳,可以保证每一次的请求都是不一样的,”跳过“浏览器的缓存机制post
不支持缓存,因为post
的目的是修改行为,如果缓存也没有太大意义,因为每次的结果并不绝对一致
解析HttpServlet源码
// 这个方法是重写了GenericServlet的service
// 可以上就是做了一个类型转换后,再将参数向下传递
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
// ServletRequest通过类型强转成HttpServletRequest
// ServletResponse通过类型强转成HttpServletResponse
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
// 接着调用了目标方法,利用了重载,参数类型不同
this.service(request, response);
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求的方法名称
String method = req.getMethod();
long lastModified;
// 接着就是判断请求方法来决定走哪个分支,最后调用哪个方法
// 这种设计模式就是模板方法
// 定义好一系列的逻辑算法,但是具体的行为模板并不负责,子类只需要去实现要完成的目标方法
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
// 以doGet举例,如果子类不重写该方法,访问该Servlet后就会有错误响应(描述方法不支持的响应)
// 如果请求是get方法,doGet就会被执行
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
this.sendMethodNotAllowed(req, resp, msg);
}
// 只有这里走了HttpServlet的service逻辑才会被调用,因为是私有方法,子类无法调用
private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
String protocol = req.getProtocol();
if (protocol.length() != 0 && !protocol.endsWith("0.9") && !protocol.endsWith("1.0")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
由源码可以得出总结:
Servlet
想要响应什么请求方式,就重写对应的方法,请求get
就子类重写doGet
,请求post
就重写doPost
- 如果又想重写
post
又想重写get
,那可以考虑重写service
方法,改掉定义好的模板(实际上并不会这么做) - 如果重写了
service
,就无法享受默认的405
错误,因为这个方法是父类的私有方法
Tomcat站点的欢迎页
访问webapp
不带任何资源路径时,就相当于访问欢迎页,在应用的WEB-INF
同层级新建一个index.html
文件,输入任意内容,然后启动站点访问webapp
,如应用根路径为xxx
,则访问http://localhost:8080/xxx
就能访问到对应的欢迎页,根本原因是因为Tomcat
有一个全局的默认配置,在Tomcat目录下/conf/web.xml
文件中,有以下welcom-file-list
配置
意思是代表着欢迎页的资源名称,优先级为在前优先,但其实是可以在应用级别的web.xml
中配置更改自己的欢迎页,局部的web.xml
配置会优先于全局的web.xml
配置,这个欢迎页除了是资源文件以外,还可以是开发者自定义的servlet
资源
public class XxServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.print("<h1>这是Servlet自定义的欢迎页</h1>");
}
}
- 配置好
XxServlet
对应的servlet
标签servlet-mapping
标签后 - 在
welcome-file
中填写mapping
的url-pattern
值,开头的/要去掉,加了反而找不到
静态资源问题
当尝试在WEB-INF
中放置静态资源,尝试通过/xxx/WEB-INF/任意资源
,会出现404
错误,WEB-INF
有保护机制,无法直接进行路径访问,所以有静态资源存放需求时,必须放在WEB-INF
外
HttpServletRequest
Tomcat
服务器会负责将这个抽象类实现,并将http
报文中的全部信息封装到这个对象当中,让开发者直接使用HttpServletRequest
编程就能获取数据。将HttpServletRequest
输出可以看到其对应的实现类
- 上面的
HTTP
知识中,描述了get
请求一般通过查询参数的形式传参给服务器,也就是/aaa/bbb?pageSize=10&page=1
这种形式,需要提出的是,存储结构并不是Map<String,String>
,而是Map<String,String[]>
,原因也很简单,用前者的数据结构会被后来者覆盖,而用数组则不会。为了演示数组结构再额外传一次pageSize:/aaa/bbb?pageSize=10&page=1&pageSize=20
,并演示几个API
的用法
@WebServlet("/test")
public class TestGetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 第一种,获取纯字符串,自己去解析
String queryString = req.getQueryString();
System.out.println("通过getQueryString获取查询参数的字符串" + queryString);
System.out.println("-----------------------------");
// 第二种,获取Map形式
Map<String, String[]> parameterMap = req.getParameterMap();
parameterMap.forEach((key, values) -> {
System.out.println("getParameterMap遍历获取到的key:" + key);
System.out.println("getParameterMap遍历获取到的values:" + Arrays.toString(values));
});
System.out.println("-----------------------------");
//第三种,通过获取所有的name,再从name获取到所有的key
Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println("parameterNames获取到的key:" + key);
System.out.println("通过key获取数组的第一个值:" + req.getParameter(key));
System.out.println("通过key获取的数组值:" + Arrays.toString(req.getParameterValues(key)));
}
}
}
获取到的数据如图
可以看到,实际上key
对应的value
是一个String
的数组,但理论上一般不会这么去传参
- 所以使用
getParameter
去获取数组的第一项即可,不需要使用getParameterValues
getParameterMap
可以返回Map对象getParameterNames
返回Map集合中所有的key
get请求也可以在请求体传递数据,感兴趣的可以自行尝试,用字节流获取getInputStream,用Postman或curl等工具直接发送请求报文(上面说过,和post没有本质求别)
get请求由于是在请求行相对简单,看看post请求
常规情况下,post
请求会将要传输的数据放在请求体中,但是同样的可以给uri加上查询参数
这里利用Postman
工具进行post
请求的发送
public class TestPostHttpServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println("parameterNames获取到的key:" + key);
System.out.println("通过key获取数组的第一个值:" + req.getParameter(key));
System.out.println("通过key获取的数组值:" + Arrays.toString(req.getParameterValues(key)));
}
}
}
执行后会发现,post
请求中Tomca
t的实现会把查询参数和请求体中的数据进行合并归纳到一个Map
当中,但是这个是有前提条件的,那就是请求头中的Content-Type
的值必须是:application/x-www-form-urlencoded
,这是浏览器默认的表单值,作用就是把表单内容中所有键(name
)值(value
)对通过=
和&
进行拼接,服务器就知道怎么去解析这段请求体了
如果是别的Content-Type
的话,服务器便无法解析,比如常用的application/json
,此时便需要用到字节输入流对象
public class TestPostHttpServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// request提供了getInputStream的方法获取字节流
ServletInputStream is = req.getInputStream();
// 字节流是默认采用ASCII码字符集,中文不适用,所以需要转成字符流
// 字符流InputStreamReader指定了字符集为UTF-8
InputStreamReader inputStreamReader = new InputStreamReader(is, StandardCharsets.UTF_8);
int b;
while ((b = inputStreamReader.read()) != -1) {
System.out.print((char) b);
}
System.out.println("\n------------------");
Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println("parameterNames获取到的key:" + key);
System.out.println("通过key获取数组的第一个值:" + req.getParameter(key));
System.out.println("通过key获取的数组值:" + Arrays.toString(req.getParameterValues(key)));
}
}
}
测试结果,把请求体的Content-Type
改成application/json
,并输入一些字符(虽然是json
格式,实际就是字符而已)
服务端得到的输出如图,可以成功的通过输入流拿到请求体,其实在后续要学习的SpringMvc
中,有一个@RequestBody
注解,其原理也是通过输入流拿到Json
字符串再转成Object
,在如今普遍前后端分离的前提下,基本都是使用Json
规格进行数据传输交互
Request接口的常用方法
getRemoteAddr
获取客户端的ip
地址getMethod
获取请求方法GET
getContextPath
获取应用(webapp
)根路径/xxx
getServletPath
获取webapp名称/xxServlet
getRequestURI
获取包含webapp
名称的路径/xxx/xxServlet/ccc
setCharacterEncoding("utf-8")
用于在Tomcat9-
中,请求体带有中文时乱码问题,Tomcat10
已经默认为utf-8
了,所以不需要处理,感兴趣的可以切换到Tomcat9
测试,注意jakarta
和javax
的包名更换- 在
tomcat8
之后,由于默认就有<Connector URIEncoding="UTF-8" />
这一行参数配置,所以请求行不会有乱码问题
请求域对象
ServletContext
是应用域,他的生命周期一直持续到Tomcat
服务器关闭,而请求域的生命周期只在当前请求内有效,直到响应结束,请求域就会被销毁,100
次请求就会有100
次请求域的生成和销毁。
HttpServlet
也提供了和ServletContext
签名一样的数据缓存方法(get,set,remove)
,用于多个Servlet
进行数据共享(转发)
// 重写doGet方法用于响应get请求
// 此时通过post请求此Servlet就会报405的错
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("user", "jack"); // 设置值
req.setAttribute("age", 100); // 设置值
String user = (String) req.getAttribute("user"); // 获取值
req.removeAttribute("user"); // 删除值
// 只在这一次请求有效
}
Request转发
转发和重定向有区别,对于客户端来说,转发还是一次请求,这里的转发指的是服务器内部转发,客户端请求的是/aaa
,在/aaa
的业务处理中又转发到了/bbb
,/bbb
又转发到了/ccc
,客户端并无感知
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("user", "jack"); // 设置值
req.setAttribute("age", 100); // 设置值
String user = (String) req.getAttribute("user"); // 获取值
req.removeAttribute("user"); // 删除值
// 获取分发器
RequestDispatcher requestDispatcher = req.getRequestDispatcher("/user");
// 也可以转发到静态资源,只要是服务器合法的资源都可以
//RequestDispatcher requestDispatcher = req.getRequestDispatcher("/login.html");
// 进行转发,并把当前的req和resp进行传递
requestDispatcher.forward(req, resp);
}
- 只要是服务器合法资源都可以转发,不一定是
servlet
,路径以/
开始,不用加webapp
根路径 - 需要调用
RequestDispatcher
的forward
方法 - 代码结合了请求域的缓存案例,被转发目标
servlet
可以从HttpServletRequest
获取设置的attribute
- 重定向有两个方法,
forward
和include
- 区别在于回不回回流,可以把
forward
看成飞镖,直线穿越,/a-->/b
,如果在/a
中处理调用了forward
后,写在forward
后面的代码不会再执行 - 而
include
就像回旋镖,/a-->/b-->/a
,在/a
中先转发到/b
,/b
处理完后回到/a
继续处理,并且/b
中不可以设置响应行和响应头,即使写了也不会有实际效果,/b
属于被包含的,只能处理响应体,最后回到/a
时,会将/b
与/a
的响应头合并再将响应报文返回 include
方法,在被包含者调用HttpServletRequest.getRequestURI()
,获取到的值是/a
的值,而forward
获取到的则是/b
- 过于简单自行测试即可
- 区别在于回不回回流,可以把
- 使用重定向方式浏览器地址栏不会变化
sendRedirec
这里提前讲一个HttpServletResponse
接口的方法sendRedirect
方法,这个叫做重定向,会造成浏览器的刷新(用Postman,curl
等发送则没有对应效果),本质是因为浏览器会去解析报文并去执行对应行为
当浏览器看到状态为302
且Location
有对应URI
时,会进行一次页面的跳转,由于是要被浏览器执行的,所以resp.sendRedirect("/xmm/wechat");
需要把webapp
的名称写上,被重定向的URI
只要是合法的服务器资源都可以
类似于用户a
打了个电话找b
,b
回复了a
并告诉a
去找c
,然后电话自动的进行了拨号给c
HttpServletResponse
用来操作响应对象,可以控制响应行,响应头,响应体的具体内容,Tomcat
也有对应的实现类
直接看代码用法
@WebServlet("/testget")
public class TestHttpServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); // 耍你404
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().println("{"status":"success","code":200}");
}
}
代码中设置了响应头,状态码,和响应体的内容。输出流可以决定返回的内容,这里有一坨毒瘤代码,我故意将响应体的状态码设置成404
,所以说如果接口返回404
不一定就是前端的问题,后端手欠也是可能存在的,除了状态行以外,其他数据都和正常请求结束一模一样
示例代码使用了WebServlet注解,简化配置
在Servlet3.0
开始,就有注解开发的方式,优点是不需要在xml
中去配置,直接通过java
代码添加注解的形式,达到一样的效果,相当于在xml中配置类了资源路径和类名映射
B/S系统的会话机制(俗称session)
当用户打开浏览器,访问站点,做他想做的事,关闭浏览器,这个过程就是一次会话,会话期间,在Java
端会有一个Java
对象叫做session
,一次会话中可以有无数个请求
之所以有这个会话机制,主要原因是因为Http
协议是无状态的,每一次请求发送都和上一次的没有关系(全新),这就导致了没有办法区分用户是否登录之类的状态。并且在请求结束后,链接就断开了,服务端没有办法可以知道客户端(浏览器)被关闭的这个动作。
原理:
jack
用Edge
浏览器访问了服务器,服务器内部生成了一个此次Edge
浏览器的专属Session
对象jack
用Chrome
浏览器访问了服务器,做同样的行为- 此时服务器内存中维护着一个
session
列表,数据结构是Map
,key
就是sessionId
,value
就是对应的Session
对象 - 在第一次请求中,用户的请求响应头带有一个
key
为Set-Cookie
,值为服务端设置的key&value
及有效期等信息,浏览器会自动解析存储做对应的动作(Cookie
),只要还在这次会话中,后续同域的请求都会自动将Cookie
的值自动带到请求头中 - 当请求头中有一个
Cookie
字段,浏览器会将当前域下的所有key&value
以字符串的形式自动带上,服务端便能从请求报文中解析来做后续处理 - 当关闭浏览器后,内存消失导致
Cookie
消失,Cookie
中存的值也消失了,相当于此次会话结束了,但这个结束只是对于客户端来说,因为服务端并不知道。那么如何才能让Session
失效呢?- 通过
web.xml
中配置session-config
的session-timeout
,传入数字,单位为分钟,默认的Tomcat
配置中这个值为30分钟,时间到了便失效(服务端的时间,不是客户端的) - 通过
Session.invalidate()
方法,调用后session
便失效
- 通过
- 以前常见的登陆场景就是利用
Cookie
和Session
机制处理的,登录后便可以在Session
域中存储相关用户信息,现在流行前后端分离用的就少很多了,但还是应该了解
HttpSession
和请求域、应用域一样,Session
对象也有setAttribute
和getAttribute
方法,用法完全一样,区别是他们的宿主对象存活时间不同
- 应按照能完成需求的最小域使用,能用请求域(
request
)存储的就不要用会话域(session
),能用会话域完成的就不要用应用域(context
)
如何在Java代码中获取Session对象
HttpSession session = req.getSession();
这行代码很关键,如何不调用这个方法,服务端是不会生成Session
对象及写sessionid
到响应头中的。作用就是根据当前会话是否已有Session
对象,有则获取返回,没有则会新建
HttpSession session = req.getSession(false);
方法还可以接受一个Boolean
值,传入false
时则意为,如果当前会话中,Session
对象已存在,则返回,没有则返回空,不会新建一个Session
对象,传入true
和空参方法作用一致
下面用3个Servlet进行Session的简单使用
LoginService 提供登录
@WebServlet("/login")
public class LoginService extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
// 只是为了演示登录,实际肯定是查询数据库
// 账号是admin,密码是123才算登录成功
if ("admin".equals(username) && "123".equals(password)) {
resp.addHeader("anim", "sha");
// 获取session对象,没有则需要新建,因为一般这里是入口
HttpSession session = req.getSession();
// 尝试获取会话域中的用户名
String sessionUsername = (String) session.getAttribute("username");
if (sessionUsername == null) {
// 用户名为空则新建一个随机用户名,这里用随机数示意
String temp = "" + Math.random() * 1000;
session.setAttribute("username", "随机的用户名:" + temp);
System.out.println("已自动生成了用户名" + temp);
} else {
// 如果能从会话域获取到用户名,则说明已经登陆过了
System.out.println("已登录过了,当前会话的用户名为:" + sessionUsername);
}
} else {
System.out.println("账号名或密码错误");
}
}
}
-
当不传用户名密码时,会输出账号名或密码错误
-
当输入了正确(预设的账号)的账号密码时,服务端会自动生成
Session
对象,并且随机生成了一个用户名,存入到当前的会话域中,这样在同一个会话的所有请求,都可以共享这个信息 -
此时再进行登录操作,会提示已登录过,并且可以将
Session
域中的对象取出
GetSession 提供获取Session域存储值
@WebServlet("/getSession")
public class GetSessionService extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 这里要传入false,原因是不需要新建session,场景应该是登录后才能访问这个Servlet
HttpSession session = req.getSession(false);
if (session != null) {
String username = (String) session.getAttribute("username");
System.out.println("随机的用户名" + username);
} else {
System.out.println("访问" + req.getServletPath() + "时检测到没有登录");
}
}
}
-
当没有登录时,直接访问由于
getSession
传入的是false
,因此不会新建Session
对象 -
进行一次登录后,再次请求时,便能获取到会话域中存储的用户名
LogoutService 提供注销功能
@WebServlet("/logout")
public class LogoutService extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false);
if (session != null) {
// 一定要把invalidate放后面执行,否则这个对象的数据就会被清空,也就拿不到所有数据了
System.out.println(session.getAttribute("username") + "用户被注销了");
session.invalidate();
} else {
System.out.println("没有session,说明没有登录,无法调用注销");
}
}
}
-
未登录去调用
-
登录后去调用
-
此时再去访问
getSession
,如果检测到没有登录,则说明session
已经失效了(浏览器可能还会将Cookie
传入到请求报文中,但这个key
对应服务端的Session
对象已失效)
Cookie
上面主要是讲的服务端Session
,这里了解一下Cookie
的应用,上面说过,服务器会在Response
对象中设置一个响应头,Set-Cookie
里面就会有key&value
的形式传送sessionid
给客户端,客户端在限定规则下的资源访问时都会自动带上cookie
,服务端接受到后就能根据sessionid
去找到对应的Session
对象,当没有传sessionid
或者传了无效的sessionid
,服务端也自然拿不到Session
对象
Cookie是在客户端的概念,有两种存储方式:
- 内存中,只要浏览器关闭了
cookie
就消失 - 硬盘文件中,不受浏览器关闭的影响,只要文件存在
cookie
就有效
了解Cookie
对象,这个类位于package jakarta.servlet.http
中,没有提供空参构造方法,必须传入一个键值对来生成Cookie
实例
下面代码演示Java
端操作Cookie
对象
@WebServlet("/operationCookie")
public class OperationCookie extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie username = new Cookie("username", "admin");
Cookie password = new Cookie("password", "123");
// username.setPath("/"),如果需要控制path的有效值,可以调用setPath进行更改
resp.addCookie(username);
resp.addCookie(password);
System.out.println("成功设置了Cookie");
}
}
当访问这个这个Servlet
时,会在响应报文中设置两个Cookie
值(忽略Idea
的值),浏览器便会自动存储到内存中
可以在浏览器的 开发者工具->Application->Cookie
中找到刚刚设置的两个Cookie
,其中的Domain
和Path
影响着访问什么资源会自动带着这俩Cookie
,像下图示例,只要访问https://localhost:9999/xmm/*
的资源,都会自动携带,这个Path
可以通过Cookie.setPath
去控制
访问一个符合https://localhost:9999/xmm/*
格式的资源,会发现Cookie
也会被带到请求报文中一并发送
获取Cookie
可以从响应报文设置Cookie
,那就可以从请求报文获取Cookie
@WebServlet("/inspectCookie")
public class InspectCookie extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 如果请求报文中一个Cookie都没有,则获取到的是null,而不是长度为0的空数组
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getValue());
System.out.println(cookie.getName());
System.out.println("---------------------");
}
} else {
System.out.println("没有Cookie");
}
}
}
- 先访问
/operationCookie
- 再访问
/inspectCookie
,只要Cookie有值,便可以获取到对应的字符串
设置Cookie的时效性
默认情况下,Cookie
保存在浏览器的内存中,浏览器关闭了就没有了,可以通过Cooke.setMaxAge
,接受一个int
类型,单位为秒去进行时效性的设置,代码进行演示
当访问Servlet
时,报文设置如下
- 可以看到
key
为0
的值,Expires
的值是epoch·time
,也就是unix
系统的起始时间,翻译过来就是这个Cookie
的有效期是1970年1月1号
,过了这一天就无效,相当于删除了这个Cookie
key
为60
的值,我当前的时间为2022年9月19号11点+
,其中Expires
的有效期格式为格林威治时间,这是个标准时间,中国所在区域为东8
区,会比这个标准时间多8
小时,11
点减去8
小时便是3点+
,此时会存在硬盘文件中,在有效期内关闭浏览器再打开浏览器访问Servlet
,Cookie
仍然有效。我这里设置的60s
失效,60s
过后,Cookie
便会消失key
为-1
的没有Expires
信息,说明时效性是这一次会话内有效
在开发中工具的Cookie
中,可以看到没有key
为0
的内容(说明如果有这个Cookie
也会被删除)
其中的HttpOnly
可以控制客户端是否能通过脚本操作,在Java
端可以用Cooke.setHttpOnly
设置是否启用。通过Javascript
端执行document.cooike
结果如下
勾选了HttpOnly
的Cookie
无法被JavaScript
获取
了解了Cookie和Session后,实现X天免登陆就有思路了
在用户登录后,通过在Java
端设置Cookie
,key
和值根据需求去设计,假如时效性为3
天,只要客户端不乱操作,后续的请求都会在Cookie
带上用户名和密码,只要能获取后反解析之类的拿到用户信息即可。如最直白的方式,将用户名和密码设置到客户端的Cookie
中,在登录的Servlet
去进行获取判断,与数据库中的数据进行比较,如果校验通过,则做重定向或放行访问之类的操作
由于客户端是不可控的,比如客户端被禁用Cookie
,那么客户端就无法接受(服务端不受影响,仍然会发送),session
机制的玩法就得通过url
重写,http://localhost:9999/xmm/getSession:jsessionid=xxxxxxxxx
通过在资源路径后面加上冒号及key&value
,但开发难度过大,一般不处理,禁用就禁用吧!
其实Session和Cookie属于HTTP规范的内容,即使不是Java换成其他语言做Web开发都需要这两套机制
Filter
过滤器可以在目标Servlet
执行之前或者之后进行过滤规则,比如很多个业务Servlet
都需要判断是否已登录,这样代码就非常冗余重复,使用过滤器只要编写一次,就可以实现按规则进行过滤
Filter的特点
- 在服务器启动的时候,
Filter
对象便会被新建 Filter
实例是单实例的- 能否走进目标
Filter
?- 请求的路径是否符合过滤器的规则
- 进入过滤器之后,目标
Servlet
能否被执行?- 取决在
Filter
,是否有调用chain.doFilter(req,resp)
- 取决在
- Filter的匹配规则
/xxx
,/yyyy
这些属于精准匹配/*
匹配所有路径*xxx
匹配以xxx
后缀的Servlet
/auth/*
匹配以/auth/
为前缀的Servlet
如何编写一个过滤器
-
编写一个类实现
jarkata.servlet.Filtter
其所有方法(有默认实现的可以不重写) -
init
方法Filter
对象被创建后调用,只调用一次doFilter
只要命中一次请求符合过滤器规则,就会执行一次,类似Servlet
的service
方法destroy
方法Filter
对象被销毁之前调用,只调用一次
-
在
web.xml
中配置Filter
,操作和Servlet
非常类似(也可以使用注解,二选一)
目标Servlet是否会被执行,取决两个关键点
- 过滤器是否写了
chain.doFilter(req,resp)
; - 请求路径是否匹配
Servlet
用一张图理解Filter
,Context
可以粗略看成Tomact
服务器,用户一个请求进来时,先被Tomcat
处理一遍,将请求报文等数据进行清洗整理,原本是直接进入Servlet
层的,但是由于请求路径匹配到了过滤器规则,则先进入过滤器执行相关操作,过滤器放行后发现没有后续过滤器了就会进入目标Servlet
,当Servlet
执行完毕后,又会回到过滤器中,过滤器处理完才到真正返回客户端响应(栈数据结构)
用代码理解
// 注解和web.xml配置各选一个
@WebFilter("/*")
public class BaseFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器创建了");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 执行目标方法前做了点啥(请求)
System.out.println("进入了过滤器");
// 执行下一个过滤器,如果不是过滤器,则直接执行目标Servlet
filterChain.doFilter(servletRequest, servletResponse);
// 执行目标方法后做了点啥(响应)
System.out.println("又回到了过滤器");
}
@Override
public void destroy() {
System.out.println("过滤器销毁了");
}
}
如果不用注解可以在web.xml配置一下代码
<filter>
<filter-name>baseFilter</filter-name>
<filter-class>cn.mgl.filter.BaseFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>baseFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
现在再去访问之前Session
案例里的随意一个Servlet
上面代码只是演示过滤器的使用方式,实际开发中更多的使用过滤器进行一些业务的前置条件校验,配置等操作
一些路径匹配规则:
/aFilter
这种属于精确匹配,路径必须完整一致/*
相当于全局匹配,只要是访问/开头的资源都会命中过滤器*.justMe
后缀匹配,只要是以.justMe
结尾的资源访问都能命中/pre/*
前缀匹配,只要是以/pre/
开头的都能命中过滤器
多个Filter
之间也有优先级关系
- 使用
web.xml
的方式进行配置时,filter-mapping
编写越前的,优先级越高 - 使用注解式声明时,按照类名的字符字典顺序排序
Filter
比Servlet
的优先级高,且他们的生命周期完全一致,默认情况下,服务器启动阶段,不管有没有客户端访问,都会被实例化,而Servlet
不会
Listener 监听器
监听器也属于Servlet
规范中,所有监听器接口名字都是以Listener
结尾
ServletContextListener
这个属于容器级别的监听器,需要实现ServletContextListener
接口,接口中有两个方法,分别在容器初始化级销毁前执行,实现接口后还需要进行配置,例子使用了WebListener
注解
@WebListener
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器被初始化了");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器被销毁了");
}
}
如果用XML则需要编写如下配置
<listener>
<listener-class>xxx.Contextlistener</listener>
</listener>
启动Tomcat
服务器,然后关掉,应该能看到以下输出
ServletContextAttributeListener
这个监听器的作用为,在对容器级别的属性进行增删改时会触发,需要实现ServletContextAttributeListener
中的所有方法,分别对应着容器属性的添加,删除,修改,并通过WebListener
注解进行配置
@WebListener
public class ContextAttributeListener implements ServletContextAttributeListener {
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
String name = scae.getName();
System.out.println("attributeAdded监听到添加了属性,名为:" + name);
}
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
String name = scae.getName();
System.out.println("attributeRemoved监听到删除了属性,名为:" + name);
}
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
String name = scae.getName();
System.out.println("attributeRemoved监听到属性被替换了,名为:" + name);
System.out.println("值为"+scae.getValue());
}
}
编写测试代码
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
ServletContext servletContext = getServletContext();
String key = "testKey";
servletContext.setAttribute(key,1); // 添加
servletContext.setAttribute(key,2); // 修改
try {
Thread.sleep(1000);
servletContext.removeAttribute(key); // 删除
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
此时访问对应Servlet
后,便会出现以下日志打印
HttpSessionListener
和HttpSessionAttributeListener
ServletRequestListener
和ServletRequestAttributeListener
这俩的用法与Context
的相当类似,只需要实现对应的方法并配置@WebLisenter
即可
ServletRequestAttributeListener
这个类比较特殊,不需要进行什么配置,但是需要某个Pojo
类实现该接口中的方法,示例中以一个User
类为实例,当User
对象被放入Session
域时则会触发valueBound方法
,当User
对象从Session
域中被删除时,则会触发valueUnbound
方法,并且通过这个监听器可以实现一个简易的在线人数统计 功能
public class User implements HttpSessionBindingListener {
public User(String username, Long id) {
this.username = username;
this.id = id;
}
@Override
public void valueBound(HttpSessionBindingEvent event) {
// 当User实例被绑定到Session中时触发
ServletContext servletContext = event.getSession().getServletContext();
Integer onlineCount = (Integer) servletContext.getAttribute("onlineCount");
// 我们可以往容器的属性中添加一个onlineCount用来记录当前登陆人数
if (onlineCount == null) {
// 无责从1起
System.out.println("第一个在线用户诞生了");
servletContext.setAttribute("onlineCount", 1);
} else {
// 有则+1
System.out.println("在线用户累加了" + (onlineCount + 1));
servletContext.setAttribute("onlineCount", ++onlineCount);
}
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
System.out.println("用户被解绑了");
// 当User实例被删除或Session被销毁了会被触发
ServletContext servletContext = event.getSession().getServletContext();
Integer onlineCount = (Integer) servletContext.getAttribute("onlineCount");
System.out.println(onlineCount);
// 只要触发必然会有在线人数,不需要判空,减就完事了
servletContext.setAttribute("onlineCount", --onlineCount);
}
String username;
Long id;
}
接着编写一个模拟用户登陆的Servlet
@WebServlet("/addUser")
public class AddUserToSession extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取Session
HttpSession session = req.getSession();
Object user = session.getAttribute("currentUser");
// 尝试从Session中获取当前用户
if (user == null) {
// 没有的话则添加一个User实例,关键是这个User对象,内容不太关心便随意了
session.setAttribute("currentUser", new User("test", 123L));
} else {
// 有则直接输出
System.out.println("当前用户存在" + user);
}
}
}
为了验证效果,还需要再编写俩个辅助的Servlet
,一个是注销的Servlet
@WebServlet("/destroySession")
public class DestroySession extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// false很关键,没有session信息时则不会新建
HttpSession session = req.getSession(false);
// 调用invalidate相当于注销,注销后由于User对象实现了对应的监听接口
// 就会触发解绑事件
session.invalidate();
}
}
还有一个是查看在线人数的Servlet
@WebServlet("/getOnlineCount")
public class OnlineCount extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 直接输出即可
System.out.println("当前在线人数为:" + req.getServletContext().getAttribute("onlineCount"));
}
}
简单讲讲测试流程
- 先用
Edge
浏览器访问addUser
,此时应该会将容器的在线人数属性设为1
,访问getOnlineCount
得到的应该是1
- 再用
Chrome
浏览器访问addUser
,此时在线人数会是2
,访问getOnlineCount
得到的应该是2
- 接着再
Chrome
浏览器访问destroySession
执行注销,再次访问getOnlineCount
得到的将是1
,则验证成功
看到这里相信对JavaWeb
有了一定的认知了,搞懂Servlet
有助于后续的StringMVC
和SpringBoot
的学习
完:)