Tomcat10
概述
Java:
- JavaSE 标准版
- JavaEE 企业版
- JavaME 微型版
Servlet就是JavaEE的13种规范之一
Web系统通信原理
- 输入URL
- 域名解析器解析 http://110.242.68.3:80/index.html
- 发送http协议到110.242.68.3主机,定位80端口
- 服务器将index.html文件发送给浏览器
- 浏览器解析并显示
从浏览器到服务器:请求 Request
从服务器到浏览器:响应 Response
WEB服务器软件
- 应用服务器:实现了JavaEE的所有规范(13个规范)
- JBOSS
- WebLogic
- WebSphere
- WEB服务器:实现了JavaEE中的Servlet + JSP规范
- Tomcat
- jetty
应用服务器是包含WEB服务器的,例如:JBOSS中内嵌了一个Tomcat服务器
Tomcat
Welcome to The Apache Software Foundation!
Tomcat是开源免费的轻量级WEB服务器,还有一个名字:catalina
解压后:

- bin:命令文件存放目录,例如启动Tomcat、关闭Tomcat
- conf:配置文件存放目录,server.xml文件可以配置端口号,默认端口号8080
- lib:核心程序目录 jar包
- logs:日志目录,启动等信息都会在这个目录中生成日志文件
- temp:临时目录,存储临时文件
- webapps:存放webapp,自带了一些应用
- work:存放JSP文件翻译之后的Java文件和class文件
启动Tomcat:
bat文件是windows操作系统专用的批处理文件,这种文件中可以编写大量的windows的dos命令
sh文件时linux环境的shell文件
startup.bat文件:
实际上执行了catalina.bat文件
catalina.bat文件:
其中需要指定 %JAVA_HOME%
org.apache.catalina.startup.Bootstrap的main方法:
tomcat是Java语言写的,启动tomcat就是执行main方法
在startup.bat文件中需要指定CATALINA_HOME:
尝试运行startup.bat文件:
需要将bin目录添加到环境变量PATH当中:
此时tomcat启动成功(指定 JAVA_HOME ):
此时在cmd输入 startup shutdown就可以启动关闭tomcat服务器,但是shutdown是windows的关机命令,可以在bin目录中将其改为stop
- 配置Tomcat服务器:
- JAVA_HOME
- CATALINA_HOME
- PATH = %JAVA_HOME%\bin;%CATALINA_HOME%\bin
- 启动Tomcat服务器:startup
- 关闭Tomcat服务器:stop(shutdown冲突)
测试Tomcat服务器是否启动成功:浏览器地址栏输入URL:http://ip地址:端口号
也就是:http://localhost:8080,出现如下界面就能启动成功了:
解决CMD中Tomcat的乱码问题
"Tomcat\apache-tomcat-10.0.12\conf\logging.properties"
Windows的控制台是GBK的编码方式
在Idea中配置Tomcat就需要将GBK改回UTF-8
实现第一个WEB程序
- 第一步:找到CATALINA_HOME\webapps目录,因为所有的webapp都放在该目录下
- 第二步:在CATALINA_HOME\webapps目录下新建子目录:oa
- 这个oa就是web应用的名字
- 第三步:在oa下新建资源文件,例如:index.html
这个界面就是从tomcat服务器中获取的资源
也可以在页面中使用超链接跳转:
注意:超链接中的ip地址和端口号是可以省略的:/oa/login.html,也就是绝对路径,以/开始,就是以webapps目录开始
如果要访问以下文件:
<a href="/oa/test/debug/d.html">
BS系统结构的角色和协议
现在有一个静态的表格:
如果想要从数据库中查询信息展示到网页上,就需要在Java程序中通过JDBC连接数据库,数据库中有多少条记录页面上就显示多少条记录,这种技术被称为动态网页技术(页面的数据是动态的,根据数据库中数据的变化而变化)

-
参与的角色:
-
浏览器软件开发团队
-
WEB Server开发团队 Tomcat、jetty、WebLogic、JBOSS、WebSphere
-
DB Server开发团队 Oracle、MySQL
-
WEB app开发团队 oa crm
-
-
角色和角色之间需要遵守哪些规范:
- WEBapp和WEBServer之间有一套规范:JavaEE规范之一:Servlet规范
- Servlet规范的作用是:app和server解耦合,app可以在任意的WEB服务器之间运行
- Servlet就是Java程序和Server之间的一套接口规范
- Browser和WebServer之间有一套协议:HTTP协议
- Webapp开发团队和DBServer开发团队之间有一套协议:JDBC协议
- WEBapp和WEBServer之间有一套规范:JavaEE规范之一:Servlet规范

