JavaWeb
可以在这个网站上面进行对应练习与资料查找。
HTML
HTML 是 Hyper Text Markup Language 的缩写。意思是超文本标记语言,它的作用是搭建网页结构,在网页上展示内容。
超文本:本身是文本,但是最终呈现出来的结果超越了文本。
标记语言:语法是由一系列标签组成的,没有常量,变量,流程控制等等功能。每个标签都有它固定和确定的页面显示效果。
双标签:标签是通过一组尖括号 + 标签名的方式来定义的。(以下双标签表示了一个段落)
<p>HTML is a very popular fore-end technology.</p>
单标签:标签不是成对出现的。
<input type="text" name="username" />
属性:href = “网址”就是属性,href是属性名,网址是属性值。
<a href="http://www.xx.com">show detail</a>
基础结构
html 文件部署在服务器上,是由浏览器负责解析和展示的。也可以在本地创建一个 html 文件,然后通过浏览器打开。html 文件是纯文本文件,普通编辑工具都可以编辑。
专业词汇:标签(tag)、属性(attribute)、文本(text)、元素(element)。
- 标签:页面上的一对
<>
。 - 属性:对标签特征进行设置的一种方式,属性一般都在开始标签中定义。例如
meta
标签就是一种属性标签。 - 文本:双标签中间的文字。
- 元素:开始标签 + 属性 + 文本 + 结束标签,称之为一个元素。
<!--这个是html注释写法-->
<!--
1.html文件的根标签 <html></html>,所有的其他标签都要放在这个标签中间
2.html根标签下由两个一级子标签<head></head>、<body></body>
<head>标签用于定义那些不展示在页面主体上,但是又很重要的内容(例如:字符集、css的引入方式、js引入)
<body>标签用于定义要展示的页面主体内容
3.<!DOCTYPE html>是文档声明,声明该文档是一个html文档,一般标准的html第一行应该是文档声明
-->
<!DOCTYPE html>
<html>
<head>
<title>我是网页标签小标题</title>
<meta charset="utf-8"/> <!--meta标签是单标签,用于告诉浏览器用什么解码方式解码-->
</head>
<body>
<h1>我是主题</h1>
<input type="text" /><br/>
<input type="password" />
<i><big>张三</big></i>
</body>
</html>
语法规则
- 根标签有且只能有一个。
- 无论是双标签还是单标签,都需要正确关闭。
- 标签可以嵌套但是不能交叉嵌套。例如
<i><big>张三</big></i>
就是一个标签嵌套。 - 注释语法为
<!-- -->
,注意不能嵌套。 - 属性必须有值,值必须加引号,H5中属性名和值相同时可以省略属性值。
- 不严格区分字符串使用单双引号。但是如果要进行字符串嵌套的话,需要单双引号交替使用。例如
"abc'aa"bb"aa'defg"
。 - 标签不严格区分大小写,但是不能大小写混用。
- 不允许自定义标签名,强行自定义无效。
vscode可能需要的插件
- Auto Rename Tage 自动修改标签对插件
- Chinese Langage Pack 汉化包
- HTML CSS Support HTML CSS支持
- Intellij IDEA Keybindings IDEA快捷键支持
- Live Server 实时加载功能的小型服务器
- open in browser 通过浏览器打开当前文件的插件
- Prettier-Code formatter 代码美化格式化插件
- Vetur VScode 中的Vue工具插件
- vscode-icons 文件显示图标插件
- Vue 3 Snipptes 生成VUE模板插件
- Vue language Features Vue3语言特征插件
- codesnap 代码截图工具
- one dark pro 一款不错的深色主题插件
标题段落和换行
标题标签一般用于在页面上定义一些标题性的内容,如新闻标题,文章标题等,有一到六级标题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>标题段落和换行</title>
</head>
<body>
<!--
标题标签h1-h6
段落 p
换行 br hr
-->
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<h4>四级标题</h4>
<h5>五级标题</h5>
<h6>六级标题</h6>
<p>段落标签测试
在段落标签中换行实际上是不具备换行效果的
</p>
<br><br><br><br> <!--四个br可以换四行-->
<hr /> <!--hr是带横线的换行 -->
<p>可以另起一段进行换行</p>
<p>在段落当中可以使用换行标签<br/>进行换行</p>
</body>
</html>
列表标签
有序列表:列表标签ol
,列表项标签li
。
无序列表:列表标签ul
,列表项标签li
。
列表也是可以嵌套的,但是要注意是嵌套在列表标签当中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>列表标签</title>
</head>
<body>
<!--
有序列表 ol
无序列表 ul
列表项 li
-->
<ol>
<li>数据类型</li>
<li>变量</li>
<li>流程控制</li>
<li>方法</li>
<li>面向对象</li>
</ol>
<ul>
<li>Java
<ol>
<li>列表的嵌套</li>
<li>注意要嵌套在列表标签当中</li>
</ol>
</li>
<li>C</li>
<li>C++</li>
<li>C#</li>
<li>python</li>
</ul>
</body>
</html>
超链接标签
点击后带有连接跳转功能的标签,也叫a标签。
href
:用于定义要跳转的目标资源地址
target
:用于定义目标资源的打开方式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>超链接标签</title>
</head>
<body>
<!--
超链接标签 a标签
href:用于定义要跳转的目标资源地址
1.完整的url地址 例如:https://blog.hnuxcc21.cn/
2.相对路径 以当前资源所在的路径为出发点去找目标资源 例如:02 标题段落换行.html
./开头 表示的当前资源所在路径,可以不写
../开头 表示当前资源的上一层路径,需要时必须显式写出
3.绝对路径 无论当前资源在哪里,始终以固定的位置作为出发点找目标资源
绝对路径以/开头,从当前项目文件夹开始找
target:用于定义目标资源的打开方式
1._self 在当前窗口打开目标资源
2._blank 开启新窗口打开目标资源
-->
<a href="https://blog.hnuxcc21.cn/" target="_blank">热心市民灰灰</a>
<br/>
<a href="02 标题段落换行.html" target="_blank">02 标题段落换行</a>
<br/>
<a href="/demo1-html/01 firstPage.html" target="_blank">01 firstPage</a>
</body>
</html>
多媒体标签
img
标签用于定义页面上的图片,是一个单标签。
其中:
src
用于定义图片的连接。title
用于定义鼠标悬停时显示的文字。alt
用于定义图片加载失败时的显示文字。width
或者height
用于定义图片的大小,而且大小缩放自动锁定。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片</title>
</head>
<body>
<!--
src:定义图片路径
title:定义鼠标悬停时的标题
alt:定义图片加载失败时显示的文字
-->
<img src="img/Elieen2.jpg"
width="1500px"
title="Elieen2"
alt="Elieen2" />
</body>
</html>
表格标签
表格标签的普通使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格标签</title>
</head>
<body>
<!--
table:代表整张表格
thead 表头
tbody 表体
tfoot 表尾
tr 表格中的一行
td 行中的一个单元格
th 自带加粗和居中效果的td
-->
<h3>员工技能竞赛评分表</h3>
<table border="1px">
<thead>
<tr>
<th>排名</th>
<th>姓名</th>
<th>分数</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>张三</td>
<td>100</td>
</tr>
<tr>
<td>2</td>
<td>李四</td>
<td>99</td>
</tr>
<tr>
<td>3</td>
<td>王五</td>
<td>98</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>平均分</td>
<td>99</td>
<td></td>
</tr>
</tfoot>
</table>
</body>
</html>
表格标签的跨行
利用rowspan
进行跨行,colspan
进行跨列。实现对单元格的侵占。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格标签</title>
</head>
<body>
<!--
table:代表整张表格
thead 表头
tbody 表体
tfoot 表尾
tr 表格中的一行
td 行中的一个单元格
th 自带加粗和居中效果的td
-->
<h3>员工技能竞赛评分表</h3>
<table border="1px">
<thead>
<tr>
<th>排名</th>
<th>姓名</th>
<th>分数</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>张三</td>
<td>100</td>
<td rowspan="6">前三名</td>
</tr>
<tr>
<td>2</td>
<td>李四</td>
<td>99</td>
</tr>
<tr>
<td>3</td>
<td>王五</td>
<td>98</td>
</tr>
<tr>
<td>总人数</td>
<td colspan="2">2000</td>
</tr>
<tr>
<td>平均分</td>
<td colspan="2">90</td>
</tr>
<tr>
<td>及格率</td>
<td colspan="2">80%</td>
</tr>
</tbody>
<tfoot></tfoot>
</table>
</body>
</html>
表单标签
表单标签,可以让用户在界面上输入各种信息并提交的一种标签,是向服务端发送数据的主要方式之一。
为了让用户能够输入信息,我们还需要设置表单项标签,包裹在表单标签当中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表单标签</title>
</head>
<body>
<!--
form标签,表单标签
action:用于定义数据的提交地址
1.url
2.相对路径
3.绝对路径
method:定义数据的提交方式
1.get 参数会以键值对形式放在url后提交 url?key1=value1&key2=value2...
数据直接暴露在地址栏上,不安全
且地址栏长度有限制,所以提交的数据量不大,且无法提交文件
2.post 参数不会直接暴露直接放到地址栏上,相对安全
数据时单独打包通过请求体发送,提交的数据量比较大,可以提交文件
相比较而言,get的效率更高
input标签,表单项标签
type:定义输入信息类型
1.text 普通文本框
2.password 密码
3.submit 提交按钮
4.reset 重置按钮
表单项标签一定要定义name属性,该属性用于明确提交时的参数名
表单项标签还需要定义value属性,该属性用于明确提交时的实参
value不定义的话,默认为用户的输入属性
-->
<form action="08 welcome.html" method="post">
<!-- 添加表单项标签,即用户输入信息的标签 -->
用户名: <input type="text" name="username"/> <br>
密码:<input type="password" name="password"/> <br>
<input type="submit" value="登录"/>
<input type="reset" value="清空"/>
</form>
</body>
</html>
表单项标签
更多的表单项标签如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表单标签</title>
</head>
<body>
<!--
input标签,表单项标签
type:定义输入信息类型
1.text 普通文本框
2.password 密码
3.submit 提交按钮
4.reset 重置按钮
5.radio 单选框,多个单选框使用相同的name属性值就会有互斥效果
利用checked可以默认勾选
6.checkbox 多选框
7.hidden 隐藏域,不显示在页面上,提交时会携带
当我们不希望某些数据发生改变,可以使用隐藏域
如果不希望数据被修改,可以加上readonly,但数据还是会被提交
如果不希望数据被提交,可以加上disabled
8.file 文件上传框
textArea标签,表单项标签,叫做文本域,又称多行文本框
select标签,下拉框
option 用于定义选项,使用时需要包裹在select当中,提交时value=文本内容
使用selected可以默认选中
-->
<form action="08 welcome.html" method="get">
<!-- 添加表单项标签,即用户输入信息的标签 -->
<input type="hidden" name="id" value="123"/>
<input type="text" name="test0" value="456" readonly/><br>
<input type="text" name="test1" value="789" disabled/><br>
用户名: <input type="text" name="username"/> <br>
密码:<input type="password" name="password"/> <br>
性别:
<input type="radio" name="gender" value="男"/> 男
<input type="radio" name="gender" value="女"/> 女
<input type="radio" name="gender" value="沃尔玛购物袋" checked /> 沃尔玛购物袋<br>
爱好:
<input type="checkbox" name="hobby" value="唱"/> 唱
<input type="checkbox" name="hobby" value="跳"/> 跳
<input type="checkbox" name="hobby" value="rap"/> rap
<input type="checkbox" name="hobby" value="篮球"/> 篮球 <br>
个人简介:
<textarea name="selfinfo" id="" cols="30" rows="10"></textarea><br>
籍贯:
<select name="pro" id="">
<option selected>--请选择--</option>
<option>京</option>
<option>津</option>
<option>冀</option>
</select><br>
选择头像:
<input type="file" name="file"><br>
<input type="submit" value="登录"/>
<input type="reset" value="清空"/>
</form>
</body>
</html>
布局相关标签
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>布局相关标签</title>
</head>
<body style="background-color: cadetblue;">
<!--
css 设置样式
通过元素的style属性进行设置
style = "样式名:样式值;样式名:样式值;..."
块元素:自己独占一行的元素 块元素的CSS样式的宽、高等等,往往都是生效的
div
可以利用display标签更改块元素的展示方式 block块 inline行内
更改展示方式为inline之后,可能会导致宽高不生效
行内元素:不会自己独占一行的元素 行内的CSS样式的宽、高等很多都是不生效的
span
-->
<div style="border: 1px solid red; width: 500px; height: 200px; margin: 10px auto;background-color: antiquewhite;">123</div>
<div style="border: 1px solid red; width: 500px; height: 200px; margin: 10px auto;background-color: aliceblue;">456</div>
<div style="border: 1px solid red; width: 500px; height: 200px; margin: 10px auto;background-color: bisque;">789</div>
<!-- span的行宽设置是不生效的 -->
<span style="border: 1px solid green;width: 500px; height: 200px;">555</span>
<!-- 利用span可以划定css样式生效范围 -->
<div style="border: 1px solid red; width: 500px; height: 200px; margin: 10px auto;background-color: bisque;">
<span style="font-size: 30px; color :aqua;font-weight: bold;">布局</span>相关标签测试文本
</div>
</body>
</html>
特殊字符
需要的特殊字符实体可以到这里查看。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>特殊字符</title>
</head>
<body>
<!--
有特殊含义的符号我们称为字符实体
对于html代码来说,某些符号是有特殊含义的
如果想显示这些特殊符号,需要将这些符号进行转义
-->
<h1>一级标题</h1>
<hr>
&gt;
</body>
</html>
CSS
CSS 是 Cascading Style Sheets的缩写,又称层叠样式表。能够对网页中元素的位置的排版进行像素级的控制,支持几乎所有字体字号样式,拥有对网页对象和模型样式编辑的能力。简单来说,CSS 可以美化页面。
CSS 的三种引入方式
行内直接引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css引入方式</title>
</head>
<body>
<!--
引入方式
方式1 行内式
通过元素的style属性引入样式
语法:style="样式名:样式值;样式名:样式值;..."
优点:直接作用在元素上,更直观
缺点:代码复用度低,不利于维护,且css和html两种代码交织在一起,影响阅读性和性能
-->
<input type="button" value="按钮"
style="width: 60px;
height: 40px;
background-color: yellowgreen;
color: white;
font-size: 20px;
font-family: '隶书';
border: 2px solid green;
border-radius: 5px;">
<input type="button" value="按钮">
</body>
</html>
通过元素选择器确定样式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css引入方式</title>
<style>
/* 元素选择器,通过标签名确定样式的作用元素 */
input {
/* 表示页面上所有名字为input的元素都会收到这个样式的影响 */
width: 60px;
height: 40px;
background-color: yellowgreen;
color: white;
font-size: 20px;
font-family: '隶书';
border: 2px solid green;
border-radius: 5px;
}
</style>
</head>
<body>
<!--
引入方式
方式2 内嵌式
通过在head标签中的style标签定义本页面的公共样式
需要通过选择器来确定样式的作用元素
-->
<input type="button" value="按钮">
</body>
</html>
通过外部样式表确定样式
input {
/* 此代码写在css文件中 */
width: 60px;
height: 40px;
background-color: yellowgreen;
color: white;
font-size: 20px;
font-family: '隶书';
border: 2px solid green;
border-radius: 5px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css引入方式</title>
<!-- href声明css样式路径 rel表示文件类型,默认为stylesheet -->
<link rel="stylesheet" href="css/btn.css">
</head>
<body>
<!--
引入方式
方式3 外部样式表
将css代码单独放入一个css文件中,哪个html文件需要这些代码,就在head中通过link标签连接
-->
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
</body>
</html>
CSS 三大选择器
元素选择器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css选择器</title>
<style>
/*
1.元素选择器 根据标签名字确定样式作用元素
语法:标签名{},大括号中的样式全部会作用到同名的标签上
缺点:某些同名的元素不希望使用某些样式,某些不同名的元素也使用该样式,都无法协调
*/
input {
width: 80px;
height: 40px;
background-color: antiquewhite;
color: rgb(86, 67, 43);
border: 3px solid green;
font-size: 22px;
font-family: '隶书';
line-height: 30px;
border-radius: 5px;
}
</style>
</head>
<body>
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
<input type="button" value="按钮">
<button>按钮</button>
</body>
</html>
id 选择器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css选择器</title>
<style>
/*
2.id选择器 根据标签的id值确定样式的作用元素
一般每个元素都有id属性,但是在一个页面中,id的值不应该相同,id具有唯一性
语法:#id值{}
缺点:因为id值有唯一性,故样式只能作用到一个元素上
*/
#btn1 {
width: 80px;
height: 40px;
background-color: antiquewhite;
color: rgb(86, 67, 43);
border: 3px solid green;
font-size: 22px;
font-family: '隶书';
line-height: 30px;
border-radius: 5px;
}
</style>
</head>
<body>
<input id="btn1" type="button" value="按钮">
<input id="btn2" type="button" value="按钮">
<input id="btn3" type="button" value="按钮">
<button id="btn4">按钮</button>
</body>
</html>
class 选择器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css选择器</title>
<style>
/*
3.class选择器 根据元素的class属性值确定样式的作用元素
元素的class属性值可以重复,而且一个元素的class属性可以有多个值
class选择器是主流的样式选择器
语法:.class属性值 {}
*/
.shapeClass {
width: 80px;
height: 40px;
border-radius: 5px;
}
.colorClass {
background-color: rgb(225, 248, 192);
color: white;
border: 3px solid green;
}
.fontClass {
font-size: 20px;
font-family: '隶书';
line-height: 30px;
}
</style>
</head>
<body>
<!-- 利用class进行样式选择 多个class样式之间利用空格分割 -->
<input id="btn1" class="shapeClass colorClass" type="button" value="按钮">
<input id="btn2" class="fontClass" type="button" value="按钮">
<input id="btn3" type="button" value="按钮">
<button id="btn4">按钮</button>
</body>
</html>
CSS 浮动
CSS的浮动(Float)使元素脱离文档流,按照指定的方向(左或右发生移动),直到它的外边缘碰到包含框或另一个浮动框的边框为止。
浮动设计的初衷是为了解决文字环绕图片的问题,浮动后一定不会将文字挡住,这是设计初衷。
文档流是文档中可显示对象在排列时所占用的位置,而脱离文档流就是在页面中不占位置了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css浮动</title>
<style>
.outerDiv {
width: 500px;
height: 300px;
border: 1px solid rgb(137, 229, 137);
background-color: beige;
}
.innerDiv {
width: 100px;
height: 100px;
border: 1px solid rgb(157, 157, 231);
/*display: inline; block块 inline行内 */
}
.d1 {
background-color: greenyellow;
float: left; /* 向左浮动 */
}
.d2 {
background-color: rgb(236, 113, 113);
float: left;
}
.d3 {
background-color: rgb(187, 141, 231);
float: left;
}
</style>
</head>
<body>
<div class="outerDiv">
<div class="innerDiv d1">diva</div>
<div class="innerDiv d2">divb</div>
<div class="innerDiv d3">divc</div>
</div>
</body>
</html>
CSS 定位
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css定位</title>
<style>
.innerDiv {
width: 100px;
height: 100px;
border: 1px solid rgb(157, 157, 231);
/*display: inline; block块 inline行内 */
}
.d1 {
background-color: greenyellow;
position: fixed;
top: 100px;
left: 100px;
}
.d2 {
background-color: rgb(236, 113, 113);
}
.d3 {
background-color: rgb(187, 141, 231);
}
/*
定位相关样式:
position:
static 默认
absolute 绝对定位
relative 相对定位,相对元素原本的位置,且不会脱离文档流
fixed 相对定位,相对浏览器窗口的位置
left:距离左边
right:距离右边
top:距离顶端
bottom:距离底端
*/
</style>
</head>
<body>
<div class="innerDiv d1">diva</div>
<div class="innerDiv d2">divb</div>
<div class="innerDiv d3">divc</div>
</body>
</html>
CSS 盒子模型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>css盒子模型</title>
<style>
.outerDiv {
width: 500px;
height: 300px;
border: 1px solid rgb(137, 229, 137);
background-color: beige;
margin: 0px auto;
}
.innerDiv {
width: 100px;
height: 100px;
border: 1px solid rgb(157, 157, 231);
/*display: inline; block块 inline行内 */
float: left;
}
/*
外边距 margin
margin 后面跟一个参数代表的是 上下左右同时修改
margin 后面跟两个参数代表的是 上下 左右
margin 后面跟四个参数代表的是 上右下左 按顺时针顺序
auto 表示自动平均分配
内边距 padding
padding 参数分配与margin一致
*/
.d1 {
background-color: greenyellow;
margin-right: 10px;
margin-top: 10px;
margin-left: 10px;
padding-top: 20px;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;
padding: 30px;
}
.d2 {
background-color: rgb(236, 113, 113);
margin-left: 10px;
margin-right: 10px;
margin-top: 10px;
margin-bottom: 10px;
}
.d3 {
background-color: rgb(187, 141, 231);
}
</style>
</head>
<body>
<div class="outerDiv">
<div class="innerDiv d1">diva</div>
<div class="innerDiv d2">divb</div>
<div class="innerDiv d3">divc</div>
</div>
</body>
</html>
JavaScript
JavaScript 是一种客户端脚本语言。是一种运行在浏览器上的小脚本语句,可以实现网页如文本活动,数据动态变化和动画特效等。又称 ECMAScript。
- JS 是一种解释型脚本语言,不同于C++、Java 等语言先编译后执行,Javascript 不会产生编译出来的字节码文件,而是在程序的运行过程中对源文件逐行进行解释。
- JS 是一种基于对象的脚本语言。但是 JS 不支持多态,所以它不是一门面向对象的编程语言。
- JS 是弱类型的语言,声明一个变量之后可以接收任何类型的数据,并且可以根据上下文自动转换类型。
- JS 是一种采用事件驱动的脚本语言,不需要经过 web 服务器就可以对用户的输入输出做出响应。
- JS 具有 ECMA 的标准,故 JS 具有跨平台性。目前 JS 已经被大多数浏览器支持。
JS 的引入方式
内嵌式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>js的引入方式</title>
<style>
.bt1 {
width: 150px;
height: 40px;
font-size: 24px;
font-family: '隶书';
background-color: yellow;
color: rgb(160, 61, 61);
border: 3px solid rgb(160, 61, 61);
border-radius: 5px;
}
</style>
<!--
js的引入方式
1.内嵌式 在head中通过一对script定义脚本代码
-->
<script>
/*
js如何声明函数?
java中的函数声明 public void suprise(){}
js中的函数声明 function suprise(){}
函数如何和单击按钮的行为绑定到一起?
onclick 单击触发
ondbclick 双击触发
如何弹窗提示?
调用alert方法
*/
function suprise() {
alert("hello world!");
}
</script>
</head>
<body>
<button class="bt1" onclick="suprise()">点我有惊喜</button>
</body>
</html>
引入外部脚本文件
//外部文件不需要书写<script>标签
function suprise() {
alert("hello world!");
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>js的引入方式</title>
<style>
.bt1 {
width: 150px;
height: 40px;
font-size: 24px;
font-family: '隶书';
background-color: yellow;
color: rgb(160, 61, 61);
border: 3px solid rgb(160, 61, 61);
border-radius: 5px;
}
</style>
<!--
js的引入方式
2.引入外部脚本文件,在head中通过一对script标签引入外部js文件
src是外部脚本文件地址,type是文件类型
注意:
1.一个html中可以有多个scipt标签
2.一对script标签不能在引入外部js的同时定义内部脚本
3.scipt标签如果用于引入外部js文件,中间最好不要有任何字符,包括空格和换行
-->
<script src="js/button.js" type="text/javascript"></script>
</head>
<body>
<button class="bt1" onclick="suprise()">点我有惊喜</button>
</body>
</html>
数据类型和变量
- 数值类型统一为
number
,不区分整数和浮点数。 - 字符串类型为
string
,不严格区分单双引号。 - 布尔类型为
boolean
。 - 引用数据类型对象为
Object
,各种对象和数组在 JS 中都是Object
类型,数组内容用[]
包裹。 - 各种函数属于
function
类型。 var
关键字可以自动判断类型。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>变量和数据类型</title>
<script>
/*
js中变量的声明 统统使用var
利用console在浏览器的控制台上打印数据
js中常见的数据类型
number 数值类型,包括了整数和小数
string 字符串类型
boolean 布尔类型
Object 引用类型
function 函数类型
如果变量未赋值的话,类型和内容均为undefined
如果变量赋值null的话,类型为Object类型
判断数据类型的运算符 typeof
*/
var i = 10;
console.log(i);
console.log(typeof i);
var str = "123";
console.log(str);
console.log(typeof str);
var judge = 1 > 10;
console.log(judge);
console.log(typeof judge);
var object = new Object();
console.log(object);
console.log(typeof object);
function fun() {
}
console.log(fun);
console.log(typeof fun);
//调用prompt可以在网站上输入信息,最终是以string类型返回的
var input = prompt("请输入信息:");
console.log(typeof input); //string
input = Number.parseInt(input); //利用parseInt可以进行string和number的转换
console.log(typeof input); //number
</script>
</head>
<body>
</body>
</html>
运算符
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>运算符</title>
<script>
/*
算数运算符 + - * / %
/0 和 %0 时不会报错,分别会打印Infinity和NaN(not a number)
复合运算符 ++ -- += -= *= /= %=
/=0 %=0 结果与算数运算符一致
关系运算符 > < >= <= != == ===
== 判断:如果两端的数据类型不一致,会尝试将两端的数据都转换为number再对比
"123" -> 123
true -> 1
false -> 0
=== 判断:如果两端的数据类型不一致,直接返回false,相同则会继续对比
逻辑运算符 || &&
条件运算符 表达式 ? 值1 : 值2
位运算符 | & ^ << >> >>>
*/
console.log(10 / 0);
console.log(10 % 0);
console.log(10 / 3);
console.log(1 == 1); //true
console.log(1 == "1"); //true
console.log(1 == true); //true
console.log(1 === 1); //true
console.log(1 === "1"); //false
console.log(1 === true); //false
</script>
</head>
<body>
</body>
</html>
分支、循环语句
JS 中 if 的条件判断与 C++ 一致,这里条件句非空会被判断为 true。
调用document.write()
方法可以向网页中打印数据,注意打印空格等字符时需要利用特殊字符。
JS 中的增强for
拿取的不再是数组内容,而是索引,且需要利用in
而不是:
遍历。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>循环语句</title>
<script>
//if条件判断,这里非空语句会被判断为true
if("false") {
console.log(true);
}
else {
console.log(false);
}
//利用dobument.write()方法可以往网页中打印数据,附带html效果
document.write("张三");
document.write("<h1>张三</h1>");
document.write("<hr>");
//打印九九乘法表
for(var i = 1; i <= 9; ++i) {
for(var j = 1; j <= i; ++j) {
document.write(i + " * " + j + " = " + i * j + " ");
}
document.write("<hr>");
}
//打印无序列表
var arr = ["北京", "上海", "广东"];
document.write("<h1>无序列表打印数组内容</h1>")
document.write("<ul>");
for(var index = 0; index <arr.length; ++index) {
document.write("<li>" + arr[index] + "</li>");
}
document.write("</ul>");
//增强for打印
for(var index in arr) {
document.write(arr[index] + "<br/>");
}
</script>
</head>
<body>
</body>
</html>
函数
- 函数没有权限控制符和异常列表。
- 不用声明返回值类型,需要返回值的话直接 return 即可,也无需 void 关键字。
- 参数列表中无需数据类型。
- 调用函数时,实参和形参的个数可以不一致。
- 函数的参数可以是函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>函数</title>
<script>
/*
函数声明的语法
1. function 函数名(){}
2. var 函数名 = function(){}
*/
function add(a, b) {
return a + b;
}
//调用函数接收结果
var sum = add(1, 2);
console.log(sum);
var sub = function(a, b) {
return a - b;
}
var sum = sub(1, 2, 3, 4, 5); //多的参数会弃用
console.log(sum);
//在函数中传递函数
function fun(getAdd) {
return getAdd(1, 2);
}
var res = fun(add);
console.log(res); //3
</script>
</head>
<body>
</body>
</html>
对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>对象</title>
<script>
/*
创建对象方法:
1. new Object()
2.{属性名:属性值,...,...,函数名:function(){}}
*/
var person = new Object();
//对象的属性和方法需要手动添加
//如果没有对应属性,则直接自动增加一个属性
person.name = "张三";
person.age = 18;
person.eat = function (food) {
console.log(this.age + "岁的" + this.name + "正在吃" + food);
}
//访问属性
console.log(person.name);
console.log(person.age);
//调用方法
person.eat("火锅");
var person = {
name:"李四",
age:10,
eat:function(food) {
console.log(this.age + "岁的" + this.name + "正在吃" + food);
}
};
//访问属性
console.log(person.name);
console.log(person.age);
person.eat("小火锅");
</script>
</head>
<body>
</body>
</html>
JSON
JSON(JavaScript Object Notation),JS对象简谱。是一种轻量级的数据交换格式。简单来说,JSON就是一种字符串格式,这种格式无论是在前端还是在后端,都可以很容易地转换成对象,所以常用于前后端数据的传递。
JSON 在客户端的使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>json串</title>
<script>
/*
json格式的语法
var personStr = '{"属性名":"属性值","属性名":"属性值",...}'
var personStr = '{"属性名":{}}' 属性值是对象
var personStr = '{"属性名":[10, 20, 30]}' 属性值是数组
var personStr = '{"属性名":[{},{},{}]}' 属性值是对象数组
属性名必须用双引号包裹
属性值如果是字符串必须用双引号包裹,如果是数字可以不处理
前后端交互的时候主要是数据,不是方法,故json格式中一般不包含方法
*/
//对象
var person = {
name:"张三",
age:18,
dog:{
dname:"小花",
dage:2
}
};
//json串
var personStr = '{"name":"张三","age":18,"dog":{"dname":"小花","dage":2}}';
console.log(personStr);
console.log(typeof personStr); //string
//通过JSON.parse(),可以将JSON串转化为一个对象
var person = JSON.parse(personStr);
console.log(typeof person); //object
//获取对象元素
console.log(person.name);
console.log(person.dog.dname);
//通过JSON.stringify(),可以将一个对象转化为一个json串
var personStr = JSON.stringify(person);
console.log(personStr);
console.log(typeof personStr);
</script>
</head>
<body>
</body>
</html>
JSON 在服务端的使用
可以使用 Jackson 在后端进行 JSON 与对象的转换。下载地址戳这里。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
public class TestJson {
@Test
public void testWriteJson() throws JsonProcessingException {
//实例化Person对象
Student stu = new Student("李四", 20, "湖南大学");
//将Person对象转换为JSON串 可以使用Gson、Jackson、FastJson转换
ObjectMapper objectMapper = new ObjectMapper();
String stuStr = objectMapper.writeValueAsString(stu);
System.out.println(stuStr);
}
@Test
public void testReadJson() throws JsonProcessingException {
String stu = "{\"name\":\"李四\",\"age\":20,\"school\":\"湖南大学\"}";
ObjectMapper objectMapper = new ObjectMapper();
Student student = objectMapper.readValue(stu, Student.class);
System.out.println(student);
}
}
JSON 和 Map、List、Array 之间的转换
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import java.util.*;
public class TestJson {
@Test
public void testMapToJson() throws JsonProcessingException {
HashMap<String, String> data = new HashMap<>();
data.put("a", "valuea");
data.put("b", "valueb");
data.put("c", "valuec");
ObjectMapper objectMapper = new ObjectMapper();
String map = objectMapper.writeValueAsString(data);
System.out.println(map); //{"a":"valuea","b":"valueb","c":"valuec"} 格式与对象直接转换一致
}
@Test
public void testListToJson() throws JsonProcessingException {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "zhangsan", "lisi", "wangwu");
ObjectMapper objectMapper = new ObjectMapper();
String listStr = objectMapper.writeValueAsString(list);
System.out.println(list); //[zhangsan, lisi, wangwu]
System.out.println(listStr); //["zhangsan","lisi","wangwu"]
}
@Test
public void testArrayToJson() throws JsonProcessingException {
Student[] stuArr = {new Student("zhangsan", 18, "aaa"),
new Student("lisi", 19, "bbb"),
new Student("wangwu", 20, "ccc")};
ObjectMapper objectMapper = new ObjectMapper();
String stuStr = objectMapper.writeValueAsString(stuArr);
System.out.println(stuStr);
//[{"name":"zhangsan","age":18,"school":"aaa"},{"name":"lisi","age":19,"school":"bbb"},{"name":"wangwu","age":20,"school":"ccc"}]
}
}
常见对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>js常用对象</title>
<script>
/*
常见对象
1.数组对象(可以理解为java中的集合)
数组的创建方式
var arr = new Array();
var arr = new Array(5);
var arr = new Array(1, 2, 3);
var arr = [1, 2, 3];
数组的API
concat 连接两个数组
pop push indexOf reverse
join 转换成字符串,可以指定分隔符
slice 选取数组的一部分,并返回一个新的数组
splice 删除或增加元素
*/
var fruits = ["apple", "orange", "banana"];
var food = ["荔枝", "油饼", "食不食"];
//concat连接两个数组
var foodAll = fruits.concat(food);
console.log(foodAll);
//push插入元素
foodAll.push("你干嘛");
console.log(foodAll);
//pop删除元素
foodAll.pop();
console.log(foodAll);
//查找
var index = foodAll.indexOf("荔枝");
console.log(index);
//逆转!
foodAll.reverse();
console.log(foodAll);
//转换成字符串
console.log(foodAll.join("-")); //食不食-油饼-荔枝-banana-orange-apple
//选取数组中的一部分
console.log(foodAll.slice(1, 3)); // [start, end)
//增加元素 从2索引开始插入元素,0表示删除0个元素
foodAll.splice(2, 0, "哈哈", "哎哟");
console.log(foodAll);
//删除元素 从2索引开始,删除4个元素(包括2索引)
foodAll.splice(2, 4); // [start, start + length]
console.log(foodAll);
//即增加又删除
foodAll.splice(2, 2, "拥护", "信徒");
console.log(foodAll);
</script>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>api测试</title>
<script>
/*
常见对象
2.boolean对象
toString 把布尔值转成字符串
valueOf 返回Boolean对象的原始值
3.Date对象
4.Math对象 和Java类似,调用方法为 Math.方法名
5.Number对象
parseInt 字符串转整数
6.String对象 和Java类似
*/
var date = new Date();
console.log(date);
//获取年份
console.log(date.getFullYear());
//获取月份 0-11
console.log(date.getMonth());
//获取日期
console.log(date.getDate());
//获取小时数
console.log(date.getHours());
//获取分钟数
console.log(date.getMinutes());
//获取秒钟数
console.log(date.getSeconds());
//设置
date.setFullYear(2025);
date.setMonth(1);
date.setDate(11);
console.log(date);
var str = "10";
var res = 10 + str;
console.log(res); //1010
console.log(typeof res); //string
var res = 10 + Number.parseInt(str);
console.log(res); //20
</script>
</head>
<body>
</body>
</html>
事件
事件可以是浏览器行为,也可以是用户行为。当这些行为发生时,可以自动触发对应的函数执行,我们称为事件的发生。
鼠标事件、键盘事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件</title>
<script>
function fun1() {
console.log("单击了");
}
function fun2() {
console.log("单击了");
}
function fun3() {
alert("双击了");
}
function fun4() {
console.log("鼠标悬停了");
}
function fun5() {
console.log("鼠标移动了");
}
function fun6() {
console.log("鼠标移开了");
}
function fun7() {
console.log("键盘按下");
}
function fun8() {
console.log("键盘抬起");
}
/*
事件的绑定方式
1.通过元素的属性绑定 on***
2.通过DOM编程动态绑定
注意事项
1.一个事件可以同时绑定多个函数,利用,隔开
2.一个元素可以同时绑定多个事件
常见的事件
1.鼠标事件
onclick 单击
ondbclick 双击
onmouseover 鼠标悬停
onmousemove 鼠标移动
onmouseleave 鼠标离开
2.键盘事件
onkeydown 键按下
onkeyup 键抬起
*/
</script>
</head>
<body>
<input
type="button"
value="按钮"
onclick="fun1(), fun2()"
ondblclick="fun3()"
/>
<br/>
<img
src="img/Elieen2.jpg"
title="Elieen2"
alt="Elieen2"
width=1000px
onmouseover="fun4()"
onmousemove="fun5()"
onmouseleave="fun6()"
/>
<br/>
<input
type="text"
onkeydown="fun7()"
onkeyup="fun8()"
/>
</body>
</html>
表单事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件</title>
<script>
/*
event.preventDefault() 阻止组件的默认行为
常见的事件
3.表单事件
onfocus 获取焦点事件 意思是鼠标点入,出现光标
onblur 失去焦点事件
onchange 当内容发生改变时会执行
onsubmit 表单提交,需要绑定在form上面
onreset 表单重置的时候触发,同样也需要绑定在form上面
*/
function testFocus() {
console.log("获得焦点了");
}
function testBlur() {
console.log("失去焦点了");
}
function testChange(value) {
console.log("内容发生改变为:" + value);
}
function testChange2(value) {
console.log("选项发生改变为:" + value);
}
function testSubmit() {
/*
弹窗的三种方式
1.alert() 信息提示框
2.prompt() 信息输入框
3.confirm() 信息确认框
*/
var flag = confirm("确定要提交表单吗?");
if(flag) {
//在这里我们有机会阻止表单的提交
//event.preventDefault(); //组织组件的默认行为
//或者return false也可以,但是在绑定的时候需要额外加上return
return false;
}
return true;
}
function testReset() {
console.log("表单重置了");
}
</script>
</head>
<body>
<form action="../demo1-html/08 welcome.html" method="get"
onsubmit="return testSubmit()"
onreset="testReset()"
>
用户名: <input
type="text"
name="username"
onfocus="testFocus()"
onblur="testBlur()"
onchange="testChange(this.value)"
/><br>
密码: <input type="password" name="password"><br>
选择籍贯: <select onchange="testChange2(this.value)">
<option selected>--请选择--</option>
<option>北京</option>
<option>上海</option>
<option>广东</option>
</select><br>
<input type="submit" value="登录">
<input type="reset" value="清空"><br>
</form>
</body>
</html>
DOM 编程绑定、触发事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件</title>
<style>
.div1 {
width: 100px;
height: 100px;
background-color: antiquewhite;
}
</style>
<script>
/*
使用DOM编程绑定事件
常见的事件
4.页面加载事件
onload 页面加载完毕事件,指浏览器把html文件全解析完毕之后,自动触发该事件
事件的触发
1.行为触发
2.DOM编程触发
*/
window.onload = function() {
//为div绑定点击事件
var div1 = document.getElementById("d1");
div1.onclick = function() {
div1.style.backgroundColor="red";
}
//通过DOM获得要绑定的元素
var btn = document.getElementById("btn1"); //此处btn为对象
//给btn绑定事件,实际上是对其某个属性赋值
btn.onclick = function() {
//alert("按钮单击了");
div1.onclick();
}
}
</script>
</head>
<body>
<div id="d1" class="div1"></div>
<button id="btn1">按钮</button>
</body>
</html>
BOM 编程
BOM 是 Browser Object Model 的简写,即浏览器对象模型。是由一系列对象组成,是访问、控制、修改浏览器的属性和方法,通过 window 对象及属性的一系列方法控制浏览器行为的一种编程。是将浏览器窗口的各个组成部分抽象成各个对象,通过各个对象的 API 操作组件行为的一种编程。
BOM 编程的对象结构如下:
window 顶级对象,代表各个浏览器窗口。
- location 对象:代表浏览器地址。
- history 对象:代表浏览器的访问历史。
- screen 对象:代表屏幕。
- navigator 对象:代表浏览器软件本身。
- document 对象:代表浏览器窗口目前解析的 html 文档。
- console 对象:代表浏览器开发者工具的控制台。
- localStorage 对象:代表浏览器的本地数据持久化存储。
- sessionStorage 对象:代表本地数据会话级存储。
window 常见API
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BOM常见API</title>
<script>
/*
window 该对象是浏览器提供的,无需new
三种弹窗方式 window.可以省略不写
1.alert 信息提示
2.prompt 信息输入 prompt是有返回值的
3.confirm 信息确认
setTimeout 设置延迟信息,第一个参数为要执行的函数,第二个参数为延迟的时间
*/
function fun1() {
window.alert("信息提示");
}
function fun2() {
var res = window.prompt("信息输入");
console.log(res);
}
function fun3() {
var flag = window.confirm("信息确认");
console.log(flag);
}
function fun4() {
window.setTimeout(function() {
console.log("hello");
}, 2000);
}
</script>
</head>
<body>
<button onclick="fun1()">信息提示框</button><br>
<button onclick="fun2()">信息输入框</button><br>
<button onclick="fun3()">信息确认框</button><br>
<button onclick="fun4()">两秒钟后向控制台打印hello</button>
</body>
</html>
其他常见属性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BOM常见API</title>
<script>
/*
history 窗口的访问历史
forward 向后翻页
back 向前翻页
go 向前翻指定的页数
location 地址栏
href 指定地址,是一个属性,不是方法
sessionStorage 会话级存储 浏览器关闭,数据就会被清除
localStorage 永久存储 浏览器关闭后,数据还在
setItem 存储数据,以键值对的方式存储
getItem 拿取数据,以键的方式拿取
removeItem 删除数据,以键的方式删除
console 控制台
log 往控制台中打印日志
*/
function funa() {
history.back();
}
function funb() {
history.forward();
}
function func() {
location.href="https://blog.hnuxcc21.cn/";
}
function fund() {
//向sessionStorage中存储数据
sessionStorage.setItem("keya", "valueA");
//向localStorage中存储数据
localStorage.setItem("keyb", "valueB");
}
function fune() {
console.log(sessionStorage.getItem("keya"));
console.log(localStorage.getItem("keyb"));
}
function funf() {
sessionStorage.removeItem("keya");
localStorage.removeItem("keyb");
}
</script>
</head>
<body>
<button onclick="funa()">上一页</button>
<button onclick="funb()">下一页</button><br>
<a href="https://blog.hnuxcc21.cn/" target="_blank" >热心市民灰灰</a><hr>
<button onclick="func()">地址跳转</button><hr>
<button onclick="fund()">存储数据</button><br>
<button onclick="fune()">读取数据</button><br>
<button onclick="funf()">清空数据</button><br>
</body>
</html>
DOM 编程
DOM 编程(Document Object Model)就是利用 document 对象的 API 完成对网页 html 文档的动态修改,以实现网页数据和样式动态变化效果的编程。
html 文档是由标签层层包裹的,故逻辑上是树形结构。DOM 树上的节点类型分为:元素结点、属性节点、文本节点。意思是,标签节点也会根据属性等不同进行划分。DOM 编程就是会去改变节点的状态,改变树的结构。
直接获取元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM编程API</title>
<script>
/*
1.获得DOM树 window.document
2.从document中获取要操作的元素
1.直接获取
2.间接获取
3.对元素进行操作
1.操作元素属性
2.操作元素样式
3.操作元素的文本
4.增删元素
*/
function fun1() {
//获得document并获得元素
var btn01 = document.getElementById("username"); //根据元素的id值获取页面上的元素
console.log(btn01.name);
}
function fun2() {
var elems = document.getElementsByTagName("input");
console.log(elems);
for(i in elems) {
console.log(elems[i]);
}
}
function fun3() {
var elems = document.getElementsByName("aaa");
for(var i = 0; i < elems.length; ++i) {
console.log(elems[i]);
}
}
function fun4() {
var elems = document.getElementsByClassName("b");
for(var i = 0; i < elems.length; ++i) {
console.log(elems[i]);
}
}
</script>
</head>
<body>
<div id="div01">
<input type="text" class="a" id="username" name="aaa">
<input type="text" class="b" id="password" name="aaa">
<input type="text" class="b" id="email">
<input type="text" class="c" id="address">
</div>
<input type="text"><br>
<input type="button" value="根据id获取指定元素" onclick="fun1()" id="btn01">
<input type="button" value="根据标签名获取多个元素" onclick="fun2()" id="btn02">
<input type="button" value="根据name属性值获取多个元素" onclick="fun3()" id="btn03">
<input type="button" value="根据class属性值获得多个元素" onclick="fun4()" id="btn04">
</body>
</html>
间接获取元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM编程API</title>
<script>
/*
1.获得DOM树 window.document
2.从document中获取要操作的元素
1.直接获取
2.间接获取
3.对元素进行操作
1.操作元素属性
2.操作元素样式
3.操作元素的文本
4.增删元素
*/
function fun1() {
//获取div父元素
var div01 = document.getElementById("div01");
//获取所有子元素
var allchild = div01.children;
for(var i = 0; i < allchild.length; ++i) {
console.log(allchild[i]);
}
//获取第一个子元素
var c1 = div01.firstElementChild;
//获取最后一个子元素
var c2 = div01.lastElementChild;
}
function fun2() {
//先获取子元素
var elem = document.getElementById("password");
//通过子元素获取父元素
var pe = elem.parentElement;
console.log(pe);
}
function fun3() {
//获取子元素
var elem = document.getElementById("password");
//获取同级别前一个兄弟元素
console.log(elem.previousElementSibling);
//获取同级别后一个兄弟元素
console.log(elem.nextElementSibling);
}
</script>
</head>
<body>
<div id="div01">
<input type="text" class="a" id="username" name="aaa">
<input type="text" class="b" id="password" name="aaa">
<input type="text" class="b" id="email">
<input type="text" class="c" id="address">
</div>
<input type="text"><br>
<hr>
<input type="button" value="通过父元素获取子元素" onclick="fun1()" id="btn01">
<input type="button" value="通过子元素获取父元素" onclick="fun2()" id="btn02">
<input type="button" value="通过当前元素获取兄弟元素" onclick="fun3()" id="btn03">
</body>
</html>
操作元素属性值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM编程API</title>
<style>
#in1 {
color: red;
border-radius: 5px;
}
</style>
<script>
function changeAttribute() {
//获取对象
var in1 = document.getElementById("in1");
//操作属性
// console.log(in1.type);
// console.log(in1.value);
in1.type="button";
in1.value="你好";
}
function changeStyle() {
var in1 = document.getElementById("in1");
//语法:元素.style.样式名=""
//注意带-的名字在这里需要转换为驼峰式
in1.style.color="green";
in1.style.borderRadius="10px";
}
function changeText() {
var div01 = document.getElementById("div01");
/*
获取文本
语法:元素名.innerText(无法识别html代码)
元素名.innerHTML(可以识别html代码)
*/
//console.log(div01.innerText);
div01.innerText="你好你好";
setTimeout(function() {
div01.innerHTML="<h1>你好你好</h1>";
}, 2000);
//console.log(div01.innerHTML);
}
</script>
</head>
<body>
<input type="text" value="hello" id="in1">
<div id="div01">
hello
</div>
<hr>
<button onclick="changeAttribute()">操作属性</button>
<button onclick="changeStyle()">操作样式</button>
<button onclick="changeText()">操作文本</button>
</body>
</html>
对页面元素进行增删
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM编程API</title>
<script>
function addCs() {
//在无序列表当中添加子元素
//需要先获取父元素,然后在其子元素中添加新元素
//创建新元素
var csli = document.createElement("li");
//设置子元素的属性和文本
csli.id="cs";
csli.innerText="长沙";
//将子元素放入父元素当中 利用appendChild
var cityul = document.getElementById("city");
cityul.appendChild(csli);
}
function addCsBeforeSz() {
//创建子元素
var csli = document.createElement("li");
//设置子元素属性
csli.id="cs";
csli.innerText="长沙";
//获取父元素,添加子元素
var cityul = document.getElementById("city");
var citysz = document.getElementById("sz");
cityul.insertBefore(csli, citysz); //第一个参数:新元素 第二个参数:参照
}
function replaceSz() {
//创建子元素
var csli = document.createElement("li");
csli.id="cs";
csli.innerText="长沙";
var cityul = document.getElementById("city");
var citysz = document.getElementById("sz");
cityul.replaceChild(csli, citysz); //第一个参数:新元素 第二个参数:要被替换的元素
}
function removeSz() {
//获得要删除的元素
var szli = document.getElementById("sz");
//调用remove删除
szli.remove();
}
function removeAll() {
var cityul = document.getElementById("city");
//不适用for循环,因为涉及到索引删除,可能会导致失效
var fc = cityul.firstChild;
while(fc != null) {
fc.remove();
fc = cityul.firstChild;
}
}
</script>
</head>
<body>
<ul id="city">
<li id="bj">北京</li>
<li id="sh">上海</li>
<li id="sz">深圳</li>
<li id="gz">广州</li>
</ul>
<hr>
<!-- 需求1 在城市列表的最后添加一个子标签 <li id="cs">长沙</li> -->
<button onclick="addCs()">增加长沙</button>
<!-- 需求2 在城市列表深圳前添加一个子标签长沙 <li id="cs">长沙</li> -->
<button onclick="addCsBeforeSz()">在深圳之前增加长沙</button>
<!-- 需求3 将城市列表中的深圳替换为长沙 <li id="cs">长沙</li> -->
<button onclick="replaceSz()">替换深圳为长沙</button>
<!-- 需求4 在城市列表中删除深圳 -->
<button onclick="removeSz()">删除深圳</button>
<!-- 需求5 清空城市列表 -->
<button onclick="removeAll()">清空列表</button>
</body>
</html>
正则表达式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正则表达式</title>
<script>
/*
语法:
var regex = new RegExp(pattern, modifiers)
或者更简洁的写法:
var regex = /pattern/modifiers
modifiers:
i 执行对大小写不敏感的匹配
g 执行全局匹配
m 执行多行匹配
*/
/* 定义一个正则表达式 */
var reg = /o/g;
//自定义一个字符串
var str = "hello world";
//校验字符串是否符合正则的规则,即字符串包不包含正则
console.log(reg.test(str));
//匹配,找出字符串中符合正则表达式的第一个位置
var res = str.match(reg);
console.log(res);
//替换
var newStr = str.replace(reg, "@");
console.log(newStr);
//前缀匹配
var str1 = "java love me";
var str2 = "I love java"
var reg = /^java/; //^用于匹配前缀
console.log(reg.test(str1)); //true
console.log(reg.test(str2)); //false
var reg = /java$/; //$用于匹配后缀
console.log(reg.test(str1)); //false
console.log(reg.test(str2)); //true
//一般前后缀一起使用来规定字符串的长度
/*
校验用户名是否合法
1.必须是字母开头
2.长度必须是6-10位
3.后面其他字符可以是大小写字母,数字和下划线
*/
var name = "asdfqwer"
var reg = /^[a-zA-Z]\w{5,9}$/;
console.log(reg.test(name));
</script>
</head>
<body>
</body>
</html>
XML
XML 是 EXtensible Markup Language 的缩写,翻译过来就是可扩展标记语言。
当然你也可以叫它为小毛驴。所以很明显,XML 和 HTML 一样都是标记语言,也就是说它们的基本语法都是标签。XML 一般作为一种配置文件来使用。
常见配置文件类型
- properties文件:例如 druid 连接池就是使用 properties 文件作为配置文件。
- XML 文件:例如 Tomcat 就是使用 XML 文件作为配置文件。
- YAML 文件:例如 SpringBoot 就是使用 YAML 文件作为配置文件。
- json 文件:通常用来作文件传输,也可以用来做前端或者移动端的配置文件。
properties 文件配置示例如下:
当需要多层级配置的时候,properties 文件就显得有些冗余。
dev.atguigu.url=jdbc:mysql://localhost:3306/atguigu
dev.atguigu.driver=com.mysql.cj.jdbc.Driver
dev.atguigu.username=root
dev.atguigu.password=root
test.atguigu.url=jdbc:mysql://localhost:3306/atguigu
test.atguigu.driver=com.mysql.cj.jdbc.Driver
test.atguigu.username=zhangsan
test.atguigu.password=123456
formal.atguigu.url=jdbc:mysql://localhost:3306/atguigu
formal.atguigu.driver=com.mysql.cj.jdbc.Driver
formal.atguigu.username=lisi
formal.atguigu.password=123
使用 xml 文件配置示例如下:
可以看出 xml 文件更加具有层次性。
<?xml version="1.0" encoding="UTF-8"?>
<!--
xml语法特点
1.根标签只能有一个,例如<jdbc></jdbc>
2.第一行永远都是<?xml version="1.0" encoding="UTF-8"?> 前面不可以放任何东西,包括空格,注释等
3.xml也是有约束的,约束用于限定xml内部能编写的内容,其约束有两种:
dtd 上手快,语法简单,但是约束不够细致
schema 上手慢,较复杂,但是约束更细致
添加约束之后,限制了内容的标签,编译器也会给出提示,方便我们进行编写
-->
<jdbc>
<dev>
<username>root</username>
<password>root</password>
<dirver>com.mysql.cj.jdbc.Driver</dirver>
<url>jdbc:mysql://localhost:3306/atguigu</url>
</dev>
<test>
<username>zhangsan</username>
<password>123456</password>
<dirver>com.mysql.cj.jdbc.Driver</dirver>
<url>jdbc:mysql://localhost:3306/atguigu</url>
</test>
<formal>
<username>lisi</username>
<password>123</password>
<dirver>com.mysql.cj.jdbc.Driver</dirver>
<url>jdbc:mysql://localhost:3306/atguigu</url>
</formal>
</jdbc>
DOM4J 进行 XML 解析(了解)
DOM4J 的使用步骤:
- 导入 jar 包 dom4j.jar。
- 创建解析器对象(SAXReader)。
- 解析 xml 获得 Document 对象。
- 获取根节点 RootElement。
- 获取根节点下的子节点。
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.List;
public class TestDom4j {
@Test
public void testRead() throws FileNotFoundException, DocumentException {
//读取jdbc.xml配置文件,获得document对象
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new FileInputStream("D:\\HNU\\HNU程序设计\\ideaC-JavaCode\\projectTest\\resources\\jdbc.xml"));
//也可以通过类加载器获得指向字节码根路径下的指定文件的输入流
//InputStream resourceAsStream = TestDom4j.class.getClassLoader().getResourceAsStream("jdbc.xml");
//从document对象上获取配置文件中的信息
//获取根标签
/*
* Node节点
* Element 元素节点
* Attribute 属性节点
* Text 文本节点
* */
Element rootElement = document.getRootElement();
System.out.println(rootElement.getName()); //jdbc
//获取子元素
List<Element> elements = rootElement.elements();//[dev, test, formal]
for (Element element : elements) {
System.out.print("\t" + element.getName());
//从元素上获取属性
Attribute idAttribute = element.attribute("id");
System.out.println(" " + idAttribute.getName() + " = " + idAttribute.getValue());
//继续读取子元素
List<Element> elements1 = element.elements();
for (Element element1 : elements1) {
System.out.println("\t\t" + element1.getName() + ":" + element1.getText());
}
}
}
}
Tomcat10
Tomcat 是一款服务器软件,是专门用来运行 WEB 应用软件的。其技术先进,性能稳定,且免费,因而深受 Java 爱好者的喜爱,成为目前比较流行的 Web 应用软件。
常见的 JavaWeb 服务器:
- Tomcat:当前应用最广的 JavaWeb 服务器。
- Jetty:更轻量级、更灵活的 servlet 容器。
- GlassFish:Oracle 开发的 JavaWeb 服务器,应用不是很广泛。
- Resin:支持 JavaEE,应用越来越广。
- Weblogic:付费,支持 JavaEE,适合大型项目。
- Websphere:付费,支持 JavaEE,适合大型项目。
Tomcat 文件夹中的 log 文件记录的是日志相关,随着使用时间增长,可能会占用比较大的空间,注意定时请理。我们平时如果要对 web 项目进行部署,需要将项目部署到 webapps 文件夹当中。
WebAPP 中的项目
项目文件结构
app:本应用目录
—- static:非必要目录,约定俗称的名字,一般存放静态资源(css、js、img等)。
——– css:存放 css 代码。
——– js:存放 js 代码。
——– img:存放图片资源。
—- WEB-INF:必要目录,必须叫 WEB-INF,是受保护的资源目录,浏览器不可以通过 url 直接访问。
——– classes:字节码根路径,java 文件经过编译后的 .class
文件都存放在这里。
——– lib:项目所需要导入的第三方 jar 包会存放在这里。
——– web.xml:项目的 web 配置文件。
—- index.html:非必要文件,index.html 为默认欢迎页。
url 的组成和项目中资源的对应关系
假设现在有一个 url 如下:http://127.0.0.1:8080/app/index.html
。
对应关系如下:
- http:表示访问资源时的协议。
- 127.0.0.1:表示服务器地址。
- 8080:表示服务器服务软件的端口号。
- app:项目的上下文路径,指代项目所在的文件夹。
- index.html:项目中的 html 页。
项目部署的另一种方式
除了直接在 webapps 当中创建目录外,我们还可以在别的地方创建项目目录,然后通过配置文件关联即可。
假设我们的项目创建在 D:\mywebapps\app
,那么,我们在 Tomcat 的 conf\Catalina\localhost
路径下创建一个 app.xml
文件,配置信息如下:
<!--
path:项目的访问路径,也是项目的上下文路径(上下文路径指的是在浏览器中的访问路径)
docBace:项目在磁盘中的实际路径
-->
<Context path="/app" docBace="D:\mywebapps\app" />
该种项目部署方式与 IDEA 在 Tomcat 上部署项目的方式一致。
IDEA 社区版构建 Web 项目
请参考这篇博客。
IDEA 部署 Web 项目原理
Java 的 web 工程经过 IDEA 构建之后会在 out 文件夹当中生成一个可以发布的 app 文件。IDEA 为了保证本地 Tomcat 的纯净,IDEA 将本地 Tomcat 进行复制,产生一个副本,在副本之中部署我们的 app 文件。这样子就可以保证磁盘上的 Tomcat 的纯净,不会有别的项目对本地磁盘上的 Tomcat 造成污染。
IDEA 创建出来的副本当中存放的仅仅是和要部署的项目相关的配置文件。然后让本地磁盘上的 Tomcat 按照副本当中的配置文件去运行我们的 web 项目。故实际上创建出来的副本大小只有几百KB。
HTTP
HTTP 超文本传输协议(Hyper Text transfer protocol),是一个属于应用层面的面向对象协议。它是一种详细规定了浏览器和万维网服务器之间互相通信的规则。客户端和服务端通信时传输的内容我们称之为报文,HTTP 协议就是规定报文的格式。客户端发送给服务器的称为“请求报文”,服务器发送给客户端的称为“响应报文”。
HTTPS(安全超文本传输协议)是使用 SSL / TLS 进行安全加密通信的 HTTP 的安全版本。
总而言之,http 涉及到:
- 客户端和服务端交互的方式。分为请求和响应,请求就是客户端携带数据向服务端发送,响应就是服务端向客户端返回。
- 客户端和服务端交互数据的格式。请求时发送的数据称为请求报文,响应时返回的数据称为响应报文。
而报文也是有规定的格式的:一个是报文首部,涉及到行信息和头部信息;另一个是报文主体。请求报文当中的信息我们分别称之为:请求行、请求头和请求体。响应报文当中的信息我们分别称之为:响应行、响应头和响应体。
HTTP 长连接和短链接
http 是一种应用层协议,规定了数据有什么,数据的格式是什么。但是数据怎么发送,这个问题不在 http 的考虑范围之内。http 协议下的默认传输协议是 tcp 协议(面向连接的协议)。tcp 协议往下通过 ip 协议确定数据传输的实体。
当网页在解析 html 文件的时候,会碰到相对应的 link
或者 script
标签,出现这些标签,意味着网页需要和服务端重新建立连接去抓取对应的 css 或者 js 文件。而客户端和服务端每一次建立和断开连接都需要执行三次握手和四次挥手协议,效率比较低,而且资源消耗也比较大。
短链接就是创建一次连接后马上关闭,而长连接就是创建一次连接之后不急着关闭,等到把需要的文件都抓取完毕之后,才关闭连接。http1.0 使用的是短链接,http1.1 之后采用的就是长连接了。并且,http1.1 之后还加入了缓存,避免频繁和服务端产生连接,提高了网页的响应速度,节省了带宽。
请求报文
请求报文格式:
- 请求首行(请求行)。
GET / POST 资源路径?参数 HTTP/1.1
- 请求头信息。(请求头)
- 空行;
- 请求体。POST 请求才有请求体。
请求行示例:
GET /web01/regist.html HTTP/1.1
请求头示例:
-使用的是键值对的方式存储,键:值
-浏览器支持的文件类型
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
-浏览器支持的压缩格式
Accept-Encoding: gzip, deflate, br
-浏览器支持的语言
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
-长连接
Connection: keep-alive
-虚拟主机地址
Host: localhost:8023
-当前页面的上一个页面的路径,可以通过此路径跳转回上一个页面,广告计费,防止盗链
Referer: http://localhost:8023/web01/regist.html
-请求协议的自动升级http的请求,服务器是https的,会自动升级为https
Upgrade-Insecure-Requests: 1
-用户系统信息
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
请求体示例:
username=zhangsan&password=123
响应报文
- 响应首行(响应行)。
协议/版本 状态码 状态码描述
- 响应头信息(响应头)。
- 空行;
- 响应体。
响应行示例:
HTTP/1.1 200 OK
通常会出现的状态码有:
- 200:请求成功,浏览器会把响应体内容显示在浏览器中。
- 302:重定向,当响应码为302时,表示服务器要求浏览器重新发一个请求,服务器会发送一个响应头 Location 指定新请求的 URL 地址。
- 304:使用了本地缓存。
- 404:请求的资源没有找到,说明客户端错误的请求了不存在的资源。
- 405:请求的方式不允许。
- 500:请求资源找到了,但是服务器内部出现了错误。
响应头示例:
-表示响应回来的数据有多长,为了方便客户端进行校验
Content-Length: 4473
-用于告诉客户端响应体里面的数据是什么类型的数据
Content-Type: text/html
-长连接多长时间没有使用就会关掉
Keep-Alive: timeout=20
-长连接
Connection: keep-alive
响应体示例:
响应报文是服务端向客户端发送的,故响应体解析后实际上就是相对应的 html、css、js 等文件。
Servlet
Servlet (server applet)是运行在服务端的 Java 小程序,是 sun 公司提供一套定义动态资源的规范。从代码层面上讲就是一个接口。用来接收、处理客户端请求、响应给浏览器的动态资源。
在整个 Web 应用中,Servlet 主要负责接收处理请求、协同调度功能以及响应数据。我们可以把 Servlet 看作是 Web 应用中的控制器。不是所有的 Java 类都能用于处理客户端请求,能处理客户端请求并做出响应的一套技术标准就是 Servlet。Servlet 是运行在服务端的,所以 Servlet 必须在 web 项目中开发并且在 Tomcat 这样的服务容器中运行。
动态资源和静态资源
静态资源:无需在程序运行时通过代码运行生成的资源,在程序运行之前就写好的资源,例如:html、css、js、img、音频文件和视频文件。
动态资源:需要在程序运行时通过代码运行生成的资源,在程序运行之前无法确定的数据,运行时动态生成,例如 Servlet、Thymeleaf。动态资源指的不是视图上的动画效果或者是简单的人机交互效果。
Servlet 运行流程
在 http 协议下,浏览器和服务器在进行数据交互的时候需要发送请求报文或者响应报文。Tomcat 在接收到请求报文之后,会将请求报文当中的信息(请求行、请求头、请求体)封装为一个 HttpServletRequest
对象,意味着这个对象包含所有请求信息。同理,Tomcat 在往浏览器发送数据的时候,会将要响应出去的信息封装成一个 HttpServletResponse
对象,该对象最后会被转换为响应报文(响应行、响应头、响应体)。
在请求动态资源时,请求行中包含了资源路径,Tomcat 会根据这个路径去找到对应的 Servlet
实现类对象,自动调用当中的 service
方法。由此,我们可以得知,相对应的动态资源处理的 Java 代码,应该写在对应的 Servlet 实现类对象重写的 service
方法中(该实现类对象需要实现 Servlet
接口或者通过继承 HttpSerlvet
类间接继承 Servlet
接口)。
service
方法有两个参数,分别是 HttpServletRequest request
和 HttpServletResponse response
,Tomcat 调用了 service
方法后,会从 request
对象中获取请求的所有信息,根据当中的参数和代码生成要响应给客户端的数据,最后将数据打包封装入 response
对象中返回。
Servlet 开发流程
首先,编写好对应的 html 文件,确定好表单要往哪个地方提交。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="userServlet" method="get">
用户名:<input type="text" name="username" ><br>
<input type="submit" value="校验">
</form>
</body>
</html>
编写对应的 Servlet
实现类:
package cn.hnu.servlet;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*
* servlet开发流程
* 1.创建java web项目,同时将tomcat添加为当前项目的依赖
* 2.重写service方法 service(HttpServletRequest req, HttpServletResponse resp)
* 3.在service方法中,定义业务处理代码
* 4.在web.xml中配置Servlet对应的请求映射路径
* */
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//1.从request对象中获取请求中的任何信息
String username = request.getParameter("username");
//根据参数名获取参数值,无论参数是在url?之后还是在请求体中
//2.处理业务的代码
String info = "<h1>YES</h1>";
if("huihui".equals(username)) {
info = "<h1>NO</h1>";
}
//3.设置Content-Type响应头
response.setContentType("text/html");
//4.将要响应的数据放入response中
PrintWriter writer = response.getWriter();//该方法返回的是一个向响应体中打印字符的打印流
writer.write(info);
}
}
最后,编写对应的 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_6_0.xsd"
version="6.0">
<!--
1.配置Servlet类,并起一个别名
servlet-class 用于告诉tomcat对应的要实例化的servlet类
servlet-name 用于关联请求的映射路径
-->
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>cn.hnu.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/userServlet</url-pattern>
</servlet-mapping>
</web-app>
Servlet_jar 包导入和 Content-Type 问题
Servlet-api.jar 包:编码的时候需要,运行的时候,在服务器的环境中,由服务器(Tomcat)提供。因此,在我们的 JavaWeb 项目中,在打包构建的时候,是无需携带此 jar 包的。
Contecnt-Type 响应头:属于 MIME 类型(又称媒体类型、文件类型或者响应的数据类型)响应头,MIME 类型用于告诉客户端响应的数据是什么类型的数据,客户端以此类型决定用什么方式解析响应。客户端发送请求报文给 Tomcat 之后,Tomcat 需要回应一个响应报文,这个时候,Tomcat 会把需要传递给客户端的文件作为响应体传输出去。而响应头中的 Content-Type 决定了客户端要以什么方式来解析这个响应体。在 Tomcat 的conf/web.xml
文件中,配置了相对应的 Content-Type 信息,使得文件类型可以和响应数据类型对应。如果没有设置 Content-Type,默认以 html 的方式解析数据。
Servlet_url-pattern 的一些特殊写法和问题
当我们使用 url 在浏览器中访问资源时,假设我们的 url 为:http://localhost:8080/demo02/s1
,http 就是传输协议,而 localhost:8080 就是本机服务器和 Tomcat 端口号,demo02 则帮助 Tomcat 定位到我们需要的项目文件。而最终,Tomcat 会在 demo02 这个项目文件中的 web.xml
配置文件里找寻 s1 的映射路径,查看到底要调用哪个 Servlet
实现类。
web.xml 文件示例:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<servlet>
<servlet-name>servlet1</servlet-name> <!-- 3.定位别名为servlet1的实现类 -->
<servlet-class>cn.hnu.servlet.Servlet1</servlet-class> <!-- 4.最终找到了实现类的具体位置 -->
<!-- 最后,利用反射获取该实现类的字节码文件,再利用反射去调用当中的service方法 -->
</servlet>
<servlet-mapping>
<servlet-name>servlet1</servlet-name> <!-- 2.s1的映射路径对应的别名为servlet1 -->
<url-pattern>/s1</url-pattern> <!-- 1.首先找到s1的映射路径 -->
</servlet-mapping>
</web-app>
关于特殊写法:
<?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_6_0.xsd"
version="6.0">
<!--
一个servlet类可以同时对应多个url-pattern,但是一个url-pattern只能对应一个servlet
一个servlet标签可以同时对应多个servlet-mapping标签
url-pattern特殊写法
1.精确匹配 例如:/servlet1
2.模糊匹配
会利用*作为通配符,*在哪里,哪里就是模糊的
/ 匹配全部,但不包含jsp文件
/* 匹配全部,包含了jsp文件
/a/* 匹配前缀,后缀模糊
*.action 前缀模糊,匹配后缀
-->
<servlet>
<servlet-name>servlet1</servlet-name>
<servlet-class>cn.hnu.servlet.Servlet1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet1</servlet-name>
<url-pattern>/s1</url-pattern> <!-- 精确匹配 -->
<url-pattern>/xx1</url-pattern> <!-- 一个servlet类可以同时对应多个url-pattern -->
</servlet-mapping>
<servlet-mapping><!-- 一个servlet标签可以同时对应多个servlet-mapping标签 -->
<servlet-name>servlet1</servlet-name>
<url-pattern>/a</url-pattern>
<url-pattern>/b</url-pattern>
</servlet-mapping>
</web-app>
Servlet 注解方式配置
为了解决编写配置文件的麻烦,可以使用 WebServlet
注解来配置映射路径。如果在注解中配置了映射路径,就不要在 web.xml
文件中配置了。
如果抛出“启动子级时出错”的异常,需要检查注解当中的路径格式是否正确(前面要加/
),或者检查是否有路径发生冲突。
@WebServlet("/userServlet") //WebServlet注解配置映射路径
//@WebServlet(urlPatterns={"/s1", "/x1", "/servlet1"}) 匹配多个路径
public class UserServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//...
}
}
Servlet 生命周期
Servlet 对象是 Servlet 容器创建的,生命周期方法都是由容器(目前我们使用的 Servlet 容器是 Tomcat)来调用的。这一点和我们之前所编写的代码有很大的不同。在今后的学习中我们会看到,越来越多的对象交给容器框架来创建,越来越多的方法由容器或者框架来调用,开发人员要尽可能多的将精力放在业务逻辑的实现上。
Servlet 生命周期:
- 实例化。(通过构造器实例化,第一次请求的时候执行,即第一次刷新网页的时候会执行。通过修改
loadOnStartup
可以修改实例化的顺序,如果序号冲突了,Tomcat 会自动协调启动顺序。这里建议令loadOnStartup >= 6
,原因是 1 - 5 已经被官方占用了) - 初始化。(调用
init
方法初始化,构造完毕后马上执行) - 接收请求,处理服务。(调用
service
方法,每次请求的时候执行,即刷新网页的时候就会执行) - 销毁。(调用
destroy
方法销毁,关闭服务的时候会执行)
Servlet 实现类在 Tomcat 之中是单例的,创建在 Tomcat 自己的堆区当中。而 Tomcat 是允许多个客户端来进行连接的,多个客户端发送请求意味着要产生多个线程,每个线程对应有着自己的线程栈,每个线程栈当中都会去调用 service
方法。如果 service
方法需要访问成员变量的话,多个线程栈当中的 service
方法会去访问堆中同一个成员变量。由此可见,这会引发线程安全问题。所以强烈不建议在 service
方法当中去改变成员变量的值。
Default-Servlet:
当我们请求动态资源的时候,Tomcat 会根据路径查询对应的 Sevlet 实现类。但是当我们请求静态资源时,这些实现类明显不符合要求,Tomcat 找了一圈发现没有一个实现类符合抓取静态资源的要求时,会把请求任务交给DefaultServlet
处理。这个时候,DefaultServlet
将调用自身方法,去文件中找到所需要的静态资源,利用 IO 流复制文件,然后分析文件的Content-Type
和大小,最终,封装成对应的Content-Type
和Content-Length
信息放入response
中,传递出去而后形成最终的响应报文。
Servlet 继承结构
顶级Servlet
接口:
public interface Servlet {
//初始化方法,构造完毕之后由Tomcat自动调用完成初始化功能
void init(ServletConfig var1) throws ServletException;
//获得ServletConfig对象的方法
ServletConfig getServletConfig();
//接收用户请求,处理请求后响应请求
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
//返回Servlet字符串形式描述信息的方法
String getServletInfo();
//销毁方法,往往用于做资源的释放工作
void destroy();
}
顶级Servlet
接口下方有一个实现类,称为GenericServlet
,是一个抽象类。侧重除了 service
方法以外的其他方法的基础处理。
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() { //构造器
}
public void destroy() {
//将抽象方法重写为空方法,这种实现叫做平庸实现
//好处便是继承本类之后该方法可重写也可不重写
}
//....部分代码省略....
//Tomcat在调用init方法时会读取配置信息进入ServletConfig对象,并将该对象传入init方法中
public void init(ServletConfig config) throws ServletException {
//将config对象存储为当前属性
this.config = config;
//调用重载的无参的init方法
this.init();
}
public void init() throws ServletException {
//平庸实现,目的是为了让程序员重写该方法
//在带有config参数的init方法中调用本方法
//即可在处理config对象信息的同时,按照程序员自己的要求进行初始化操作
}
//....部分代码省略....
//service方法,作了抽象声明
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
//....部分代码省略....
}
GenericServlet
下方还有一个抽象类 HttpServlet
继承。侧重 service
方法的处理。
public abstract class HttpServlet extends GenericServlet {
public void service(ServletRequest req,
ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {//参数的父转子
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
//调用下方重载的service方法
this.service(request, response);
}
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod(); //获取请求的方式 GET POST DELETE...
long lastModified;
//根据请求的方式调用相对应的do...代码
if (method.equals("GET")) {
//...省略部分代码...
this.doGet(req, resp);
} else if (method.equals("HEAD")) {
//...省略部分代码...
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
//如果不重写service方法,也可以重写doPost或者doGet方法
protected void doPost(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_post_not_supported");
this.sendMethodNotAllowed(req, resp, msg);
}
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
this.sendMethodNotAllowed(req, resp, msg);
}
private void sendMethodNotAllowed(HttpServletRequest req,
HttpServletResponse resp, String msg) throws IOException {
String protocol = req.getProtocol();
if (protocol.length() != 0 && !protocol.endsWith("0.9") && !protocol.endsWith("1.0")) {
resp.sendError(405, msg); //故意响应405请求方式不允许
//所以自己定义的Servlet类如果不重写service方法的话,最终是会响应405的
} else {
resp.sendError(400, msg);
}
}
}
我们自定义的 Servlet
类继承了 HttpServlet
然后重写了 service
方法:
public class MyServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//接收用户信息
//做出响应
}
}
最终总结如下:
- 部分程序员推荐在
servlet
中重写do...
处理请求。例有是service
方法中可能做了一些处理,如果我们直接重写service
方法的话,父类中service
方法处理的功能则失效。 - 目前直接重写
service
也没有问题。 - 后续使用
SpringMVC
框架后,我们则无需继承HttpServlet
了,处理请求的方法也无需do... / service
了。 - 如果
doGet
和doPost
中方式一致,可以在其中一个方法中调用另一个方法。 - 继承
HttpServlet
之后,要么重写service
方法,要么重写doGet
或者doPost
方法。
ServletConfig
ServletConfig
是为 Servlet
提供初始配置参数的一种对象,每个 Servlet
都有自己独立唯一的 ServletConfig
对象。容器会为每个 Servlet
实例化一个 ServletConfig
对象,并通过 Servlet
生命周期的 init
方法传入给 Servlet
作为属性。
我们可以在 web.xml
配置文件中利用<init-param>
进行如下的配置:
<?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_6_0.xsd"
version="6.0">
<servlet>
<servlet-name>servlet1</servlet-name>
<servlet-class>cn.hnu.servlet.Servlet1</servlet-class>
<!-- 配置servlet的初始参数 -->
<init-param>
<param-name>keya</param-name>
<param-value>valuea</param-value>
</init-param>
<init-param>
<param-name>keyb</param-name>
<param-value>valueb</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>servlet1</servlet-name>
<url-pattern>/servlet1</url-pattern>
</servlet-mapping>
</web-app>
Tomcat 会自动生成一个ServletConfig
对象,当 Tomcat 读取到配置文件中的信息之后,会将这些信息传入到自身的ServletConfig
对象中,然后再调用Servlet
实现类的init
方法将ServletConfig
对象传入,进行初始化的信息配置。
以下是ServletConfig
对象的基本用法:
package cn.hnu.servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
public class Servlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获取Config对象
ServletConfig servletConfig = getServletConfig();
//获取初始配置信息
//根据参数名获取参数值
String keya = servletConfig.getInitParameter("keya");
System.out.println(keya); //valuea
//获取所有初始参数的名字
//Enumeration是早期的iterator
Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
//hasMoreElements会判断有没有下一个参数,如果有,返回true,如果没有返回false
while (initParameterNames.hasMoreElements()) { //相当于iterator.hasNext()
String name = initParameterNames.nextElement();//相当于iterator.next()
//再根据参数名获取参数值
System.out.println(servletConfig.getInitParameter(name));
}
}
}
利用注解的方式也可以进行初始值的配置:
@WebServlet(
urlPatterns = "/servlet1",
initParams = {@WebInitParam(name = "keya", value = "valuea"),
@WebInitParam(name = "keyb", value = "valueb")}
)
public class Servlet1 extends HttpServlet {
//...
}
ServletContext
ServletContext
对象有称呼为上下文对象,或者叫应用域对象。容器会为每一个 app 创建一个独立的唯一的 ServletContext
对象。也就是说,这个 ServletContext
对象是所有 Servlet
共享。ServletContext
可以为所有的 Servlet
提供初始的配置参数。
ServletConfig
和 ServletContext
的关系如下:
//app
// init-param ---> ServletConfigA ---> ServletA <--+
// |
// init-param ---> ServletConfigB ---> ServletB <--+--- ServletContext <--- Context-param
// |
// init-param ---> ServletConfigC ---> ServletC <--+
web.xml
配置文件示例:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!-- 利用context-param配置所有servlet的初始信息 -->
<context-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</context-param>
<context-param>
<param-name>username</param-name>
<param-value>zhangsan</param-value>
</context-param>
</web-app>
以下是 ServletContext
对象的基本用法:
package cn.hnu.servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet(
urlPatterns = "/servlet1",
initParams = {@WebInitParam(name = "keya", value = "valuea"),
@WebInitParam(name = "keyb", value = "valueb")}
)
public class Servlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获取ServletContext对象,有三种方法
ServletContext servletContext1 = getServletContext();
ServletContext servletContext2 = this.getServletConfig().getServletContext();
ServletContext servletContext3 = req.getServletContext();
//上述三个servletContext均相同
//根据名字获取参数
String encoding = servletContext1.getInitParameter("encoding");
System.out.println(encoding);
//获取所有参数名字
Enumeration<String> initParameterNames1 = servletContext1.getInitParameterNames();
while (initParameterNames1.hasMoreElements()) {
String name = initParameterNames1.nextElement();
System.out.println(servletContext1.getInitParameter(name));
}
}
}
ServletContext 获取资源路径和上下文路径
方法 | 说明 |
---|---|
public String getRealPath() |
获取资源路径 |
public String getContextPath() |
获取上下文路径 |
package cn.hnu.servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servlet3")
public class Servlet3 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//向项目的upload目录中写入一个文件
//获取ServletContext对象
ServletContext servletContext = getServletContext();
//获取一个指向项目部署位置下的某个文件或目录路径
String path = servletContext.getRealPath("upload");
System.out.println(path);
//接下来就可以利用IO流对这个文件或者文件夹进行操作了...
//后续会学习在项目中使用绝对路径和相对路径来找项目资源
//使用绝对路径来进行查找的时候需要带上下文路径
//获得项目部署的上下文路径
String contextPath = servletContext.getContextPath();
System.out.println(contextPath);
}
}
ServletContext 域对象相关的 API
- 域对象:一些用于存储数据和传递数据的对象。传递数据不同的范围,我们称之为不同的域,不同的域对象代表不同的域。共享数据的范围也不同。
ServletContext
代表应用,所以ServletContext
域也叫应用域,是 webapp 中最大的域,可以在本应用内实现数据的共享和传递。- webapp 中的三大域对象,分别是:应用域、会话域、请求域。
API 如下:
方法 | 说明 |
---|---|
void setAttribute(String key, Object value) |
向域中存储、修改数据 |
Object getAttribute(String key) |
获得域中的数据 |
void removeAttribute(String key) |
移除域中的数据 |
package cn.hnu.servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servlet3")
public class Servlet3 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//设置数据
ServletContext servletContext = getServletContext();
servletContext.setAttribute("keya", "valuea");
servletContext.setAttribute("keya", "aaa"); //aaa会覆盖valuea
//拿取数据
Object oa = servletContext.getAttribute("keya");
//移除数据
servletContext.removeAttribute("keya");
}
}
HttpServletRequest
获取请求行信息相关的 API:
方法 | 说明 |
---|---|
StringBuffer getRequestURL() |
获取客户端请求的 url |
String getRequestURI() |
获取客户端请求项目中的具体资源 |
int getServerPort() |
获取客户端发送请求时的端口 |
int getLocalPort() |
获取本应用在所在容器的端口 |
int getRemotePort() |
获取客户端程序的端口 |
String getScheme() |
获取请求协议 |
String getProtocol() |
获取请求协议及版本号 |
String getMethod() |
获取请求方式 |
获得请求头信息相关:
方法 | 说明 |
---|---|
String getHeader(String headerName) |
根据头名称获取请求头 |
Enumeration<String> getHeaderNames() |
获取所有的请求头名字 |
String getContentType() |
获取 content-type 请求头 |
获得请求参数相关:
方法 | 说明 |
---|---|
String getParameter(String parameterName) |
根据请求参数名获取请求单个参数值 |
String[] getParameterValue(String parameterName) |
根据请求参数名获取多个参数值数组 |
Enumeration<String> getParameterNames() |
获取所有请求参数名 |
Map<String, String[]> getParameterMap() |
获取所有请求参数的键值对组合 |
BufferedReader getReader() |
获取读取请求体的字符输入流(用于读取非键值对形式的数据) |
ServletInputStream getInputStream() |
获取读取请求体的字节输入流(用于读取文件) |
int getContentLength() |
获取请求体长度的字节数 |
其他API:
方法 | 说明 |
---|---|
String getServletPath() |
获取请求的 Servlet 的映射路径 |
ServletContext getServletContext() |
获取 ServletContext 对象 |
Cookie[] getCookies() |
获取请求中的所有 cookie |
HttpSession getSession() |
获取 Session 对象 |
void setCharacterEncoding(String ecoding) |
设置请求体字符集 |
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet("/servlet04")
public class Servlet04 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//行相关 GET/POST uri http/1.1
System.out.println(req.getMethod()); //获取请求方式
System.out.println(req.getScheme()); //获取请求协议
System.out.println(req.getProtocol()); //获取协议及版本号
System.out.println(req.getRequestURI());//获取请求路径,项目内的资源路径 /demo03/servlet04
System.out.println(req.getRequestURL());//获取请求路径,项目内资源的完整路径 http://ip:port/demo03/servlet04
/*
* URI 统一资源标识符 interface URI 资源定位的要求和规范
* URL 统一资源定位符 class URL implements URI 网络中的一个具体的资源路径
* */
System.out.println(req.getLocalPort());//获取本应用容器的端口号 Tomcat:8080
System.out.println(req.getRemotePort());//获取客户端软件的端口号
System.out.println(req.getServerPort());//获取客户端发请求时使用的端口号
/*
* 一种可能出现的情况如下:
*
* 客户端 --> 代理服务器 --> Tomcat
*
* 这个时候客户端软件的端口号和客户端发送请求时使用的端口号就不一致了
* */
//头相关 key:value
System.out.println("Accept:" + req.getHeader("Accept"));//根据名字单独获取某个请求头
Enumeration<String> headerNames = req.getHeaderNames();//获取本次请求中所有的请求头
while(headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
System.out.println(name + ":" + req.getHeader(name));
}
}
}
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
@WebServlet("/servlet05")
public class Servlet05 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//请求参数相关
//接收请求中的参数
//根据参数名获取键值对形式的参数
String username = req.getParameter("username");
System.out.println(username);
String userpwd = req.getParameter("password");
System.out.println(userpwd);
//根据参数名获得多个参数值
String[] hobbies = req.getParameterValues("hobby");
System.out.println(Arrays.toString(hobbies));
//获取所有参数名
Enumeration<String> parameterNames = req.getParameterNames();
while(parameterNames.hasMoreElements()) {
String pname = parameterNames.nextElement();
String[] parameterValues = req.getParameterValues(pname);
System.out.println(Arrays.toString(parameterValues));
}
//返回所有参数的Map集合 key = 参数名 value = 参数值
Map<String, String[]> parameterMap = req.getParameterMap();
for (Map.Entry<String, String[]> stringEntry : parameterMap.entrySet()) {
System.out.println(stringEntry.getKey() + " = " +
Arrays.toString(stringEntry.getValue()));
}
//其他API
System.out.println(req.getServletPath());
}
}
HttpServletResponse
设置响应行相关 API:
方法 | 说明 |
---|---|
void setStatus(int code) |
设置响应状态码 |
设置响应头相关:
方法 | 说明 |
---|---|
void setHeader(String headerName, String headerValue) |
设置、修改响应头键值对 |
void setContentType(String contentType) |
设置 content-type 响应头及响应字符集(设置MIME类型) |
设置响应体相关:
方法 | 说明 |
---|---|
PrintWriter getWriter() |
获得响应体放入信息的字符输出流(用于响应普通数据) |
ServletOutputStream getOutputStream() |
获得响应体放入信息的字节输出流(用于响应文件) |
void setContentLength(int length) |
设置响应体的字节长度,其实就是在设置content-type响应头 |
其他 API:
方法 | 说明 |
---|---|
void sendError(int code, String message) |
向客户端响应错误信息的方法,需要指定响应码和响应信息 |
void addCookie(Cookie cookie) |
向响应体中增加 cookie |
void setCharacterEncoding(String encoding) |
设置响应体字符集 |
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/servlet06")
public class Servlet06 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
String info = "<h1>hello world!</h1>";
//设置响应行相关的API
resp.setStatus(200);
//设置响应头相关API
resp.setHeader("aaa", "valuea");
resp.setHeader("Content-Type", "text/html");
resp.setContentType("text/html");
resp.setContentLength(info.length());
//设置响应体内容API
PrintWriter writer = resp.getWriter();
writer.write(info);
}
}
请求转发和响应重定向
请求转发和响应重定向是 web 应用间接访问项目资源的两种手段,也是 Servlet 控制页面跳转的两种手段。请求转发通过 HttpServletRequest
实现,响应重定向通过 HttpServletResponse
实现。
请求转发举例:张三找李四借钱,李四没有,李四找王五,让王五借给张三。
响应重定向举例:张三找李四借钱,李四没有,李四让张三找王五借钱,张三自己再去找王五借钱。
请求转发
客户端向 Tomcat 发送请求后,Tomcat 产生了 request
对象和 response
对象,假设把这两个对象交给 ServletA
处理,ServletA
判断后发现自己没办法处理,于是将这两个对象再传递给 ServletB
进行处理,ServletB
处理完毕之后,最终再转换成响应报文交还给客户端。
请求转发时,请求和响应对象会继续传递给下一个资源,意味着请求中的参数可以继续向下传递。请求转发是服务器内部的行为,客户端只产生了一次请求,客户端是不知道内部的消息传递的。
请求转发的特点:
- 请求转发是通过 HttpServletRequest 对象实现的。
- 请求转发是服务器内部行为,对客户端是屏蔽的。
- 客户端只产生了一次请求,服务端只产生了一对 request 和 response 对象。
- 客户端的地址栏是不变的。
- 请求的参数是可以继续传递的。
- 目标资源可以是动态资源,也可以是类似 html 文件的静态资源。
- 目标资源也可以是 WEB-INF 下受保护的资源,该方式也是 WEB-INF 下资源的唯一访问方式。
- 目标资源不可以是外部的资源。
request 对象通过调用getRequestDispatcher
方法获得转发器,然后再让转发器调用forward
方法进行请求转发:
package cn.hnu.servlet;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
System.out.println("servletA执行了");
String money = req.getParameter("money");
System.out.println("servletA: " + money);
//请求转发给servletB,获得请求转发器
RequestDispatcher requestDispatcher = req.getRequestDispatcher("servletB");
//除了转发给其他servlet实现类,还可以转发给其他文件
//RequestDispatcher requestDispatcher = req.getRequestDispatcher("aaa.html");
//RequestDispatcher requestDispatcher = req.getRequestDispatcher("WEB-INF/b.html");
//让请求转发器做出转发动作
requestDispatcher.forward(req, resp);
}
}
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletB")
public class ServletB extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
System.out.println("servletB执行了");
String money = req.getParameter("money");
System.out.println("servletB: " + money);
}
}
响应重定向
客户端向 Tomcat 发送请求后,Tomcat 产生了 request
对象和 response
对象,假设把这两个对象交给 ServletA
处理,ServletA
判断后发现自己没办法处理,于是设置响应状态码为302,接着再设置一个响应头 Location,其值为对应目标资源的 url,最后返回响应报文给客户端。客户端收到响应报文后再根据 Location 的地址去找响应的资源。所以,两次转发的 request
对象和 response
对象是不一致的。
响应重定向特点:
- 重定向是通过
HttpServletResponse
对象实现。 - 响应重定向是在服务端提示下的,客户端自发的一种行为。
- 客户端的地址栏是变化的,客户端产生了多次请求。
- 请求产生了多次,此时请求中的参数不能相互传递。
- 目标资源可以是静态资源。
- 目标资源不可以是 WEB-INF 下的资源。因为相当于是客户端自己去访问受保护资源,故最终会访问失败。
- 目标资源可以是外部资源。
- 同样能够实现页面跳转,考虑到路径问题,优先使用响应重定向。
response
对象通过调用 sendRedirect
方法进行重定向:
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servlet1")
public class Servlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//接收用户请求
System.out.println("servlet1执行了");
//响应重定向
//resp.setStatus(302);
//resp.setHeader("location", "servlet2");
resp.sendRedirect("servlet2"); //同时设置了302状态码和Location响应头的值
//目标资源可以是外部资源
//resp.sendRedirect("https://www.leetcode.com");
}
}
web 乱码和路径问题
web 乱码
Tomcat 控制台乱码:修改 tomcat/conf/logging.properties
中的 UTF-8 为 GBK 即可。
GET 提交乱码:一种方式是更改 html 文件中的 charset
属性,即更改客户端数据的编码方式;又因为 GET 方式提交会把数据放在 url 中提交,故我们可在 tomcat/conf/server.xml
文件第68行的标签中添加 URIencoding="GBK"
,即更改后端的解码方式。
POST 提交乱码:POST 提交方式不再把数据缀在 url 后提交,而是放到请求体中进行提交,所以我们需要更改后端的解码方式,利用 response
对象的setCharacterEncoding
方法来设置请求体解码时使用的字符集。
响应乱码:Tomcat10 中,响应体默认使用 UTF-8 编码方式,但是客户端不一定使用 UTF-8 的解码方式,故我们需要设置响应体的编码字符集和客户端的保持一致,这个时候,依旧可以利用 response
对象的setCharacterEncoding
方法来设置请求体解码时使用的字符集。又可以通过 Content-Type
响应头指定字符集,告诉客户端,应该使用哪一种解码方式进行解码:response.setContentType("text/html;charset=UTF-8")
。两种方法可以同时使用,保证响应体的编码和解码一致。
路径问题
前端相对路径问题
<!--
相对路径
相对于当前资源的所在位置,以当前资源所在位置为出发点寻找目标资源
语法:不以/开头
./表示当前资源的路径
../表示当前资源的上一层路径
-->
假设客户端的请求路径为:http://localhost:8080/demo05/index.html
,那么该路径称为当前资源的请求路径,而当前资源是 index.html
,而当前资源的所在路径为:http://localhost:8080/demo05/
。相对路径就是在当前资源所在路径后直接拼接目标资源的路径,然后发送请求找目标资源。
假设现在有如下标签:<img src="static/img/Elieen2.jpg" />
,则目标资源路径是 static/img/Elieen2.jpg
(static 和 index.html 均在 web 目录之下)。这个时候相对路径会直接拼接为:http://localhost:8080/demo05/static/img/Elieen2.jpg
。
假设当前资源的请求路径为:http://localhost:8080/demo05/a/b/c/test.html
,则在拼接的时候是往 a/b/c
后面拼接的,假设现在有如下标签:<img src="static/img/Elieen2.jpg" />
,则最后拼接出来的路径会是:http://localhost:8080/demo05/a/b/c/static/img/Elieen2.jpg
,会导致资源找不到。这个时候,需要用到../
返回上一级目录,标签应该写为:<img src="../../../static/img/Elieen2.jpg" />
。
假设当前利用请求转发访问 WEB-INF 目录下的内容(WEB-INF/views1.html
),当前资源的请求路径为:http://localhost:8080/demo05/view1Servlet
,则当前资源的所在路径是:http://localhost:8080/demo05/
,所以要拼接的话,不能直接看磁盘结构写出<img src="../../static/img/Elieen2.jpg" />
,因为这样会导致抵消掉两个目录结构,我们应该把../
去掉。
故我们不能只看项目目录结构而去分析路径,而是要通过分析客户端的 url 请求路径来进行路径的判断。
前端绝对路径问题
<!--
绝对路径
始终以固定的路径作为出发点去找目标资源,和当前资源的所在路径没有关系
语法:以/开头
不同的项目中,固定的路径出发点可能不一致
-->
在绝对路径的配置下,客户端会在 localhost:8080
后方直接开始进行拼接,所以绝对路径要求我们写出项目的上下文路径:<img src="/demo05/static/img/Elieen2.jpg" />
。绝对路径相对于相对路径而言,目标资源路径的写法不会受到当前资源路径的影响,不同的位置,绝对路径写法一致。
但是绝对路径需要我们补充上下文路径,相对于项目而言,上下文路径是可能发生改变的。一旦项目的上下文路径发生了了改变,我们再回去修改路径的内容,就比较麻烦。
这个时候,我们可以在 html 文件中 head
标签的位置插入一个 base
标签,通过 base
标签当中的 href 属性,定义相对路径的公共前缀,通过公共前缀把一个相对路径转换为一个绝对路径。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
<!--
当前页面中,所有不加任何修饰的相对路径前,会自动补充href的内容
-->
<base href="/demo05/">
</head>
<body>
<img src="static/img/Elieen2.jpg" />
</body>
</html>
重定向和请求转发中的路径问题
重定向问题与前端路径问题一致,使用相对路径时应该注意,适当使用 ../
返回上一级文件。
@WebServlet("x/y/z/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//重定向,相对路径
resp.sendRedirect("../../../servletB");
//重定向,绝对路径
resp.sendRedirect("/demo05/servletB");
//绝对路径,利用ServletContext获取资源上下文路径
ServletContext sc = req.getServletContext();
String contextPath = sc.getContextPath();
resp.sendRedirect(contextPath + "/servletB");
}
}
请求转发的路径问题,在相对路径的处理上,与重定向一致。绝对路径的处理,只需要加上 /
即可。
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//请求转发
//这个时候的 / 代表的就是 http://localhost:8080/demo05/
req.getRequestDispatcher("/servletB").forward(req, resp);
}
}
解决上下文路径问题
所以,我们考虑把项目的上下文路径直接写成一个 /
,之后的写法我们就直接在相对路径前面加上 /
就可以。
会话管理
会话指的是客户端和服务端的一次请求和响应,也可以叫做一次问答。HTTP 是无状态协议,即 HTTP 协议不对请求和响应之间的通信状态进行保存,也就是说,HTTP 协议这个级别,不会对请求和响应做持久化处理,服务器没记录浏览器的特征。
会话管理一般通过两种方式进行,一种是 Cookie,另一种是 Session。Cookie 是在客户端保留少量数据的技术,主要通过响应头向客户端响应一些客户端需要保存的信息。Session 是在服务器保留更多数据的技术,主要通过 HttpSession 对象保存一些和客户端相关的信息,一个 Session 对象对应了一个客户端。Cookie 和 Session 配合记录请求状态。
可以理解为去银行存款,第一次去的时候,银行既会给我们开银行账户(Session),也会给我们一张银行卡(Cookie),等到我们第二次去银行的时候,便可以根据银行卡(Cookie)找到属于我们自己的银行账户(Session)。
Cookie
Cookie 是一种客户端会话技术,Cookie 由服务器产生,它是服务器存放在浏览器的一小份数据,浏览器以后每次访问该服务器的时候都会将这小份数据携带到服务器去。
客户端第一次发送请求给服务端,这个时候,request
对象并未携带 Cookie,服务端发现该情况后,在响应时,会将 Cookie 封装入 response
之中进行返回,此后客户端上便保留了 Cookie。客户端第二次发送请求时,request
对象便会携带 Cookie,而服务端便可以使用该 Cookie 进行其他的操作。
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//创建cookie
/*
* cookie可以产生多个
* cookie内部是使用键值对的方式存储信息的
* */
Cookie cookie1 = new Cookie("keya", "valuea");
Cookie cookie2 = new Cookie("keyb", "valueb");
//将cookie放入response对象
resp.addCookie(cookie1);
resp.addCookie(cookie2);
}
}
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletB")
public class ServletB extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获取请求中携带的cookie
Cookie[] cookies = req.getCookies();
//请求中的多个cookie会进入上方数组,请求中如果没有cookie,数组为null
if(null != cookies) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + " = " + cookie.getValue());
}
}
}
}
Cookie 的时效性
默认情况下 Cookie 的有效期是一次会话范围内(浏览器不关闭,Cookie 就一直存在),我们可以通过 Cookie 的 setMaxAge
方法让 Cookie 持久化保存在浏览器上。cookie.setMaxAge(int expiry)
参数单位是秒,表示 Cookie 的持久化时间,如果设置参数为0,表示将浏览器中保存的 Cookie 删除。
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//创建cookie
Cookie cookie = new Cookie("keya", "valuea");
//设置cookie存放时间为5分钟
cookie.setMaxAge(300);
//将cookie放入response对象
resp.addCookie(cookie);
}
}
Cookie 的请求路径
Cookie 在请求的时候,默认都会携带,我们可以通过 setPath
方法对 Cookie 的路径进行设置。让 Cookie 在请求特定资源的时候才携带。
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//创建cookie
Cookie cookie = new Cookie("keya", "valuea");
//设置cookie存放时间为5分钟
cookie.setMaxAge(300);
//设置cookie的发送路径
cookie.setPath("/demo06/servletB");
//将cookie放入response对象
resp.addCookie(cookie);
}
}
Session
HttpSession 是一种保留更多信息在服务端的一种技术,服务器会为每一个客户端开辟一块内存空间,即 session 对象,客户端在发送请求时,都可以使用自己的 session,这样服务端就可以通过 session 来记录某个客户端的状态了。
客户端第一次请求的时候是不会携带 Cookie 的,当服务端检测到没有 Cookie 携带之后,会在后端创建一个 Session 对象用于存储更多数据,同时将一个名为 JSESSIONID
的对象作为 Cookie 返回给 response
对象。此后,客户端只要访问到服务器,其携带的 Cookie 中便带有 JSESSIONID
对象,可以根据这个对象找到属于自己的 Session 对象。Session 用于存放一些比较敏感、安全性需求较高的数据。Session 的使用需要配合 Cookie。
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/servlet1")
public class Servlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//接收请求中的username参数
String username = req.getParameter("username");
//获得session对象
/*会先判断请求中有没有特殊的Cookie(JSESSIONID)
* 如果没有,getSession方法会创建一个新的session返回并且自动存放JSESSIONID的Cookie
* 如果有,那么会根据JSESSIONID去找对应的session对象
* 如果找到了,则返回对应的session对象
* 如果没找到,则创建一个新的session对象并且自动存放JSESSIONID的Cookie
* */
HttpSession session = req.getSession();
//获取sessionId以及判断session的新旧
System.out.println(session.getId());
System.out.println(session.isNew());
//将username存入session
session.setAttribute("username", username);
//客户端响应信息
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("成功");
}
}
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/servlet2")
public class Servlet2 extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获得session对象
HttpSession session = req.getSession();
//读取session当中的数据
System.out.println(session.getId());
System.out.println(session.isNew());
String username =(String) session.getAttribute("username");
System.out.println("servlet2 got username: " + username);
}
}
Session 的时效性
用户量很大之后,Session 对象相应也要创建很多,如果一味创建不释放,那么服务器的内存迟早要被耗尽。客户端关闭行为无法被服务端直接侦测,或者客户端较长时间不操作也经常出现,类似这些情况,就需要对 session 的时限进行限制了。
session 对象在创建之后,默认生存 30 分钟,在 tomcat/conf/web.xml
中可以进行配置。也可以自己在项目的 web.xml
文件中设置 session 的生存时长。
<?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_6_0.xsd"
version="6.0">
<session-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>
也可以在 Java 代码中利用 session.setMaxInactiveInterval()
方法设置最大闲置时间,利用 session.invalidate()
直接让 session 失效(即手动销毁)。
三大域对象
域对象:一些用于存储数据和传递数据的对象,传递数据不同的范围,我们称之为不同的域,不同的域对象代表不同的域,共享数据的范围也不同。
web 项目中,我们需要熟练使用的域对象分别是:请求域、会话域、应用域:
- 请求域:HttpServletRequest,传递数据的范围是一次请求之内及请求转发。
- 会话域:HttpSession,传递数据的范围是一次会话之内,可以跨多个请求。
- 应用域:ServletContext,传递数据的范围是本应用之内,可以跨多个会话。
域对象 API:
方法 | 说明 |
---|---|
void setAttribute(String name, String value) |
向域对象中添加、修改数据 |
Object getAttribute(String name) |
从域对象中获取数据 |
removeAttribute(String name) |
移除域对象中的数据 |
package cn.hnu.servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//向请求域存放数据
req.setAttribute("request", "requestMessage");
//向会话域存放数据
HttpSession session = req.getSession();
session.setAttribute("session", "sessionMessage");
//向应用域存放数据
ServletContext application = getServletContext();
application.setAttribute("application", "applicationMessage");
}
}
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/servletB")
public class ServletB extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获取请求域中的数据
String request = (String) req.getAttribute("request");
System.out.println("请求域:" + request);
//获取会话域中的数据
String session = (String) req.getSession().getAttribute("session");
System.out.println("会话域:" + session);
//获取应用域中的数据
String application = (String) getServletContext().getAttribute("application");
System.out.println("应用域:" + application);
}
}
总结:
- 请求转发时,请求域可以传递数据。(请求域内一般放本业务有关的数据,比如:查询到的所有部门信息)
- 同一个会话内,不用请求转发,会话域可以传递数据。(会话域内一般放本次会话的客户端状态有关的数据,比如:当前客户端登录的用户)
- 同一个APP内,不同的客户端,应用域可以传递数据。(应用域内一般放本程序应用有关的数据,比如:Spring 框架的 IOC 容器)
过滤器
过滤器,是 JavaEE 技术规范之一,作用目标资源的请求进行过滤的一套技术规范,是 JavaWeb 项目中最为实用的技术之一。Filter 的应用包括但不限于:登录检查,解决网站乱码,过滤敏感字符,日志记录,性能分析…
客户端向服务端发送请求后,可以在 Request 和 Servlet 之间介入一个 Filter 对资源进行过滤。
开发过滤器流程:
- 实现 Filter 接口。(
jakarta.servlet.Filter
包下) - 重写过滤方法。
- 配置过滤器(web.xml 或者注解的方式配置)。
Filter API:
方法 | 说明 |
---|---|
default public void init(FilterConfig filterConfig) |
初始化方法,由容器调用并传入初始配置信息 filterConfig 对象 |
public void doFilter(ServletRequest req, ServletResponse resp) |
过滤方法,核心方法。过滤请求,决定是否放行,相应之前的其他处理等都在该方法中 |
default public void destory() |
销毁方法,容器在回收过滤器对象之前调用的方法 |
package cn.hnu.filters;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 日志过滤器,主要记录请求的历史,将日志打印到控制台上
*/
public class LoggingFilter implements Filter {
/*
* 过滤请求和相应的方法
* 1.请求到达目标资源之前先经过该方法
* 2.该方法有能力控制请求是否继续向后到达目标资源
* 3.请求到达目标资源后,响应之前,还会经过该方法
* */
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
/*
* 重写的代码包括:
* 1.请求到达目标资源前的功能代码
* 例如判断是否登录、校验权限是否满足
* 2.放行代码
* 3.response对象在转换成响应报文之前的功能代码
* */
//参数强转
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//请求到达目标资源前的功能代码
//请求到达目标资源前打印日志 yyyy-MM-dd HH:mm:ss ***被访问了
String requestURI = req.getRequestURI();
String dateTime = simpleDateFormat.format(new Date());
System.out.println(requestURI + "在" + dateTime + "被访问了");
long t1 = System.currentTimeMillis();
//放行代码
filterChain.doFilter(servletRequest, servletResponse);
long t2 = System.currentTimeMillis();
//response对象在转换成响应报文之前的功能代码
//响应之前打印日志 ***在yyyy-MM-dd HH:mm:ss的请求耗时x毫秒
System.out.println(requestURI + "在" + dateTime + "的请求耗时" + (t2 - t1) + "毫秒");
}
}
<?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_6_0.xsd"
version="6.0">
<filter>
<filter-name>loggingFilter</filter-name>
<filter-class>cn.hnu.filters.LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loggingFilter</filter-name>
<!--
url-pattern 根据请求的资源路径对指定的请求进行过滤
/* 过滤全部资源
/a/* 过滤以a开头的资源
*.html 过滤以html为后缀的资源
servlet-name 根据请求的servlet的别名,对指定的servlet资源进行过滤
一个filter-mapping中可以出现多个url-pattern或者servlet-name
-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Filter 的生命周期
package cn.hnu.filters;
import jakarta.servlet.*;
import java.io.IOException;
/*
* 1.构造 构造器 项目启动后进行构造
* 2.初始化 init 构造完毕执行
* 3.过滤 doFilter 每次请求便执行
* 4.销毁 destroy 服务关闭时执行
* */
public class LifeCycleFilter implements Filter {
public LifeCycleFilter() {
System.out.println("构造");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化");
System.out.println(filterConfig.getInitParameter("dateTimePattern"));
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("过滤方法");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("销毁方法");
}
}
过滤器链的作用
一个 Web 项目中,可以同时定义多个过滤器,多个过滤器对同一个资源进行过滤时,工作位置有先后,整体形成一个工作链,称之为过滤器链。
过滤器链中各个过滤器的执行顺序与配置文件中<filter-mapping>
标签的配置先后顺序有关。每个过滤器的 doFilter
方法由三个部分构成,分别是:请求前代码、放行代码、响应后代码。过滤器链中代码执行顺序是:Filter1的请求前代码 -> Filter1 的放行代码 -> Filter2的请求前代码 -> Filter2的放行代码 -> Servlet -> Filter2的响应后代码 -> Filter1的响应后代码
。如果是通过注解的方式配置过滤器路径,则影响过滤器执行顺序的是过滤器类名。
Filter 注解
@WebFilter(
filterName = "loggingFilter",
initParams = {@WebInitParam(name = "dateTimePattern", value = "yyyy-MM-dd HH:mm:ss")},
urlPatterns = {"/servletA", "*.html"},
servletNames = {"serlvetBName"}
)
public class LoggingFilter implements Filter {...}
监听器
监听器是专门用于对域对象身上发生的事件或状态改变进行监听和相应处理的对象。是 GOF 设计模式中,观察者模式的典型案例。监听器使用的感受类似于 JS 中的事件,被观察的对象发生某些情况时,自动触发代码的执行。监听器并不监听 web 项目中的所有组件,仅仅是对三大域对象做相关事件的监听。
web 中定义八个监听器接口作为监听器的规范,这八个接口按照不同的标准可以形成不同的分类。
按照监听的对象分类:
- application 域监听器:
ServletContextListener
、ServletContextAttributeListener
。 - session 域监听器:
HttpSessionListener
、HttpSessionAttributeListener
、HttpSessionBindingListener
、HttpSessionActivationListener
。 - request 域监听器:
ServletRequestListener
、ServletRequestAttributeListener
。
按照监听的事件分类:
- 域对象的创建和销毁监听器:
ServletContextListener
、HttpSessionListener
、ServletRequestListener
。 - 域对象数据增删改事件的监听器:
ServletContextAttributeListener
、HttpSessionAttributeListener
、ServletRequestAttributeListener
。 - 其他监听器:
HttpSessionBindingListener
、ServletRequestAttributeListener
。(前者用于监听自身被放入 Session 对象的行为、后者用于监听某个对象在 Session 中的序列化和反序列化,又称为 Session 的钝化和活化)
以后的使用中,ServletContextListener
在 SSM 和 Springboot 中使用比较多,我们可能需要往其中增加一些配置信息。
application 域监听器
ServletContextListener
监听 ServletContext
对象的创建与销毁。ServletContextEvent
对象代表从 ServletContext
对象身上捕获到的事件,通过这个事件对象我们可以获取到 ServletContext
对象。
方法 | 说明 |
---|---|
contextInitialized(SevletContextEvent sce) |
ServletContext 创建时调用 |
contextDestory(ServletContextEvent sce) |
ServletContext 销毁时调用 |
ServletContextAttributionListener
监听 ServletContext
中属性的添加、移除和修改。
方法 | 说明 |
---|---|
attributeAdded(ServletAttributeContextEvent scab) |
向 ServletContext 中添加属性时调用 |
attributeRemoved(ServletAttributeContextEvent scab) |
从 ServletContext 中移除属性时调用 |
attributeReplaced(ServletAttributeContextEvent scab) |
当 ServletContext 中的属性被修改时调用 |
ServletContextAttributeEvent
对象代表属性变化事件,包含方法如下:
方法 | 说明 |
---|---|
getName() |
获取修改或添加的属性名 |
getValue() |
获取被修改或添加的属性值 |
getServletContext() |
获取 ServletContext 对象 |
package cn.hnu.listener;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
@WebListener //监听器不需要考虑路径问题
public class MyApplicationListener
implements ServletContextListener, ServletContextAttributeListener {
//ServletContextListener重写方法
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext application = sce.getServletContext(); //获得当前域对象
System.out.println(application.hashCode() + "应用域初始化了");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContext application = sce.getServletContext();
System.out.println(application.hashCode() + "应用域销毁了");
}
//ServletContextAttributeListener重写方法
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
ServletContext application = scae.getServletContext();
String key = scae.getName();
String value =(String) scae.getValue();
System.out.println(application.hashCode() + "应用域增加了" + key + ":" + value);
}
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
ServletContext application = scae.getServletContext();
String key = scae.getName();
String value =(String) scae.getValue();
System.out.println(application.hashCode() + "应用域移除了" + key + ":" + value);
}
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
ServletContext application = scae.getServletContext();
String key = scae.getName();
String value =(String) scae.getValue(); //获取旧的值
String newValue =(String) application.getAttribute(key); //利用application拿新值
System.out.println(application.hashCode() + "应用域修改了" +
key + ":" + value + "为:" + newValue);
}
}
其他监听器示例
@WebListener
public class MyRequestListener
implements ServletRequestListener, ServletRequestAttributeListener {
//ServletRequestListener重写方法
@Override
public void requestDestroyed(ServletRequestEvent sre) {
//任何一个请求域对象的销毁都会触发该方法
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
//任何一个请求域对象的初始化都会触发该方法
}
//ServletRequestAttributeListener重写方法
@Override
public void attributeAdded(ServletRequestAttributeEvent srae) {
//任何一个请求域中增加了数据都会触发该方法
}
@Override
public void attributeRemoved(ServletRequestAttributeEvent srae) {
//任何一个请求域中移除了数据都会触发该方法
}
@Override
public void attributeReplaced(ServletRequestAttributeEvent srae) {
//任何一个请求域中修改了数据都会触发该方法
}
}
@WebListener
public class MySessionListener
implements HttpSessionListener, HttpSessionAttributeListener {
//HttpSessionListener重写方法
@Override
public void sessionCreated(HttpSessionEvent se) {
//任何一个session域对象的创建都会触发该方法
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
//任何一个session域对象的销毁都会触发该方法
}
//HttpSessionAttributeListener重写方法
@Override
public void attributeAdded(HttpSessionBindingEvent se) {
//任何一个session域中增加了数据都会触发该方法
}
@Override
public void attributeRemoved(HttpSessionBindingEvent se) {
//任何一个session域中移除了数据都会触发该方法
}
@Override
public void attributeReplaced(HttpSessionBindingEvent se) {
//任何一个session域中修改了数据都会触发该方法
}
}
@WebListener
public class SessionBindingListener
implements HttpSessionBindingListener {
//使用时需要将本类对象放入session中或从session中移除
@Override
public void valueBound(HttpSessionBindingEvent event) {
//当前监听器实例放入某个session中作为数据 绑定
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
//当前监听器示例从某个session中移除 解绑定
}
}
public class MyActivationListener implements HttpSessionActivationListener {
//使用时需要将本类对象放入session中
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
//Session对象即将进行钝化之前产生的监听
}
@Override
public void sessionDidActivate(HttpSessionEvent se) {
//Session对象活化完毕之后执行
}
}
Ajax
Ajax(Asynchronous JavaScript and XML),异步的 JavaScript 和 XML,是一种使用现有标准的新方法。Ajax 最大的优点是可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容。Ajax 不需要任何浏览器插件,但需要用户允许 JavaScript 在浏览器上执行。XMLHttpRequest 只是实现 Ajax 的一种方式。
发送请求的方式:
- 浏览器输入地址。
- html 中的
script
、img
和link
等标签,可以让网页放松请求回去抓取相关文件。(自动的请求,无需我们手动触发) - html 中的
a
、form
等标签。(需要我们手动控制进行提交) - 运行 Js 代码产生请求。(上述三种发送请求的方式均需要在新的页面上响应信息,但是我们也可以通过 Js 代码动态处理页面信息的更新)
应用示例:注册时,当我们输入用户名之后,需要判断数据库中该用户名是否被占用,此时可以使用 Js 代码进行动态判定。
Ajax 实现方式:
- 原生 Js 实现方式,代码繁琐,且涉及到回调函数问题。
- 使用第三方封装好的工具,例如 jquery。
- 使用框架,例如 VUE 框架中的 axios。
同步交互和异步交互
同步交互:客户端如果没有进行请求操作,服务端则闲置。客户端如果产生了请求操作,服务端便开始工作,客户端闲置。这可能会导致客户端等待的时间较长。
异步交互:通过 Js 的 XMLHttpRequest
发送请求,客户端发送请求后,服务端开始工作,同时客户端也在同时工作,用户可以继续在客户端处理相关信息。
原生 Js 实现 Ajax
准备相关Servlet:
package cn.hnu.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//接收参数
String username = req.getParameter("username");
//做出响应
resp.getWriter().write("hello" + username);
}
}
html 文件如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
<script>
function getMessage() {
//实例化XMLHttpRequest对象
var request = new XMLHttpRequest();
//设置回调函数
/*
* request的状态有4种:readyState 1 2 3 4
* request.status 响应状态码
* 这个函数会在状态改变的时候调用,所以一共会调用4次,最后一次意味着后端发生响应了
* 我们只需要关注最后一次的调用即可\
* request.responseText 代表后端响应体中的数据
* */
request.onreadystatechange = function () {
if(request.readyState == 4 && request.status == 200) {
//如果是最后一次响应并且响应成功了
//接收响应结果,处理结果
console.log(request.responseText);
//将信息放到指定位置
var element = document.getElementById("message");
element.value = request.responseText;
//或者利用bom编程跳转
//window.location.href="...";
}
}
//设置发送请求的方式和请求的资源
request.open("GET", "/hello?username=zhangsan");
//发送请求
request.send();
}
</script>
</head>
<body>
<button onclick="getMessage()">点我试逝</button>
<input type="text" id="message">
</body>
</html>
PostMan
PostMan 是一个接口测试工具。在做接口测试的时候 PostMan 相当于一个客户端,它可以模拟用户发起的各类 HTTP 请求,将请求数据发送至服务端,获取对应的响应结果,从而验证响应中结果数据是否和预期相匹配,并确保开发人员能够及时处理接口中的 bug。
基本介绍
在实际项目中,前后端工程师会在一起讨论约定关于接口的一些处理工作,这里的“接口”不再是 Java 当中我们熟知的 Interface 了,而是指业务接口。业务接口在这里体现为前后端在实现业务传输数据的时候,数据到底以何种规范来发送。官网下载地址。
Web 实际开发相关
MVC 模式
MVC(Model View Controller)是软件工程中的一种软件架构模式,它把软件系统分为模型、视图和控制器三个基本部分。用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。使得代码更加符合高内聚、低耦合的特点,更加符合开闭原则。
Model 模型层:
- 存放和数据库对象的实体类以及一些用于存储非数据库表完整相关的 VO 对象。
- 存放一些对数据进行逻辑运算操作的一些业务处理代码。
View 视图层:
- 存放一些试图文件相关的代码 html、css 等。
- 在前后端分离的项目中,后端已经没有视图文件,该层次已经演化为独立的前端项目。
Controller 控制层:
- 接收客户端请求,获得请求数据。(Servlet)
- 将准备好的数据响应给客户端。
项目结构
- pojo 包:pojo(Plain Old Java Object),普通 java 对象,该类用于定义项目需要的 javabean 类。在 pojo 包中,我们也会存储数据库表对应的 java 类,以及 VO 对象(多表查询的结果集对应的对象,一般在 pojo 包中再创建一个 vo 包来存储)。
- dao 包:dao(Data Access Object),数据访问对象,该类用于定义针对数据库的增删改查方法,一般对于一个表定义一个顶层接口,而后其下的实现类实现对应接口。(可在 dao 包下创建一个 impl 包,存储实现类)
- service 包:用于编写相对应的业务处理功能,例如做一些规则校验等逻辑操作。同样,每个数据库表都要有一个专门的类进行操作。同样也可以创建相对应的接口。(可在 service 包下创建一个 impl 包,存储实现类)
- contorller 包:使用 Servlet 技术进行服务端和客户端的请求和响应,里面的类需要继承
HttpServlet
,并声明好请求的映射路径。考虑到需要使用 Servlet 技术,故在这个包下,我们一般不再去定义接口,以免造成不必要的麻烦。 - filter 包:存放过滤器,过滤器在进行注解时需要判断好过滤范围。
- common 包:用于存放 JSON 串对应的类对象。
- test 包:用于存放测试类。
- util 包:用于存放工具类。
客户端 ---> Controller(Servlet) ---> Service ---> DAO
pojo
pojo 包注意以下几点:
- 实体类的类名和表格名称应该对应。
- 实体类的属性名和表格的列名应该对应。
- 每个属性都必须是私有的。
- 每个属性都应该具备基本 javabean 方法。
- 应该实现序列化接口(缓存、分布式项目数据传递可能会将对象序列化)。
- 应该重写 hashCode 和 equals 方法(HashMap 需要使用)。
- toString 方法可写可不写。
dao
我们一般会使用 BaseDao 来进行对数据库的基本操作,同时,会封装特定接口来实现对不同表的操作。接口与类的结构示例如下:
public interface SysScheduleDao {
int addSchedule(SysSchedule schedule);
List<SysSchedule> findAll();
}
public class SysScheduleDaoImpl extends BaseDao implements SysScheduleDao {
//继承BaseDao得以使用基本的操作方法,实现接口得以进行特定的方法封装
@Override
public int addSchedule(SysSchedule schedule) {
String sql = "insert into sys_schedule values(null, ?, ?, ?)";
return baseUpdate(sql, schedule.getUid(),
schedule.getTitle(), schedule.getCompleted());
}
@Override
public List<SysSchedule> findAll() {
String sql = "select sid, uid, title, completed from sys_schedule";
return baseQuery(SysSchedule.class, sql);
}
}
public class TestSysScheduleDao {
private static SysScheduleDao sysScheduleDao;
@BeforeClass
public static void initScheduleDao() {
//调用时我们可以给接口new一个实现类对象
//这样子就可以使用封装后的方法,并且不会调用到基本的操作方法
sysScheduleDao = new SysScheduleDaoImpl();
}
@Test
public void testAddSchedule() {
int rows = sysScheduleDao.addSchedule(new SysSchedule(null, 2, "学习数据库", 1));
System.out.println(rows);
}
@Test
public void testFindAll() {
List<SysSchedule> all = sysScheduleDao.findAll();
all.forEach(System.out::println);
}
}
controller
controller 层负责使用 Servlet 技术进行 service
方法的调用。为了区分要调用的是哪些方法,我们可以将对应 Servlet 实现类的路径进行更改,利用通配符来确定调用哪个方法。
/*
* 增加日程请求 /schedule/add
* 查询日程请求 /schedule/find
* 修改日程请求 /schedule/update
* 删除日程请求 /schedule/remove
* */
@WebServlet("/schedule/*")
public class SysScheduleController extends BaseController {
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
/*需要判断此次的操作是增删改查当中的哪个方法*/
String requestURI = req.getRequestURI(); // schedule/add
String[] split = requestURI.split("/");
String methodName = split[split.length - 1];
if("add".equals(methodName)) {
add(...);
}
else if("find".equals(methodName)) {
find(...);
}
else if("update".equals(methodName)) {
update(...);
}
else if("remove".equals(methodName)) {
remove(...);
}
}
protected void add(...) throws ServletException, IOException {...}
protected void find(...) throws ServletException, IOException {...}
protected void update(...) throws ServletException, IOException {...}
protected void remove(...) throws ServletException, IOException {...}
}
观察到我们需要通过方法的名字来调用方法,一个一个写这样做太麻烦了,我们需要增加代码的可复用性。所以,我们可以通过反射调用方法来解决这个问题。考虑到每个 Servlet 实现类都需要进行这种操作,故我们可以将这种调用方法的方式抽成一个 BaseController
,其他的 Servlet 实现类只需要继承 BaseController
即可。
public class BaseController extends HttpServlet {
//BaseController需要继承HttpServlet并重写service方法
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
/*需要判断此次的操作是增删改查当中的哪个方法*/
String requestURI = req.getRequestURI(); // schedule/add
String[] split = requestURI.split("/");
String methodName = split[split.length - 1];
//使用反射通过方法名来获取下面的方法
//注意,class文件的获取不能写死,需要使用this关键字
try {
Method method = this.getClass().getDeclaredMethod(methodName,
HttpServletRequest.class, HttpServletResponse.class);
method.setAccessible(true); //暴力反射
method.invoke(this, req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
* 增加日程请求 /schedule/add
* 查询日程请求 /schedule/find
* 修改日程请求 /schedule/update
* 删除日程请求 /schedule/remove
* */
@WebServlet("/schedule/*")
public class SysScheduleController extends BaseController {
//实现类继承BaseController,不需要重写service,调用的时候会默认调用父类的service方法
//故Servlet实现类只需要编写对应的增删改查等方法即可
protected void add(...) throws ServletException, IOException {...}
protected void find(...) throws ServletException, IOException {...}
protected void update(...) throws ServletException, IOException {...}
protected void remove(...) throws ServletException, IOException {...}
}
MD5 加密
package cn.hnu.schedule.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class MD5Util {
public static String encrypt(String strSrc) {
try {
char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for(int i = 0; i < bytes.length; ++i) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错");
}
}
}
WebUtil
工具类,前后端可以使用 WebUtil 进行 JSON 串的传递。
package cn.hnu.schedule.util;
import cn.hnu.schedule.common.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
public class WebUtil {
private static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
//设置JSON和Object转换的时间日期格式
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
public static <T> void writeJson(HttpServletResponse resp, Result<T> result) {
resp.setContentType("application/json;charset=UTF-8");
try {
String info = objectMapper.writeValueAsString(result);
resp.getWriter().write(info);
} catch (IOException e) {
e.printStackTrace();
}
}
public static <T> T readJson(HttpServletRequest request, Class<T> clazz) {
T t = null;
BufferedReader reader = null;
try {
reader = request.getReader();
StringBuffer buffer = new StringBuffer();
String line = null;
while((line = reader.readLine()) != null) {
buffer.append(line);
}
t = objectMapper.readValue(buffer.toString(), clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
return t;
}
}
校验登录状态
一些资源是需要保证用户在登录之后才能进行操作的,故我们可以利用 Filter 在前面挡一下,判断用户是否登录,再来决定之后的操作。
那如何确定用户已经登录了呢?关于这个问题,我们可以在用户登录成功之后将用户信息存放入 Session 域当中(可以以对象的方式存入)。这样,Filter 就可以去判断 Session 域对象中是否有登录信息,就可以做出相对于的逻辑判断了。
package cn.hnu.schedule.filter;
import cn.hnu.schedule.pojo.SysUser;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
//确定过滤器的作用范围
@WebFilter(urlPatterns = {"/static/html/showSchedule.html", "/schedule/*"})
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
//参数父转子
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//获得session域对象
HttpSession session = req.getSession();
//从session域对象中获得用户对象
SysUser sysUser =(SysUser) session.getAttribute("sysUser");
//判断用户对象是否为空
if(null == sysUser) { //如果没有登录信息,需要重定向到login.html上
resp.sendRedirect("/login.html");
}
else { //如果登录过,则放行
filterChain.doFilter(req, resp);
}
}
}
校验注册状态
注册提交前需要检验用户名是否被占用。可以利用 Ajax 完成该操作。
需要解决的问题有:
- 相应乱码问题。
- 后端响应回来的信息应当有一个统一的格式,前后端共同遵守。
- 校验不通过,我们需要阻止表单的提交。
首先,来考虑前后端数据响应格式问题,这个格式一般是前后端一起约定好的,当然,在此我们可以响应一个 JSON 串。我们在这里暂时先约定本 JSON 包含的具体格式:
- code:业务状态码。注意这里的业务状态码不是响应状态码。
- message:业务状态码的补充说明和描述。
- data:本次响应的数据。
我们使用枚举类型来表示 code 和 message 的映射关系:
package cn.hnu.schedule.common;
public enum ResultCodeEnum {
//以下的书写方式实际上是在调用私有的构造方法
SUCCESS(200, "success"),
USERNAME_ERROR(501, "usernameError"),
PASSWORD_ERROR(503, "passwordError"),
NOTLOGIN(504, "notLogin"),
USERNAME_USED(505, "usernameUsed");
private Integer code;
private String message;
private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
使用 Result 类来表示 JSON 串:
package cn.hnu.schedule.common;
public class Result<T> {
private Integer code;
private String message;
private T data;
public Result() {
}
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<>();
if (data != null) {
result.setData(data);
}
return result;
}
public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
//设置成功状态
public static <T> Result<T> ok(T data) {
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
public Result<T> message(String msg) {
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code) {
this.setCode(code);
return this;
}
//Setter and Getter...
}
客户端的 Controller 层,我们先判断用户名是否被占用:
@WebServlet("/user/*")
public class SysUserController extends BaseController {
private SysUserService userService = new SysUserServiceImpl();
protected void checkUsernameUsed(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
//接收参数
String username = req.getParameter("username");
//调用服务层业务处理方法查询该用户名是否有对应用户
SysUser sysUser = userService.findByUsername(new SysUser(null, username, null));
//result默认成功
Result result = Result.ok(null);
if(null != sysUser) {
//用户名以被占用
result = Result.build(null, ResultCodeEnum.USERNAME_USED);
}
//将result对象转换为JSON串响应给客户端
ObjectMapper objectMapper = new ObjectMapper();
String info = objectMapper.writeValueAsString(result);
//告诉客户端这个是一个字符串
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write(info);
}
}
配置 axios 信息:
import axios from 'axios'
//创建instance实例
const instance = axios.create({
baseURL:"http://localhost:8023/"
})
//添加请求拦截
instance.interceptors.request.use(
config=>{
return config
},
error=>{
return Promise.reject(error)
}
)
//添加响应拦截
instance.interceptors.response.use(
response=>{
return response
},
error=>{
return Promise.reject(error)
}
)
//暴露instance
export { instance }
Js 代码对 resp 写出的 JSON 串做出判断和响应:
async function checkUsername() {
let usernameReg = /^[a-zA-Z0-9]{5,10}$/
if(!usernameReg.test(registUser.username)) {
usernameMsg.value = "用户名格式有误"
return false
}
//校验用户名是否被占用
let promise = instance.post(`user/checkUsernameUsed?username=${registUser.username}`)
let { data } = await promise
//console.log(response)
//用户名被占用
if(data.code !== 200) {
usernameMsg.value = '用户名被占用'
return false
}
usernameMsg.value = "用户名格式正确"
return true
}
Token 口令
我们在判断用户是否登录的时候,可能会用到 Session 对象或者是 SessionStorage,这么做一旦用户量增多,会对服务器造成一定的压力,我们可以采用口令的方式来解决上述问题。
当用户向服务器发送登录请求之后,服务器在后端产生一个 token 口令响应回客户端。token 是包含了用户信息(用户id)的密文,客户端接收到 token 后将 token 存储到 LocalStorage 中。后续客户端发送任何请求时判断本地 token 只要不为空,都将 token 以请求头的形式发送给服务器。而服务器只需要接收校验 token 信息,就可以判断用户是否登录。
具体登录操作时,用户第一次登录,第一步会请求服务端向客户端响应一个 token。但是,客户端是不具备解析 token 的能力的。所以,第二步需要客户端携带着 token 再次发送一个请求,让客户端解析出对应的用户信息并响应给客户端。客户端把用户信息存储在 pinia 中,进行数据共享。故,实际上的登录是有两次请求的。
可以利用 JwtHelper
来进行相对应的操作:
public class JwtHelper {
private static long tokenExpiration = 24 * 60 * 60 * 1000; //token过期时间
private static String tokenSignKey = "123456"; //生成token密文时携带的信息赘余
//生成token字符串
public static String createToken(Long userId) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取userId
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer) claims.get("userId");
return userId.longValue();
}
//判断token是否有效
public static boolean isExpiration(String token) {
try {
boolean isEpire = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getExpiration().before(new Date());
//没有过期,有效,返回false
return false;
} catch (Exception e) {
//过期出现异常,返回true
return true;
}
}
}
分页
分页时,前端需要向后端响应一个 json 串,包括了搜索关键字、搜索的页面内容类型、页码数(当前是第几页)、页大小(一页有多少内容)。而后端响应回去的应该也是一个 json 串,包括了具体内容、页码数、页大小、总页数(一共可以分几页)、总记录数(一共有多少条记录)。
其他
方法和类上方要写注释,类上方的注释要表明作者和时间,方法的注释要表明参数和返回值。
服务端在向前端响应数据时,需要根据情况做一些保护,例如向前端响应用户信息的时候,需要将密码隐藏,否则前端可以直接拿到用户的密码。
token 口令在由客户端传递向服务端时一般会包含在请求头中。
用户的 token 口令是有时效性的,假设用户在登录后一段时间 token 口令失效了,那么这个时候意味着用户的登录超时,需要阻止用户的操作,让用户重新登录。这就需要我们在一些操作中提供必要的检验 token 口令是否失效的 Filter 过滤器。
考虑到 Web 项目可能有多个客户端同时访问服务器,故建议使用 StringBuffer 处理字符串的拼接。
写代码时需要随时注意判空!!!