JavaWeb(二): Servlet 和 JSP
前言
Servlet 是 JavaEE 规范之一,本文介绍了关于 Servlet 的一些相关概念、开发流程、常用类&接口、项目实践;JSP + EL + JSTL;session&cookie 机制以及 Filter 过滤器和 Listener 监听器
参考视频:B站老杜(结合视频效果更加~)
Servlet
相关概念
系统架构
C / S 架构
- Client / Server(客户端 / 服务器端的交互形式)
- 常见的系统:QQ、微信、支付宝…
- 优点
- 速度快(软件中大部分数据都集成到客户端)
- 体验好
- 界面炫酷(专门的语言实现,更灵活)
- 服务器压力小(大部分数据集成到客户端,很少传输到服务器)
- 安全(大量数据在客户端有缓存,而服务器只有一个,避免服务器造成的风险)
- 缺点:升级麻烦,维护成本高
B / S 架构
B / S 架构是特殊的 C / S 架构,Client 是固定不变的浏览器
Browser / Server(浏览器 / 服务器的交互形式)
支持语言:HTML、CSS、JavaScript
常见的系统:京东、淘宝、百度…
优点:升级方便(只升级服务端代码即可),维护成本低(企业大多采用 B / S 架构)
缺点:速度慢、体验不好、界面不炫酷
B/S 结构的系统通信原理
WEB 系统的访问过程
- 打开浏览器
- 找到地址栏
- 输入一个合法的网址
- 回车
- 浏览器上展示响应的结果
关于域名
- 浏览器上输入域名,回车之后,域名解析器会将域名解析出来一个具体的 IP 地址和端口号
IP 地址
- 同一个网络当中,IP 地址是唯一的,用来标识计算机
端口号
- 一个端口代表一个软件
- 同一台计算机上,端口号唯一
WEB 系统的通信步骤
- 第一步:用户输入网址 URL
- 第二步:域名解析器进行域名解析
- 第三步:浏览器在网络中根据 IP 地址搜素主机
- 第四步:根据端口号,在主机上定位相应软件
- 第五步:得到资源名
- 第六步:将文件响应到浏览器上
- 第七步:浏览器接收到来自服务器的代码
- 第八步:浏览器渲染,执行代码,展示效果
WEB 服务器软件
WEB 服务器有哪些
- Tomcat(WEB 服务器)
- Jetty(WEB 服务器)
- JBOSS(应用服务器)
- WebLogic(应用服务器)
- WebSphere(应用服务器)
WEB 服务器和应用服务器的关系
- WEB 服务器只实现了 JavaEE 中的 Servlet + JSP 两个核心的规范
- 应用服务器实现了 JavaEE 的所有规范
B/S结构系统的角色和协议
角色
- 浏览器软件开发团队(谷歌、火狐…)
- WebServer 的开发团队(Tomcat、Jetty、Weblogic…)
- DB Server 的开发团队(Oracle、MySQL…)
- Webapp 的开发团队(JavaWeb 程序员)
规范、协议
- Webapp 的开发团队和 WebServer 的开发团队之间的规范:JavaEE 规范之一 Servlet 规范
- Servlet 规范的作用:WebServer 和 Webapp 解耦合
- Browser 和 WebServer 之间的传输协议:HTTP协议(超文本传输协议)
- Webapp 开发团队和 DB Server 的开发团队之间的规范:JDBC 规范
关于 JavaEE 版本
- 目前最高版本为 JavaEE8
- JavaEE 被 Oracle 捐献给了 Apache,改名为 Jakarta EE
- JavaEE8 时类名:javax.servlet.Servlet
- JavaEE9 时类名:jakarta.servlet.Servlet
配置 Tomcat 服务器
环境变量
- JAVA_HOME=JDK 的根
- CATALINA_HOME=Tomcat 服务器的根
- PATH=%JAVA_HOME%\bin;%CATALINA_HOME%\bin
启动 Tomcat:startup.bat
关闭 Tomcat:shutdown.bat(注意一定要带.bat,否则会关机!!!这里我们可以重命名为 stop,避免手误)
测试 Tomcat 有没有成功启动
- 打开浏览器,输入 URL
- http://ip地址:端口号
- ip 地址为 127.0.0.1 或 localhost
- 端口号为 8080
解决 Tomcat 服务器在 DOS 命令窗口的乱码问题(控制台乱码)
将 CATALINA_HOME/conf/logging.properties 中的内容修改如下:
java.util.logging.ConsoleHandler.encoding = GBK
Hello Servlet
实现一个最基本的 web 应用
- 找到 CATALINA_HOME\webapps 目录
- 在该目录下新建一个子目录,例如 test
- 在 test 目录下新建一个 index.html 文件(编写其中的内容)
- 启动 Tomcat 服务器
- 打开浏览器,地址栏输入 http://localhost:8080/test/index.html
模拟 Servlet 本质
充当 Sun 公司的角色,制定 Servlet 规范
1 | package javax.servlet; |
充当 Tomcat 服务器的开发者
1 | package org.apache; |
充当 Webapp 的开发者
1 | package com.shameyang.servlet; |
对于我们 JavaWeb 程序员来说,我们只需做两件事:
编写一个类实现 Servlet 接口
将编写的类配置到配置文件中,在配置文件中指定请求路径和类名的关系
注意:
- 配置文件名固定
- 配置文件的存放路径固定
- 文件名、文件路径都是 SUN 公司制定的 Servlet 规范中的明细
关于 Servlet
- 遵循 Servlet 的 webapp,可以放在不同的 WEB 服务器中运行
- Servlet 包括:
- 规范了哪些接口
- 规范了哪些类
- 规范了一个 web 应用中应该有哪些配置文件
- 规范了一个 web 应用中配置文件的名字
- 规范了一个 web 应用配置文件存放的路径
- 规范了一个 web 应用配置文件的内容
- 规范了一个合法有效的 web 应用的目录结构
开发第一个 Servlet
开发步骤
第一步:在 webapps 目录下新建一个根目录,例如 crm
第二步:根目录下新建一个目录:WEB-INF
- Servlet 规范中规定,必须全部大写
第三步:WEB-INF 目录下,新建一个目录:classes
- 该名字也是规定的,必须全部小写
- 该目录下存放的一定是 class 文件
第四步:WEB-INF 目录下,新建一个目录:lib
- 该目录非必须,但是需要使用第三方 jar 包时,需要放到该目录下
第五步:WEB-INF 目录下,新建一个文件:web.xml
该目录必须有,这个配置文件中描述了请求路径和 Servlet 类之间的对照关系
这个文件最好直接从其他的 webapp 复制粘贴
1
2
3
4
5
6
7
8
9
10
<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_6_0.xsd"
version="6.0"
metadata-complete="true">
</web-app>
第六步:编写一个 Java 程序,必须实现 Servlet 接口
- Servlet 接口不在 JDK 中(因为 Servlet 是 JavaEE 的)
- Tomcat 服务器实现了 Servlet 规范,所以在 Tomcat 服务器中有该接口,CATALINA_HOME\lib 目录下有一个 servlet-api.jar,解压之后会看到 Servlet.class
第七步:编译我们编写的 Java 程序
重点:如何让程序编译通过?
配置环境变量 CLASSPATH
CLASSPATH=.;servlet-api.jar的路径
第八步:将编译后的 class 文件拷贝到 WEB-INF\classes 目录下
第九步:在 web.xml 中编写配置信息,关联请求路径和 Servlet 类名
- 专用术语:在 web.xml 文件中注册 Servlet 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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_6_0.xsd"
version="6.0"
metadata-complete="true">
<!--servlet描述信息-->
<!--任意一个servlet都对应一个servlet-mapping-->
<servlet>
<servlet-name>aaaa</servlet-name>
<!--必须是带有包名的全限定类名-->
<servlet-class>com.shameyang.servlet.HelloServlet</servlet-class>
</servlet>
<!--servlet映射信息-->
<servlet-mapping>
<!--这个也是随便的,但是要和上面一样-->
<servlet-name>aaaa</servlet-name>
<!--这里需要一个路径,路径必须以 / 开始-->
<url-pattern>/...</url-pattern>
</servlet-mapping>
</web-app>第十步:启动 Tomcat 服务器
第十一步:浏览器上输入 url
- http://127.0.0.1:8080/crm/配置文件中的路径
- 非常重要:浏览器上的请求路径必须和 web.xml 中的 url-pattern 一致
- 注意:浏览器上的请求路径与 web.xml 中的 url-pattern 的唯一区别:浏览器上的请求路径带项目名:/crm
浏览器上要编写的路径太复杂时,可以使用超链接(html 页面必须放到 WEB-INF 目录外)
总结
合法的 webapp 目录结构
1 | webapproot |
浏览器发送请求,到最终服务器调用 Servlet 中的方法,是怎样的过程?
- 用户输入 URL,或者点击超链接
- 然后 Tomcat 服务器收到请求,截取路径
- Tomcat 服务器根据截取的路径找到项目根目录
- Tomcat 服务器在 web.xml 文件中查找 url-pattern 路径对应的 Servlet 实现类
- Tomcat 服务器通过反射机制,创建实现类的对象
- Tomcat 服务器 调用该对象的 service 方法
向浏览器响应一段 HTML 代码
1 | public void service(ServletRequest request, ServletResponse response) { |
Servlet 中连接数据库
因为 Servlet 是 Java 程序,所以 Servlet 中可以编写 JDBC 代码连接数据库
在一个 webapp 中去连接数据库,需要将驱动 jar 包放到 WEB-INF/lib 目录下
集成开发环境开发 Servlet
使用 IDEA 开发 Servlet
第一步:New Project
- Empty Project 起名为:javaweb
第二步:新建模块
File –> new –> Module
这里新建的是一个 JavaSE 的模块(先不新建 Java Enterprise 模块)
这个 Module 会被自动放在 javaweb 的 project 下边
第三步:让 Module 变成 JavaEE 的模块
在 Module 上右键:Add Framework Support
选择 Web Application
选择之后,IDEA 会自动生成一个符合 Servlet 规范的 webapp 目录结构
这个 web 目录就代表 webapp 的根
第四步(非必须):删除生成的 index.jsp 文件
第五步:导入 jar 包,编写 XXXServlet
导入 jar 包:
File –> Project Structure –> Module –> Dependencies –> + 号添加 jsp-api.jar 和 servlet-api.jar
实现 Servlet 接口中的方法
第六步:WEB-INF 目录下新建子目录 lib,将连接数据库的 jar 包放到里边
第七步:在 web.xml 中完成 XXXServlet 类的注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>xxxServlet</servlet-name>
<servlet-class>com.shameyang.javaweb.servlet.XXXServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>xxxServlet</servlet-name>
<url-pattern>/...</url-pattern>
</servlet-mapping>
</web-app>第八步:给一个 html 页面,在页面中编写一个超链接,用户点击这个超链接后,发送请求,Tomcat 执行后台的 XXXServlet
- 该 html 只能放在 WEB-INF 目录外
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>xxx page</title>
</head>
<body>
<!--这里的 pname 是项目名,无法动态获取,先写死-->
<a href="/pname/...">xxx list</a>
</body>
</html>第九步:IDEA 连接 Tomcat 服务器
- 关联的过程中,将 webapp 部署到 Tomcat 服务器中
- 右上角小锤子右边:Add Configuration
- 添加 Tomcat Server –> Local
- Deployment(用来部署 webapp),添加 Artifact,修改 Application context 修改为 项目名
第十步:启动 Tomcat 服务器
第十一步:打开浏览器,地址栏输入 http://127.0.0.1/pname/index.html
Servlet 对象的生命周期
Servlet 对象的生命周期由 Tomcat 服务器负责
- Tomcat 服务器又称为:WEB 容器(WEB Container)
- 又可以理解为 WEB 容器管理 Servlet 对象的死活
- 我们自己创建的 Servlet 对象不受 WEB 容器管理
- WEB 容器创建的 Servlet 对象都会被放到一个集合中(HashMap)
Servlet 对象创建:用户请求时
Tomcat 服务器启动时,Servlet 对象不会实例化
在实现 Servlet 接口类中创建一个无参构造方法,启动时我们会发现该方法不执行
启动时做了什么?
Tomcat 服务器会把 web.xml 文件中的请求路径和类名放到 Map 集合中
怎样在服务器启动时,创建 Servlet 对象
- 在 web.xml 文件 servlet 标签中添加
<load-on-startup>整数</load-on-startup>
数字越小,优先级越高
Servlet 对象是单例的
- 单实例,但是 Servlet 类不符合单例模式,所以称之为假单例(真单例模式,构造方法是私有化的)
- init 方法只在第一次用户发送请求时执行
- destroy 方法只在服务器关闭时调用一次
类和接口
Servlet 接口中的方法
init
初始化方法,只执行一次
初始化的一些代码可以放在该方法中
service
处理用户请求的核心方法
destroy
销毁 Servlet 对象前会调用一次,只执行一次
释放资源的代码可以写在该方法中
GenericServlet 抽象类
由于 Servlet 接口中许多方法不常用,所以我们可以通过适配器模式,只重写 service 方法
jakarta.servlet 包下有一个 GenericServlet 类, 我们直接继承就可以
GenericServlet 是抽象类,其中有一个抽象方法 service
GenericServlet 类实现 Servlet 接口
该类是一个适配器
什么是适配器?
以手机为例,直接充到 220V 电源就废了,所以需要通过充电器,充电器就是一个适配器
之后编写的 Servlet 类都继承 GenericServlet 类,重写 service 方法即可
改造 GenericServlet 类
为了方便子类的 service 方法调用 ServletConfig 对象,将其设置为成员变量
以下代码是模仿源码写的,jakarta.servlet 包下有一个 GenericServlet 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public abstract class GenericServlet implements Servlet {
// ServletConfig成员变量,方便子类的 service 方法中调用
private ServletConfig servletConfig;
/**
* 将该init方法设置为final,防止子类重写,改变servletConfig
* 再调用一个可以继承的init方法,便于子类重写
*/
public final void init(ServletConfig servletConfig) throws ServletException {
this.servletConfig = servletConfig;
this.init();
}
/**
* 提供子类重写的init方法
*/
public void init() {
}
public ServletConfig getServletConfig() {
return servletConfig;
}
/**
* 将核心方法设置为抽象方法,方便子类调用
*/
public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException;
public String getServletInfo() {
return null;
}
public void destroy() {
}
}1
2
3
4
5
6
7
8
9public class XXXServlet extends GenericServlet {
public void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException {
// 在XXXServlet中使用ServletConfig对象
ServletConfig servletConfig = this.getServletConfig();
System.out.println("查看能否调用:" + servletConfig);
}
}
ServletConfig 接口
ServletConfig 接口中的方法都被 GenericServlet 重写了,我们也可以继承 GenericServlet 调用方法
什么是 ServletConfig?
- Servlet 对象的配置信息对象
- ServletConfig 对象中封装了
<servlet></servlet>
中的配置信息(web.xml 中 servlet的配置信息)
一个Servlet 对象对应一个 ServletConfig 对象,默认情况下,在用户发送第一次请求时创建
- ServletConfig 对象由 Tomcat 服务器创建
- Tomcat 服务器调用 Servlet 对象的 init 方法时需要传一个 ServletConfig 对象的参数
- ServletConfig 接口的实现类是 Tomcat 服务器给实现的
常用方法
1 | public String getInitParameter(String name); // 通过初始化参数的name获取value |
ServletContext 接口
ServletContext 是一个接口,Tomcat 实现了该接口
- 对于一个 webapp 来说,所有 Servlet 对象共享一个 ServletContext 对象
ServletContext 对象在服务器启动时创建,关闭时销毁。
ServletContext 对象是应用级对象,也被称为 Servlet 上下文对象。一个 ServletContext 对象通常对应一个 web.xml 文件
- 什么时候向 ServletContext 这个应用域当中绑定数据?
- 第一:所有用户共享的数据
- 第二:共享的数据量很小
- 第三:共享的数据很少的修改操作
- 向应用域中绑定数据,就相当于把数据放到了缓存(Cache)中,用户访问时直接从缓存中取,减少 IO 的操作,大大提升系统的性能
- 见过的缓存技术:
- 字符串常量池
- 整数型常量池 [-128~127]
- 数据库连接池(提前创建好 N 个连接对象,并放到集合中,使用连接对象时,直接从缓存中取,省去了连接对象的创建过程)
- 线程池(提前创建好 N 个线程对象,存储到集合中,用户请求时,直接从线程池中获取线程对象)】
- 后期还会学习更多的缓存技术:redis、mongoDB…
ServletContext 的常用方法
1 | public String getInitParameter(String name); |
1 | <!--以上两个方法是 ServletContext 对象的方法,获取如下的配置信息--> |
1 | public String getContextPath(); // 获取应用的根路径 |
1 | // 通过 ServletContext 对象可以记录日志 |
操作域的方法
1 | //存(向 ServletContext 应用域中存数据) |
补充:HTTP 协议
什么是协议?
- 协议是某些人或某些组织提前制定好的一套规范,大家都按照这个规范来,做到沟通无障碍
什么是 HTTP 协议?
HTTP 协议:W3C 制定的一种超文本传输协议(通信协议)
这种协议游走在 B 和 S 之间,B –> S 或 S –> B 都需要遵循 HTTP 协议。这样 B 和 S 才能解耦合
什么是解耦合?
B 和 S 互相不依赖
HTTP 协议包括:
- 请求协议
- 响应协议
请求协议(B –> S)
请求行
请求方式(7种)
get(常用)、post(常用)、delete、put、head、options、trace
URI
HTTP 协议版本号
请求头
- 请求的主机
- 主机的端口
- 浏览器信息
- 平台信息
- cookie等信息
- …
空白行
- 用来区分“请求头”和“请求体”
请求体
- 向服务器发送的具体数据
响应协议 (S –> B)
- 状态行
- 第一部分:协议版本号(HTTP/1.1)
- 第二部分:状态码
- 200 表示请求响应成功,正常结束
- 404 表示访问资源不存在
- 405 表示前端发送的请求方式和后端请求的处理方式不一致
- 500 表示服务器端的程序出现异常
- 以 4 开始一般是浏览器端的错误
- 以 5 开始一般是服务器端的错误
- 第三部分:状态的描述信息
- ok 正常成功结束
- not found 资源找不到
- 响应头
- 响应的内容类型
- 响应的内容长度
- 响应的时间
- …
- 空白行
- 用来区分“响应头”和“响应体”
- 响应体
- 响应的正文,这些内容是一些字符串,这个字符串被浏览器渲染,解释并执行,最终展示出效果
GET 请求和 POST 请求的区别
地址栏显示:
- get 请求发送数据时,数据会在挂在 URI 后边,发送的数据会显示在浏览器地址栏(get 请求在请求行上发送数据)
- post 请求发送数据时,在请求体当中发送数据,不会显示在浏览器地址栏
数据类型和数据量:
- get 请求只能发送普通的字符串、无法发送大数据量
- post 请求可以发送任何类型的数据、可以发送大数据量
适用范围:
- get 请求比较适合从服务器端获取数据
- post 请求比较适合向服务器端传送数据
安全性:
- get 请求是安全的,因为 get 请求只是为了从服务器端获取数据
- post 请求是危险的,因为 post 请求向服务器端提交数据,如果这些数据通过后门的方式进入到服务器,是很危险的。所以一般在拦截请求时,拦截 post 请求
缓存:
get 请求支持缓存
- 任何一个 get 请求的响应结果都会被浏览器缓存
- 发送 get 请求后,会先从本地缓存查找,找不到再从服务器获取,这种方法提高了用户体验
post 请求不支持缓存
发送的数据格式相同:
- name=value&name=value&name=value…
HttpServlet 类
HttpServlet 类是专门为 HTTP 协议准备的,比 GenericServlet 更适合 HTTP 协议下的开发
HttpServlet 在哪个包?
- jakarta.servlet.http
http 包下的接口和类
- jarkarta.servlet.http.HttpServlet(HTTP 协议专用的 Servlet 类,抽象类)
- jarkarta.servlet.http.HttpServletRequest(HTTP 协议专用的请求对象)
- jarkarta.servlet.http.HttpServletResponse(HTTP 协议专用的响应对象)
避免 405 错误
- 前后端的请求方式要一致
- 前端发送 get 请求时,就重写 doGet 方法
- 前端发送 post 请求时,就重写 doPost 方法
- …
继承 HttpServlet 重写 service
- 可以重写,但是由于覆盖了 service,享受不到 405 错误
HttpServletRequest 接口
HttpServletRequest 对象由 Tomcat 服务器负责创建,封装了 HTTP 的请求协议
面向 HttpServletRequest 接口编程,调用方法就可以获取请求的信息
常用的方法
获取前端浏览器用户提交的数据
1
2
3
4String getParameter(String name); // 获取value数组中的第一个元素。最常用
Map<String, String[]> getParameterMap(); // 获取Map
Enumeration<String> getParameterNames(); // 获取Map中的所有key
String[] getParameterValues(String name); // 根据key获取value前端 form 提交数据后,采用什么数据结构存储这些数据?
name=value&name=value&name=value…
Map 集合来存储
1
2
3
4
5
6
7
8Map<String, String>
这种想法是错误的
如果采用以上的数据结构存储,会发现key重复时value覆盖
Map<String, String[]>
key存储String
value存储String[]
这样就可以避免value被覆盖
request 对象又称为“请求域”对象
请求域对象比应用域对象的范围小很多。生命周期很短
请求域只在一次请求内有效
1
2
3void setAttribute(String name, Object obj); // 向域中绑定数据
Object getAttribute(String name); // 从域中根据name获取数据
void removeAttribute(String name); // 将域中绑定的数据移除
跳转(一个 Servlet 中访问另一个 Servlet 的 request 对象)
转发(一次请求)
1
2
3
4
5
6
7// 第一步:获取请求转发器对象
RequestDispatcher dispatcher = request.getRequestDispatcher("/另一个Servlet的路径");
// 第二步:调用转发器的forward方法完成跳转/转发
dispatcher.forward(request, response);
// 第一步和第二步结合
request.getRequestDispatcher("/另一个Servlet的路径").forward(request, response);
两个 Servlet 怎样共享数据?
将数据放到 ServletContext 应用域中,但是占用资源太多,不建议使用
放到 request 域当中,然后 AServlet 转发给 BServlet
注意:转发的路径不加项目名
request 对象中容易混淆的两种方法
1 | // uri?username=xxx&userpwd=123,获取的是用户在浏览器上提交的数据 |
HttpServletRequest 常用的其他方法
1 | // 获取客户端的IP地址 |
最终的 Servlet 类开发
- 第一步:编写一个 Servlet 类,直接继承 HttpServlet
- 第二步:重写 doGet 或 doPost 方法
- 第三步:将 Servlet 类配置到 web.xml 中
- 第四步:准备前端的页面(form 表单),form 表单中指定请求路径
关于一个 web 站点的欢迎页面
在访问一个 webapp 时,如果没有指定资源路径,默认会访问欢迎页面
怎样设置欢迎页面?
第一步:在 webapp 根目录下创建一个 html 文件
第二步:配置 web.xml 文件
1
2
3
4
5
6
7
8
9
10
11
<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">
<welcome-file-list>
<welcome-file>xxx.html</welcome-file>
</welcome-file-list>
</web-app>第三步:启动服务器,访问 http://localhost:8080/项目名
一个 webapp 可以有多个欢迎页面,越靠上优先级越高
当文件名为 index.html 时,不需要在 web.xml 中配置欢迎页面
Tomcat 已经提前配置好了
配置欢迎页面的两个地方:
webapp 内部的 web.xml 文件中(局部配置)
CATALINA_HOME/cof/web.xml 文件中(全局配置)
注意:局部优先(就近原则)
项目实践
使用纯粹的 Servlet 完成单表【对部门的】增删改查操作(B/S 结构)
实现步骤
第一步:准备一张数据库表(sql 脚本)
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 部门表
set character_set_client = 'utf8';
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;第二步:准备一套 HTML 页面(项目原型)
- 欢迎页面:index.html
- 列表页面:list.html
- 新增页面:add.html
- 修改页面:edit.html
- 详情页面:detail.html
第三步:分析系统功能
什么叫做一个功能?
- 只要这个操作连接了数据库,就表示一个独立的功能
包括哪些功能
- 查看部门列表
- 查看部门详细信息
- 删除部门
- 新增部门
- 跳转到修改页面
- 修改部门
第四步:在 IDEA 当中搭建开发环境
创建一个 webapp
向 webapp 中添加连接数据库的 jar 包(mysql 驱动)
- 在 WEB-INF 目录下新建一个 lib,将 mysql 的 jar 包拷贝进来
JDBC 工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public class DBUtil {
private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
private static String driver = bundle.getString("driver");
private static String url = bundle.getString("url");
private static String user = bundle.getString("user");
private static String password = bundle.getString("password");
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接对象
* @return conn 连接对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
// 获取连接
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
/**
* 释放资源
* @param conn 连接对象
* @param ps 数据库操作对象
* @param rs 结果集对象
*/
public static void close(Connection conn, Statement ps, ResultSet rs) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (rs != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}第五步:实现第一个功能——查看部门列表
一:先修改前端页面的超链接
1
<a href="/oa/dept/list">查看部门列表</a>
二:编写 web.xml 文件
1
2
3
4
5
6
7
8<servlet>
<servlet-name>list</servlet-name>
<servlet-class>com.shameyang.oa.web.action.DeptListServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>list</servlet-name>
<url-pattern>/dept/list</url-pattern>
</servlet-mapping>三:编写 DeptListServlet 类继承 HttpServlet,然后重写 doGet 方法
1
2
3
4
5
6
7public class DeptListServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
}
}四:在 DeptListServlet 类的 doGet 方法中连接数据库,查询所有的部门,动态展示部门列表页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int i = 0;
while (rs.next()) {
String no = rs.getString("no");
String name = rs.getString("name");
String loc = rs.getString("loc");
out.print(" <tr>");
out.print(" <td>" + (++i) + "</td>");
out.print(" <td>" + no + "</td>");
out.print(" <td>" + name + "</td>");
out.print(" <td>" + loc + "</td>");
out.print(" <td>");
out.print(" <a href=\"\">删除</a>");
out.print(" <a href=\"\">修改</a>");
out.print(" <a href=\"\">详情</a>");
out.print(" </td>");
out.print(" </tr>");
}第六步:实现功能——查看部门详情
从前端往后端一步步实现,先找到用户点击的详情在哪里
在后端 java 程序中找到
1
out.print("<a href='写一个路径'>详情</a>");
该超链接的路径应该带项目名,我们也可以实现动态获取
1
out.print("<a href='" + contextPath + "/dept/detail?deptno=" + no + "'>详情</a>");
上边为什么要用 ?deptno=
根据 HTTP 协议规定,向服务器提交数据的格式:URI?name=value&name=value
解决 404 问题,写 web.xml 文件
1
2
3
4
5
6
7
8<servlet>
<servlet-name>detail</servlet-name>
<servlet-class>com.shameyang.oa.web.action.DeptDetailServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>detail</servlet-name>
<url-pattern>/dept/detail</url-pattern>
</servlet-mapping>编写 DeptDetailServlet 类继承 HttpServlet,然后重写 doGet 方法
1
2
3
4
5
6
7public class DeptDetailServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
}
}doGet 方法中连接数据库,根据部门编号查询部门信息,动态展示部门详情页面
1
2
3
4
5
6
7
8
9
10
11
12
13conn = DBUtil.getConnection();
String sql = "select dname, loc from dept where deptno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, deptno);
rs = ps.executeQuery();
if (rs.next()) {
String dname = rs.getString("dname");
String loc = rs.getString("loc");
out.print(" 部门编号:" + deptno + "<br>");
out.print(" 部门名称:" + dname + "<br>");
out.print(" 部门位置:" + loc + "<br>");
}第七步:删除部门功能
从前端页面开始,用户点击删除按钮时,提示用户是否删除,防止误删
1
2
3
4
5
6
7
8<a href="javascript:void(0)" onclick="del(10)">删除</a>
<script type="text/javascript">
function del(dno) {
if (window.confirm("确定删除吗?")) {
document.location.href = "/oa/dept/delete?deptno=" + dno;
}
}
</script>以上的前端代码要写到后端的 java 代码中
- DeptListServlet 类的 doGet 方法中,使用 out.print() 方法,将以上代码输出到浏览器上
编写 web.xml 文件
1
2
3
4
5
6
7
8
9<!--删除部门-->
<servlet>
<servlet-name>delete</servlet-name>
<servlet-class>com.shameyang.oa.web.action.DeptDeleteServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>delete</servlet-name>
<url-pattern>/dept/delete</url-pattern>
</servlet-mapping>编写 DeptDeleteServlet 类继承 HttpServlet,然后重写 doGet 方法
1
2
3
4
5
6
7public class DeptDeleteServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 根据部门编号,删除部门
}
}删除成功或失败的处理(这里我们采用转发,没有使用重定向)
1
2
3
4
5
6
7
8
9// 判断删除成功还是失败
if (count == 1) {
// 删除成功则跳转到部门列表页面
// 执行另一个Servlet,需要转发
req.getRequestDispatcher("/dept/list").forward(req, resp);
} else {
// 删除失败
req.getRequestDispatcher("/error.html").forward(req, resp);
}第八步:新增部门功能
获取 add 页面
1
out.print("<a href='" + contextPath + "/add.html'>新增部门</a>");
add.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html lang="en">
<head>
<meta charset="UTF-8">
<title>新增部门</title>
</head>
<body>
<h1>新增部门</h1>
<hr>
<form action="/oa/dept/save" method="post">
部门编号:<input type="text" name="deptno"/><br>
部门名称:<input type="text" name="dname"/><br>
部门位置:<input type="text" name="loc"/><br>
<input type="submit" value="保存"/>
</form>
</body>
</html>编写 web.xml
1
2
3
4
5
6
7
8
9<!--新增部门-->
<servlet>
<servlet-name>add</servlet-name>
<servlet-class>com.shameyang.oa.web.action.DeptDeleteServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>add</servlet-name>
<url-pattern>/dept/save</url-pattern>
</servlet-mapping>编写 DeptSaveServlet 类继承 HttpServlet,然后重写 doPost 方法
1
2
3
4
5
6
7public class DeptSaveServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 获取部门信息
}
}解决 405 问题
- 由于 DeptSaveServlet 中重写的是 doPost 方法,转发到 /dept/list 后,DeptListServlet 类中我们重写的是 doGet 方法,所以会导致 405 问题
- 我们可以在 DeptListServlet 类中重写 doPost 方法,然后调用 doGet 方法来解决
第九步:修改部门信息功能
- 跳转到修改信息页面
- 点击修改按钮完成修改,返回部门列表页面
优化实践项目:模板方法设计模式
由于我们之前的 oa 项目,每个功能对应一个 Servlet,这种开发模式会导致类爆炸问题(类太多)
可以使用模板方法设计模式进行优化,每个功能对应一个方法
1 |
|
web 应用中资源的跳转(转发&重定向)
一个 web 应用中,可以用过两种方式完成资源的跳转:
- 转发
- 重定向
转发和重定向的区别?
代码上的区别
转发
1
request.getRequestDispatcher("/xxx").forward(request, response);
重定向
1
response.sendRedirect(request.getContextPath() + "/xxx");
形式上的区别
- 转发是一次请求,请求结束后地址栏上的地址不变
- 重定向是两次请求,最终显示在地址栏上的地址为重定向的地址
转发和重定向本质上的区别
转发:是由 WEB 服务器控制的。A 资源跳转到 B 资源,由服务器内部完成
重定向:是浏览器完成的。具体跳转到哪个资源,由浏览器决定
转发
重定向
转发与重定向的选择
- 如果在一个 Servlet 当中向 request 域绑定了数据,希望从另一个 Servlet 中把里边的数据取出来时,使用转发机制
- 剩下所有的请求均使用重定向
Servlet 注解式开发
Servlet 3.0之后,推出了各种 Servlet 基于注解式开发,将配置信息直接写到 Java 类中
并不是说有了注解之后,web.xml 文件就不需要了:
- 一些需要变化的信息,要写到 web.xml 文件中。一般都是 注解+配置文件 的开发模式
- 一些不会经常变化修改的配置建议使用注解
我们的第一个注解:jakarta.servlet.annotation.WebServlet,在类上使用 @WebServlet
@WebServlet 中常用的属性
属性 | 描述 |
---|---|
name | 用来指定 Servlet 的名字。等同于:<servlet-name> |
urlPatterns | 用来指定 Servlet 的映射路径。可以指定多个字符串,等同于:<url-pattern> |
value | 与 urlPatterns 一样,都是用来指定 Servlet 映射路径的。当注解的属性名为 value 时,属性名可以省略 |
loadOnStartup | 用来指定在服务器启动阶段是否加载该 Servlet。等同于:<load-on-startup> |
initParams | 用来指定初始化参数。等同于:<init-param> |
注意:属性是一个数组,如果数组中只有一个元素,大括号可以省略
注解对象的使用格式:@注解名称(属性名=属性值, 属性名=属性值…)
纯 Servlet 开发的缺陷
我们的 java 程序中,前端代码与后端代码写在一起,存在许多缺陷:
- 编写难度大
- 可读性差
- 程序的耦合度非常高
- 维护成本高
- 修改一点前端代码,就会重新编译 java 代码,生成新的 class 文件,打包成新的 war 包,重新发布
这时就需要用到 JSP 进行优化了,可以参考 JSP 部分的笔记
JSP
概述
JSP 实际上就是一个 Servlet
- index.jsp 访问时,会自动翻译生成 index_jsp.java,然后自动编译生成 index_jsp.class
- index_jsp 继承了 HttpJspBase,HttpJspBase 继承了 HttpServlet,所以 JSP 实际上就是一个 Servlet 类
- Jsp 和 Servlet 的生命周期一样,没有区别
- Jsp 也是单例的(假单例)
JSP 是什么
- JavaServer Pages,基于 Java 实现的服务器端的页面
- JSP 就是一个 Java 程序(本质上是一个 Servlet)
- JSP 也是 JavaEE 的十三个子规范之一
- JSP 是一种规范,每个 WEB 容器/ WEB 服务器都会遵循该规范,按照该规范“翻译”
- 每个 WEB 容器/ WEB服务器都会内置 JSP 翻译引擎
JSP 与 Servlet 的区别?
- 职责不同
- Servlet 的职责是:收集数据(处理业务)
- JSP 的职责是:展示数据
JSP 的扩展名一定是 jsp 吗
不一定是 jsp,可以进行配置
在 CATALINA_HOME/conf/web.xml 中可以配置 jsp 文件的扩展名
1 | <servlet-mapping> |
xxx.jsp 文件对于服务器来说,只是一个普通的文本文件,web 容器会将 xxx.jsp 文件最终生成 Java 程序。最终执行时和 jsp 文件无关了
基础语法
解决响应时中文乱码问题:
1 | <%"text/html;charset=UTF-8" %> contentType= |
JSP 代码中编写 Java 代码:
<% Java code %>
- Java 代码会被写到 service 方法内部
- 注意:此处的 Java code 相当于 Java 的“方法体”内,需要符合语法规则
<%! Java code %>
- Java 代码会被写到类体的位置
- 这个语法很少使用,存在线程安全问题
输出语句 <% %>
1
2
3
4<%
String name = "tom";
out.write("name = " + name);
%>以上代码中的 out 是 JSP 九大内置对象之一,可以直接使用,必须只能在 service 方法内使用
如果输出的内容中没有 Java code,直接在 JSP 中编写即可
输出语句 <%= %>
这个符号会被翻译成 out.print();
输出变量时,使用该符号比较方便
1
2<% String name = "tom";%>
<%= name %>
JSP 的注释
1 | <%-- 这是JSP的专业注释,不会被翻译到 Java 源代码中 --%> |
总结
- JSP 中直接编写普通字符串
- 翻译到 service 方法的 out.writer(“这里”);
- <% %>
- 翻译到 service 方法体内部
- <%! %>
- 翻译到 service 方法外
- <%= %>
- 翻译到 service 方法内,翻译为:out.print();
- <%@page contentType=”text/html;charset=UTF-8” %>
- page 指令,通过 contentType 属性设置响应的类型
指令
指令的作用:指导 JSP 的翻译引擎如何工作
指令包括哪些?
- include:包含指令,在 JSP 中完成静态包含,很少用了
- taglib:引入标签库的指令。在 JSTL 部分学习
- page:目前重点学习的指令
指令的使用语法:<%@指令名 属性名=属性值 属性名=属性值…%>
page 指令当中的常用属性
1 | <%"true|false" %> session= |
1 | <%"text/html" %> contentType= |
1 | <%"UTF-8" %> pageEncoding= |
1 | <%import="java.util.*" %> |
1 | <%"/error.jsp" %> errorPage= |
1 | <%"true" %> isErrorPage= |
九大内置对象
pageContext(jakarta.servlet.jsp.PageContext) 页面作用域
request(jakarta.servlet.http.HttpServletRequest) 请求作用域
session(jakarta.servlet.http.HttpSession) 会话作用域
application(jakarta.servlet.ServletContext) 应用作用域
- pageContext < request < session < application
- 以上四个作用域都有:setAttribute、getAttribute、removeAttribute 方法
- 使用原则:尽可能使用小的域
exception(java.lang.Throwable)
config(jakarta.servlet.ServletConfig)
page(java.lang.Object) 其实是 this,当前的 servlet 对象
out(jakarta.servlet.jsp.JspWriter) 负责输出
response(jakarta.servlet.http.HttpServletResponse) 负责响应
Servlet + JSP 改造 oa 项目
Servlet 收集数据(处理业务),JSP 展示数据
将之前准备的 html 文件改为 jsp 文件,完成所有页面的正常流转(修改超链接的请求路径)
例如 list.jsp
1 | <%@ page contentType="text/html;charset=UTF-8" %> |
上边 <%= request.getContextPath() %>
是动态获取应用的根路径
修改超链接的请求路径,映射到对应的 Servlet 类
例如,修改 index.jsp
1 | <%@ page contentType="text/html;charset=UTF-8" %> |
映射到对应的 Servlet 类,执行相应的操作
1 |
|
怎样将数据库中查询到的零散数据带到 JSP 中?
将零散的数据封装成 Java 对象
1
2
3
4
5
6
7Dept dept = new Dept();
dept.setDeptno(deptno);
dept.setDname(dname);
dept.setLoc(loc);
// 将部门对象放到 list 集合中
depts.add(dept);将集合放到请求域中
1
request.setAttribute("deptList", depts);
转发(不要重定向)
1
request.getRequestDispatcher("/list.jsp").forward(request, response);
JSP 中
从 request 域中取出 List 集合
遍历 List 集合,取出每个部门对象,动态生成 tr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<%
// 从 request 域中取出集合
List<Dept> deptList = (List<Dept>) request.getAttribute("deptList");
// 循环遍历
int i = 0;
for (Dept dept : deptList) {
%>
<tr>
<td><%=++i%></td>
<td><%=dept.getDeptno()%></td>
<td><%=dept.getDname()%></td>
<td>
<a href='javascript:void(0)' onclick="del()">删除</a>
<a href='edit.jsp'>修改</a>
<a href='detail.jsp'>详情</a>
</td>
</tr>
<%
}
%>合并修改与详情的超链接,减少代码复用
1
2<a href='<%=request.getContextPath()%>/dept/detail?f=edit&deptno=<%=dept.getDeptno()%>'>修改</a>
<a href='<%=request.getContextPath()%>/dept/detail?f=detail&deptno=<%=dept.getDeptno()%>'>详情</a>修改 doDetail 方法中转发的代码,实现修改与详细页面共用同一段代码
1
2String forward = "/" + request.getParameter("f") + ".jsp";
request.getRequestDispatcher(forward).forward(request, response);
实现用户登录功能
当前 oa 项目存在的问题:任何人都可以进行 CRUD 这些危险的操作。所以需要加一个登录功能
实现登录功能
步骤1:数据库中创建一个用户表:t_user
存储用户的登录信息:用户名和密码
密码一般在数据库中存储的是密文(这里先使用明文方式)
用户表中插入数据
1
2
3
4
5
6
7
8
9
10drop 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 ('tom', '123321');
步骤2:实现一个登录页面
提供一个登陆的表单。有用户名和密码输入的框
用户点击登录按钮,post 方式提交表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎使用OA系统</title>
</head>
<body>
<h1>用户登录</h1>
<hr>
<form action="<%=request.getContextPath()%>/user/login" method="post">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录">
</form>
</body>
</html>
步骤3:后台提供一个 Servlet 类处理登录的请求
登陆成功:跳转到部门列表页面
登录失败:跳转到失败页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class UserServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 连接数据库,验证用户名和密码
Boolean flag = false;
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
String sql = "select * from t_user where username=? and password=?";
ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
rs = ps.executeQuery();
if (rs.next()) {
flag = true;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn, ps, rs);
}
// 登录成功或失败
if (flag) {
response.sendRedirect(request.getContextPath() + "/dept/list");
} else {
response.sendRedirect(request.getContextPath() + "/error.jsp");
}
}
}
步骤4:再提供一个登陆失败的页面
存在的问题:
- 目前的登录功能只是一个摆设,如果知道后端的请求路径,照样可以在不登陆的情况下访问
- 这个登录没有真正起到拦截的作用
需要 session 机制和 cookie 机制解决
EL 表达式
EL 表达式是什么
- Expression Language(表达式语言)
- EL 表达式可以代替 JSP 中的 java 代码,让 JSP 文件中的程序看起来更加整洁、美观
- EL 表达式可以算是 JSP 语法的一部分,EL 表达式归属于 JSP
EL 表达式出现在 JSP 中主要是:
- 从某个作用域中取数据
- 将取出的数据转换成字符串
- 将字符串输出到浏览器上
语法格式
- ${表达式}
例如:
没有使用 EL 表达式
1
2
3
4
5<%
request.setAttribute("username", "admin");
%>
<%= request.getAttribute("username") %>使用 EL 表达式
1
2
3
4
5<%
request.setAttribute("username", "admin");
%>
${username}
EL 表达式的使用:
1 | <% |
${xxx} 与 ${“xxx”} 的区别
${xxx} 表示从某个域中取出数据,数据的 name 是 xxx
取数据之前一定有这样的代码:域.setAttribute(“xxx”, 对象)
${“xxx”} 表示直接将 xxx 作为普通字符串输出到浏览器
EL 表达式优先从小范围中取数据
- pageContext < request < session < application
EL 表达式中有四个隐含的隐式的请求范围:
- pageScope
- requestScope
- sessionScope
- applicationScope
EL 表达式对 null 进行了预处理,如果是 null,则向浏览器输出一个空字符串
EL 表达式从 Map 集合中取数据
- ${map.key}
EL 表达式从数组中取数据
- ${array[index]}
EL 表达式获取应用的根路径
- ${pageContext.request.contextPath}
page 指令中,有一个属性,可以忽略 EL 表达式
1 | <%"true|false" %> isElIgnored= |
以上是忽略整个页面的 EL 表达式
如果想要忽略某个 EL 表达式,可以在表达式前加一个反斜杠 \
EL 表达式的内置对象
pageContext
与 JSP 中的 pageContext 是同一个对象
param
paramValues
initParam
其他(非重点)
EL 表达式中的运算符
1 | 1.算术运算符 |
JSTL 标签库
什么是 JSTL 表达式?
- Java Standard Tag Lib(Java 标准的标签库)
- JSTL 标签库通常结合 EL 表达式一起使用,目的是让 Java 代码消失
- 标签是写在 JSP 当中的,但实际上还是要执行对应的 Java 程序(Java 程序在 jar 包中)
使用 JSTL 标签库步骤:
第一步:引入 JSTL 标签库对应的 jar 包
- tomcat 10之后引入的 jar 包是:
- jakarta.servlet.jsp.jstl-2.0.0.jar
- jakarta.servlet.jsp.jstl-api-2.0.0.jar
- 在 IDEA 中怎么引入?
- 在 WEB-INF 下新建 lib 目录,然后将 jar 包拷贝到 lib 当中,然后 “Add Lib…”
- 与 mysql 的数据库驱动一样,都是放到 WEB-INF/lib 目录下
- 什么时候要把 jar 包放到 WEB-INF/lib 目录下?该 jar 是 tomcat 服务器没有的
- tomcat 10之后引入的 jar 包是:
第二步:在 JSP 中引入要使用的标签库
使用 taglib 指令引入标签库
<%@taglib prefix=”” uri=”” %>
1
2
3
4
5<%"c" uri="http://java.sun.com/jsp/jstl/core" %> prefix=
<%--
这个是核心标签库
prefix="这里随便起名字,核心标签库,大家默认叫 c"
--%>
第三步:在需要使用标签的位置使用即可
JSTL 标签的原理
1 | <%"c" uri="http://java.sun.com/jsp/jstl/core" %> prefix= |
以上 uri 后边的路径实际指向了 xxx.tld 文件
tld 文件是一个 xml 配置文件
在 tld 文件中描述了“标签”和“java 类”之间的关系
c.tld 文件在 jakarta.servlet.jsp.jstl-2.0.0.jar 里边的 META-INF 目录下
源码解析:配置文件 tld
1 | <tag> |
核心标签库 core 中的常用标签
c:if
1
<c:if test="boolean 类型,支持 EL 表达式"></c:if>
c:forEach
1
2
3<c:forEach items="集合,支持 EL 表达式" var="集合中的元素" varStatus="元素状态对象">
${元素状态对象.count}
</c:forEach>1
2
3<c:forEach var="i" begin="1" end="10" step="2">
${i}
</c:forEach>c:choose c:when c:otherwise
1
2
3
4
5
6
7
8
9
10
11
12
13
14<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>
EL + JSTL 改造 oa 项目
引入 jar 包
EL 表达式 + JSTL 改造代码:部分代码举例
部门详情
1
2
3
4
5
6
7
8
9
10
11
12
13<%-- old --%>
<%@ page import="com.shameyang.oa.bean.Dept" %>
<%@ page contentType="text/html;charset=UTF-8" %>
<%
// 从 request 域中取出数据
Dept deptInfo = (Dept)request.getAttribute("deptInfo");
%>
部门编号:<%=deptInfo.getDeptno()%><br>
部门名称:<%=deptInfo.getDname()%><br>
部门位置:<%=deptInfo.getLoc()%><br>1
2
3
4
5
6
7<%-- new --%>
<%@ page contentType="text/html;charset=UTF-8" %>
部门编号:${deptInfo.deptno}<br>
部门名称:${deptInfo.dname}<br>
部门位置:${deptInfo.loc}<br>部门列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<%-- old --%>
<%@ page import="java.util.List" %>
<%@ page import="com.shameyang.oa.bean.Dept" %>
<%"text/html;charset=UTF-8" %> contentType=
<%
// 从 request 域中取出集合
List<Dept> deptList = (List<Dept>) request.getAttribute("deptList");
// 循环遍历
int i = 0;
for (Dept dept : deptList) {
%>
<%=++i%>
<%=dept.getDeptno()%>
<%=dept.getDname()%>
<a href='javascript:void(0)' onclick="del(<%=dept.getDeptno()%>)">删除</a>
<a href='<%=request.getContextPath()%>/dept/detail?f=edit&deptno=<%=dept.getDeptno()%>'>修改</a>
<a href='<%=request.getContextPath()%>/dept/detail?f=detail&deptno=<%=dept.getDeptno()%>'>详情</a>
<%
}
%>1
2
3
4
5
6
7
8
9
10
11
12
13<%-- new --%>
<%"c" uri="http://java.sun.com/jsp/jstl/core" %> prefix=
<%"text/html;charset=UTF-8" %> contentType=
<c:forEach items="${deptList}" varStatus="deptStatus" var="dept">
${deptStatus.count}
${dept.deptno}
${dept.dname}
<a href='javascript:void(0)' onclick="del(${dept.deptno})">删除</a>
<a href='<%=request.getContextPath()%>/dept/detail?f=edit&deptno=${dept.deptno}'>修改</a>
<a href='<%=request.getContextPath()%>/dept/detail?f=detail&deptno=${dept.deptno}'>详情</a>
</c:forEach>
base 标签改造
- HTML 中的
<base>
标签可以设置网页的基础路径,这样我们在超链接中的路径就可以变得简洁了 - base 标签通常出现在 head 标签中
<base href="http://localhost:8080/oa/">
- 在页面中,没有以 / 开始的路径,都会自动在路径前加上 base 中的路径
- 注意:JS 代码中最好不要依赖 base 标签,使用全路径
1 | <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/"> |
Session 会话机制
session 机制属于 B/S 结构的一部分
什么是会话?
- 用户打开浏览器,进行一系列操作,最终关闭浏览器。整个过程叫做:一次会话
- 会话在服务器端也有一个对应的 Java 对象:session
- 一个会话中包含多次请求
在 Java 的 Servlet 规范中,session 对应的类名:HttpSession(jakarta.servlet.http.HttpSession)
session 对象的最主要作用:保存会话状态
为什么需要 session 对象保存会话状态?
- 因为 HTTP 协议是无状态协议,无状态协议可以降低服务器的压力
- 无状态:请求的时候,B 和 S 是连接的,但是请求结束后,连接就断开了
- 只要 B 和 S 断开了,服务器不知道浏览器关闭的动作
为什么不使用 request 对象或 ServletContext 对象保存会话状态?
- request 是请求域,ServletContext 是应用域
- request 对象是一次请求一个对象,域太小
- ServletContext 对象只有一个,域太大
request 请求域(HttpServletRequest)、session 会话域(HttpSession)、application 域(ServletContext)
- request < session < application
session 对象的实现原理
- JSESSIONID=xxx 以 Cookie 的形式保存在浏览器内存中,浏览器关闭,这个 cookie 就没有了
- session 列表是一个 Map,key 是 sessionid,value 是 session 对象
- 用户第一次请求:服务器生成 session 对象,同时生成 id,将 id 发送给浏览器
- 用户第二次请求:自动将浏览器内存中的 id 发送给服务器,服务器根据 id 查找 session 对象
- 关闭浏览器,内存消失,cookie 消失,sessionid 消失,会话等同于结束
session 对象超时销毁
web.xml 文件中可以进行配置(默认 30 min)
1
2
3<session-config>
<session-timeout>超时多少分钟后销毁</session-timeout>
</session-config>
手动销毁 session 对象
1 | if (session != null) { |
Cookie 禁用,session 对象还能找到吗?
- cookie 禁用:服务器正常发送 cookie 给浏览器,但是浏览器拒收了
- 所以,session 对象找不到了。每次请求都会创建新的 session 对象
Cookie 禁用,session 机制怎么实现?
- 使用 URL 重写机制
url;jsessionid=xxx
- URL 重写机制会提高开发者的成本。开发人员在编写时,后边都要加 jsessionid,由于 id 是动态的,给开发带来了很大的难度和成本。所以大部分网站这样设计:你要是禁用 cookie,你就别用了
关闭 session
1 | <% "false" %> session= |
Session 改造 oa 项目
用户登录界面中
1 | // 登录成功或失败 |
部门列表 service 方法中
1 | // 如果用户登录了,就可以执行对应的操作 |
Cookie
session 的实现原理中,每个 session 对象都会关联一个 sessionid,例如:
- JSESSIONID = xxx
- 以上键值对数据其实就是 cookie 对象
关于 cookie
- cookie 最终保存在浏览器客户端上
- 可以保存在运行内存中(浏览器关闭 cookie 就消失了)
- 也可以保存在硬盘文件中(永久保存)
- cookie 的作用
- cookie 和 session 机制都是为了保存会话的状态
- cookie 将会话状态保存在浏览器客户端
- session 将会话状态保存在服务器端
session 和 cookie 机制都是 HTTP 协议的一部分
java 的 servlet 中,对 cookie 提供了哪些支持?
提供一个 Cookie 类专门表示 cookie 数据(jakarta.servlet.http.Cookie)
java 程序把 cookie 数据发送给服务器
1
response.addCookie(cookie);
HTTP 协议中这样规定
- 任何一个 cookie 都是由 name 和 value 组成的
- 当浏览器发送请求时,会自动携带该 path 下的 cookie 数据给服务器
关于 cookie 的有效时间
java 程序设置 cookie 有效时间
1
cookie.setMaxAge(60 * 60); // 设置 cookie 在一小时后失效
没有设置 cookie 有效时间
- 默认保存在浏览器的运行内存中,浏览器关闭则 cookie 消失
cookie 有效时间 = 0
- cookie 被删除,同名 cookie 被删除
cookie 有效时间 < 0
- 表示不会存储到硬盘文件上,和不设置有效时间效果相同
手动设置 cookie 的 path(关联路径)
1 | cookie.setPath("/xxx"); |
接收浏览器的 cookie
1 | request.getCookies(); |
Cookie 实现十天内免登录
先实现登录功能
- 登录成功
- 跳转到部门列表页面
- 登录失败
- 跳转到错误页面
修改前端页面
- 登录页面设置一个复选框,十天内免登录
- 用户选择复选框,表示支持免登录功能
- 用户没有选择,则表示不支持免登录功能
修改 Servlet 中的 login 方法
- 如果用户登录成功,并且选择了十天内免登录功能,这时应该在 Servlet 中的 login 方法中创建 cookie,存储用户名和密码,并设置路径、有效期,将 cookie 响应给浏览器(浏览器将自动保存在硬盘文件中10天)
用户再次访问该网站首页时,有两个走向:
- 部门列表页面
- 登录页面
- 上边两个页面,要用 java 程序控制
Filter 过滤器
关于 Filter 过滤器
- Filter 是 Servlet 规范中的过滤器,可以解决 Servlet 类中代码复用问题
- Filter 可以在 Servlet 目标程序执行前或执行后,添加过滤规则
- 一般情况下,公共代码写在过滤器中
怎样写过滤器?(与 Servlet 相似)
第一步:编写一个 Java 类,实现 jakarta.servlet.Filter 接口,并实现这个接口中的方法(init 和 destroy 可以不重写)
- init 方法
- doFilter 方法:在该方法中编写过滤规则
- destroy 方法
注意:默认情况下,Servlet 对象在服务器启动时不会被创建,但是 Filter 对象在服务器启动时会被创建
第二步:
使用 @WebFilter 注解
或在 web.xml 文件中配置 Filter 过滤器
- 关联路径的 Servlet 类,要走该过滤器
1
2
3
4
5
6
7
8<filter>
<filter-name>loginFilter</filter-name>
<filter-class>com.shameyang.oa.web.filter.LoginCheckFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginFilter</filter-name>
<url-pattern>/dept/*</url-pattern>
</filter-mapping>
第三步:在 Filter 类的 doFilter 方法中执行
chain.doFilter(request, response);
- 当所有过滤器的 doFilter 方法执行完之后,就会执行 Servlet 中的方法
关于配置 Filter 路径
- 精确匹配:/xxx.xxx、/xxx/xxx
- 匹配所有路径:/*
- 后缀匹配:*.xxx
- 不要以 / 开头
- 前缀匹配:/xxx/*
在 web.xml 文件中,filter-mapping 标签配置的位置越考上,对应的 Filter 过滤器的优先级越高
过滤器的调用顺序,相当于栈的数据结构
过滤器中有一个设计模式
- 责任链设计模式
- 过滤器最大的优点:
- 在程序编译阶段不会确定调用顺序,调用顺序由 web.xml 文件中的顺序决定(符合 OCP 开闭原则)
- 责任链设计模式的核心思想
- 在程序运行阶段,动态组合程序的调用顺序
Listener 监听器
关于 Listener
- Listener 也是 Servlet 规范中的一员
- 在 Servlet 中,所有监听器接口都是由 Listener 结尾
- 在某个特殊时刻想要执行某段代码时,需要使用对应的监听器
Servlet 规范中提供的监听器
- jakarta.servlet.
- ServletContextListener
- ServletContextAttributeListener
- ServletRequestListener
- ServletRequestAttributeListener
- jakarta.servlet.http.
- HttpSessionListener
- HttpSessionAttributeListener
- HttpSessionBindingListener
- HttpSessionIdListener
- HttpSessionActivationListener
怎样实现监听器?
第一步:编写一个类,实现监听器接口,重写方法
第二步:
配置 web.xml 文件
1
2
3<listener>
<listener-class>实现监听器接口的类</listener-class>
</listener>也可以使用注解 @WebListener
第三步:在重写的方法中编写代码
- 不需要手动调用,服务器会在监听器对应的时刻自动调用