服务器的本质是:
- 解析请求资源
- 将请求信息封装为request、创建response对象
- 创建servlet对象
- 调用service方法
模拟Servlet本质
-
SUN公司:制定Servlet规范
package javax.servlet; /* SUN公司制定的规范/接口 * */ public interface Servlet { /** * 提供服务的方法 */ void service(); } -
WebApp开发者
package com.eun.servlet; import javax.servlet.Servlet; /* * WebApp开发者 * */ public class UserListServlet implements Servlet { @Override public void service() { System.out.println("UserListServlet Service"); } } public class BankServlet implements Servlet { @Override public void service() { System.out.println("BankServlet Service"); } } public class UserLoginServlet implements Servlet { @Override public void service() { System.out.println("UserLoginServlet Service"); } } -
Tomcat服务器开发者
package org.apache; import java.util.Scanner; public class Tomcat { public static void main(String[] args) { System.out.println("org.apache.Tomcat server startup"); //Scanner模拟用户请求 /* 用户访问服务器是通过浏览器上的请求路径URL,URL不同执行的Servlet也是不同的 /userlist -> UserListServlet /bank -> BankServlet * */ System.out.print("输入访问路径:"); Scanner s = new Scanner(System.in); String path = s.next(); //获取到了请求路径 //Tomcat应该通过请求路径找到对应的Servlet //请求路径path和xxxServlet之间的关系由谁指定? } }![请求路径和XXXServlet之间应该有一个对照关系]()
我们通过配置文件指定请求路径和Servlet之间的对应关系:
/bank=com.eun.servlet.BankServlet
/list=com.eun.servlet.UserListServlet
/login=com.eun.servlet.UserLoginServlet
通过请求路径path作为key就能拿到value,只需要解析配置文件就行了
所以在Tomcat服务器当中:
//Scanner模拟用户请求
/*
用户访问服务器是通过浏览器上的请求路径URL,URL不同执行的Servlet也是不同的
/userlist -> UserListServlet
/bank -> BankServlet
* */
System.out.print("输入访问路径:");
Scanner s = new Scanner(System.in);
String path = s.next(); //获取到了请求路径
//Tomcat应该通过请求路径找到对应的Servlet
//请求路径path和xxxServlet之间的关系由谁指定?
ResourceBundle bundle = ResourceBundle.getBundle("web");
String serv = bundle.getString(path);
Class<?> clazz = Class.forName(serv);
Object obj = clazz.newInstance();
if (obj instanceof Servlet servletInstance) {
servletInstance.service();
}
拿到请求路径后通过属性配置文件获取到value值,通过反射机制创建value实例;Servlet开发时需要遵守规范,也就是都实现了Servlet接口,就可以将Object类型转换为Servlet类型,调用接口中对应的方法
对于我们来说,只需要做两件事:
- 编写类实现Servlet接口
- 将编写的类配置到配置文件中,在配置文件中指定 请求路径 和 类名 的关系
Tomcat服务器是已经写好的,这也就意味着:配置文件 web.xml 的名称和路径都是规定好的
严格来说Servlet并不只是一个接口,同时规定了:
- webapp的目录结构
- webapp的配置文件名称和路径
- webapp的Java程序放在哪里
第一个Servlet
webapp
|----WEB-INF
|----classes
|----lib
|----web.xml
|----html
|----css
|----javascript
...
遵循Servlet规范的webapp就可以在不同的Web服务器中运行
- 规范了接口
- 规范了类
- 规范了配置文件名称、路径、内容
- 规范了目录结构
开发步骤:
-
webapps新建目录 crm
-
crm中新建目录
WEB-INF -
WEB-INF下新建目录classes,存放编译后的 .class 文件 -
WEB-INF下新建目录lib,存放第三方jar包(MySQL) -
WEB-INF下新建文件web.xml,配置文件,描述请求连接和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="false">
</web-app>
-
定义类 继承Servlet接口
-
该接口是JavaEE规范的,不在JDK当中
-
Tomcat面向Servlet接口进行调用,该接口就在Tomcat当中
解压之后:
![image-20230428171917434]()
完整类名是:
"jakarta\servlet\Servlet"在JavaEE7中的类名是:
javax.servlet.Servlet,从 JakartaEE9 开始(Tomcat10)实现的就是jakarta\servlet\Servlet接口
-
JavaEE最高版本是JavaEE8,JavaEE被Oracle捐献给Apache,Apache将 JavaEE 改为了 JakartaEE
JavaEE8对应的Servlet类名是 javax.servlet.Servlet, JakartaEE 9对应的类名是 jakarta\servlet\Servlet
Users of Tomcat 10 onwards should be aware that, as a result of the move from Java EE to Jakarta EE as part of the transfer of Java EE to the Eclipse Foundation, the primary package for all implemented APIs has changed from javax.* to jakarta.*. This will almost certainly require code changes to enable applications to migrate from Tomcat 9 and earlier to Tomcat 10 and later. A migration tool has been developed to aid this process.
Servlet核心接口:
.Java源代码在哪里都可以,只需要将编译后的.class文件放在 webapps/crm/WEB-INF/classes当中
Tomcat的lib目录下有一个servlet.jar,需要将这个类路径配到classpath当中:
CLASSPATH=.;D:\Tomcat\apache-tomcat-10.0.12\lib\servlet-api.jar
需要注意的是,配置这个环境变量只是让java程序可以正常编译生成class文件,与Tomcat的运行没有关系;Tomcat知道自身的jar包在lib目录下。
将写好的文件拷贝到WEB-INF下的classes当中:
注册Servlet:在web.xml中编写配置信息,将 请求路径 和 Servlet类名 关联在一起
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>cn.eun.demo.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/abc</url-pattern>
</servlet-mapping>
启动Tomcat服务器,输入url-pattern
发送请求后,Tomcat服务器会根据请求路径url-pattern找到servlet-name,根据servlet-name创建servlet-class的实例
main方法在Tomcat服务器当中。
Tomcat启动的时候调用main方法,我们只需要编写Servlet的实现类,并且注册Servlet就可以了。
如果浏览器上编写的路径太复杂,可以使用超链接
注意:HTML页面只能放在WEB-INF之外
可以省略协议、IP、端口号:
但是在Request请求中访问的还是localhost:8080/crm/index.html
- 浏览器发送请求,到最终服务器调用servlet的方法的过程
浏览器发送请求,Tomcat服务器接收到请求截取路径/crm/abc;Tomcat服务器找到crm项目,在crm下WEB-INF下web.xml文件中查找/abc对应的servlet-class,内部反射创建该类对象,调用该对象的service方法
响应到浏览器
response对象可以获取输出流,输出流的write方法可以返回内容给浏览器,该流:
- 不需要flush
- 不需要close
可以设置响应内容类型为普通的HTML代码:
response.setContentType("text/html")
但是这个操作只能在获取输出流getWriter之前执行:

public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
reponse.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print("<h1>Hello First Servlet<h1>");
}
Servlet连接数据库
需要将驱动放在crm/WEB-INF/lib下
Idea
-
集成开发工具很多,其中目前使用比较多的是:
- IntelliJ IDEA(这个居多,IDEA在提示功能方面要强于Eclipse,也就是说IDEA使用起来比Eclipse更加智能,更好用。JetBrain公司开发的。收费的。)
- Eclipse(这个少一些),Eclipse目前还是有团队使用,只不过处于减少的趋势,自己从事工作之后,可能会遇到。Eclipse是IBM团队开发的。Eclipse寓意是“日食”。“日食”表示将太阳吃掉。太阳是SUN。IBM团队开发Eclipse的寓意是吞并SUN公司,但是2009年的时候SUN公司被Oracle公司并购了。IBM并没有成功并购SUN公司。
-
使用IDEA集成开发工具开发Servlet
- 第一步:New Project(我比较习惯先创建一个Empty Project【空工程】,然后在空工程下新建Module【模块】,这不是必须的,只是一种习惯,你可以直接新建非空的Project),这个Empty Project起名为:javaweb(不是必须的,只是一个名字而已。一般情况下新建的Project的名字最好和目录的名字一致。)
- 第二步:新建模块(File --> new --> Module...)
- 这里新建的是一个普通的JavaSE模块(这里先不要新建Java Enterprise模块)
- 这个Module自动会被放在javaweb的project下面。
- 这个Module起名:servlet01
- 第三步:让Module变成JavaEE的模块。(让Module变成webapp的模块。符合webapp规范。符合Servlet规范的Module)
- 在Module上点击右键:Add Framework Support...(添加框架支持)
- 在弹出的窗口中,选择Web Application(选择的是webapp的支持)
- 选择了这个webapp的支持之后,IDEA会自动给你生成一个符合Servlet规范的webpp目录结构。
- 重点,需要注意的:在IDEA工具中根据Web Application模板生成的目录中有一个web目录,这个目录就代表webapp的根
- 第四步(非必须):根据Web Application生成的资源中有index.jsp文件,这里先删除这个index.jsp文件。
- 第五步:编写Servlet(StudentServlet)
- class StudentServlet implements Servlet
- 这个时候发现Servlet.class文件没有。怎么办?将CATALINA_HOME/lib/servlet-api.jar和jsp-api.jar添加到classpath当中(这里的classpath说的是IDEA的classpath)
- File --> Project Structrue --> Modules --> + --> Add JARS....
- 实现jakarta.servlet.Servlet接口中的5个方法。
- 第六步:在Servlet当中的service方法中编写业务代码(我们这里连接数据库了。)
- 第七步:在WEB-INF目录下新建了一个子目录:lib(这个目录名可不能随意,必须是全部小写的lib),并且将连接数据库的驱动jar包放到lib目录下。
- 第八步:在web.xml文件中完成StudentServlet类的注册。(请求路径和Servlet之间对应起来)
<?xml version="1.0" encoding="UTF-8"?> <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"> <servlet> <servlet-name>studentServlet</servlet-name> <servlet-class>com.bjpowernode.javaweb.servlet.StudentServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>studentServlet</servlet-name> <url-pattern>/servlet/student</url-pattern> </servlet-mapping> </web-app>-
第九步:给一个html页面,在HTML页面中编写一个超链接,用户点击这个超链接,发送请求,Tomcat执行后台的StudentServlet。
-
student.html
-
这个文件不能放到WEB-INF目录里面,只能放到WEB-INF目录外面。
-
student.html文件的内容
-
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>student page</title> </head> <body> <!--这里的项目名是 /xmm ,无法动态获取,先写死--> <a href="/xmm/servlet/student">student list</a> </body> </html>
-
-
第十步:让IDEA工具去关联Tomcat服务器。关联的过程当中将webapp部署到Tomcat服务器当中。
- IDEA工具右上角,绿色小锤子右边有一个:Add Configuration
- 左上角加号,点击Tomcat Server --> local
- 在弹出的界面中设置服务器Server的参数(基本上不用动)
- 在当前窗口中有一个Deployment(点击这个用来部署webapp),继续点击加号,部署即可。
- 修改 Application context为:/xmm
-
第十一步:启动Tomcat服务器
- 在右上角有绿色的箭头,或者绿色的小虫子,点击这个绿色的小虫子,可以采用debug的模式启动Tomcat服务器。
- 我们开发中建议适用debug模式启动Tomcat
-
第十二步:打开浏览器,在浏览器地址栏上输入:http://localhost:8080/xmm/student.html
Servlet对象的生命周期
Servlet对象的生命周期是Tomcat负责的,Tomcat也被称为 Web容器(Web Container)
- 思考:手动new的Servlet对象受Web容器管理吗?
不受Web容器管理的。
web容器底层应该有一个HashMap这样的集合,在这个集合当中存储了Servlet对象和请求路径之间的关系

- 服务器启动时Servlet对象是否被创建?
给定两个Servlet,并且提供无参构造方法:
发现在启动时没有执行无参构造:
也就是说Servlet并不在Tomcat启动时创建对象,Tomcat启动时会解析web.xml文件,并且将请求路径和类名放在map集合当中
- 指定服务器启动时创建Servlet对象
这个整数是创建的优先级,数字越小优先级越高
就会先创建B,再创建A
- Servlet对象的生命周期
public class AServlet implements Servlet {
public AServlet() {
System.out.println("AServlet的无参构造执行了");
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("AServlet's init method execute");
}
@Override
public void destroy() {
System.out.println("AServlet's destroy method execute");
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("AServlet's service method execute");
}
}
默认情况下启动Tomcat时,没有任何输出
当第一次发送请求:
AServlet对象被实例化、init方法执行、service方法执行
第一次发送请求会执行构造方法、init() 、service()
第二次发送请求:
service方法执行
也就说明:Servlet对象是单例的,构造方法只会执行一次
对象存储在堆内存当中,Servlet对象的成员变量会有线程安全问题
- Servlet对象是单例(但是Servlet类并不符合单例模式,也被称为 ‘假单例’ ,单例模式的构造方法是私有化的)
- 第一次发送请求执行无参数构造方法,该方法也执行一次
- 只要发送请求,service方法一定会被调用
关闭服务器时,控制台输出了:
关闭服务器时destory方法被调用,并且只调用一次
服务器关闭需要销毁Servlet对象的内存,在销毁之前会自动调用该对象的destory方法。
Servlet对象可能开启了流、连接等资源,在关闭服务器之前需要在destory方法中进行销毁。
- 如果指定有参构造(无参构造不提供)
在发送请求时:

服务器内部错误的状态码都是500
结论:Servlet中不建议提供构造方法,Servlet规范提供了init方法来代替构造方法(初始化数据库连接池等)
适配器改造Servlet
Servlet接口中的getServletInfo、getServletConfig是不常用的,可以通过适配器模式改造:
抽象类继承Servlet,对不常用的方法进行空实现,常用的方法不覆盖(将实现的负担转移到下层)
public abstract class ServletApdater implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
对于Servlet app来说,只需要实现常用的方法就可以了:
public class ApdaterDemo extends ServletApdater{
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}
}
改造ServletApdater
对于init方法:
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
是Tomcat服务器调用的,其中的ServletConfig类型对象的创建和传递都是Tomcat服务器完成的。
伪代码:
public class Tomcat{
main(String[] args){
Class clazz = Class.forName("com.eun.servlet.LoginServlet");
Servlet servlet = (Servlet)clazz.getConstructor().newInstance();
ServletConfig servletConfig = new org.apache.catalina.core.StandardWrapperFacade();
servlet.init(servletConfig);
}
}
可以在init方法中使用servletConfig对象
也就是说创建的其实是StandardWrapperFacade对象
StandardWrapperFacade实现了ServletConfig接口,方法参数servletConfig是一个局部变量
但是ServletConfig需要在getServlet()中返回,如果是局部变量的话init方法结束内存就被释放,需要将ServletConfig设置为成员变量
因为父类中的ServletConfig方法被设置为私有的,只能通过公开方法访问。
- 如果子类要对父类中的init方法进行覆盖,但是一定要保证servletConfig对象的初始化:
保证对象的初始化:
但是这样做子类不能覆盖,可以再重载一个init方法(模板方法设计模式):
子类只需要实现重载的init方法就可以了。
为了保护ServletConfig的初始化,使用final限定该方法不能被重写,在该方法执行的过程中调用另一个可以被重写的方法就可以了。
最终的适配器类:
public abstract class ServletApdater implements Servlet {
private ServletConfig servletConfig;
@Override
public final void init(ServletConfig servletConfig) throws ServletException {
this.servletConfig = servletConfig;
this.init();
}
public abstract void init();
@Override
public ServletConfig getServletConfig() {
return servletConfig;
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
子类只需要实现init(重载)和service方法就可以了。
ServletApdater并不需要我们写,jakarta.servlet.GenericServlet已经写好了:
只是其中的重载init并非指定为抽象的,可以不覆盖。
并且其中的init(ServletConfig)方法没有final修饰,这是不好设计的。
在继承GenericServlet时注意覆盖的一定是无参的init:
ServletConfig
Jakarta.servlet.ServletConfig是Servlet规范的一部分,规定Tomcat在创建Servlet对象调用init方法时必须传入配置信息对象
GenericServlet中的方法:
- 谁实现了ServletConfig接口?传递的ServletConfig对象是谁?
Tomcat服务器传递的参数是StandardWrapperFacade实现类(由Tomcat实现)
如果把Tomcat服务器换成jetty服务器,输出ServletConfig的结果时就不是这个结果了,但是都实现了ServletConfig这个规范
- 对于两个Servlet实例来说,传递的并不是同一个ServletConfig对象:
public class ConfigTestServlet extends GenericServlet {
public void service(ServletRequest request, ServletResponse response){
ServletConfig servletConfig = this.getServletConfig();
System.out.println("ConfigTest01的ServletConfig对象是:" + servletConfig);
//ConfigTest01的ServletConfig对象是:org.apache.catalina.core.StandardWrapperFacade@51a63c19
}
}
public class ConfigTest02 extends GenericServlet {
public void service(ServletRequest request, ServletResponse response){
ServletConfig servletConfig = this.getServletConfig();
System.out.println("ConfigTest02的ServletConfig对象:" + servletConfig);
//ConfigTest02的ServletConfig对象:org.apache.catalina.core.StandardWrapperFacade@56738124
}
}
-
ServletConfig对象和Servlet是一对一
-
ServletConfig对象是在调用init方法之前创建的。
ServletConfig对象是Servlet对象的配置信息对象
配置信息有:
也就是Web.xml文件中的配置信息内容。
例如获取<servlet-name>:
@Override
public void service(ServletRequest request, ServletResponse response){
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
ServletConfig servletConfig = this.getServletConfig();
System.out.println(servletConfig);//org.apache.catalina.core.StandardWrapperFacade@1ac0d76b
writer.println("ConfigTest01的ServletConfig对象是:" + servletConfig + "<br>");
writer.println("servletConfig.getServletName(): " + "<servlet-name>" +
servletConfig.getServletName() + "</servlet-name>");
// servletConfig.getServletName(): <servlet-name>ConfigTestServlet</servlet-name>
}
对于getInitParameterNames方法,在web.xml文件中是可以指定初始化配置信息的:
这些初始化配置信息被Tomcat封装到了ServletConfig对象当中,通过该对象就可以获取这些配置信息
可以遍历Enumeration集合:
注意:配置文件web.xml重写之后一定要restart server
注意:GenericServlet实现了ServletConfig接口,其中重写了这4个方法,也就是说在我们调用的时候并不需要通过getServletConfig方法获取ServletConfig对象,直接通过this调用就可以了:
只要是继承了GenericServlet的子类都可以通过this调用ServletConfig的4个方法
调用的方法:
ServletContext
jakarta.servlet.ServletContext是Servlet规范的一部分
ServletConfig接口中有方法:
public ServletContext getServletContext(){}
返回的也是一个接口类型:
//第一种:通过ServletConfig获取ServletContext:
//ServletContext application = this.getServletConfig().getServletContext();
//第二种:通过GenericServlet获取ServletContext
ServletContext application = this.getServletContext();
System.out.println("servletContext = " + application);
//servletContext = org.apache.catalina.core.ApplicationContextFacade@30e37e9e
对于两个Servlet app来说:

两个不同的Servlet对象获取的竟然是同一个ServletContext
ServletContext是Tomcat实现的,实现类:org.apache.catalina.core.ApplicationContextFacade
- ServletContext在Web服务器启动时创建,由Web服务器创建,ServletContext在服务器关闭时销毁
- 对于一个webapp来说,只有一个ServletContext(可以理解为对应了xml文件)
- ServletContext被称为 Servlet对象的上下文对象、Servlet对象的环境对象、应用域
- 放在ServletContext对象中的数据一定是所有对象共享的
- 注意:Tomcat是一个容器,一个容器当中可以放多个webapp,一个webapp对应一个ServletContext对象
后面还会学习其他域:请求域、会话域;如果所有用户共享一份数据,这个数据很少被修改并且数据量很少可以放在应用域当中
实际上向应用域当中绑定数据,相当于把数据放在了缓存当中,然后用户访问的时候直接从缓存中获取,减少IO操作,可以极大提升系统性能
常用方法
- 获取上下文的初始化参数:
public String getInitParameter(String name)
public Enumeration<String> getInitParameterNames()
在web.xml文件中可以配置上下文的初始化参数
上下文初始化参数:所有Servlet对象都是共享的、应用级配置信息
例如:
注意:多个context-param必须配置到不同的选项当中
- 获取应用上下文的根路径
public String getContextPath()
可以动态获取应用的根路径
应用在最终部署的名称是不确定的,在程序中必须动态获取
- 获取文件的绝对路径
public String getRealPath()
其中的/index.html的位置:
应用的根路径就是web目录,/就是指代根路径(默认的起点就是从应用的根路径开始启动)
可以屏蔽操作系统的差异
- 记录项目日志
public void log(String message)
public void log(String message,Throwable t)
默认情况下,日志会自动记录到:
CATALINA_HOME/logs
但是Idea可以创建多个Tomcat,日志会默认记录到Idea相关的目录下

查看日志记录:

每一个文件夹都是一个Tomcat副本,都是参照apache-tomcat-10.0.12生成的
其中的文件类型:
如果使用log(String msg,Throwable t):
int age = 17;
if (age < 18){
application.log("age lt 18",new Throwable("age lt exception"));
}
但是控制台没有发生异常,只是模拟记录
- 以流的形式获取根路径下的资源
public InputStream getResourceAsStream(String path)
这里的path和getRealPath(String path)的path参数写法是一样的
- 向应用域中读写数据
ServletContext又被称为应用域,如果所有用户共享一份数据,这个数据很少被修改并且数据量很少,可以放在应用域当中
- 数据量小:数据量太大会占用过多的堆内存,并且此对象生命周期与服务器同步,只有服务器关闭这个对象才会被销毁(影响服务器性能)
- 修改很少:共享数据的修改操作必然会存在并发带来的安全问题
应用域相当于一个缓存,放在缓存中的数据下次使用时不需要再次获取。
public void setAttribute(String name,Object value);
public Object getAttribute(String name);
public void removeAttribute(String name)
例如:在AServlet对象中写入User,在BServlet中读取User:
//AServlet:
User user = new User("jack", "123");
application.setAttribute("userObj",user);
//BServlet:
Object userObj = servletContext.getAttribute("userObj");
writer.println(userObj + "<br>");
先访问a,再访问b:

先访问b,输出的就是null
以后编写Servlet类的时候,实际上不会继承GenericServlet类的,BS架构的系统是基于Http超文本传输协议的,在Servlet规范当中提供了一个类:HttpServlet,专门为HTTP协议准备的Servlet类;我们编写的Servlet类要继承HttpServlet,使用HttpServlet处理Http协议更便捷
对于HttpServlet,假设我们用户第一次发生请求:
-
执行GenericServlet的
init(ServletConfig config)方法 -
内部
this.init() -
执行子类
service(ServletRequest request,SevletResponse response)![image-20230501190046978]()
由于ServletRequest的继承结构:
在HttpServletRequest的实现类RequestFacade参数传入的时候,正常情况下会先执行HttpServletRequest参数对应的方法,但这样做是不符合设计思想的,Tomcat底层一定有机制保证service(ServletRequest request,ServletResponse response)先执行
-
执行
service(HttpServletRequest request,HttpServletResponse response)
HttpServlet
- 缓存机制
- 堆内存的字符串常量池
- 堆内存的整数型常量池
- 线程池:在Tomcat服务器启动的时候,会先创建好N多个线程Thread对象,然后将线程对象放到集合当中,称为线程池。用户发送请求过来之后,需要有一个对应的线程来处理这个请求,这个时候线程对象就会直接从线程池中拿,效率比较高。所有的WEB服务器,或者应用服务器,都是支持多线程的,都有线程池机制。
- 连接池:JVM是一个进程,MySQL数据库是一个进程,进程和进程之间建立连接打开通道是很耗费资源的,可以提前创建好N个Connection对象
- Redis
- ServletContext对象的应用域
Http协议
Http协议是w3c指定的超文本传输协议,B/S架构的系统需要遵守Http协议
对请求协议的测试,请求协议的格式有get/post:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>http test</title>
</head>
<body>
<!--html中的路径是要带上应用的根路径的-->
<h1>get请求:</h1>
<form action="http://localhost:8080/servlet05/getServlet" method="get">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="pwd">
<input type="submit" value="login">
</form>
<h1>post请求:</h1>
<form action="http://localhost:8080/servlet05/postServlet" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="pwd">
<input type="submit" value="login">
</form>
</body>
</html>
处理的service方法:
public class PostServlet extends GenericServlet {
@Override
public void service(ServletRequest request, ServletResponse response){
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
String msg = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fromPostServlet</title>
</head>
<body>
<h1>from post Servlet</h1>
"""
+
"""
"""
+
"""
</body>
</html>
""";
writer.write(msg);
}
}
- 查看协议的内容
chrome -> F12 -> network
Http协议包括:
-
请求协议:浏览器向服务器发送请求的具体格式,包括四部分
-
请求行
- 请求方式:7种方式,GET、POST、DELETE、PUT、HEAD、OPTIONS、TRACE
- URI:统一资源标识符,代表网络中某个资源的名字,不能定位到该资源
/servlet05/index.html- URL:统一资源定位符,通过URL是可以定位到该资源的
http://localhost:8080/servlet05/index.html
- URL:统一资源定位符,通过URL是可以定位到该资源的
- 协议版本号:HTTP/1.1
-
请求头:请求主机、主机端口、浏览器信息、平台信息、cookie信息等
-
空白行:分隔请求头和请求体
-
请求体:向服务器发送的具体数据
-
具体报文
- get:在请求行上发送数据,数据挂载在URI后面,数据回显在地址栏
GET /servlet05/getServlet?username=jack&pwd=123 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.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Connection: keep-alive Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784; Idea-20cff30e=c7317713-3c11-4ec2-93f2-5bfe89b01897; b-user-id=c1429b2f-d0fa-ab23-b39c-3283219755ab Host: localhost:8080 Referer: http://localhost:8080/servlet05/index.html Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" /*空白行*/ /*请求体*/get只能发送普通的字符串,字符串的长度有限制(不同浏览器限制不同),无法发送大数据量
- post:在请求体中发送数据,不会回显到地址栏上
POST /servlet05/postServlet 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.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: max-age=0 Connection: keep-alive Content-Length: 25 Content-Type: application/x-www-form-urlencoded Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784; Idea-20cff30e=c7317713-3c11-4ec2-93f2-5bfe89b01897; b-user-id=c1429b2f-d0fa-ab23-b39c-3283219755ab Host: localhost:8080 Origin: http://localhost:8080 Referer: http://localhost:8080/servlet05/index.html Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" /*空白行*/ username=zhangsan&pwd=123 /*请求体*/post可以发送任何类型的数据,包括普通字符串、流媒体等信息;可以发送大数据量(文件上传)
-
-
目前为止,只有表单中的method属性被指定为post时才能发送POST请求,其他全为GET请求
-
浏览器地址栏直接请求、超链接、form的默认提交都是get
提交信息的格式:name=value&name=value
重点:name就是form表单提交的输入域的name,value就是控件的value
-
响应协议:服务器向浏览器发送响应的具体格式,包括四部分
-
状态行:三部分组成
- 协议版本号 HTTP/1.1
- 状态码:
- 200 请求响应成功,正常结束;
- 404 访问资源不存在,前端错误;
- 405 前端发送的请求方式与后端的处理方式不一致,例如前端发送post请求,后端按照get请求处理就会有405错误
- 500 服务器端程序出现异常,后端错误
- 以4开始的一般是前端错误,5开始的一般是后端错误
- 状态描述信息:
- ok 正常结束成功
- not found 资源找不到
-
响应头
- 响应内容类型、响应内容长度、响应时间、日期...
-
空白行:分隔响应头和响应体
-
响应体:响应的正文,字符串
-
具体报文
HTTP/1.1 200 ok //状态行 Content-Type: text/html;charset=UTF-8 /* Content-Length: 162 Date: Tue, 02 May 2023 01:56:15 GMT 响应头 Keep-Alive: timeout=20 Connection: keep-alive */ //空白行 <!DOCTYPE html> //响应体 <html lang="en"> <head> <meta charset="UTF-8"> <title>fromGetServlet</title> </head> <body> <h1>from get Servlet</h1> </body> </html>
-
get和post的区别
-
get请求发送数据的时候,数据会挂在URI的后面,并且在URI后面添加一个“?”,"?"后面是数据。这样会导致发送的数据回显在浏览器的地址栏上。(get请求在“请求行”上发送数据)
-
http://localhost:8080/servlet05/getServlet?username=zhangsan&userpwd=1111
这种方式是不对的,用户名和密码都应该用post发送
-
-
post请求发送数据的时候,在请求体当中发送。不会回显到浏览器的地址栏上。也就是说post发送的数据,在浏览器地址栏上看不到。(post在“请求体”当中发送数据)
-
get请求只能发送普通的字符串。并且发送的字符串长度有限制,不同的浏览器限制不同。
-
get请求无法发送大数据量。
-
post请求可以发送任何类型的数据,包括普通字符串,流媒体等信息:视频、声音、图片。
-
post请求可以发送大数据量,理论上没有长度限制。
-
get请求在W3C中是这样说的:get请求比较适合从服务器端获取数据。
-
post请求在W3C中是这样说的:post请求比较适合向服务器端传送数据。
-
get请求是绝对安全的,因为get请求只是为了从服务器上获取数据
-
post请求是危险的,因为post请求是向服务器提交数据,如果这些数据中留有后门是危险的。
所以一般情况下拦截请求的时候都会选择拦截(监听)post请求
- get请求是支持缓存的,post请求不支持缓存
- 访问一张图片,访问过后会将图片放在浏览器的缓存当中,如果第二次请求路径没有发生变化就从浏览器的缓存中拿取
任何一个get请求的最终响应结果都会被浏览器缓存起来,在浏览器的缓存当中,一个get请求的路径对应一个资源
实际上,只要发送get请求,浏览器做的第一件事就是从本地缓存中查找资源,找不到才会向服务器发送请求
post请求之后的响应结果不会被浏览器缓存起来,这个缓存没有意义。
假设有需求:不希望get请求查找缓存,每一次都去服务器上查找资源。
只要每一次请求对应的请求路径不同就可以了,可以在路径后加一个系统毫秒数(时间戳):
http://localhost:8080/servlet05/index.html?t=系统毫秒数
系统毫秒数是随时发生变化的,一定会从服务器端获取。
GET请求和POST请求如何选择?
- 怎么选择GET请求和POST请求呢?衡量标准是什么呢?你这个请求是想获取服务器端的数据,还是想向服务器发送数据。如果你是想从服务器上获取资源,建议使用GET请求,如果你这个请求是为了向服务器提交数据,建议使用POST请求。
- 大部分的form表单提交,都是post方式,因为form表单中要填写大量的数据,这些数据是收集用户的信息,一般是需要传给服务器,服务器将这些数据保存/修改等。
- 如果表单中有敏感信息,还是建议适用post请求,因为get请求会回显敏感信息到浏览器地址栏上。(例如:密码信息)
- 做文件上传,一定是post请求。要传的数据不是普通文本。
- 其他情况都可以使用get请求。
模板方法设计模式
当前有两个类:
核心算法day和大部分代码都是相同的,只有doSome方法的内容不同
可以改进:
final修饰的核心算法受到了保护,可能出现不同行为的方法设置为抽象的,这就是模板类
模板方法定义核心的算法骨架,具体的实现步骤延迟到子类当中实现
HttpServlet
jakarta.servlet.http.HttpServlet
HttpServlet:专门为HTTP协议准备的Servlet类,使用HttpServlet处理Http协议更便捷
-
http包下的类和接口:
jakarta.servlet.http.*-
jakarta.servlet.http.HttpServlet (HTTP协议专用的Servlet类,抽象类)
-
jakarta.servlet.http.HttpServletRequest (HTTP协议专用的请求对象)
-
jakarta.servlet.http.HttpServletResponse (HTTP协议专用的响应对象)
-
- HttpServletRequest对象(简称request)封装了请求协议的全部内容,Web服务器将请求协议中的数据解析出来封装到request对象当中
- HttpServletResponse对象:专门用来响应Http协议到浏览器的
执行过程:
//1. 通过无参数构造方法创建对象
public HelloServlet() {
}
//2. 执行HttpServlet的init(ServletConfig config)方法
// 执行父类GenericServlet的 init(ServletConfig config)方法 模板方法
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config; // 局部
this.init();
}
// 内部调用init()方法(供子类覆盖)
public void init() throws ServletException {
// NOOP by default
}
//3. 执行HttpServlet的service(ServletRequest request,ServletResponse response)方法
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
//接口类型转换
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
service(request, response); //调用参数类型一致的方法
}
// 调用 service(HttpServletRequest request,HttpServletResponse response)
/*模板方法*/
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//获取请求方式
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
注意:在HttpServlet中模板方法是protected void service(HttpServletRequest req, HttpServletResponse resp),
其中doGet(req, resp)等方法的实现延迟到子类中完成
正常情况下,发送GET请求就重写doGet方法,发送POST请求就重写doPost方法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/servlet06/hello">hello(get)</a>
<br>
<form action="/servlet06/hello" method="post"><input type="submit" value="hello(post)"></form>
</body>
</html>
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response){
PrintWriter writer = response.getWriter();
writer.write("<h1>doGet</h1>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response){
PrintWriter writer = response.getWriter();
writer.write("<h1>doPost</h1>");
}
这样做是没有问题的。
但是如果发送到是GET请求,而后台重写的是doPost方法:

因为此时访问的是父类HttpServlet中的doGet()方法,在父类中的这个方法是:
假设此处是用户登录Servlet,要求前端必须使用POST请求,如果使用GET就报错,在后端就只重写doPost方法
总结:405错误是因为HttpServlet中的doXxx()方法执行了
如果直接重写service(HttpServletRequest req, HttpServletResponse resp)方法,是无法感知到405错误的
405代表了前端发送的请求不是服务器需要的请求方式
有些程序员为了避免405将doGet和doPost方法都重写了,这样做是不好的。
Servlet开发步骤
- 第一步:编写一个Servlet类,直接继承HttpServlet
- 第二步:重写doGet方法或者重写doPost方法,到底重写谁后端需求决定。
- 第三步:将Servlet类配置到web.xml文件当中。
- 第四步:准备前端的页面(form表单),form表单中指定请求路径即可。
Web站点的Welcome Page
设置了Welcome Page后,访问这个webapp时没有指定任何资源路径会默认访问欢迎页面
一般的访问方式:http://localhost:8080/login/index.html
如果访问的是:http://localhost:8080/login 只是这个站点,没有指定具体的路径默认访问Welcome Page
通过http://localhost:8080/servlet07访问的就是指定的Welcome Page /loginPage.html
- 一个Webapp是可以设置多个欢迎页面的,越上方的优先级越高
<welcome-file-list>
<welcome-file>loginPage.html</welcome-file>
<welcome-file>dir1/dir2/backLoginPage.html</welcome-file>
</welcome-file-list>
<!--路径最好不要以 / 开始-->
如果第一个找不到就会展示第二个
当文件名称为 index.html时,Tomcat提前将名称为index.html的页面配置为WelcomePage
WelcomPage的配置
- 在webapp下的WEB-INF内web.xml文件中配置(局部配置)
- 在CATALINA_HOME/conf/web.xml文件中配置(全局配置)
局部配置是优先的。
Welcome Page可以是一个Servlet
Welcome Page其实就是服务器当中的一个资源,可以是静态的HTML页面也可以是动态的Java程序
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.write("<h1>Welcome to MyServlet</h1>");
}
}
- 放在WEB-INF路径下的资源是受保护的,不能通过浏览器访问,静态资源要放在WEB-INF目录之外
HttpServletRequest接口详解
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print(request); //org.apache.catalina.connector.RequestFacade@72761ab9
}
HttpServletRequest是一个接口,全称是:jakarta.servlet.http.HttpServletRequest
HttpServletRequest的实现类是org.apache.catalina.connector.RequestFacade实现的
这个对象是Tomcat创建的,封装了Http的请求协议,需要哪些信息面向接口编程就可以了
注意:Request和Response对象每次请求都是不同的,生命周期只有一次请求
常用方法
如何存储前端的数据?
如果当前有如下的前端界面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register User</title>
</head>
<body>
<form action="http://localhost:8080/servlet08/request" method="post">
username:<input type="text" name="username"> <br>
password:<input type="password" name="pwd"> <br>
interest:
smoke<input type="checkbox" name="interest" value="smoke">
drink<input type="checkbox" name="interest" value="drink">
perm <input type="checkbox" name="interest" value="perm">
<input type="submit" value="register">
</form>
</body>
</html>
提交的数据格式:
username=admin&pwd=123&interest=drink&interest=perm
应该采取Map集合存储,因为都是Key-Value的结构;但是对于interest属性来说,key重复,value覆盖
可以将Key存储String,value存储String数组
Map<String, String[]>
key存储String
value存储String[]
key value
-------------------------------
username {"abc"}
userpwd {"111"}
aihao {"s","d","tt"}
注意:前端表单提交数据的时候,假设提交了120这样的“数字”,其实是以字符串"120"的方式提交的,所以服务器端获取到的一定是一个字符串的"120",而不是一个数字。(前端永远提交的是字符串,后端获取的也永远是字符串。)
一般情况下不从Map集合中获取,效率较低
父类ServletRequest中:
- 获取前端提交的数据
Map<String,String[]> getParameterMap(); //获取map集合
Enumeration<String> getParameterNames(); //获取所有key
String[] getParameterValues(String name); //根据key获取map集合的value
String getParameter(String name); //大部分情况下数组中是一个元素 可以获取数组中的这一个元素
//这四个方法和获取用户提交的数据有关系
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] value = entry.getValue();
System.out.println(key + " : " + Arrays.toString(value));
}
}
/**
username : [zhangsan]
pwd : [123]
inters : [smoke, smoke]
*/
注意:如果数组中有多个参数,使用request.getParamter()获取的结果是数组中的第一个元素
request 请求域对象
请求域:可以在一次请求范围内进行共享数据,一般用于请求转发的多个Servlet中共享数据。
-
请求域对象比应用域对象的范围小很多,请求域只在一次请求内有效,一个请求对象request对应一个请求域
-
void setAttribute(String name,Object obj); Object getAttribute(String name); void removeAttribute(String name); -
尽量使用小的域对象,占用的资源较少
在同一个Servlet中进行请求域存取:
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Date nowTime = new Date();
request.setAttribute("sysTime",nowTime);
Object sysTime = request.getAttribute("sysTime");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print(sysTime); //Wed May 03 11:03:44 CST 2023
}
}
这样做是没有问题的
在不同的Servlet中进行存取:
//在A中向Request存:
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Date nowTime = new Date();
request.setAttribute("sysTime",nowTime);
}
}
//在B中从Request取:
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Object sysTime = request.getAttribute("sysTime");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.print(sysTime);
}
}
首先访问/servlet04/a,将数据存入请求域;然后访问/servlet04/b,读取刚才存入的数据

这样做是读不到数据的,因为第一次请求结束之后请求域就被销毁了,/servlet04/b是第二次访问请求,两个是完全不同的请求域
不能在AServlet中手动new BServlet的对象,自己new的Servlet对象生命周期不受Tomcat的管理
使用Servlet中的请求转发机制可以将AServlet和BServlet放在一次请求当中,也就是通过AServlet跳转到BServlet,
请求转发机制
执行了AServlet之后,跳转到BServlet
- 获取请求转发器:
public RequestDispatcher getRequestDispatcher(String path) //path是xml文件中指定的path
Tomcat通过path从xml文件中获取servlet-class。
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Date nowTime = new Date();
request.setAttribute("sysTime",nowTime);
//相当于把 /b这个路径包装到请求转发器当中,实际上是把下一个转发的资源的路径告知Tomcat服务器
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/b");
//调用requestDispatcher的forward方法进行转发
requestDispatcher.forward(request,response);
}
}
注意:转发(forward())是一次请求,传递的都是同一个request对象,request和response都是要转发给下一个资源的
这时还是不能直接访问/servlet04/b,这样做请求无法转发到BServlet的request上,必须通过/servlet04/a访问
- 多个Servlet间共享数据
- 数据放在ServletContext中,但是不建议这样做
- 数据放在Request请求域当中,使用请求转发机制。
转发的下一个资源可以是html等静态资源,只是转发的路径必须以 / 开始,不加项目名
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Date nowTime = new Date();
request.setAttribute("sysTime",nowTime);
request.getRequestDispatcher("/index.html").forward(request,response);
}
}
index.html:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>index html page</h1>
</body>
</html>
常用方法
- 获取客户端IP地址
public String getRemoteAddr(); //获取客户端IP地址
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String remoteAddr = request.getRemoteAddr(); //获取客户端的地址
System.out.println(remoteAddr);
InetAddress address = InetAddress.getByName(remoteAddr);
System.out.println(address.getHostAddress());
System.out.println(address.getHostName());
}
以127.0.0.1:8080/servlet09/...访问时:
- 设置请求体的字符集
public void setCharacterEncoding(String env);
POST请求在请求体中提交数据,setCharacterEncoding()是处理post请求的乱码问题
有如下html页面:
在提交的时候指定为POST方式提交,用户名提交为中文;在Tomcat10中是没有问题的,但是在Tomcat9及以下版本就会出问题,就需要设置字符集
Tomcat10的字符集默认是UTF-8,Tomcat9及之前的字符集是ISO-8859-1
对于response来说也是如此:
response.setContentType("text/html"); //Tomcat9及之前这样返回的中文也会乱码
response.setContentType("text/html;charset=UTF-8");
- get请求的乱码问题
get请求的数据提交是在URI之后进行提交的,也就是在请求行上提交的
这是Connector标签的相关配置,在conf-server.xml中:
其中的URIEncoding:
就是URI的编码方式,默认值为UTF-8
但是在Tomcat7及之前,URIEncoding的默认编码方式是ISO-8859-1
解决方案:修改CATALINA_HOME/conf/server.xml中
- 获取应用的根路径
public String getContextPath();// 获取应用的根路径
在一个HttpServlet中,可以通过request获取应用的根路径:
也可以通过HttpServlet本身获取应用的根路径:
HttpServlet的父类是GenericServlet,GenericServlet implements Servlet,ServletConfig,实现了ServletConfig中的方法:
getServletContext():
getServletContext(){
return this.getServletConfig().getServletContext();
}
- 获取前端请求方式
public String getMethod();
String method = request.getMethod();
System.out.println(method); //GET
- 获取请求URI
public String getRequestURI();
String requestURI = request.getRequestURI();
System.out.println(requestURI);// /servlet04/a
- 获取Servlet路径:不带项目名
public String getServletPath();
String servletPath = request.getServletPath();
System.out.println("servletPath = " + servletPath); // servletPath = /a
Servlet的单表练习
使用Servlet完成单表的CRUD操作
-
实现步骤:
- 第一步:准备数据库表,对部门表进行增删改查
drop table if exists dept; create table dept( deptno int primary key, dname varchar(255), loc varchar(255) ); insert into dept(deptno,dname,loc) values(10,'销售部','北京'); insert into dept(deptno,dname,loc) values(20,'研发部','上海'); insert into dept(deptno,dname,loc) values(30,'技术部','广州'); insert into dept(deptno,dname,loc) values(40,'媒体部','深圳'); commit; select * from dept;-
第二步:准备界面原型
新增界面 add.html、修改页面 edit.html、详情界面 detail.html 、欢迎页面 index.html、部门列表页面 list.html
保证页面流转
-
第三步:分析系统功能,只要一个操作连接了数据库就代表了一个功能
- 查看部门列表
- 保存部门
- 删除部门
- 查看部门详细信息
- 跳转到修改页面(占位符的值也需要查询数据库)
- 修改部门
-
第四步:搭建开发环境
- 创建webapp
- 添加第三方jar包(MySQL驱动等)
- JDBC工具类
- 将HTML页面放在Web目录下
-
实现功能:查看部门列表
-
假设从前端开始:从用户点击按钮开始
-
查看部门列表:连接数据库,查询所有用户信息展示到页面上;修改前端页面中的超链接,用户最先点击的就是这个超链接
-
写web.xml文件
-
DeptListServlet重写doGet,查询数据库中所有的部门
-
此时需要将查询的结果传递到前端页面上,但是此时只能替换整个网页:
在Java程序中拼接字符串,将结果写出
-
实现 查看部门详情
这一步也需要查询数据库,需要跳转到一个Servlet当中:
window.location.href = 'http://localhost:8080/oa/dept/detail'; //前端中都需要加项目名但是项目名不应该给定,应该动态的获取:
"window.location.href = 'http://localhost:8080" + request.getContextPath() + "/dept/detail';"此时应该传递部门编号,在发送请求的时候:
对应了:
这样就需要在创建动态内容时进行额外处理:
在请求头中:
- 实现删除功能
需要向Servlet传递要删除的编号
这一步的操作是相同的,在Servlet中根据传递的deptno进行删除,但是在删除之后应该回到部门列表的页面,这个操作可以使用请求转发机制完成
这样做可以获取最新的数据
注意:删除或者新增最好手动提交事务
也可以根据返回值count进行删除成功或者失败的处理。
- 新增 功能
-
-
这样做是有一个问题的,在新增部门页面提交过来的请求是POST请求,通过请求转发机制是一个请求,在/dept/list中指定的处理方式是GET,405错误
解决方案:
- 可以在/dept/list中重写doPost方法,内部调用doGet方法,这是不好的设计
- 重定向
实现部门的修改:
点击这个修改,跳转到修改页面:
这次实际上是一个查询操作,根据deptno从数据库中查出对应的信息,设为默认值
用户输入修改后的信息 点击确认修改,这次才是一个update的操作
第一次点击修改:从数据库中查询数据展示:
注意只能设置为readonly,disabled的数据不会被表单提交
第二次点击修改:向数据库中更新数据
更新完毕转发回主页面
建议从前端向后端一步一步实现,首先要考虑的是用户点击的是什么,用户点击的东西在哪里
转发 重定向
- 在一个web应用中如何完成资源的跳转?
-
转发:是同一次请求
request.getRequestDispatcher("/资源路径").forward(request,response);//获取请求转发器对象 调用forward方法转发 //转发是一次请求,传参request就是为了保证在跳转调用相关方法时传递的都是同一个request对象,对应了一个请求域
- 重定向:由response对象完成操作
response.sendRedirect(request.getContextPath() + "/资源路径");
重定向的path必须添加项目名:response具有响应能力,自动将路径响应给浏览器,浏览器自发的向服务器发送了一次全新的请求,浏览器发送请求必须带项目名
response对象将 /servlet10/b响应给浏览器,浏览器又自发的向服务器发送了一次全新的请求http://localhost:8080/servlet10/b
所以重定向一次浏览器一共发送了两次请求:
-
第一次访问/servlet10/a,重定向response返回 /servlet10/b
-
浏览器自发的请求/servlet10/b
最终在地址栏上显示的地址一定是最后一次请求的地址,所以重定向会导致浏览器地址栏上的地址发生改变
-
转发是由Tomcat控制的,从A资源跳转到B资源,跳转动作在Tomcat内部完成
-
重定向完全是由浏览器控制下一个资源的走向
重定向无法访问到第一次请求域中的数据。
转发和重定向的选择:
- 在上一个servlet当中向request域中绑定了数据,希望在下一个servlet中使用这些数据,使用转发机制
- 其余所有的请求使用重定向
在单表练习中,保存修改的部门信息时使用POST请求接收表单数据,保存完毕之后应该跳转到部门列表页面,部门列表只重写了GET方法,此时就可以使用重定向:
浏览器自发的请求是GET请求,就可以避免在部门列表Servlet中重写doPost方法调用doGET
保存失败也建议使用重定向:
重定向可以是任何资源
转发导致的刷新问题
有如下页面:
对应的Servlet:
保存完毕之后通过转发跳转到成功页面
如果在此处不断的刷新,就相当于一直在提交地址栏上的:http://localhost:8080/servlet10/save?no=5&name=zhangsan
本质上就是重复发送了这次请求,使用重定向可以解决这个问题:
重定向之后浏览器自发请求的地址:
即使刷新也是请求的success界面,这个界面在浏览器的缓存之中
注解式开发
仅仅一个单表查询,xml文件中就有如此多的内容;
采用这种方式对于一个大的项目来说web.xml文件会非常庞大;而且在web.xml文件中配置的信息是很少修改的
Servlet3.0后推出了各种基于Servlet的注解式开发
优点:
- 开发效率高,不需要编写大量的配置信息,直接在Java类上使用注解进行标注
- web.xml文件变小了(注解 + 配置文件 组合式开发)
一些不经常变化的配置建议使用注解。
jakarta.servlet.annotation.WebServlet
- name属性:指定Servlet的name,等同于
<servlet-name> - utlPatterns属性:指定Servlet的url,patterns以s结尾,可以指定多个路径,也就是指定值可以是数组
- value属性:等同于urlPatterns
- loadOnStartUp属性:服务器启动阶段创建Servlet对象
- initParams属性:初始化参数
是注解类型 WebInitParam类型的数组
对于value属性来说,因为其他属性都指定了默认值,在指定注解的时候可以省略,所以value对应的属性可以省略 value =
但是如果同时指定其他属性值,value = 就不能省略了。
模板方法解决类爆炸
上例中的开发方式是一个servlet请求对应了一个Servlet类,这样随着请求的增多就会出现 “类爆炸”的问题
可以使用模板方法设计模式改造:
接收所有的请求,重写HttpServlet的Service(HttpServletRequest,HttpServletResponse)方法
这样就是一个Servlet请求对应一个方法,一个业务模块对应一个Servlet类,部门相关的就对应一个DeptServlet
不建议重写doGet和doPost方法,可能同时处理这两种请求
此时注解还有一种更好的写法:
所有的 /dept/开始的请求都走这个Servlet
改进之后的项目:
注意:如果使用模糊匹配在获取路径的时候就不能使用getServletPath()
JavaServer Pages
Java程序编写前端代码太麻烦,程序耦合度太高,难于维护;并且修改前端代码就要重新编译Java程序,生成新的class文件,打一个新的war包,重新发布。
JSP是Java程序,是基于Java语言实现的服务器端的页面,本质上还是Servlet。
JSP是一套规范,所有的Web容器都会遵守这套规范,都是按照这套规范进行翻译的。
每一个Web容器/web服务器都会内置一个JSP翻译引擎
其中什么都没写,启动服务器:
访问这个路径:
底层会将.jsp文件翻译生成.java文件,Tomcat会将.java文件编译生成.class文件,也就是访问index.jsp底层执行的是index_jsp.class程序
访问index.jsp,执行的是index_jsp.class中的方法
index_jsp.java:
继承自HttpJspBase,而HttpJspBase:
继承自HttpServlet,说明访问.jsp底层执行的是一个Servlet,JSP实际上就是一个Servlet
- 访问index.jsp的时候,会自动翻译生成index_jsp.java,会自动翻译生成index_jsp.class,index_jsp类继承自HttpJspBase,HttpJspBase继承自Servlet
JSP的生命周期和Servlet完全相同(也都是假单例)
第一次访问JSP的时候比较慢,会从.jsp -> .java -> .class -> 无参构造 -> init() -> service()
第二次访问JSP会直接调用service方法,比较快
对JSP进行错误调试的时候,应该打开.java文件检查
有如下jsp文件:
abc
在生成的index_jsp.java的_jspService(HttpServletRequest,HttpServletResponse)中:
index.jsp文件:
<html>
<head>
<title>my first jsp page</title>
</head>
<body>
<h1>my first jsp page</h1>
</body>
</html>
对应的index_jsp.java文件:
在jsp文件中直接编写文字,都会被翻译到Servlet类的service方法的out.write(“”)当中,输出到浏览器
如果想在jsp中写Java代码,需要加一些特殊符号,JSP翻译引擎会根据不同的特殊符号将内容翻译到.java文件的不同位置
对于响应的中文内容:
需要对JSP加一个page指令,解决乱码问题:
在_jsp.java 代码中:
JSP默认不是UTF-8
JSP虽然本质上是个Servlet,但是和Servlet的职责不同
Servlet的职责:收集数据
JSP的职责:展示数据
基础语法
在JSP中编写Java程序:
<% code line %>
在<%%>中编写的内容被视为Java程序,被翻译到Servlet类的Service方法内部
<%@page contentType="text/html; UTF-8" %>
<%
System.out.println("hello jsp");
%>
访问这个jsp就会在控制台中输出hello jsp
这样直接暴露在service()方法内部,在<%%>中写代码就是在方法体中写代码
会直接退出虚拟机,Tomcat服务器立即停止工作
如果在jsp的脚本块中出现了语法错误:
访问时错误代码500:
这个错误是在.java -> .class编译时发生的问题
- 注释
JSP的注释:
<%--
--%>
注释内容不会被翻译到.java当中;HTML的注释:
<!--
-->
这个注释还是会被翻译到.java文件当中
- 在
service()方法之外写东西:声明语法
<%!
%>
还是编译报错
但是这种方法不建议使用,在service方法之外写的是静态变量/实例变量,Servlet对象只有一个,多线程并发会有线程安全问题
输出语句
向浏览器输出Java变量
<%@page contentType="text/html; UTF-8" %>
<%
String name = "jack";
//输出name到浏览器
%>
这段代码是翻译在_jspService()方法内部的:
可以调用参数 HttpServletResponse response将这个内容输出到浏览器:
这样做是没有问题的,但是在_jspService()方法当中:
在方法内部向浏览器输出时使用的就是变量out:
在JSP中我们也可以直接使用:
这个out是JSP的九大内置对象之一,可以直接使用(只能在service方法内部使用)
异常对象此时还没有启用
如果在以下程序中要输出c:
可以这样写:
但是这样做太麻烦,每次都要调用out.write()方法处理
可以直接使用语法:
<%= c %>
可以将其中的内容当作Java代码处理后输出,翻译为如下代码,翻译到_jspService()方法内部
如果输出的内容中有变量,使用这种语法可以省去<% out.write() %>的操作
示例:
<%="登录成功,欢迎 " + username%>
改造oa
-
使用Servlet收集数据,处理业务
-
使用JSP展示数据
改造index.jsp:
<%= request.getContextPath%>获取应用根路径 /oa
注意:
<%= %>当中可以随意加空格,因为从标签开始到标签结束看作一个整体
- 改造list.jsp
在之前的项目中使用Servlet查询完毕数据之后在Servlet中拼接字符串输出网页;
使用jsp就可以在Servlet中查询数据,将数据封装在请求域当中,将请求转发到jsp上展示数据
转发到:
<%@ page import="com.eun.oa.bean.Dept" %>
<%@ page import="java.util.LinkedHashSet" %>
<%@page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>部门列表页面</title>
</head>
<body>
<h1 align='center'>部门列表</h1>
<hr>
<table border='1px' align='center' width = '50%'>
<tr>
<th>序号</th>
<th>部门编号</th>
<th>部门名称</th>
<th>操作</th>
</tr>
<%
LinkedHashSet<Dept> set = (LinkedHashSet) request.getAttribute("depts");
for (Dept d : set) {
String deptno = d.getDeptno();;
String dname = d.getDname();
String loc = d.getLoc();
%>
<tr align='center'>
<td><%=deptno%></td>
<td><%=dname%></td>
<td><%=loc%></td>
<td>
<button onclick='del()'>删除</button>
<button onclick='edit()'>修改</button>
<button onclick='detail()'>详情</button>
</td>
</tr>
<%}%>
</table>
<hr>
<button onclick='add()'>新增部门</button>
<script !src=''>
function add() {
window.location.href = '<%=request.getContextPath()%>/add.jsp';
}
function del() {
window.location.href ='<%=request.getContextPath()%>/del.jsp';
}
function edit() {
window.location.href = '<%=request.getContextPath()%>/edit.jsp';
}
function detail() {
window.location.href = '<%=request.getContextPath()%>/detail.jsp';
}
</script>
</body>
</html>
其中展示的核心:
在list_jsp.java 文件中:
JSP翻译引擎替我们做了输出的工作
这个示例很好的体现了:
- Servlet只负责处理 数据
- JSP负责展示 数据
JSP文件的扩展名是可以配置的,在CATALINA_HOME/conf下:
对于修改 和 详情 来说,实现的功能都是从数据库里面获取数据并展示,只是修改在展示之后多了一个向数据库中保存数据的操作,可以考虑将这两个功能合并:
- 第一种方法:
给两个不同的路径:
两个路径指向同一个Servlet:
在Servlet中对ServletPath(或者RequestURI)进行判断:
- 第二种方法:
给两个相同的路径,不同的query参数:
拦截同一个路径:
直接获取query参数进行跳转:
Session
任何用户都可以访问oa系统,对系统中的数据进行增删改的操作,需要设置一个登录功能;登录成功才能访问系统
-
数据库中添加user表,存储登录信息
drop table if exists t_user; create table t_user( id int primary key auto_increment, username varchar(255), password varchar(255) ); insert into t_user(username, password) values('admin','123456'); insert into t_user(username, password) values('zhangsan','123456'); -
实现登录页面,输入用户名和密码提交表单(实际开发中存储的密码也不能是明文 MD5加密)
-
对应Servlet处理对应请求
登录成功:跳转到部门列表页面
登录失败:跳转到失败页面
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
String pwd = request.getParameter("pwd");
Connection conn = null;
PreparedStatement prep = null;
ResultSet set = null;
try {
conn = DBUtil.getConnection();
String sql = "select * from t_user where username = ? and password = ? ";
prep = conn.prepareStatement(sql);
prep.setString(1,username);
prep.setString(2,pwd);
set = prep.executeQuery();
if (set.next()){
response.sendRedirect(request.getContextPath() + "/dept/list");
} else {
response.sendRedirect(request.getContextPath() + "/loginError.jsp");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(set,prep,conn);
}
}
但是此时还有问题,只要知道部门地址/dept/list还是可以访问的;如果提交http://localhost:8080/oa/dept/del?deptno=30
可以直接删除部门信息,这个登录没有起到真正拦截的作用;应该设置只处理登录用户的请求
Session就可以解决这个问题
B/S结构系统的会话跟踪技术
会话一词,指聚谈、对话。最早出现在欧阳修《与吴正肃公书》一文中:“前约临行少留会话,终不克遂,至今为恨。”
从打开一个浏览器访问某个站点,到关闭这个浏览器的整个过程中,称为一次会话。
会话机制最主要的目的是帮助服务器记住客户端状态(标识用户,跟踪状态)。目前服务器与客户端的通讯都是通过HTTP协议,而HTTP协议是一种无状态协议,客户端第二次来访时,服务器不知道客户端之前是否来访过,Web服务器本身不能识别出哪些请求是同一个浏览器发出的;即:浏览器的每一次请求都是完全独立的。
为了更好的用户体验(比如实现购物车),就有了会话机制,服务器可以记住并区分客户端,把同一个客户端的操作归类在一起。
由于某些原因,HTTP必须保持无状态,所以会话跟踪技术就成了对HTTP无状态协议的一种扩展。
假如Http协议是有状态的,1000个人访问百度,百度服务器就要维持1000个人的状态,服务器压力很大,无状态的协议可以降低服务器的压力
基于上面的分析(会话是为了唯一标识一个用户并记录其状态),既然一个会话可以帮助服务器断定一个客户端,那么反向推导得到的结论就是:当服务器无法断定客户端时,一次会话就结束了,无法断定客户端的情况:
- session失效(服务器)
- cookie失效(客户端)
会话的基本原则:双方共存(session和cookie)
Java中,会话跟踪常用的有两种技术:Cookie和Session,并且Session底层依赖于Cookie(也可以使用自定义token)
Cookie是服务器响应(客户端必须先访问服务器)给客户端,并且存储在客户端的一份数据;下次客户端访问服务器时,自动携带Cookie,服务器根据Cookie就可以区分客户端。
服务器如何将Cookie响应给客户端:
浏览器访问这个Servlet,Servlet通过Response将Cookie以HTTP响应头的形式发送给浏览器,也就是Set-cookie:
HTTP响应头都是以键值对的形式组成,可以一键一值,也可以一键多值

浏览器在接收到响应头后,会将他们作为Cookie文件存储在客户端,当后续请求同一个服务器,在发送请求时会自动携带Cookie信息

因为现在客户端已经有Cookie了,以后每次访问服务器都会带上Cookie,如何在服务器获取客户端的Cookie?
注意,虽然请求头中cookie的内容是name=eun;time=6pm,但是并不需要我们在Servlet中用分号切割字符串。只要调用request.getCookies();即可得到cookie数组(自动切割成数组)
但是,这种方式传给浏览器的Cookie会随着浏览器的关闭而消失,这也就是为什么登录京东,添加商品进购物车后关闭浏览器就需要重新登录的原因
Cookie的两种类型
- 会话cookie (session cookie)
- 持久性cookie (Persistent cookie)
上文中服务器向浏览器响应的cookie就是会话cookie,会话cookie被保存在浏览器的缓存中。
持久性Cookie只需要设置Cookie的持久化时间即可
cookie.setMaxAge(10 * 60); //10 min
设置持久化时间 > 0,即可将cookie在客户端持久化
在客户端收到的响应报文中,set-cookie多了一个Expires 过期时间字段
cookie.setMaxAge(0) 删除客户端的cookie信息
Session
cookie是存于客户端的,如果保存了敏感信息,明文传输是不安全的,并且信息太多会影响传输效率;所以出现了Session技术
Session是存储与服务器的,是一个HashMap集合。
此时,不再把name=eun;time=6pm这样的数据作为Cookie放在请求头/响应头中传递了,而是只给浏览器传递一个JSESSIONID(实际上也是cookie),真正的信息存储在服务器端的Session对象中,响应给浏览器的Cookie只是Session的id,即JSESSONID。下次访问该网站时,将JSESSONID带上,就可以在服务器上找到对应的Session,相当于带去了用户信息
返回的cookie也是区分会话cookie或持久性cookie的。
其实,在服务器端创建了session对象之后,即使不写response.addCookie("JSESSIONID"),JSESSIONID也会被作为cookie返回
只要在服务器端创建了Session,JSESSIONID默认作为Cookie返回
Session序列化
session序列化其实是一个默认行为,比如当前有上千万个用户在线,用户登录信息都在各自的session中,当服务器不得不重启时,为了不让当前服务器保存的session对象丢失,服务器会将当前内存中的session序列化到磁盘当中,等待重启完毕重新读回内存;这些操作在浏览器端的用户是感知不到的,因为session还在,不需要重新登录
以Tomcat为例,服务器的Session都会被保存在work目录的对应项目下,关闭服务器时,当前内存中的session会被序列化在磁盘中,变成一个叫SESSIONS.ser的文件
session的钝化与活化
自从改用Session后,由于Session都存在服务器端,当在线用户过多时,会导致Session猛增,无形中加大了服务器的内存负担。于是,服务器有个机制:如果一个Session长时间无人访问,为了减少内存占用,会被钝化到磁盘上。
也就是说,Session序列化不仅仅是服务器关闭时才发生,当一个Session长时间不活动,也是有可能被序列化到磁盘中。当该Session再次被访问时,才会被反序列化。这就是Session的钝化和活化。
与服务器关闭时Session的序列化不同的是:1.每个Session单独一个文件,而不是SESSIONS.ser。2.即使Session活化回到内存,磁盘的文件也不消失
还有个问题需要解决:Session被序列化了,存在Session中的值怎么办?比如之前有这么一步操作:
HttpSession session= request.getSession();
session.setAttribute("user", new User("eun", 26));
此时Session中有一个User对象,直接序列化User从内存中消失,无法随Session一起序列化到磁盘。如果希望Session中的对象也一起序列化到磁盘,该对象必须实现序列化接口。
Tomcat session
- 一次会话对应N次请求
- HttpServletRequest中:
在jakarta规范中,session对应的类名:jakarta.servlet.http.HttpSession
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response){
//获取session对象
HttpSession session = request.getSession();
response.getWriter().print(session);
}
-
在同一个浏览器中,不管发送多少次请求得到对象的hashCode都是相同的,也就是多次请求对应一个session对象
-
如果关闭浏览器再打开进行请求,得到的内存地址就变化了,也就是创建了一个新的session对象。
-
使用Edge浏览器访问:
使用FireFox浏览器访问:
使用Chrome浏览器访问:
服务器为这三个浏览器分别分配了一个session对象
对于OA项目的登录功能:
- 不能根据(Session是否存在)JSESSION判断用户是否登录,即使身份认证失败,服务器也会对该次请求创建Session对象(在首页是JSP的情况下),如果根据(Session是否存在)JSESSIONID判断下次访问时还是无法拦截。
- 用户信息不能存入请求域,因为一次请求结束请求对象就被销毁,下次请求时无法获取状态
- 用户信息不能存入应用域,因为这样做所有用户的登录状态都会被共享,且登录状态永不停止
可以在登录成功之后将用户信息存入会话域当中,在每次发送请求时对会话域中的信息进行判断,如果有用户信息就响应请求
session的实现原理 cookie
//从服务器中获取当前的session对象,如果session对象不存在就新建
HttpSession session = request.getSession();
//从服务器中获取session对象,如果session对象不存在不会新建,返回null
HttpSession session = request.getSession(false);
不同浏览器访问时获取的不是同一个session对象,关闭浏览器再访问获取的也不是同一个session对象
服务器端在一般情况下是不知道浏览器是否关闭的,一般采取 “ 超时机制 ”,一段时间后没有请求会将这个session销毁了
例如京东商城的长时间未操作跳转回登录页面的功能
- session对象是存储在服务器端的
- 一个session对应一个会话,一个会话对应N个请求
web服务器中有一个session列表,类似于Map集合,Map集合的key是JSESSIONID,value是session对象
用户发送第一次请求的时候,服务器会创建一个新的session对象,同时给session对象生成一个id,web服务器会将session的id响应给浏览器,浏览器将id保存在缓存中
用户发送第二次请求的时候,会自动将浏览器缓存中的session id发送给服务器,服务器获取到session id,从session列表中查找到对应的session对象
关闭浏览器之后,缓存清空,发送的请求不带有session id,服务器会创建一个新的session对象
一次会话:从session对象的创建,到session对象的死亡
session对象的销毁是超时机制完成的,或者是安全退出,手动销毁
配置session对象的超时机制:
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<!--session对象的超时时间是30分钟-->
如果浏览器没有关闭,30分钟内没有操作session对象被销毁,代表会话结束
session id 是以 cookie的形式保存在浏览器中
Cookie: JSESSIONID=1DBBDBFA59546E5E350FA3F25B671D02;
浏览器只要关闭,cookie就没有了
cookie是可以禁用的:
服务器正常发送cookie给浏览器,浏览器拒收cookie,这就导致在每次请求时都没有携带cookie,服务器都会新建一个session对象
服务器底层的session对象都没有销毁,等待会话超时
会话超时默认值:
- cookie禁用时,session机制还能实现吗?
可以,使用URL重写机制:
注意:url后面加的是分号 ;,jsessionid在服务器的响应报文中可以获取
获取到的还是同一个session对象
提高开发成本,每一个请求路径都要添加sessionid
- request 请求域 请求级别
- session 会话域 用户级别
- application 应用域 项目级别
改造OA项目
登录成功之后,将登录信息存储到session中,如果有用户信息就代表用户登录成功了,session中没有用户信息代表用户没有登陆过,跳转到登录页面
不能指定为false:此时必须获取到session对象,有了session对象才能将数据存储到会话域当中
在访问页面时,不能创建session对象,只能获取,保证在一次会话当中
如果存入应用域,只要有一个用户登录成功,所有用户都能查找到usernmae
不能只根据session是否存在来判断登录状态:最先访问index.jsp界面,JSP会在访问时就创建session对象,假如判断session是否为空,即使身份认证失败也可以访问所有界面
但是可以在JSP中禁用session,访问JSP中不生成session
此时服务器就不会在第一次请求来时创建session对象:
在部门列表界面展示 欢迎 + 用户名:
但是不能加禁用session指令,如果加了只能通过request获取session
安全退出
获取session对象,销毁session
Cookie
Session的实现原理中,Session id 就是 JSESSIONID,也就是Cookie
在上文中,服务器响应cookie给浏览器,浏览器将cookie保存在缓存中(也可以持久化到硬盘文件上)
问题:
- cookie如何生成?
- cookie保存在哪里?
- 浏览器什么时候会发送cookie、发送哪些cookie?
cookie和session是会话保持的共存条件,cookie是将会话状态保持在客户端上,session是将会话状态保持在服务器上
案例一:
JD在未登录的情况下向购物车中放10件商品,然后关闭浏览器,再次打开电脑访问JD的时候,购物车中的商品还在
在服务器端将商品编号以cookie的形式保存在硬盘文件当中了
会话的状态:购物车中的10件商品
登录之后:将购物车中的商品保存到数据库当中
案例二:
163邮箱30天免登录
选择30天免登录后服务器设置cookie的过期时间是30天
cookie中存储的是加密后的账户和密码,设置cookie的过期时间是30天
ctrl + shift + del 清除浏览器缓存
在其他浏览器是否可以取消30天免登录?
- 改密码,服务器session中存储的用户密码和实际密码不同,访问失败
- 改密码,原先浏览器中存储的cookie中的加密后的密码直接失效
Cookie和Session机制都是HTTP协议的一种扩展
HTTP协议中规定,cookie是key-value结构
Java中对Cookie提供了一个Cookie类:jakarta.servlet.http.Cookie
只有一个有参构造方法
HttpServletResopnse中有方法:
这个方法将cookie发送给浏览器客户端。
Http协议规定:当浏览器发送请求时,会自动携带该Path下的cookie数据提交给服务器
在第一次请求时,请求报文中:
携带了JSESSIONID,这个session对象是在访问JSP时创建的
- 设置cookie的有效期:
BJ是东八区,这个时间要加上8个小时
设置cookie的有效时间 > 0,cookie一定会存储在硬盘文件当中
设置cookie的有效时间 = 0:
可以让浏览器删除cookie
设置cookie的有效时间 < 0,该cookie不会被存储(不会被存储到硬盘文件当中,和没调这个方法一样)
- cookie的路径
默认情况下,测试在访问哪些路径时会携带cookie信息:
第一次访问localhost:8080/servlet13/cookie/generate,生成cookie:
此时如果开启新窗口直接访问:http://localhost:8080/servlet13/cookie/generate,会在请求时携带这个cookie数据:
如果开启新窗口访问:localhost:8080/servlet13/cookie/,会携带cookie信息
如果访问:localhost:8080/servlet13/cookie2,这是一个不存在的路径,但是访问这个路径时没有携带cookie
访问:http://localhost:8080/servlet13/cookie/abc,不存在该路径,携带了cookie
假设第一次生成cookie时请求路径为:http://localhost:8080/servlet13/cookie/generate
则cookie关联的默认Path是:http://localhost:8080/servlet13/cookie/以及它的全部子路径
只要浏览器的请求路径是这个路径或这个路径下的子路径,这次请求都会携带这个cookie信息
设置cookie后:
在响应头中:
/servlet13及所有子路径都会携带这个cookie信息
- 在服务器接收客户端发来的cookie
<a href="<%=request.getContextPath()%>/cookie/generate">服务器生成cookie,将cookie响应给浏览器,浏览器保存cookie</a>
<br>
<a href="<%=request.getContextPath()%>/sendCookie">浏览器发送cookie给服务器</a>
上面的超链接生成cookie,通过response方法将cookie返回给浏览器,下面的超链接点击时会携带cookie的数据给服务器,在服务器端解析:
注意:没有cookie发送时getCookies()返回值是null
Cookie实现10天免登录
在登录页面 index.jsp中需要判断:
-
以cookie形式登录
- 跳转到login方法,从cookie中获取用户名、密码
-
输入用户名、密码登录
- 输入用户名、密码,请求到login方法
- 判断是否需要设置cookie
如果需要设置cookie:
注意:只有响应给浏览器才能被覆盖
在login方法中根据用户名、密码进行身份验证
注意:不能response.flushBuffer()后再sendRedirect(),不能在提交响应内容之后进行重定向操作。
需要注意的是,在设置cookie的时候,一定要设置path:
只要访问这个应用,浏览器就要携带这两个cookie
因为默认的path是 /oa/user/login,如果想达成免登录的效果必须设置为 /oa,也就是系统的根路径
疑问:
如果修改登录密码,直接使用reLogin方法重新登录并清除cookie吗?
JSP
指令
JSP指令:指导JSP翻译引擎如何工作
<%@taglib prefix=""%> #引入标签库的指令 JSTL标签库
<%@include file=""%> #在JSP中完成静态包含
<%@page%> #
- Page指令
<%page 属性名=属性值 属性名=属性值 属性名=属性值%>
常用属性:
<%@page contentType="text/html;charset=UTF-8" %> #设置响应内容类型
<%@page pageEncoding="UTF-8" %> #设置响应时字符集 在上一属性中已被包含
<%@page import="java.util.HashSet"%> #导包
<%@page errorPage="error.jsp" %> #当前页面出现异常之后,跳转到error.jsp页面
<%@page isErrorPage="true" %> # 在errorPage中启用exception
<%
String name = null;
name.toString();//空指针异常
%>
如果没有设置errorPage,此时访问就是500 服务器内部错误,设置之后就显示errorPage
但是,此时JSP中出错后台没有任何异常信息,后台不知道错误出在哪里。
在errorPage中可以启用JSP九大内置对象之一的Exception对象,这个对象就是刚刚发生的异常对象
<%@page isErrorPage="true" %>
<h1>ERROR</h1>
<%
exception.printStackTrace(); //打印异常信息到控制台
%>
九大内置对象
/*4个域对象*/
final jakarta.servlet.jsp.PageContext pageContext; //页面作用域 最小
final jakarta.servlet.http.HttpServletRequest request; //请求作用域
jakarta.servlet.http.HttpSession session; //会话作用域
final jakarta.servlet.ServletContext application; //应用作用域 最大
/**/
java.lang.Throwable exception;
/**/
final jakarta.servlet.ServletConfig config;
/**/
final java.lang.Object page = this; //HttpJspBase 父类 Servlet
/**/
jakarta.servlet.jsp.JspWriter out; /*输出*/
final jakarta.servlet.http.HttpServletResponse response; /*响应*/
EL表达式
Expression Language 表达式语言
JSP中混合Java代码,维护麻烦;EL表达式可以代替JSP中的Java代码,让JSP看起来更整洁;EL表达式是JSP的一部分
EL表达式出现在JSP主要是:从某个作用域中获取数据,转换为字符串,输出到浏览器
- 从某个域中取数据(pageContext、request、session、application)
- 将数据转换为字符串 调用toString方法
- 将字符串输出到浏览器
${}就可以完成这三个工作。
获取对象的属性:
注意:EL表达式中不能添加双引号,如果带双引号会认为是普通字符串直接输出到浏览器
其中的username并不是属性名,而是:
userObj.getUsername()
/*去掉get U变为小写 去掉最后的()*/
${userObj.username}
可以验证一下:
- 测试一:
将getUsername()方法注释掉:
在JSP文件中:
访问JSP文件:
- 测试二:
对getUsername()重命名:
在JSP中:
访问:
- 测试三:
设置一个getEmail()方法,但是不提供email属性,返回固定值:
JSP:
访问:
EL表达式中的 . 并不是对象调用属性,而是调用get方法
- 如果访问的属性是引用数据类型中的成员变量:
JSP:
只与get方法的方法名有关
域的优先级
<%@page contentType="text/html;charset=UTF-8" %>
<%
pageContext.setAttribute("data","pageContext");
request.setAttribute("data","request");
session.setAttribute("data","session");
application.setAttribute("data","application");
%>
${data}
=作用域越小,优先级越高=
在没有指定范围的前提下,默认从小的范围中取数据
EL表达式有四个隐含的范围对象:
- pageScope
- requestScope
- sessionScope
- applicationScope
在<%%>中,使用的是JSP的九大内置对象
在${}中,使用的是四个隐含范围对象
指定范围:
在实际开发中,不同域中的名称一般都是不一样的。
- 如果获取时属性名输错了:
使用两种方式获取的结果是不一样的:
严格意义上说EL表达式比域获取更强大
${username}等同于request.getAttribute("username") != null ? request.getAttribute("username") : ""
表面上是EL表达式,最终还是要变为Java代码的
特殊字符
可以使用中括号加字符串访问对应的属性,这样也是调用get方法获取
如果不加双引号,EL表达式取不到数据
- 特殊情况
在访问时获取不到数据
可以获取到数据
如果属性名中含有特殊字符,就必须使用中括号语法
Map集合使用EL表达式
示例
数组使用EL表达式
EL表达式不会下标越界:
只会显示空白,不会有任何错误
- 引用类型数组
- 集合
- ArrayList
-
- Set
set集合没有下标,不能使用[下标]获取 500错误
忽略EL表达式
这样做就可以忽略EL表达式,将其看作普通的字符串
但是这样会将整个页面中的EL表达式全忽略了,如果只忽略其中的某个:
pageContext
pageContext是页面上下文对象,这个对象可以get其他对象:
比如获取request对象:
在页面上显示:
看起来是完全相同的,没必要调用getRequest获取(但实际上通过pageContext获取的request对象是ServletRequest)
但是在EL表达式当中,没有request这个对象(requestScope只代表请求范围,不等同于request对象)
EL表达式有隐含的对象:pageContext,这个对象与JSP九大内置对象中的pageContext是同一个对象,这些方法是提供给EL表达式中使用的:
获取时也是通过get方法获取
但是,这两个表达式并不是完全等价的:
对于<%pageContext.getRequest()%>来说,获取到的是ServletRequest对象:
而在EL表达式中${pageContext.request}获取的是HttpServletRequest对象,EL表达式进行了强制类型转换
在获取应用的根路径时,就可以:
EL表达式隐含对象
- pageContext
- param
- paramValues
- initParam
- 其他(不是重点)
- param
获取请求参数:
可以直接使用隐含对象param获取:
- paramValues
假设由复选框提交的数据:localhost:8080/jsp/6.jsp?hobby=smoke&hobby=drink
如果直接获取:
request.getParameter()获取到的应该是第一个提交的数据:
这时如果使用<%%>获取就需要使用request.getParameterValues("hobby"),使用EL表达式就需要使用${paramValues.hobby}
- initParam
假设web.xml文件中配置了如下的初始化信息:
获取时:
EL表达式中的运算符
-
算术运算符
+ - * / -
关系运算符
== != >= < <= eq gt lt${"abc" == "abc"}<br> <%--true--%> <% String s1 = new String("hehe"); String s2 = new String("hehe"); request.setAttribute("a",s1); request.setAttribute("b",s2); %> ${a == b} <%--true--%> ${a eq b} <%--true--%> <% Student stu1 = new Student("zhangsan"); Student stu2 = new Student("zhangsan"); request.setAttribute("s1",stu1); request.setAttribute("s2",stu2); %> <%-- == eq != 都会调用equals方法--%> ${s1 == s2} <%--equals is executed true--%> ${s1 eq s2} <%--equals is executed true--%> ${s1 != s2} <%--equals is executed false--%> ${!(s1 eq s2)} <%--equals is executed false--%> ${not(s1 eq s2)} <%--equals is executed false--%> <%--判断是否为空,如果为空结果是true--%> ${empty param.username} ${not empty param.username} ${empty param.pwd == null}<%--true或false永远不是null--%> -
逻辑运算符
! && || not and or -
条件运算符
? : -
取值运算符
[] . -
empty运算符
判断是否为空,为空返回true
JSTL标签库
Java Standard Tag Lib
JSTL标签库通常结合EL表达式一起使用,目的是让JSP中的Java代码消失
-
引入JSTL标签库jar包
Tomcat10后的jar包:
jakarta.servlet.jsp.jstl-2.0.0.jar
jakarta.servlet.jsp.jstl-api-2.0.0.jar
-
在JSP中引入要使用的标签库:使用taglib指令
JSTL提供了很多标签,需要使用<%@taglib prefix=""%>指定要使用哪个标签
核心标签库
核心标签库前缀一般都是c,这时c就代表了这个标签库
其中的:
如果导错包会导致不可预知的500错误
指向了一个tld文件:
<tag>
<description>
Catches any Throwable that occurs in its body and optionally
exposes it.
</description>
<name>catch</name> 名字
<tag-class>org.apache.taglibs.standard.tag.common.core.CatchTag</tag-class> Java类
<body-content>JSP</body-content> 标签体中可以出现的内容 JSP标识标签体中可以出现符合JSP语法的所有内容:EL表达式
<attribute>
<description>
Name of the exported scoped variable for the
exception thrown from a nested action. The type of the
scoped variable is the type of the exception thrown. 描述
</description>
<name>var</name> 属性名
<required>false</required> 是否必须
<rtexprvalue>false</rtexprvalue> 该属性值是否支持EL表达式
</attribute>
</tag>
<c:catch var=""> var中的内容不是必须的,属性值不能写EL表达式
JSP...
</c:catch>
tld文件就是一个xml文件,描述了 标签 和 Java类 之间的关系和标签中属性值的规范
比如foreach标签:
描述了items指定要遍历的集合,并且该属性值可以使用EL表达式
var属性:
- 在需要的位置使用标签
示例:request域有如下数据:
要求遍历并输出
使用Java代码:
使用JSTL标签库:
常用的标签:
- if标签
test属性:必需、支持EL表达式、boolean类型
显示:
其他属性:
作用:将var指定的v存储到scope域,v:if条件比较的结果值
- foreach
var用来指定循环中的变量,begin开始,end结束,step步长
底层实际上将i存储在pageContext中,所以才能使用EL表达式将其取出
varStatus状态对象的count属性,以1开始逐一递增
- when - otherwise
显示效果:
总结
jstl中的核心标签库core当中有哪些常用的标签呢?
-
c:if
- <c:if test="boolean类型,支持EL表达式"></c: if>
-
c:forEach
- <c:forEach items="集合,支持EL表达式" var="集合中的元素" varStatus="元素状态对象"> ${元素状态对象.count} </c: forEach>
- <c:forEach var="i" begin="1" end="10" step="2"> ${i} </c: forEach>
-
c:choose c:when c:otherwise
<c:choose>
<c:when test="${param.age < 18}">
青少年
</c:when>
<c:when test="${param.age < 35}">
青年
</c:when>
<c:when test="${param.age < 55}">
中年
</c:when>
<c:otherwise>
老年
</c:otherwise>
</c:choose>
JSTL改造OA
改造list中的循环展示数据:
前端HTML代码中,有一个base标签,这个标签可以设置整个网页的基础路径,通常出现在head标签中
因为是以 /结尾的,后续路径前可以不加 /
base路径只对页面中没有以/开始的路径有效
但是在某些情况下可能对JS代码失效,最好在JS中的请求路径都以项目名开始
通过EL表达式可以设置为完全动态的路径:
<base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
注意不能加空格,最后的/一定要添加
设置Session失效的问题
在之前的项目中,点击 是可以清除cookie的,因为在此时调用的是setCookie方法,在方法内部:
重置了cookie的路径为/oa,这样是可以清空cookie的
但是在点击安全退出时,执行的方法是:
这个方法的思路是:通过request对象获取cookie,将cookie的生命时间设置为0,对应的响应报文:
在点击安全退出时,发送的请求路径为:
这样做是删不掉cookie的
原因在于:
- cookie:属性名username,关联path
/oa(在选择免登录时设置的cookie) - cookie:属性名username,关联path
/oa/user/logout
就会认为这两个是不同的cookie,通过下方的cookie删除上面的那个是删不掉的,通过上面的cookie删掉下方的是可行的,因为/oa是包含/oa/user/logout的
原因也可能在于:在/oa/user/logout中设置的cookie,path被设置为/oa/user/logout,自动被/oa的cookie覆盖了
解决方法:设置cookie的路径等于前文中设置的cookie路径
此时的响应报文:
当前的项目还是存在缺陷的,在DeptServlet中:
首先要获取session,判断session是否存在,是否有name属性
如果在每个业务模块中都进行这些判断,比如订单模块,也需要对登录状态进行判断
这样每次都要重复写这段代码,没有达到代码复用的效果
可以在操作前进行过滤:过滤掉没有登录的用户。
Filter 过滤器
DeptServlet、OrderServlet、EmpServlet 每一个Servlet都是处理自己相关的业务,在这些Servlet执行之前都要判断用户是否登陆了,如果用户登录就可以继续操作,如果没有登录,这段判断用户是否登录的代码是固定的,并且在每一个Servlet类当中都要重新编写,显然代码没有得到重复利用(可能都要解决中文乱码问题)
可以使用Servlet规范中的Filter解决这个问题
Servlet程序就是最终要执行的目标,可以通过过滤器Filter来添加过滤代码,这个过滤代码可以添加到Servlet执行之前,也可以添加到Servlet执行之后,Filter可以在目标程序执行之前过滤,也可以在目标程序执行之后过滤
从上图可知:过滤器也是与用户的请求路径有关的。
- 如果有多个过滤器:
步骤:
-
自定义Java类实现接口
jakarta.servlet.Filter,实现所有方法init和destory方法是default修饰的
对Filter中的方法测试:
需要在web.xml文件中配置Filter:
在服务器启动时:
执行无参构造和init方法,这说明Filter和Servlet还是有区别的
- Servlet对象默认情况下,在服务器启动时不会创建对象
- Filter对象默认情况下,在服务器启动时创建对象
- Servlet和Filter都是单实例的
服务器关闭时destory方法执行
此时 /abc路径不能与 /a.do /b.do路径匹配上,不会执行后续的servlet
如果将Filter的路径改为a.do:
这时与AServlet的请求路径是相同的,再访问/servlet14/a.do:
AServlet中的doGet方法是没有执行的,需要在doFilter方法中对其设置:
访问/a.do:
可以观察到:
- 对AServlet、BServlet都进行过滤:
也可以进行前缀匹配:
注意:*.do前面是不能带 /的,这种写法叫扩展匹配
也可以后缀匹配所有路径:
- Filter的优先级天生比Servlet优先级高,如果存在/a的Filter和Servlet,一定先执行Filter
多个Filter的优先级
web.xml文件中有如下配置:
- 访问
a.do时,两个过滤器都会进行过滤,哪个过滤器会先进行过滤?
可以看到,MyFilter中的doFilter方法先执行,Filter2中的doFilter方法后执行
如果将web.xml中的位置进行更换:(只更换filter-mapping的位置就可)
执行顺序就改变了:
过滤器是有优先级的,filter-mapping的顺序决定了Filter执行的优先级
或者使用@WebFilter({"*.do"}):
这时的执行顺序:
MyFilter1先执行
如果将MyFilter1改名为MyFilterB,MyFilter2改名为MyFilterA:
执行的顺序就改变了,说明如果使用注解,过滤器执行的顺序是按照字典序(一般都是配置在xml文件中)
FilterA和FilterB,先执行FilterA
Filter 的生命周期:
- 默认在服务器启动时创建,执行init
- 每次请求执行doFilter -> chain.doFilter(request,response)
- 停止时销毁
Filter的优先级比Servlet高
责任链设计模式
public class Test {
public static void main(String[] args) {
System.out.println("main begin");
m1();
System.out.println("main over");
}
private static void m1() {
System.out.println("m1 begin");
m2();
System.out.println("m1 over");
}
private static void m2() {
System.out.println("m2 begin");
m3();
System.out.println("m2 over");
}
private static void m3() {
System.out.println("target is executed");
}
}
这样做的话,在编译阶段就确定了调用关系;如果想改变调用关系,必须修改Java源代码,也就要重新编译、测试、发布,违背OCP
过滤器最大的优点:编译阶段没有确定调用顺序,Filter的调用顺序是配置到web.xml文件当中的,只要修改web.xml中filter-mapping的顺序就可以调整Filter的执行顺序,显然Filter的执行顺序是在程序运行阶段动态组合的,这就是责任链设计模式
- 核心思想:在程序运行阶段,动态的组合程序的调用顺序
使用过滤器改造oa
目前写成 /*,表示所有的请求均拦截,即使是正常的登录请求也会被拦截(无限重定向)
不能拦截的情况
- 已经登录,不能拦截
- 要去登录 (/user/login),不能拦截
- 访问WelcomeServlet(访问index.jsp),不能拦截
Listener 监听器
在Servlet中,所有监听器接口都是以 “Listener”结尾,监听器实际上是Servlet规范留下的 “ 特殊时刻 ”,在某个特殊时刻想执行一段代码,就要用到对应的监听器
Servlet规范中提供的监听器:
-
jakarta.servlet包下:
- ServletContextListener
- ServletContextAttributeListener
- ServletRequestListener
- ServletRequestAttributeListener
-
jakarta.servlet.http包下:
-
HttpSessionListener
-
HttpSessionAttributeListener 监听session域中数据的变化
-
HttpSessionBindingListener 不需要加WebListener注解
User类实现了HttpSessionBindingListener 监听器,在存入session时会触发HttpSessionBindingListener 的方法和HttpSessionAttributeListener 中的方法
-
HttpSessionIdListener 监听session id 的改变
-
HttpSessionActivationListener 监听session对象的钝化或活化
-
域对象监听器
以ServletContextListener为例(监听ServletContext域对象的状态),实现监听器的步骤:
-
编写类实现ServletContextListener接口,并且实现里面的方法。
这两个方法都有ServletContextEvent对象
-
在web.xml文件中对ServletContextEvent进行配置
也可以不使用配置文件,使用注解:
-
所有监听器中的方法都不需要手动调用,由服务器自动调用
- 某个特殊事件发生的时候,被Web服务器自动调用
示例:
在服务器启动时执行contextInitialized()方法,关闭时执行contextDestroyed()方法
如果希望在服务器启动时执行某段程序,可以将程序写在这个方法中
HttpSessionListener、ServletRequestListener都是和这个类很相似的:
对于HttpSessionListener,其中的destroyed方法会在session对象被销毁(或者是手动调用invalidate()方法)
对Session进行测试:
- 如果请求访问的是一个普通的servlet:
此时发送请求:
说明session对象未创建。
如果在doGet方法中手动获取session对象:
此时会创建session对象,但是sessionid此时还未进行分配。
- 如果访问的是一个启用session的jsp
http://localhost:8080/servlet15/index.jsp
会创建session对象
域数据监听器
HttpSessionAttributeListener、ServletRequestAttributeListener、ServletContextAttributeListener都是监听域对象存取数据的监听器,以HttpSessionAttributeListener为例:
测试程序:
执行过程:
HttpSessionBindingListener
当前有两个类:
测试程序:
控制台在Cat对象被存入后输出 Cat data is bound
说明:
HttpSessionBindingListener:监听实现该接口的对象,该对象向session域中存取才会触发HttpSessionAttributeListener:监听session对象,对session域进行操作时触发
记录在线用户人数
如果用session对象的数量统计在线人数的话,只要访问index.jsp就会创建一个session对象;同一个用户也可能有很多个不同的session对象;不够精确。
对于当前的OA项目来说,用户首先访问index.jsp页面,此时会创建session对象,只有在登录成功后才会将username存储到session域当中;如果关闭浏览器再次访问,不管是使用cookie登录还是账号密码登录,正常情况下此次访问不能记录为一次新的访问;但是这样做一定会创建一个新的session对象,所以完整的做法是:维护一个集合,存储当前登录的用户的用户名(假定唯一)
想精确(排除相同用户)就使用HashSet
,大致估计可以使用int count
用户登录成功的标志:User类型对象(用户名)存储在session当中
集合的选择:HashSet 去重、效率高,可以避免在每次添加前判断集合中是否存在这个元素,如果存在默认就不会添加该元素
-
监听session域,每当有新的add请求发送时,判断集合中是否存在这个用户名,如果没有就将这个用户名存储到集合中;
采用这种方式,如果在客户端登陆后清除cookie再次登录,会创建一个新的session对象,users集合中已经存储了用户名,会试图将用户名再次加入users集合当中。
-
在登录成功后,判断集合中是否存在这个用户名,如果没有就将这个用户名存储到集合中;
在点击 退出系统 / session失效时,将集合中对应的用户名删除,在线人数就是集合的元素个数
集合在实际开发中通常存储在应用域当中,但对集合进行操作要注意线程安全问题
HttpSessionBindingListener实现在线人数统计
HttpSessionBindingListener在实际开发中更适合做这件事情,因为用户一定会对应一个bean实体类
application并发操作会存在线程安全问题
这样做可以将用户登录/退出都设置在同一个类当中,内聚性更高,但是这种方法无法避免同一个用户多次登录的情况
即使不清除cookie,每次登录都会进行这个操作
但是可能存在问题:
-
安全退出是可以监听到的,session过期是否可以被监听到?
-
这个代码是存在线程安全问题的
MVC架构模式
假设当前要处理转账功能:
/**
* 不使用MVC架构模式的前提下,完成银行账户转账
*/
@WebServlet("/transfer")
public class AccountTransferServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
Connection connection = null;
PreparedStatement prep = null;
ResultSet set = null;
try {
connection = DBUtil.getConnection();
String selectBalanceFromAct = "select balance from t_act where actno = ?";
prep = connection.prepareStatement(selectBalanceFromAct);
prep.setString(1,fromActno);
set = prep.executeQuery();
if (set.next()){
double balance = set.getDouble("balance");
System.out.println(balance);
if (balance < money){
/*余额不足*/
throw new MoneyNotEnoughException("余额不足");
}
/*余额充足*/
connection.setAutoCommit(false); /*关闭自动提交*/
String updateFromAct = "update t_act set balance = balance - ? where actno = ?";
prep = connection.prepareStatement(updateFromAct);
prep.setDouble(1,money);
prep.setString(2,fromActno);
int count = prep.executeUpdate();
String updateToAct = "update t_act set balance = balance + ? where actno = ?";
prep = connection.prepareStatement(updateToAct);
prep.setDouble(1,money);
prep.setString(2,toActno);
count += prep.executeUpdate();
if (count != 2){
throw new AppException("App异常");
}
connection.commit();
out.print("转账成功");
}
} catch (Exception e) {
out.print(e.getMessage());
try {
if (connection != null)
connection.rollback();
} catch (SQLException exc) {
exc.printStackTrace();
}
} finally {
DBUtil.close(set,prep,connection);
}
}
}
AccountTransferServlet 负责了:
- 数据的接收
- 核心业务逻辑
- 数据库的CRUD
- 数据的展示
问题:
- 代码的复用性太差,如果单独查询余额还需要再写一个Servlet,此时的查询余额的方法耦合在整个doPost内部
- 原因:不符合单一职责原则,没有进行职能分工,代码和代码之间的耦合度太高
- 代码扩展性差
- 不符合单一职责原则
- 数据库和业务逻辑混合,应该分别处理
理论基础
分层的原因:希望达到单一职责,代码的耦合度降低,扩展力提高,组件的可复用性增强
- M Model : 数据/业务,处理业务/数据
- V View : 视图/展示,展示数据
- C Controller : 控制器,核心
MVC是通过控制器C调度M、V完成业务的处理

抽取层
- AccountDao类:处理数据的CRUD
- Dao Data Access Object(数据访问对象)
- DAO实际上是一种设计模式,属于JavaEE的设计模式之一(不是23种设计模式)
- DAO只负责表的CRUD,没有任何的业务逻辑在里面
- 一般情况下,一张表对应一个DAO对象
public class AccountDao {
int insert();
int deleteByActno();
int update();
Account selectByActno();
List<Account> selectAll();
}
在数据库中查到的是一条记录,对应到Java中应该是一个对象:将零散的数据封装为一个对象
public class Account {
private long id;
private String actno;
private double balance;
}
但是在数据库中查询到的balance可能是null,如果null赋值给double类型会报错,可以使用包装类进行改进:
public class Account {
private Long id;
private String actno;
private Double balance;
}
这个bean类也被称为:
-
pojo 对象 ,Plain Ordinary Java Object 简单的Java对象
-
domain对象,领域模型对象
最终得到的DAO类:
public class AccountDao {
/*插入账户信息*/
public int insert(Account account){
int count = 0;
return count;
}
/*根据主键删除*/
public int deleteById(Long id){
int count = 0;
return count;
}
/*根据账号删除*/
public int deleteByActno(String actno){
int count = 0;
return count;
}
/*更新记录*/
public int update(Account account){
int count = 0;
return count;
}
/*根据账号查询*/
public Account selectByActno(String actno){
return new Account();
}
/*查询所有账户信息*/
public List<Account> selectAll(){
return null;
}
}
DAO中的代码和业务没有任何关系,只是对数据进行增删改查的。
- AccountService:解决核心业务逻辑
AccountService:专门处理Account业务的类,在该类只专注业务,也可以叫:AccountBiz
该类中的方法名一定要体现处理哪些业务
- AccountServlet:Controller,负责调度
Controller中有Service,Service中有Dao
这就是三层架构:

其中的三层架构:

对于MVC模式:
其中的Model包含了:
- pojo、bean、domain
- Service
- Dao
Model调用Service、Dao的时候需要使用pojo
SSM:
- Spring:整个项目所有对象的创建以及维护对象和对象之间的关系
- SpringMVC:体现了MVC架构模式
- MyBatis:持久层框架
接下来需要解决事务的问题。
解决MVC的事务问题
如果此时发生异常,转账失败但是fromAct账户会减少对应的金额
service中一个方法就是一个完整的业务流程,控制事务应该在方法执行时开启事务,所有操作成功之后提交事务
事务是在service层进行控制的,一般情况下一个业务方法对应一个事务
这样还不能达成想要的效果,因为Dao层中的连接对象并不是service层中关闭了自动提交的connection对象
设置conn为Dao层方法的参数:
但是这样做Dao层方法都带有conn参数,显然是不合适的
第二种思路:
但是这样做在service层就需要添加JDBC的代码,这是不好的做法,另一种思路是在Dao中不关闭数据库连接,封装commit和rollback方法供service层调用,只适用于目前的情况
这种方法就需要额外增加一个Connection局部变量
在service层中:
ThreadLocal
在第一种思路的基础上进行改进,去掉Dao层方法中的conn参数
- service方法和Dao层方法的调用都是在同一个线程当中,使conn对象绑定到线程上

可以设置一个Map集合,将线程和Connection绑定在一起
封装在DBUtil当中:
在Dao,Service中调用:
MyThread类在java.lang.ThreadLocal中已经实现了,改造完成的DBUtil工具类:
在close方法中需要进行额外的处理:
关闭连接之后,如果不从线程池中移除,下次获取到的还是关闭后的连接,无法进行数据库操作
Tomcat服务器内置了一个线程池,核心线程是不会销毁的,一定要关闭连接
根本原因是Tomcat服务器支持线程池的;
在这两个查询方法中不能关闭connection对象,否则在后面的update方法中不能获取到同一个数据库连接对象
不同功能的类放在不同的包下
三层架构:
MVC:
面向接口编程
层与层之间是面向接口编程的,最终的结构:
在web层调用service层时,使用多态:
service层调用dao层,也使用多态:
不足
-
事务:在service层控制了事务,service方法中的事务控制代码太突兀了
动态代理机制
-
虽然面向接口编程了,但是
使用类名,耦合度太高了。



浙公网安备 33010602011771号