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,则获取到的就是/xxxservletContext.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是URLHTTP协议版本号 普遍为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请求中Tomcat的实现会把查询参数和请求体中的数据进行合并归纳到一个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获取请求方法GETgetContextPath获取应用(webapp)根路径/xxxgetServletPath获取webapp名称/xxServletgetRequestURI获取包含webapp名称的路径/xxx/xxServlet/cccsetCharacterEncoding("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和HttpSessionAttributeListenerServletRequestListener和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的学习
完:)