javaweb

web前端开发(==接口文档模拟网站 yapi —可以自定义api接口供前端调用==)

==html:超文本标记语言==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
1·html标签不区分大小写
2·html标签属性值单双引号都可
3·html语法松散
4.<img src = "绝对路径或者相对路径 ./代表当前目录(可以省略)(当前文件所在目录)../上一级目录" width = "(单位 px像素,%相对于父元素的百分比)" height = "">正文
5.<h1/6>标题</h>
6.<hr>水平分割线
7.<audio src = "url" controls = "controls(可以省略)">音频MP3
8.<video src = "url" controls = "controls(可以省略)" width height>视频MP4
9.<p>段落
10.&nbsp;空格
11.<strong/b>强调</strong>
12.<span>没有语义的布局标签 一行可以显示多个 不可以设置宽高 宽高默认由内容撑开
13.<a href = "url" target = "_self:默认 在当前页面打开 _blank:在空白页面打开">超链接标签
14.<br>换行
15.<div>没有语义的布局标签 一行只显示一个 宽度默认父容器 高度默认内容 可以设置宽高
16.<table border = "边框宽度" width = "表格宽度" cellspacing = "单元之间的空间">定义表格整体 包含多个tr

<tr><th>表头 列名</th> <th>表头 列名2</th> <th>表头 列名3</th></tr>

<tr>表格的行 包含多个td <td>单元格 内容</td> <td>单元格 内容2</td> <td>单元格 内容3</td></tr>
···多行
</table>
17.表单标签<form action = "url" method = "GET/POST">
get:在url后面拼接表单数据 ?username=zhangsan&age=12 url长度有限制 不能提交大表单 默认值
post:在消息体中传递,参数无限制
表单项 可以用<label></label>标签包裹 点击label标签区域即可响应
<input type = "text" name = "变量名" value = "变量值">
<input type = "radio(单选按钮)" value = "男">
<input type = "checkbox(复选按钮)" value = "java">java
<input type = "file(文件上传)" name = "photo">
<input type = "date(日期)" name = "date">
<input type = "time(时间)" name = "time">

<input type = "password" name = "password">
<input type = "email" name = "email">
<input type = "number" name = "number"><!--会限制输入格式-->

<input type = "submit" value = "提交">
<input type = "button" value = "按钮">
<input type = "reset" value = "重置">
<select>定义下拉列表
<option value = ""></option>
<option value = ""></option>
</select>
<textarea name = "">定义文本域
</form>

==css:层叠样式表==

1
2
3
4
5
6
7
8
9
10
11
12
1.引入方式:行内<h1 style = ""> 内嵌<style></style> 外联.css <link rel = "stylesheet" href = "css/news.css">引入
2.选择器:id选择器#>类选择器.>元素选择器 标签名
3.font-size:字体大小
4.color:文本颜色 十六进制
5.text-decoration:文本修饰 none表示标准文本
6.line-height:行高
7.text-indent:定义首行内容缩进px
8.text-align:规定元素中文本水平对齐方式center left right
9.页面布局:有外到内:margin->border->padding->content
10.div width height默认设置是content的宽高 加上box-sizing:border-box 改成了整个盒子的宽高 有px %(相对于父元素)表示
11.margin:(上右下左px) ((上下)(左右)) auto 默认居中展示 其余一样
12.margin-top -left······ 其余一样

==javascript(脚本语言 不用编译)==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
1.引入方式:内:<script>可以出现在html中任意位置 </script>
外:<script src = ".js"></script>
2.区分大小写 结尾分号可以不要
3.注释 与Java一致 //单行 /*多行*/
4.输出: window.alert("")//弹出提示框
document.write("")//浏览器展示
console.log("")//控制台输出
5.定义变量:var a = "b123" //1不用指定数据类型 2全局变量 3可以重复定义
let //1局部变量只在{}里面有效 2不可重复定义
const //只读常量
//未定义变量初始值为undefined 包括数组
6.数据类型: typeof a //显示a的数据类型
//注意 typeof null 返回的是object对象 这是早期js的错误 被沿用了下来
7.==//会进行类型转换 与 === 不会进行类型转换
8.类型转换:其他类型转化为数字:parseInt("12")//12
parseInt("12a45")//12
parseInt("a45")//NaN(not a number)
其他类型转化为boolean:if()//0,NaN,空串"",null,undefined为false 其他为true
9.函数: 定义:function functionName(参数一,参数二){}//不用指定类型
var functionName = function(参数一,参数二){}//匿名函数
调用:函数名(参数)
10.对象:1>Array:定义:var 变量名 = new Array(元素列表);//动态长度 类型可变
var 变量名 = [元素列表];//java是大括号
访问:arr[index] = 值;//为数组指定的元素赋值
属性:length //返回数组中的元素数量 会一直到最后一个有值元素 尽管中间有undefined
方法:forEach(function()) //遍历数组中每个有值的元素 调用一次传入函数
arr.forEach(function(e){})//匿名函数 e为拿到的值
//ES6箭头函数 (...) =>{}
arr.forEach((e)=>{})//简化匿名函数
push() //将新元素贴加到末尾 并返回数组长度
splice(index,删除几个) //从数组中删除元素
2>String: 定义:var 变量名 = new String("...");
var 变量名 = "...";
属性:length//字符串长度
方法:charAt()//返回指定位置的字符串
indexof()//返回指定字符串的位置
trim()//去除字符串两边的空格
substring()//提起两个索引之间的字符串[start,end)
3>自定义对象:定义:var 对象名 = {
属性名1:值1,
属性名2:值2,
函数名称:function(形参列表){}
};
调用方式:对象名.属性名;
对象名.函数名();
实例化对象:var 变量名 = new 对象名();
4>JSON: 本质:是一种字符串格式 //多用于做数据载体
定义:var userjson = '{
"name":"zs",
"age":20, //全部用双引号引起来
"addr":["bj","sh"]
}';
多个userjson对象可以构成数组[json1,json2] 也是JSON数据格式
JSON字符串转js对象:var jsObject = JSON.parse(userjson)
js对象转json字符串:var jsString = JSON.Stringfy(jsObject)
5>BOM:浏览器对象模型 //将浏览器的组成部分封装成了对象
组成:Window //浏览器窗口对象
Navigator //浏览器对象
Screen //屏幕对象
History //历史记录对象
Location //地址栏对象
Window://Window.可以省略
属性:history location navigator
方法:alert() //警示消息
confirm() //显示一段带有确认(返回值为true)和取消(返回值为false)按钮的对话框
setInterval(function(){},time) //指定时间周期调用函数
setTimeout(function(){},time) //指定时间长度之后调用函数
Location: (Window.)Location.属性 //Window.可以省略
属性:href //设置返回完整的url
location.href = "url"//会自动跳转到url
6>DOM:文档对象模型 //将标记语言(html)的各个组成部分封装为对象
组成:Document:整个文档对象
Element:元素对象
Attribute:属性对象
Text:文本对象
Comment:注释对象
操作:1通过Document对象获取Element对象
//id获取唯一单个对象
var h1 = Document.getElementById('h1');
//标签名获取对象组
var divs = Document.getElementsByTagName('div');
//name属性获取对象组
var name = Document.getElementsByName('name');
//类名获取队小组
var cls = Document.getElementsByClassName('cls');
2查询参考手册 根据需求调用元素对象属性或者方法实现功能
11.事件监听 1事件:
onclick:点击事件
onblur:元素失去焦点 //点击别处
onfocus:元素获得焦点 //点击输入框
onload:某个元素或页面加载完成
onsubmit:表单提交时触发事件
onkeydown:某个键盘键被按下
onmouseover:鼠标移到某元素上
onmouseout:鼠标从某元素移开
2事件绑定:
方式一:
<input type = "button" id = "btn" onclick = "func()">
方式二:
<script>
document.getElementsById('btn').onclick=function(){}
</script>



VUE(前端框架)

优点:免除DOM操作,简化书写
基于M-V-VM模型 实现数据双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//进入vue.js文件
<script src = "js/vue.js"></script>
//创建vue对象
<script>
new Vue({
el:#app,
data:{
message:"hello",
},
methods:{
函数名:function(){}
或者 函数名(){} //:function可以省略
}
})
</script>
//显示调用vue对象
<div id = "app">
<input type = "text" v-model = "message">
//v-model相当于把value与vue中的message进行了双向绑定
{{message}}
</input>
</div>

常用指令

1
2
3
4
5
6
7
v-bind:绑定属性值 //简写为 :name=""
v-model:为表单元素创建双向数据绑定(绑定的是value与属性值)
v-on:绑定事件 //简写为 @click = "func1()" 在methods中声明方法(@+事件名(没有on))
v-if -else -else-if:判定为true渲染元素//<span v-if = "判断条件"></span> //v-else不用加条件
v-show:与if的区别在于切换的是display的值 //<span v-show = "判断条件"></span>
v-for:列表渲染,遍历 //<span v-for = "(arr,index) in arrs">{{arr}}</span>
//获取的arr为对象 要显示调用相关信息得放在{{}}表达式里

生命周期(Vue从创建到完成所经历的8个阶段)
重点mounted://挂载完成时调用 一般在此时向服务器请求信息

ajax(asynchronous)(异步)

优点:可以不更新全部页面完成网页数据更新
原生ajax请求数据过程:(现在基本不用了 存在浏览器兼容性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
//1创建XMLHttpRequest请求对象
var xmlHttpRequest = new XMLHttpRequest();
//2向服务器发送请求
xmlHttpRequest.open('GET/POST','url');
xmlHttpRequest.send();

//3获取服务器响应的数据
xmlHttpRequest.onreadystatechange = function(){
if(xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200){
Document.getElementById("").innerHTML = xmlHttpRequest.responseText;//responseText表示以字符串的形式返回数据
}
}
</script>

axios请求数据过程:(axios是对原生的ajax的封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
//1引入axios文件(需要自己去github或者官网下载)
npm install axios

//2导入axios
import axios form "axios";
axios({
method:'GET/POST',
url:"",
data:{},
}).then((result)=>{//result为回调成功后传入的整个消息体 要获取传回的数据为result.data对象 并且再按照对象调用对应属性即可
console.log(result);
});//成功回调函数(ES6箭头函数显示)

//写法2
axios.get('url').then(()=>{})
axios.post('url','传递的json数据').then(()=>{})
</script>

前后端分离开发模式

基本理论

需求分析 ->定义接口文档-> 前后端按照接口文档分离开发-> 前后端分别测试-> 前后端联合测试

前端工程化:
用处:用于企业级开发,讲究模块化 工程化 ==组件化(页面 页面局部 单个DOM元素块均可组件化)== 规范化 自动化
环境准备:vue-cli (vue官方提供的一个脚手架 用于快速生成一个vue模板)
依赖环境:NodeJS

一些常见前端更改操作:

1更改端口号:在==vue.config.js==文件中增加

1
2
3
devServer:{
port:7000,
}

vue基础语法与详细信息见官网快速入门
.vue文件基本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
html文件
</template>
<script>
export default{
data:function(){ //简写:data(){}
return {
message:"123"
}
},
methods:{

}
}
</script>
<style>
css
</style>

element

美化封装的DOM元素组件
使用element组件过程:
1去官网复制对应的自己需要的组件
2粘贴并进行个性化修改
常见组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//el-button type指定button类型
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>

//el-table tableData为数组类型 border指定有无边框
//el-table-column为一列元素 prop指定显示数组中的什么值 label表头
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>

return {
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀区金沙江路 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: "上海市普陀区金沙江路 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: "上海市普陀区金沙江路 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: "上海市普陀区金沙江路 1516 弄",
},
],
};
//详细可以查看官网 都有解释
//el-pagination 分页
//el-dialog 对话框
//el-form 表单

vue路由

使用前同axios 需要先下载导入
vue-router:路由器类router/index.js 指定url与对应的渲染组件

1
2
3
4
<router-link to = "url"></router-link>
<router-view></router-niew>
//流程:
//router-link 向vue-router路由表发送请求 vue-router更新router-view所展示的组件 并且main.js默认启动的只有app.vue 所以 router-view需要挂在app.vue中(默认路径是'/' 可以用redirect:进行重定向)

打包部署

打包:==>形成一个dist文件夹
nginx:轻量级高效web服务器
部署:(nginx部署静态资源html)
将dist文件中的所有文件复制到nginx的html文件夹中 并且启动nginx.exe文件//任务管理器中可能出现无反应 由于nginx默认占用80端口 所以一般都需要在conf目录下更改nginx.conf文件中的server->listerner:80;//90 or

后端==(接口测试工具postman)==

maven

作用:依赖管理 同一项目结构(不同idea均可运行) 项目构建(跨平台)
maven坐标: //定位项目或者依赖 可以实现项目和依赖的相互导入

1
2
3
4
<groupid></groupid>//定义当前maven项目隶属组织名称(一般是域名反写)
<artifactid></artifactid>//定义当前maven项目名称(通常是模块名称)
<version></version>//版本号
<scope><scope>//限制依赖范围:默认为compile(main,test,package) test(test)

依赖配置:
//先在本地仓库找 没有才去远程仓库下载
//不知道依赖坐标信息可以去官网查找==mvnrepository.com==
//依赖更改后要刷新一下maven才导入成功

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupid></groupid>//定义当前maven项目隶属组织名称(一般是域名反写)
<artifactid></artifactid>//定义当前maven项目名称(通常是模块名称)
<version></version>//版本号
</dependency>
</dependencies>

依赖传递:
//依赖具有传递性 一个项目依赖了另一个项目 也会继承他的依赖
//依赖可以排除

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupid></groupid>//定义当前maven项目隶属组织名称(一般是域名反写)
<artifactid></artifactid>//定义当前maven项目名称(通常是模块名称)
<version></version>//版本号
<exclusions>
<exclusion>
<groupid></groupid>//定义当前maven项目隶属组织名称(一般是域名反写)
<artifactid></artifactid>//定义当前maven项目名称(通常是模块名称)
//不用加版本号
</exclusion>
</exclusions>
</dependency>

maven生命周期:
clean->compile->test->package->install

Springboot

==注意版本匹配==
maven 3.6 jdk11 springboot2.x.x(springboot3.x.x要求jdk至少17)

HTTP协议

基于TCP协议:安全 面向连接
基于请求-响应模型:一次请求 一次响应
HTTP是无状态协议,对于数据处理没有记忆能力,多次数据请求不能共享,但是速度快

请求数据格式:
请求行(请求方式(GET/POST)请求地址(URL)协议(HTTP/1.1))
请求头(一些基本信息)
请求体(POST请求传递数据)

响应数据格式:

响应行(响应协议(HTTP/1.1) 状态码(200) 描述(OK))

响应头(一些基本信息)

响应体(响应数据)

状态码分类 说明
1xx 响应中 — 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略
2xx 成功 — 表示请求已经被成功接收,处理已完成
3xx 重定向 — 重定向到其它地方,让客户端再发起一个请求以完成整个处理
4xx 客户端错误 — 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx 服务器端错误 — 处理发生错误,责任在服务端,如:服务端抛出异

常见状态码:
200 客户端请求成功
404 请求资源不存在 url有误 或者资源删除
500 服务器错误

Web服务器

对http协议操作进行了封装 简化web开发

TomCat

轻量级web服务器 支持servlet jsp等少量javaEE规范
也称为web容器 servlet容器
springboot-web起步依赖(starter)中有内嵌tomcat

springboot-web中的请求响应

基于内嵌的tomcat,前端控制器
tomcat可以识别前端控制器==(DispacherServlet)==会获取前端请求并且将请求消息封装到一个对象==HttpServletRequest==
==(DispacherServlet)==也会封装响应消息==HttpServletResponse==为对象并由tomcat返回给前端
简单数据请求基本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//原始方式
@RequestMapping("/requestParam")
public String requestParam(HttpServletRequest request){

System.out.println(request.getParameter("age")+request.getParameter("name"));

return "ok";
}
//简化格式
@RequestMapping("/requestParam")
//@RequestParam(value = "name")方法形参名称与请求参数名称不匹配可以使用 默认属性required = true代表参数必须传递
public String requestParam(@RequestParam(value = "name" required = true)String username, Integer age){

System.out.println(username+","+age);

return "ok";
}
//对象接收 记得导入类
@RequestMapping("/requestPojo")

public String requestParam(User user){

System.out.println(user.toString());

return "ok";
}
//数组接收 请求数据格式 ?arr=1&arr=2 形参名称与传递数据名称须保持一致
@RequestMapping("/requestArr")
public String requestArr(String[] arr){
return Arrays.toString(arr);
}
//集合接收 请求数据格式 ?list=1&list=2 形参名称与传递数据名称须保持一致
@RequestMapping("/requestList")
//接收?list=1&list=2 数据默认是数组格式接收 所有要重新指定类型@RequestParam
public String requestList(@RequestParam List<String> list){
System.out.println(list);
return "OK";
}
//日期接收 形参名称与传递数据名称须保持一致
@RequestMapping("/requestDate")
//@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")指定时间格式 ymd不能乱改
public String requestDate(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
return time.toString();
}
//JSON格式数据请求
@RequestMapping("/requestJson")
//@RequestBody指明json数据封装为对象
public String requestJson(@RequestBody User user){
return user.toString();
}
//传递数据 对象的属性名与传递数据名称须保持一致
{
"name":"zhangsan",
"age":12,
"address":{
"province":"bejing",
"city":"beijing"
}
}
//对象
public class User {
private Integer age;
private String name;
private Address address;
}
public class Address {
private String province;
private String city;
}

//path参数请求 形参名与传递数据名称须保持一致
@RequestMapping("/requestPath/{id}/{name}")
//@PathVariable用来绑定对应参数
public String requestPath(@PathVariable String name, @PathVariable Integer id){
return name+","+id;
}

响应
基于@ResponseBody这个注解(@restController = @Controller+@ResponseBody)
作用:将方法返回值直接响应,如果返回值是实体对象/集合,将会转为JSON字符串格式返回

统一响应结果

return Result.success(object);//数据object又会被转为JSON格式返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 统一响应结果封装类
*/
public class Result {
private Integer code ;//1 成功 , 0 失败
private String msg; //提示信息
private Object data; //数据 date

public Result() {
}
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}

public static Result success(Object data){
return new Result(1, "success", data);
}
public static Result success(){
return new Result(1, "success", null);
}
public static Result error(String msg){
return new Result(0, msg, null);
}

@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}

有关XML文件解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//依赖注入->pom.xml
<!-- 解析XML -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
//解析工具 utils文件夹中定义工具类
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class XmlParserUtils {

public static <T> List<T> parse(String file , Class<T> targetClass) {
ArrayList<T> list = new ArrayList<T>(); //封装解析出来的数据
try {
//1.获取一个解析器对象
SAXReader saxReader = new SAXReader();
//2.利用解析器把xml文件加载到内存中,并返回一个文档对象
Document document = saxReader.read(new File(file));
//3.获取到根标签
Element rootElement = document.getRootElement();
//4.通过根标签来获取 user 标签
List<Element> elements = rootElement.elements("emp");

//5.遍历集合,得到每一个 user 标签
for (Element element : elements) {
//获取 name 属性
String name = element.element("name").getText();
//获取 age 属性
String age = element.element("age").getText();
//获取 image 属性
String image = element.element("image").getText();
//获取 gender 属性
String gender = element.element("gender").getText();
//获取 job 属性
String job = element.element("job").getText();

//组装数据
Constructor<T> constructor = targetClass.getDeclaredConstructor(String.class, Integer.class, String.class, String.class, String.class);
constructor.setAccessible(true);
T object = constructor.newInstance(name, Integer.parseInt(age), image, gender, job);

list.add(object);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}

}
//调用解析
//获取动态url
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
//调用解析类返回对应List
//定义Emp类来承载xml文件单个对象
List<Emp> EmpList = XmlParserUtils.parse(file, Emp.class);
//数据处理
//List.stream()生成流对象 使用流操作(如forEach、map、filter、reduce等)来进行集合操作
EmpList.stream().forEach(emp -> {
if(emp.getGender().equals("1")){
emp.setGender("男");
}else{
emp.setGender("女");
}
//<!-- 1: 讲师, 2: 班主任 , 3: 就业指导 -->
if(emp.getJob().equals("1")){
emp.setJob("讲师");
}
else if(emp.getJob().equals("2")){
emp.setJob("班主任");
}
else if(emp.getJob().equals("3")){
emp.setJob("就业指导");
}
});

springboot三层架构

controller:控制层,接收前端发送的请求,对请求进行处理,响应数据
service:业务逻辑层,处理具体的业务逻辑
dao:数据访问层(持久层),负责数据访问,包括增删除改
优点:复用性强,便于维护,利于拓展

分层解耦

IOC控制反转 对象的创建控制权由程序自身转移到外部(容器)
具体操作:通过@Component注解将具体实现接口对象交给容器
DI依赖注入 容器为应用程序提供所需要的资源
具体操作:不用自己new实例化对象(比如service层调用dao层接口) 在需要实例化对象的地方通过注解@Autowired在容器中找到对应具体实现接口对象并赋值给变量==(默认按照接口类型自动装配 所以同一接口实现类不能同时交给容器)==
注:
给实现类加上@Component 会将实现类(不是接口)交给IOC容器管理,以bean对象进行管理
给成员变量加上@Autowired(不是局部变量)IOC容器会提供对应的bean对象 并赋值给变量

IOC

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

注解 说明 位置
@Controller @Component的衍生注解 标注在控制器类上
@Service @Component的衍生注解 标注在业务类上
@Repository @Component的衍生注解 标注在数据访问类上(由于与mybatis整合,用的少)
@Component 声明bean的基础注解 不属于以上三类时,用此注解

bean

在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。

注意事项:

  • 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。(一般都默认)
  • 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。(集成在了@RestController中)
  • 使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,==默认扫描的范围是SpringBoot启动类所在包及其子包==。
虽然可以配置扫描范围 但是一般建议按照springboot结构设置三层结构

DI

@AutoWired:默认按照类型进行自动装配
==(默认按照装配类实现的接口类型自动装配 所以同一接口实现类不能同时交给容器 会出现多个同类型bean)==
如果同类型的bean有多个:
@Primary 放在指定bean上
@AutoWired+@Qualifier(“bean的名称”)
@Resource(name = “bean的名称”)//类名首字母小写
@Resource与@AutoWired的区别:
@AutoWired由spring提供 @Resource由jdk提供(idea会自动导入对应包)
@AutoWired按照类型装配 @Resource按照bean名称进行装配

数据库

==一些基本概念:==

  • DB DBMS SQL

==主流关系数据库:==

  • oracle mysql

mysql数据库

==mysql数据模型:关系型数据库==

  • 简单理解:关系型数据库是由多张能相互连接的二维表组成的数据库(X,Y)

==在mysql中,数据库以文件夹形式存放==

sql语言—结构化查询语言(查询关系数据)

==通用语法==

  • 分号结尾
  • 不区分大小写
  • 注释 单行(– )(#) 多行/* */

==sql分类==

1
2
3
4
DDL:操作数据库,表
DML:对数据增删改查
DQL:对数据查询
DCL:权限控制

==DDL==

操作数据库

1
2
3
4
5
1.查询	show databases;#查询当前目录下的所有数据库名称
2.创建 create database if not exists 数据库名称;
3.删除 drop database if exists 数据库名称;#判断是否存在
4.使用 select database(); #查看当前使用数据库
use 数据库名称;#使用某个数据库

操作表(先进入数据库 use)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.查询表	show tables;#展示有哪些表
desc 表名称;#查询表结构

2.创建表 create table 表名 (
字段名1 数据类型1,
字段名2 数据类型2,
字段名3 数据类型3 #不能加逗号
#varchar变长 char定长
);

3.修改表 1.修改表名 alert table 表名 reaname to 新的表名;
2.添加一列 alert table 表名 add 列名 数据类型;
3.修改数据类型 alert table 表名 modify 列名 新数据类型;
4.修改列名和数据类型 alert table 表名 change 列名 新的列名 新数据类型;
5.删除列 alert table 表名 drop 列名;

4.删除表 drop table 表名称;#也可以判断是否存在表

图形化工具

navicat
mysqlworkbench(这个也很好用 但是用的人很少)
idea(与spring一起用的比较多 具体连接数据库方式比较简单 不懂csdn上找一下)

==DML==

添加

1
insert into 表名(列名1,2,3)values (1,2)

修改

1
update 表名 set 列名1 = 值1 ,列名2 = 值2,[where 条件] #一般都要加条件 否则所有数据都要被修改

删除

1
delete from 表名 where 条件 #删除n条记录

查询

1
2
3
4
5
6
7
select 列字段 (*代表所有列)(distinct 代表去除重复项)
from 表 (as起别名)
where 条件
group by 分组 (聚合函数count(计数) max min sum )
having 分组条件
oder by 排序条件 (ASC 升序 DESC降序)
limit 起始索引,查询记录数(mysql中是这样)

外键约束

目前不推荐物理外键(foreign key)一般在业务逻辑层 通过代码实现数据库的一致性完整性

外键指向 多指向一
数据库中的五种约束:非空 主键 唯一 默认 外键

一对一 任一方加外键实现 并且加上unique唯一
一对多 多的那方向一的那方加外键实现
多对多 外键+中间表实现

多表查询(笛卡尔积->要通过where条件消除无效的查询记录)

内连接 会排除掉where条件中的null属性记录
(显示(join))
(隐示)
外连接
左外连接(left join)查询左边表所有记录 不会排除掉where条件中的右边表null属性记录
右外连接(right join)查询右边表所有记录 不会排除掉where条件中的左边表null属性记录

嵌套查询(子查询)

from 子表

事务

一组操作的集合 不可分割 要么同时成功 要么同时失败
事务与事务之间是隔离的

开启事务 begin #写在执行语句前
提交事务 commit #事务没提交前只能在事务内看到执行结果 指令失败没办法提交
回滚事务 rollback #指令失败或错误可以回滚 回到事务执行前

事务的四大特性

原子性:事务是不可分割的最小单元 要么全部成功 要么全部失败
一致性:事务完成时 所有数据必须保持一致状态
隔离性:独立环境运行
持久性:事务一旦提交或回滚 他对数据的操作是永久的

索引

帮助数据库高效获取数据的数据结构
==提高查询的效率 但是降低增删改的效率==

1
2
3
4
5
6
#创建索引
create [unique] index 索引名称 on 表名(列名);
#查看表中所有索引
show index from 表名;
#删除索引
drop index 索引名称 on 表名;

==注意==
创建主键 会默认给主键创建索引
添加唯一约束 会添加唯一索引

默认查询是全表扫描
索引默认结构是B+树结构
不采取二叉搜索树/红黑树的原因:层级较深 搜索速率慢

B+树特点:
每一个节点可以存储多个key(每一个key对应一个指针)
所有数据都存储在叶子节点,非叶子节点仅用于存放key
叶子之间形成双向链表,便于排序和区间范围查询

Mybatis

是一款持久层(dao层)框架—用于简化JDBC开发

mysql中创建数据库中文问题

注意:
记得把编码改成utf-8 不然无法插入或解析中文字符

JDBC(不做详细讲述)

JDBC只是一套sun公司提供的java操作数据库接口 具体实现是由具体数据库公司实现的
现在已经基本很少用了 代码量很大 单一连接每次都要创建费时费资源

数据库连接池(也是mybatis与原生JDBC的一大区别)

是一个容器 负责管理 分配数据库连接 可重用数据库连接不用新建 并且超时会收回连接
优势:资源复用,提高响应速度
接口:DataSource(sun公司提供 每个连接池都必须实现)
产品:Druid(alibaba),Hikari(springboot默认)(想要切换直接pom导入Druid依赖即可)

springboot基于mybatis连接mysql数据库

起步依赖(mybatis、mysql驱动、lombok)

lombok是用于简化对象类
用注解的形式简化了Getters/Getters/ToString等方法

application.properties中引入数据库连接信息

提示:可以把之前项目中已有的配置信息复制过来即可

1
2
3
4
5
6
7
8
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url(自定义)(mybatis是数据库名)
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码(自定义)
spring.datasource.password=123456

基本mybatis操作(增删除改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//查询全部数据
@Mapper//在运行时,会自动生成该接口的实现类对象(代理对象),并且将对象交给IOC容器管理 所以该接口不用实现 并且由于被IOC容器作为bean对象管理 在调用时通过@Autowired注解声明对象即可调用
public interface UserMapper {
//查询注解 该注解可以在调用方法时执行sql语言,并且返回结果封装为对象 多个对象封装为List即可
@Select("select * from user")//同时还有delete update等注解用于传递对应的sql语言
public List<User> list();
}
//删除
@Mapper
public interface EmpMapper {
//值得注意的是:
@Delete(" delete from emp where id = #{id}") //#{变量名} 用于动态绑定传入参数 #{}是预编译执行 ~~<详细信息参考>预编译sql sql注入 参数占位符~~
public void deleteOne(Integer id);
}
//新增
//@Options主键返回 keyProperty指定返回主键将封装到emp对象的id属性里面 useGeneratedKeys返回新增行主键
@Options(keyProperty = "id",useGeneratedKeys = true)
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) "+
"values(#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime}) ")
public void insert(Emp emp);

//更新数据
@Update("update emp set username = #{username}, name = #{name}, gender =#{gender}, image= #{image}, job= #{job}, entrydate= #{entrydate}, "+
"dept_id = #{deptId}, update_time = #{updateTime} where id = #{id}")
public void update(Emp emp);
//根据主键查询

@Select("select id, username, password, name, gender, image, job, entrydate, " +
"dept_id deptId, create_time createTime, update_time updateTime from emp where id = #{id}")
//方法一:dept_id deptId等起了别名 使对象属性名与数据库中的字段名一致才能封装 不然返回null
//方法二:@Result注解 (复杂很少用)
//方法三:在application.properties加入配置项mybatis.configuration.map-underscore-to-camel-case=true 开启驼峰命名自动映射 (但是数据库字段名 和 对象属性名必须按照规定格式写)
public Emp select(Integer id);

//条件查询
@Select("select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and " +
"entrydate between #{start} and #{end} order by update_time DESC")
//concat语句用来拼接动态量%name% 因为动态量不能直接写在''以内
public List<Emp> selectByCondition(String name, Short gender, LocalDate start, LocalDate end);
//单独使用mybatis的时候需要在形参面前加一个@Param("形参名")

开启驼峰命名自动映射

1
2
3
//在application.properties加入配置项
mybatis.configuration.map-underscore-to-camel-case=true
//开启驼峰命名自动映射 (但是数据库字段名 和 属性对象名必须按照规定格式写)

XML映射文件完成基本mybatis操作

  • xml配置==文件夹==(放置在resource文件夹里)的名称与Mapper接口==文件夹==名称一致 并且路径相同(同包同名)(一个包里可以有多个接口或与其对应的多个xml配置文件)
  • xml<mapper>字段的namespace属性与Mapper全限定名一致(引用路径)
  • sql语句的id与Mapper中的方法名一致 并保持返回类型一致(引用路径)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    //xml sql文件头(都需要)
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    //com.itheima.mapper同包同名 namespace指定映射的接口类
    <mapper namespace="com.itheima.mapper.EmpMapper">
    //<sql>可以封装固定sql语句 id指定调用名称 集成代码 通过<include>调用
    <sql id="common">
    select id,
    username,
    password,
    name,
    gender,
    image,
    job,
    entrydate,
    dept_id,
    create_time,
    update_time
    from emp
    </sql>


    //批量删除操作 id与方法名一致
    <delete id="deleteByIds">
    delete from emp where id in
    //<foreach>循环
    //collection指定循环list(通过形参传入)
    //separator指定元素之间的分隔符
    //item指定拿到的每个元素
    //open指定循环完成语句开头的字符
    //close指定循环完成语句结束的字符
    <foreach collection="ids" separator="," item="id" open="(" close=")">
    #{id} //动态传入item拿到的id
    </foreach>
    </delete>


    //查询操作 id绑定方法名 resultType指定单条记录所封装的类型
    <select id="selectByCondition" resultType="com.itheima.pojo.Emp">
    //<include>通过refid调用封装的sql
    <include refid="common"></include>
    //<where>除去开头多余的and
    //以及所有if条件都不成立时 去掉where
    <where>
    //<if>判断标签 test指定判断条件 结果为true时拼接对应sql语句
    //加上if判断条件 防止传入null数据查询
    <if test="name != null">
    name like concat('%',#{name},'%')
    </if>
    <if test=" gender != null">
    and gender = #{gender}
    </if>
    <if test="start != null and end != null">
    and entrydate between #{start} and #{end}
    </if>
    order by update_time DESC
    </where>

    </select>

    //更新操作 id绑定方法名
    <update id="update">
    update emp
    //<set>标签去除结尾多余的,号
    <set>
    //加上if判断条件 防止不传入数据时 数据被改为null
    <if test = "username != null">username = #{username},</if>
    <if test = "name != null">name = #{name},</if>
    <if test = "gender != null">gender =#{gender},</if>
    <if test = "image != null">image= #{image},</if>
    <if test = "job != null">job= #{job},</if>
    <if test = "entrydate != null">entrydate= #{entrydate},</if>
    <if test = "deptId != null">dept_id = #{deptId},</if>
    <if test = "updateTime != null">update_time = #{updateTime}</if>
    </set>
    where id = #{id}
    </update>
    </mapper>

mybatisX插件

提高mybatisX开发效率

预编译SQL

介绍
预编译SQL有两个优势:

  1. 性能更高
  2. 更安全(防止SQL注入)

性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)

更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。

SQL注入

SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。

参数占位符

在Mybatis中提供的参数占位符有两种:${…} 、#{…}

  • #{…}

    • 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值
    • 使用时机:参数传递,都使用#{…}
  • ${…}

    • 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
    • 使用时机:如果对表名、列表进行动态设置时使用

注意事项:在项目开发中,建议使用#{…},生成预编译SQL,防止SQL注入安全。

日志输入

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开application.properties文件

  2. 开启mybatis的日志,并指定输出到控制台

1
2
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

案例实操

Rest风格—定义接口规范

1
2
3
4
5
6
7
8
//GET查询 /users/1 
@GetMapping
//POST新增 /users
@PostMapping
//PUT修改 /users
@PutMapping
//DELETE删除 /users/1
@DeleteMapping

日志记录

1
2
3
4
5
6
@Slf4j
Controller(){
方法(){
log.info("日志信息");
}
}

抽取相同url

1
2
@RequestMapping("/depts")//抽取共同路径
之后的mapping就可以只写剩余的url

设定默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@RestController
@RequestMapping("/emps")
public class EmpController {

@Autowired
private EmpService empService;
@GetMapping
//@RequestParam(defaultValue = "默认值")//返回形参为null时设定默认值
public Result list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize){
PageBean pageBean;
pageBean = empService.list(page,pageSize);

return Result.success(pageBean);
}

}

pageHelper插件依赖实现分页数据返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//注入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
//service实现
@Override
public PageBean list(Integer page, Integer pageSize) {
//启动分页插件 设置起始索引和页大小
PageHelper.startPage(page,pageSize);
//获取所有数据库数据
List<Emp> emplist = empMapper.list();
//强制转换(Emp为单条记录类型)
Page<Emp> p = (Page<Emp>) emplist;
//调用方法获取总记录数量以及当前页记录并封装到pageBean对象里
PageBean pageBean = new PageBean(p.getTotal(),p.getResult());
return pageBean;
}

文件上传

本地存储 (用的很少)(可以了解具体流程)

想要完成文件上传这个功能需要涉及到两个部分:

  1. 前端程序
  2. 服务端程序

我们先来看看在前端程序中要完成哪些代码:

1
2
3
4
5
6
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<input type="submit" value="提交">
</form>

上传文件的原始form表单,要求表单必须具备以下三点(上传文件页面三要素):

  • 表单必须有file域,用于选择要上传的文件

    1
    <input type="file" name="image"/>
  • 表单提交方式必须为POST

    通常上传的文件会比较大,所以需要使用 POST 提交方式

  • 表单的编码类型enctype必须要设置为:multipart/form-data

    普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data
    如果不设置 当传递file类型表单数据的时候只会传递文件的名字

后端接收数据:

UploadController代码:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) {
log.info("文件上传:{},{},{}",username,age,image);
return Result.success();
}

}

通过后端程序控制台可以看到,上传的文件是存放在一个临时目录

表单提交的三项数据(姓名、年龄、文件),分别存储在不同的临时文件中:
当我们程序运行完毕之后,这个临时文件会自动删除。
所以,我们如果想要实现文件上传,需要将这个临时文件,要转存到我们的磁盘目录中。

本地存储到磁盘

代码实现:

  1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
  2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法:

  • String getOriginalFilename(); //获取原始文件名
  • void transferTo(File dest); //将接收的文件转存到磁盘文件中
  • long getSize(); //获取文件的大小,单位:字节
  • byte[] getBytes(); //获取文件内容的字节数组
  • InputStream getInputStream(); //获取接收到的文件内容的输入流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+originalFilename));

return Result.success();
}

}

优化(使上传文件不同名):保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//构建新的文件名
String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+newFileName));

return Result.success();
}

}

优化:上传大文件(spring默认上传文件大小最多为1MB)

那么如果需要上传大文件,可以在application.properties进行如下配置:

1
2
3
4
5
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

如果直接存储在服务器的磁盘目录中,存在以下缺点:

  • 不安全:磁盘如果损坏,所有的文件就会丢失
  • 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
  • 无法直接访问

为了解决上述问题呢,通常有两种解决方案:

  • 自己搭建存储服务器,如:fastDFS 、MinIO
  • 使用现成的云服务,如:阿里云,腾讯云,华为云

阿里OSS

  • 注册阿里云OSS账户

  • 开创一个OSS对象

  • 创建一个Bucket

    Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

  • 拿到自己阿里云账户的密钥ID和密码(记得保存 现在不可查看了)

  • 根据OSS的SDK开创一个工具类

    SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。

    简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。

引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Component//交给IOC容器管理
public class AliOSSUtils {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
private String endpoint = "https://oss-cn-shanghai.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户
private String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
private String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
// 填写Bucket名称,例如examplebucket。
private String bucketName = "web-framework01";

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();

// 避免文件覆盖
String originalFilename = multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);

//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;

// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}

springboot配置文件(yml/yaml)

yml配置文件的基本语法

  • 大小写敏感
  • 数值前边必须有空格,作为分隔符
  • 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • #表示注释,从这个字符一直到行尾,都会被解析器忽略

了解完yml格式配置文件的基本语法之后,接下来我们再来看下yml文件中常见的数据格式。在这里我们主要介绍最为常见的两类:

  1. 定义对象或Map集合
  2. 定义数组、list或set集合

对象/Map集合

1
2
3
4
user:
name: zhangsan
age: 18
password: 123456

数组/List/Set集合

1
2
3
4
hobby: 
- java
- game
- sport

配置文件定义以及调用数据

定义数据

1
2
3
4
5
6
7
aliyun:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
accessKeyId: LTAI4GCH1vX6DKqJWxd6nEuW
accessKeySecret: yBshYweHOpqDuhCArrVHwIiBKpyqSL
bucketName: web-397

调用数据两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//方法一:通过注解@value("${配置文件中的变量名}")注入
@Component
public class AliOSSUtils {

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;

@Value("${aliyun.oss.bucketName}")
private String bucketName;

//省略其他代码...
}
//方法二:重新定义一个属性类 用来封装同一级下的属性 调用时通过@Autowired注入 并且get/set方法获取对应数值

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/*阿里云OSS相关配置*/
@Data //生成属性get/set方法
@Component //将类交给IOC容器管理
@ConfigurationProperties(prefix = "aliyun.oss") //指定前缀名
public class AliOSSProperties {
//区域
private String endpoint;
//身份ID
private String accessKeyId ;
//身份密钥
private String accessKeySecret ;
//存储空间
private String bucketName;
}

登录+会话技术+统一拦截

登录后端设计思路

​ 登录服务端的核心逻辑就是:接收前端请求传递的用户名和密码 ,然后再根据用户名和密码查询用户信息,如果用户信息存在,则说明用户输入的用户名和密码正确。如果查询到的用户不存在,则说明用户输入的用户名和密码错误。

会话技术

  • 在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。

    在用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。

    比如:打开了浏览器来访问web服务器上的资源(浏览器不能关闭、服务器不能断开)

    • 第1次:访问的是登录的接口,完成登录操作
    • 第2次:访问的是部门管理接口,查询所有部门数据
    • 第3次:访问的是员工管理接口,查询员工数据

    只要浏览器和服务器都没有关闭,以上3次请求都属于一次会话当中完成的。

需要注意的是:会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

服务器会接收很多的请求,但是服务器是需要识别出这些请求是不是同一个浏览器发出来的。比如:1和2这两个请求是不是同一个浏览器发出来的,3和5这两个请求不是同一个浏览器发出来的。如果是同一个浏览器发出来的,就说明是同一个会话。如果是不同的浏览器发出来的,就说明是不同的会话。而识别多次请求是否来自于同一浏览器的过程,我们就称为会话跟踪。

我们使用会话跟踪技术就是要完成在同一个会话中,多个请求之间进行共享数据。

为什么要共享数据呢?

由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据呢?此时就需要在一次会话的多次请求之间进行数据共享

会话跟踪技术有三种:

  1. Cookie(客户端会话跟踪技术)
    • 数据存储在客户端浏览器当中
  2. Session(服务端会话跟踪技术)(是基于cookie的)
    • 数据存储在储在服务端
      前两种现在用的很少了 了解即可
  3. 令牌技术

** 方案一 - Cookie**

cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。

比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。

服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。

为什么这一切都是自动化进行的?

是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:

  • 响应头 Set-Cookie :设置Cookie数据的

  • 请求头 Cookie:携带Cookie数据的

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@RestController
public class SessionController {

//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}

//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}

优缺点

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
  • 缺点:
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 不安全,用户可以自己禁用Cookie
    • Cookie不能跨域

跨域介绍:

  • 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
  • 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
  • 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
  • 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
  • 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域

区分跨域的维度:

  • 协议
  • IP/协议
  • 端口

只要上述的三个维度有任何一个维度不同,那就是跨域操作

方案二 - Session

Session,它是服务器端会话跟踪技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。

  • 获取Session

    如果我们现在要基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID,我们称之为 Session 的ID。

  • 响应Cookie (JSESSIONID)

    接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。

  • 查找Session

    接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。

    这样我们是不是就可以通过 Session 会话对象在同一次会话的多次请求之间来共享数据了

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@RestController
public class SessionController {

@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());

session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}

@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());

Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}

优缺点

  • 优点:Session是存储在服务端的,安全
  • 缺点:
    • 服务器集群环境下无法直接使用Session
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 用户可以自己禁用Cookie
    • Cookie不能跨域

PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。

服务器集群环境为何无法使用Session?

  • 首先第一点,我们现在所开发的项目,一般都不会只部署在一台服务器上,因为一台服务器会存在一个很大的问题,就是单点故障。所谓单点故障,指的就是一旦这台服务器挂了,整个应用都没法访问了。

  • 所以在现在的企业项目开发当中,最终部署的时候都是以集群的形式来进行部署,也就是同一个项目它会部署多份。比如这个项目我们现在就部署了 3 份。

  • 而用户在访问的时候,到底访问这三台其中的哪一台?其实用户在访问的时候,他会访问一台前置的服务器,我们叫负载均衡服务器,我们在后面项目当中会详细讲解。目前大家先有一个印象负载均衡服务器,它的作用就是将前端发起的请求均匀的分发给后面的这三台服务器。

  • 此时假如我们通过 session 来进行会话跟踪,可能就会存在这样一个问题。用户打开浏览器要进行登录操作,此时会发起登录请求。登录请求到达负载均衡服务器,将这个请求转给了第一台 Tomcat 服务器。

    Tomcat 服务器接收到请求之后,要获取到会话对象session。获取到会话对象 session 之后,要给浏览器响应数据,最终在给浏览器响应数据的时候,就会携带这么一个 cookie 的名字,就是 JSESSIONID ,下一次再请求的时候,是不是又会将 Cookie 携带到服务端?

    好。此时假如又执行了一次查询操作,要查询部门的数据。这次请求到达负载均衡服务器之后,负载均衡服务器将这次请求转给了第二台 Tomcat 服务器,此时他就要到第二台 Tomcat 服务器当中。根据JSESSIONID 也就是对应的 session 的 ID 值,要找对应的 session 会话对象。

    我想请问在第二台服务器当中有没有这个ID的会话对象 Session, 是没有的。此时是不是就出现问题了?我同一个浏览器发起了 2 次请求,结果获取到的不是同一个会话对象,这就是Session这种会话跟踪方案它的缺点,在服务器集群环境下无法直接使用Session。

**方案三 - 令牌技术 **

这里我们所提到的令牌,其实它就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串。

如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。

接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。

接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。

此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。

优缺点

  • 优点:
    • 支持PC端、移动端
    • 解决集群环境下的认证问题
    • 减轻服务器的存储压力(无需在服务器端存储)
  • 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

针对于这三种方案,现在企业开发当中使用的最多的就是第三种令牌技术进行会话跟踪。而前面的这两种传统的方案,现在企业项目开发当中已经很少使用了。所以在我们的课程当中,我们也将会采用令牌技术来解决案例项目当中的会话跟踪问题。

JWT令牌

JWT介绍
JWT全称:JSON Web Token (官网:https://jwt.io/)

  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

    简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:”HS256”,”type”:”JWT”}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:”1”,”username”:”Tom”}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定==签名算法==计算而来。

JWT是如何将原始的JSON格式数据,转变为字符串的呢?

其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码

Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号

需要注意的是Base64是编码方式,而不是加密方式。
最后的签名是经过签名算法加密的 不是base64编码过来的 所以用base64并不能解析出来

JWT令牌最典型的应用场景就是登录认证:

  1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
  2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
  3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。

生成和校验

首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:

1
2
3
4
5
6
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

我们在使用JWT令牌时需要注意:

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。

  • 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

登录下发令牌

JWT工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//在utils包下创建工具类
public class JwtUtils {

private static String signKey = "itheima";//签名密钥
private static Long expire = 43200000L; //有效时间

/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间 单位ms
.compact();//生成jwt
return jwt;
}

/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}

登录成功,生成JWT令牌并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
@Slf4j
public class LoginController {
//依赖业务层对象
@Autowired
private EmpService empService;

@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
//调用业务层:登录功能
Emp loginEmp = empService.login(emp);

//判断:登录用户是否存在
if(loginEmp !=null ){
//自定义信息
Map<String , Object> claims = new HashMap<>();
claims.put("id", loginEmp.getId());
claims.put("username",loginEmp.getUsername());
claims.put("name",loginEmp.getName());

//使用JWT工具类,生成身份令牌
String token = JwtUtils.generateJwt(claims);
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}

过滤器Filter

服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。
两种解决方案:

  1. Filter过滤器
  2. Interceptor拦截器

什么是Filter?

  • Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
    • 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

过滤器的基本使用操作:

  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
  • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

定义过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义一个类,实现一个标准的Filter过滤器的接口
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}

@Override //拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}

@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
  • init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。

  • doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。

  • destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。

当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

1
2
3
4
5
6
7
8
9
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {

public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}

}

Filter详解

  1. 过滤器的执行流程
  2. 过滤器的拦截路径配置
  3. 过滤器链

** 执行流程**

过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。

** 拦截路径**

执行流程我们搞清楚之后,接下来再来介绍一下过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

过滤器链

所谓过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。

比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。

而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。

访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。

先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。

过滤器执行顺序:
其实是和过滤器的类名有关系。以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。

登录校验-Filter

登录校验过滤器:LoginCheckFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login


//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("/login")){
chain.doFilter(request, response);//放行请求
return;//结束当前方法的执行
}


//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);


//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return;
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return;
}


//6.放行
chain.doFilter(request, response);

}
}

在上述过滤器的功能实现中,我们使用到了一个第三方json处理的工具包fastjson。我们要想使用,需要引入如下依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

拦截器Interceptor

什么是拦截器?

  • 是一种动态拦截方法调用的机制,类似于过滤器。
  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。

拦截器的作用:

  • 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。

拦截器的使用步骤和过滤器类似,也分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");

return true; //true表示放行
}

//目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}

//视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}

注意:

​ preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行

​ postHandle方法:目标资源方法执行后执行

​ afterCompletion方法:视图渲染完毕后执行,最后执行

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration  
public class WebConfig implements WebMvcConfigurer {

//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;


@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}

** Interceptor详解**

  1. 拦截器的拦截路径配置
  2. 拦截器的执行流程

拦截路径
在配置文件中,
通过addPathPatterns("要拦截路径")方法,就可以指定要拦截哪些资源。
调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

执行流程

  • 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。

  • Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。

  • 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。

  • 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

以上就是拦截器的执行流程。通过执行流程分析,过滤器和拦截器之间的区别:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

登录校验- Interceptor

登录校验拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//自定义拦截器
@Component //当前拦截器对象由Spring创建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
//前置方式
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
//1.获取请求url
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行

//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");

//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return false;//不放行
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");

//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return false;
}

//6.放行
return true;
}

注册配置拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration  
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");//也可以不排除 在拦截器中做了判断
}
}

全局异常处理器

定义全局异常处理器

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class GlobalExceptionHandler {

//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息

//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

Spring事务

Spring事务管理

回顾:
事务的定义:一组操作的集合 不可分割 要么同时成功 要么同时失败
事务与事务之间是隔离的

开启事务 begin #写在执行语句前
提交事务 commit #事务没提交前只能在事务内看到执行结果 指令失败没办法提交
回滚事务 rollback #指令失败或错误可以回滚 回到事务执行前

事务的四大特性

原子性:事务是不可分割的最小单元 要么全部成功 要么全部失败
一致性:事务完成时 所有数据必须保持一致状态
隔离性:独立环境运行
持久性:事务一旦提交或回滚 他对数据的操作是永久的

spring对事务进行了封装 通过注解@Transaction方法或类或接口实现,一旦出错了就回滚方法等(回到方法执行前)

Transactional注解

@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

@Transactional注解书写位置:

  • 方法
    • 当前方法交给spring进行事务管理
    • 当前类中所有的方法都交由spring进行事务管理
  • 接口
    • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

开启事务管理日志

说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了

1
2
3
4
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug

事务进阶

@Transactional注解当中的两个常见的属性:

  1. 异常回滚的属性:rollbackFor
  2. 事务传播行为:propagation

rollbackFor

默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。

假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Autowired
private EmpMapper empMapper;


@Override
@Transactional(rollbackFor=Exception.class)//指定所有异常都回滚
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);

//模拟:异常发生
int num = id/0;

//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}

propagation

介绍

@Transactional注解当中的第二个属性propagation,这个属性是用来配置事务的传播行为的。

事务的传播行为:

  • 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

例如:两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法。

所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。

我们要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

对于这些事务传播行为,我们只需要关注以下两个就可以了:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW(在记录错误日志时可能会用到@Transactional(propagation = Propagation.REQUIRES_NEW))
  • REQUIRED :大部分情况下都是用该传播行为即可。

  • REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

AOP基础

AOP概述

  • AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。

AOP面向方法编程,在不改变原方法的基础上,增强方法的实现(简单理解就是通过动态代理一个模板方法 拿到原方法 继续在开头或结尾增加代码)

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

AOP和动态代理技术是非常类似的。 我们所说的模板方法,其实就是代理对象中所定义的方法,那代理对象中的方法以及根据对应的业务需要, 完成了对应的业务功能,当运行原始业务方法时,就会运行代理对象中的方法,从而实现增强方法操作。

其实,AOP面向切面编程和OOP面向对象编程一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)。

AOP的优势:

  1. 减少重复代码
  2. 提高开发效率
  3. 维护方便

AOP快速入门

需求:统计各个业务层方法执行耗时。

实现步骤:

  1. 导入依赖:在pom.xml中导入AOP的依赖
  2. 编写AOP程序:针对于特定方法根据业务需要进行编程

pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP程序:TimeAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();

//执行原始方法
Object result = pjp.proceed();

//记录方法执行结束时间
long end = System.currentTimeMillis();

//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

return result;
}
}

通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下:

  • 记录系统的操作日志
  • 权限控制
  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

AOP核心概念

1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

​ 连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

​ 在SpringAOP提供的JoinPoint当中,封装了连接点方法在执行时的相关信息。

2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

​ 在入门程序中是需要统计各个业务方法的执行耗时的,此时我们就需要在这些业务方法运行开始之前,先记录这个方法运行的开始时间,在每一个业务方法运行结束的时候,再来记录这个方法运行的结束时间。

​ 但是在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。

3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

​ 切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。

​ 在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点.

4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)

​ 当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。

​ 切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)

5. 目标对象:Target,通知所应用的对象

​ 目标对象指的就是通知所应用的对象,我们就称之为目标对象。

AOP过程

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

AOP进阶

  1. 通知类型
  2. 通知顺序
  3. 切入点表达式
  4. 连接点

通知类型

Spring中AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知 在方法运行前运行advice代码
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");

}

//环绕通知 在方法运行前后运行advice代码
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//方法运行前
log.info("around before ...");

//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();

//方法运行后
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}

//后置通知 在方法运行后运行advice代码 无论方法是否抛出异常
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}

在使用通知时的注意事项:

  • @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

解决切入点表达重复的问题:抽取

Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j
@Component
@Aspect
public class MyAspect1 {

//切入点方法(公共的切入点表达式)
//注意返回值是void
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){

}

//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){ //方法名无所谓
log.info("before ...");

}

//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//方法名无所谓 需要抛出proceed的异常
log.info("around before ...");

//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行

log.info("around after ...");
return result;
}

//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){//方法名无所谓
log.info("after ...");
}

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){//方法名无所谓
log.info("afterReturning ...");
}

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){//方法名无所谓
log.info("afterThrowing ...");
}
}

需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:

全类名.方法名(),具体形式如下:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.itheima.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}

通知顺序

在不同切面类中,默认按照切面类的类名字母排序:

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行

如果我们想控制通知的执行顺序有两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
  2. 使用Spring提供的@Order注解

使用@Order注解,控制通知的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}

//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}

//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
}

//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
}
}

通知的执行顺序大家主要知道两点即可:

  1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
  2. 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序

切入点表达式

切入点表达式:

  • 描述切入点方法的一种表达式

  • 作用:主要用来决定项目中的哪些方法需要加入通知

  • 常见形式:

    1. execution(……):根据方法的签名来匹配

    2. @annotation(……) :根据注解匹配

execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

1
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数(全类名)) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)

  • 包名.类名: 可省略

  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

示例:

1
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

可以使用通配符描述切入点

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式的语法规则:

  1. 方法的访问修饰符可以省略
  2. 返回值可以使用*号代替(任意返回值类型)
  3. 包名可以使用*号代替,代表任意包(一层包使用一个*
  4. 使用..配置包名,标识此包以及此包下的所有子包
  5. 类名可以使用*号代替,标识任意类
  6. 方法名可以使用*号代替,表示任意方法
  7. 可以使用 * 配置参数,一个任意类型的参数
  8. 可以使用.. 配置参数,任意个任意类型的参数

注意事项:

  • 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
1
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))

切入点表达式的书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头

  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性

  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包

@annotation

实现步骤:

1.编写自定义注解

自定义注解:MyLog

1
2
3
4
@Target(ElementType.METHOD) //指定注解作用域--只作用于方法
@Retention(RetentionPolicy.RUNTIME) //指定注解生效时间--运行时生效
public @interface MyLog {
}

2.在业务类要做为连接点的方法上添加自定义注解

在需要标记的方法名前加注解

1
@MyLog //自定义注解(表示:当前方法属于目标方法)

3.在切面类中识别方法

切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知

//前置通知
@Before("@annotation(com.itheima.anno.MyLog)")//只需要指定注解全类名即可
public void before(){
log.info("MyAspect6 -> before ...");
}

//后置通知
@After("@annotation(com.itheima.anno.MyLog)")//只需要指定注解全类名即可
public void after(){
log.info("MyAspect6 -> after ...");
}
}
  • execution切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
  • annotation 切入点表达式
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

连接点

连接点可以简单理解为可以被AOP控制的方法。

而在SpringAOP当中,连接点又特指方法的执行。

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Slf4j
@Component
@Aspect
public class MyAspect7 {

@Pointcut("@annotation(com.itheima.anno.MyLog)")
private void pt(){}

//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
}

//后置通知
@Before("pt()")
public void after(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
}

//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);

//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);

//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));

//执行原始方法
Object returnValue = pjp.proceed();

return returnValue;
}
}

SpingBoot原理

配置优先级

在SpringBoot项目当中除了3种配置文件(properties,yml,yaml)外,SpringBoot为了增强程序的扩展性,除了支持配置文件的配置方式以外,还支持另外两种常见的配置方式:

  1. Java系统属性配置 (格式: -Dkey=value)
1
-Dserver.port=9000
  1. 命令行参数 (格式:–key=value)
1
--server.port=10010

优先级: 命令行参数 > 系统属性参数 > properties参数 > yml参数 > yaml参数

在jar包中设置java属性和命令行参数

1
java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010

注意事项:

  • Springboot项目进行打包时,需要引入插件 spring-boot-maven-plugin (基于官网骨架创建项目,会自动添加该插件)

在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。优先级(从低到高):

  • application.yaml(忽略)
  • application.yml
  • application.properties
  • java系统属性(-Dxxx=xxx)
  • 命令行参数(–xxx=xxx)

Bean管理

  1. 如何从IOC容器中手动的获取到bean对象
  2. bean的作用域配置
  3. 管理第三方的bean对象

获取Bean

默认情况下,SpringBoot项目在启动的时候会自动的创建IOC容器(也称为Spring容器),并且在启动的过程当中会自动的将bean对象都创建好,存放在IOC容器当中。应用程序在运行时需要依赖什么bean对象,就直接进行依赖注入就可以了。

而在Spring容器中提供了一些方法,可以主动从IOC容器中获取到bean对象,下面介绍3种常用方式:

  1. 根据name获取bean
1
Object getBean(String name)
  1. 根据类型获取bean
1
<T> T getBean(Class<T> requiredType)
  1. 根据name获取bean(带类型转换)
1
<T> T getBean(String name, Class<T> requiredType)
  • 想获取到IOC容器,直接将IOC容器对象注入进来就可以了

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {

@Autowired
private ApplicationContext applicationContext; //IOC容器对象

//获取bean对象
@Test
public void testGetBean(){
//根据bean的名称获取
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
System.out.println(bean1);

//根据bean的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);

//根据bean的名称 及 类型获取
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
}
}

注意事项:

  • 上述所说的 【Spring项目启动时,会把其中的bean都创建好】还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的bean而言。

Bean作用域

在前面我们提到的IOC容器当中,默认bean对象是单例模式(只有一个实例对象)。那么如何设置bean对象为非单例呢?需要设置bean的作用域。

在Spring中支持五种作用域,后三种在web环境才生效:

作用域 说明
singleton 容器内同名称的bean只有一个实例(单例)(默认)
prototype 每次使用该bean时会创建新的实例(非单例)
request 每个请求范围内会创建新的实例(web环境中,了解)
session 每个会话范围内会创建新的实例(web环境中,了解)
application 每个应用范围内会创建新的实例(web环境中,了解)

设置一个bean的作用域:

  • 可以借助Spring中的@Scope注解来进行配置作用域

注意事项:

  • IOC容器中的bean默认使用的作用域:singleton (单例)

  • 默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)

1
2
@Scope("prototype") //bean作用域为非单例
@Lazy //延迟加载 在第一次调用bean对象时加载 而不是IOC容器初始化时加载

注意事项:

  • prototype的bean,每一次使用该bean的时候都会创建一个新的实例
  • 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性

第三方Bean

但是在我们项目开发当中,有一种情况就是这个类它不是我们自己编写的,而是我们引入的第三方依赖当中提供的。

  • 如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。

第三方类一般是只读的 不能修改 所以不能加注解

解决方案:在配置类中定义@Bean标识的方法

  • 如果需要定义第三方Bean时, 通常会单独定义一个配置类

    在之前interceptor(拦截器)中也有过配置类@Configuration

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Configuration //配置类  (在配置类当中对第三方bean进行集中的配置管理)
    public class CommonConfig {

    //声明第三方bean
    @Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
    //通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名
    //如果第三方的bean依赖于其他bean对象,直接在bean方法中设置形参即可 容器会自动装配???
    public SAXReader reader(DeptService deptService){
    System.out.println(deptService);
    return new SAXReader();
    }

    }

注意事项 :

  • 通过@Bean注解的name或value属性可以声明bean的名称,如果不指定,默认bean的名称就是方法名。

  • 如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。

关于Bean只需要保持一个原则:

  • 如果是在项目当中我们自己定义的类,想将这些类交给IOC容器管理,我们直接使用@Component以及它的衍生注解来声明就可以。
  • 如果这个类它不是我们自己定义的,而是引入的第三方依赖当中提供的类,而且我们还想将这个类交给IOC容器管理。此时我们就需要在配置类中定义一个方法,在方法上加上一个@Bean注解,通过这种方式来声明第三方的bean对象。

SpringBoot回顾

Spring是目前世界上最流行的Java框架,它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中提供了很多优秀的框架,而所有的框架都是基于一个基础框架的SpringFramework(也就是Spring框架)。而前面我们也提到,如果我们直接基于Spring框架进行项目的开发,会比较繁琐。

这个繁琐主要体现在两个地方:

  1. 在pom.xml中依赖配置比较繁琐,在项目开发时,需要自己去找到对应的依赖,还需要找到依赖它所配套的依赖以及对应版本,否则就会出现版本冲突问题。
  2. 在使用Spring框架进行项目开发时,需要在Spring的配置文件中做大量的配置,这就造成Spring框架入门难度较大,学习成本较高。

基于Spring存在的问题,官方在Spring框架4.0版本之后,又推出了一个全新的框架:SpringBoot。

通过 SpringBoot来简化Spring框架的开发(是简化不是替代)。我们直接基于SpringBoot来构建Java项目,会让我们的项目开发更加简单,更加快捷。

SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。

通过SpringBoot所提供的起步依赖,就可以大大的简化pom文件当中依赖的配置,从而解决了Spring框架当中依赖配置繁琐的问题。

通过自动配置的功能就可以大大的简化框架在使用时bean的声明以及bean的配置。我们只需要引入程序开发时所需要的起步依赖,项目开发时所用到常见的配置都已经有了,我们直接使用就可以了。

起步依赖

为什么我们只需要引入一个web开发的起步依赖springboot-starter-web,web开发所需要的所有的依赖都有了呢?

  • 因为Maven的依赖传递。
  • 在SpringBoot给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters)。

  • 比如:springboot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。

结论:起步依赖的原理就是Maven的依赖传递。

自动配置

概述

SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。

比如:我们要进行事务管理、要进行AOP程序的开发,此时就不需要我们再去手动的声明这些bean对象了,我们直接使用就可以从而大大的简化程序的开发,省去了繁琐的配置操作。

在配置类上添加了一个注解@Configuration,而@Configuration底层就是@Component

所以配置类最终也是SpringIOC容器当中的一个bean对象
这也是为什么我们可以在配置类中通过@bean将第三方bean交给IOC容器管理

SpringBoot项目在启动时通过自动配置完成了起步依赖中所提供的bean对象的创建以及配置类直接加载到当前项目的IOC容器中,所以我们可以没有定义bean对象,但是可以注入相关的bean对象。

自动配置底层解析

概述

解析自动配置的原理就是分析在 SpringBoot项目当中,我们引入对应的依赖之后,是如何将依赖jar包当中所提供的bean以及配置类直接加载到当前项目的SpringIOC容器当中的。

情况:当我们新建一个模块,并且其中有bean对象和配置类,引入模块依赖,但是模块bean并没有导入当前项目IOC容器。

  • 原因在我们之前讲解IOC的时候有提到过,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。
  • SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。

解决方案:

  • 方案1:@ComponentScan 组件扫描(在启动类中加入)
  • 方案2:@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)

方案一

@ComponentScan组件扫描

1
2
3
4
5
6
7
8
9
@SpringBootApplication
//在启动类中加入
@ComponentScan({"com.itheima","com.example"}) //指定要扫描的包
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

缺点:

  1. 使用繁琐
  2. 性能低

结论:SpringBoot中并没有采用以上这种方案。

方案二

@Import导入

  • 导入形式主要有以下几种:
    1. 导入普通类
    2. 导入配置类
    3. 导入ImportSelector接口实现类

1). 使用@Import导入普通类:

1
2
3
4
5
6
7
@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

2). 使用@Import导入配置类:

  • 配置类
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
}

@Bean
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
}
}
  • 启动类
1
2
3
4
5
6
7
@Import(HeaderConfig.class) //导入配置类
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

3). 使用@Import导入ImportSelector接口实现类:

  • ImportSelector接口实现类
1
2
3
4
5
6
7
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//返回值字符串数组(数组中封装了全限定名称的类)
return new String[]{"com.example.HeaderConfig"};
//这里同上述一致 将HeaderConfig交给了IOC容器
}
}
  • 启动类
1
2
3
4
5
6
7
8
9
@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {

public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

springboot中采取的依赖导入方式:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。

  • 比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxxx开头的注解,注解中封装的就是@Import注解

4). 使用第三方依赖提供的 @EnableXxxxx注解

  • 第三方依赖中提供的注解
1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig {
}
  • 在使用时只需在启动类上加上@EnableXxxxx注解即可
1
2
3
4
5
6
7
8
@EnableHeaderConfig  //使用第三方依赖提供的Enable开头的注解
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

自动配置源码跟踪

源码跟踪技巧:

在跟踪框架源码的时候,一定要抓住关键点,找到核心流程。一定不要从头到尾一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上对整个流程或者整个原理有一个认识,有精力再去研究其中的细节。

要搞清楚SpringBoot的自动配置原理,要从SpringBoot启动类上使用的核心注解@SpringBootApplication开始分析

在@SpringBootApplication注解中包含了:

  • 元注解(不再解释)
  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

@SpringBootConfiguration

  • @Target({ElementType.TYPE})
  • @Retention(RetentionPolicy.RUNTIME)
  • @Documented
  • @Configuration
  • @Indexed

@SpringBootConfiguration注解上使用了@Configuration,表明SpringBoot启动类就是一个配置类。

@Indexed注解,是用来加速应用启动的(不用关心)。

@ComponentScan

@ComponentScan注解是用来进行组件扫描的,扫描启动类所在的包及其子包下所有被@Component及其衍生注解声明的类。

SpringBoot启动类,之所以具备扫描包功能,就是因为包含了@ComponentScan注解。

@EnableAutoConfiguration(自动配置核心注解)

  • @Target({ElementType.TYPE})
  • @Retention(RetentionPolicy.RUNTIME)
  • @Documented
  • @Inherited
  • @AutoConfigurationPackage
  • @Import({AutoConfigurationImportSelector.class})

使用@Import注解,导入了实现ImportSelector接口的实现类。

@AutoConfigurationImportSelector

AutoConfigurationImportSelector类是ImportSelector接口的实现类。

AutoConfigurationImportSelector类中重写了ImportSelector接口的selectImports()方法:

selectImports()方法底层调用getAutoConfigurationEntry()方法,获取可自动配置的配置类信息集合

getAutoConfigurationEntry()方法通过调用getCandidateConfigurations(annotationMetadata, attributes)方法获取在配置文件中配置的所有自动配置类的集合

getCandidateConfigurations方法的功能:

获取所有基于META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件、META-INF/spring.factories文件中配置类的集合

  • 通常在引入的起步依赖spring-boot-autoconfigure包中,都有包含以上两个文件

自动配置源码小结

自动配置原理源码入口就是@SpringBootApplication注解,在这个注解中封装了3个注解,分别是:

  • @SpringBootConfiguration
    • 声明当前类是一个配置类
  • @ComponentScan
    • 进行组件扫描(SpringBoot中默认扫描的是启动类所在的当前包及其子包)
  • @EnableAutoConfiguration
    • 封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)
      • 在实现类重写的selectImports()方法,读取当前项目下所有依赖jar包中META-INF/spring.factories、META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports两个文件里面定义的配置类(配置类中定义了@Bean注解标识的方法)。

当SpringBoot程序启动时,就会加载配置文件当中所定义的配置类,并将这些配置类信息(类的全限定名)封装到String类型的数组中,最终通过@Import注解将这些配置类全部加载到Spring的IOC容器中,交给IOC容器管理。

条件注入:@Conditional

在声明bean对象时,上面有加一个以@Conditional开头的注解,这种注解的作用就是按照条件进行装配,只有满足条件之后,才会将bean注册到Spring的IOC容器中

@Conditional注解:

  • 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。
  • 位置:方法、类
  • @Conditional本身是一个父注解,派生出大量的子注解:
    • @ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
    • @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。
    • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。

梳理一下自动配置原理:(简化过程 详细请看上面底层解析)

自动配置的核心就在@SpringBootApplication注解上,SpringBootApplication这个注解底层包含了3个注解,分别是:

  • @SpringBootConfiguration

  • @ComponentScan

  • @EnableAutoConfiguration

@EnableAutoConfiguration这个注解才是自动配置的核心。

  • @Enable 开头的注解底层,它就封装了一个注解 import 注解,它里面指定了一个类,是 ImportSelector 接口的实现类。在实现类当中,我们需要去实现 ImportSelector 接口当中的一个方法 selectImports 这个方法。这个方法的返回值代表的就是我需要将哪些类交给 spring 的 IOC容器进行管理。
  • 此时它会去读取两份配置文件,一份儿是 spring.factories,另外一份儿是 autoConfiguration.imports。而在 autoConfiguration.imports 这份儿文件当中,它就会去配置大量的自动配置的类。
  • 而前面我们也提到过这些所有的自动配置类当中,所有的 bean都会加载到 spring 的 IOC 容器当中吗?其实并不会,因为这些配置类当中,在声明 bean 的时候,通常会加上这么一类@Conditional 开头的注解。这个注解就是进行条件装配。所以SpringBoot非常的智能,它会根据 @Conditional 注解来进行条件装配。只有条件成立,它才会声明这个bean,才会将这个 bean 交给 IOC 容器管理。

自定义starter起步依赖

所谓starter指的就是SpringBoot当中的起步依赖。在SpringBoot当中已经给我们提供了很多的起步依赖了,我们为什么还需要自定义 starter 起步依赖?这是因为在实际的项目开发当中,我们可能会用到很多第三方的技术,并不是所有的第三方的技术官方都给我们提供了与SpringBoot整合的starter起步依赖,但是这些技术又非常的通用,在很多项目组当中都在使用。

业务场景:

  • 我们前面案例当中所使用的阿里云OSS对象存储服务,现在阿里云的官方是没有给我们提供对应的起步依赖的,这个时候使用起来就会比较繁琐,我们需要引入对应的依赖。我们还需要在配置文件当中进行配置,还需要基于官方SDK示例来改造对应的工具类,我们在项目当中才可以进行使用。
  • 大家想在我们当前项目当中使用了阿里云OSS,我们需要进行这么多步的操作。在别的项目组当中要想使用阿里云OSS,是不是也需要进行这么多步的操作,所以这个时候我们就可以自定义一些公共组件,在这些公共组件当中,我就可以提前把需要配置的bean都提前配置好。将来在项目当中,我要想使用这个技术,我直接将组件对应的坐标直接引入进来,就已经自动配置好了,就可以直接使用了。我们也可以把公共组件提供给别的项目组进行使用,这样就可以大大的简化我们的开发。

在SpringBoot项目中,一般都会将这些公共组件封装为SpringBoot当中的starter,也就是我们所说的起步依赖。

SpringBoot官方starter命名: spring-boot-starter-xxxx

第三组织提供的starter命名: xxxx-spring-boot-starter

在自定义一个起步依赖starter的时候,按照规范需要定义两个模块:

  1. starter模块(进行依赖管理[把程序开发所需要的依赖都定义在starter起步依赖中])
  2. autoconfigure模块(自动配置)(autoconfigure所需要的依赖依然要定义在该模块中)

将来在项目当中进行相关功能开发时,只需要引入一个起步依赖就可以了,因为它会将autoconfigure自动配置的依赖给传递下来。

Web后端开发总结

web后端开发现在基本上都是基于标准的三层架构进行开发的,在三层架构当中,Controller控制器层负责接收请求响应数据,Service业务层负责具体的业务逻辑处理,而Dao数据访问层也叫持久层,就是用来处理数据访问操作的,来完成数据库当中数据的增删改查操作。

在三层架构当中,前端发起请求首先会到达Controller(不进行逻辑处理),然后Controller会直接调用Service 进行逻辑处理, Service再调用Dao完成数据访问操作。

如果我们在执行具体的业务处理之前,需要去做一些通用的业务处理,比如:我们要进行统一的登录校验,我们要进行统一的字符编码等这些操作时,我们就可以借助于Javaweb当中三大组件之一的过滤器Filter或者是Spring当中提供的拦截器Interceptor来实现。

而为了实现三层架构层与层之间的解耦,就要用到Spring框架当中的第一大核心:IOC控制反转与DI依赖注入。

所谓控制反转,指的是将对象创建的控制权由应用程序自身交给外部容器,这个容器就是我们常说的IOC容器或Spring容器。

而DI依赖注入指的是容器为程序提供运行时所需要的资源。

除了IOC与DI我们还讲到了AOP面向切面编程,还有Spring中的事务管理、全局异常处理器,以及传递会话技术Cookie、Session以及新的会话跟踪解决方案JWT令牌,阿里云OSS对象存储服务,以及通过Mybatis持久层架构操作数据库等技术。

Filter过滤器、Cookie、 Session这些都是传统的JavaWeb提供的技术。

JWT令牌、阿里云OSS对象存储服务,是现在企业项目中常见的一些解决方案。

IOC控制反转、DI依赖注入、AOP面向切面编程、事务管理、全局异常处理、拦截器等,这些技术都是 Spring Framework框架当中提供的核心功能。

Mybatis就是一个持久层的框架,是用来操作数据库的。

在Spring框架的生态中,对web程序开发提供了很好的支持,如:全局异常处理器、拦截器这些都是Spring框架中web开发模块所提供的功能,而Spring框架的web开发模块,我们也称为:SpringMVC

SpringMVC不是一个单独的框架,它是Spring框架的一部分,是Spring框架中的web开发模块,是用来简化原始的Servlet程序开发的。

外界俗称的SSM,就是由:SpringMVC、Spring Framework、Mybatis三块组成。

基于传统的SSM框架进行整合开发项目会比较繁琐,而且效率也比较低,所以在现在的企业项目开发当中,基本上都是直接基于SpringBoot整合SSM进行项目开发的。

Maven高级

Maven 是一款构建和管理 Java 项目的工具

Maven高级内容包括:

  • 分模块设计与开发
  • 继承与聚合
  • 私服

分模块设计与开发

介绍

所谓分模块设计,顾名思义指的就是我们在设计一个 Java 项目的时候,将一个 Java 项目拆分成多个模块进行开发。

不采取分模块设计的问题总结起来,主要两点问题:不方便项目的维护和管理、项目中的通用组件难以复用。

分模块设计就是将项目按照功能/结构拆分成若干个子模块,方便项目的管理维护、拓展,也方便模块键的相互调用、资源共享。

总结

1). 什么是分模块设计:将项目按照功能拆分成若干个子模块

2). 为什么要分模块设计:方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享

3). 注意事项:分模块设计需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分

继承与聚合

在分模块之后,我们会发现存在不同模块引用同一依赖这种情况,这就会导致多次重复操作,并且还可能有依赖版本不匹配的问题。

Maven 的继承用来解决这问题的。

继承

  • 概念:继承描述的是两个工程间的关系,与java中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承,同样不支持多继承父工程,但是可以通过多重继承实现。

  • 作用:简化依赖配置、统一管理依赖

  • 子工程继承实现:
    实现

    1
    2
    3
    4
    5
    6
    7
    <parent>
    <groupId>...</groupId>
    <artifactId>...</artifactId>
    <version>...</version><!-- 父工程坐标 -->
    <relativePath>....</relativePath><!-- 父工程相对路径 -->
    </parent>

父工程实现:
实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.itheima</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging><!-- 父工程需要指定打包方式为pom -->

<dependencies><!-- 在父工程中配置各个工程共有的依赖(子工程会自动继承父工程的依赖) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>

Maven打包方式:

  • jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)
  • war:普通web程序打包,需要部署在外部的tomcat服务器中运行
  • pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理

注意:

  • 在子工程中,配置了继承关系之后,坐标中的groupId是可以省略的,因为会自动继承父工程的 。
  • relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查找该工程)。
    • ../ 代表的上一级目录

此时,我们已经将各个子工程中共有的依赖(lombok),都定义在了父工程中,子工程中的这一项依赖,就可以直接删除了。删除之后,我们会看到父工程中配置的依赖 lombok,子工程直接继承下来了。

版本锁定

问题:如果项目拆分的模块比较多,每一次更换版本,我们都得找到这个项目中的每一个模块,一个一个的更改。 很容易就会出现,遗漏掉一个模块,忘记更换版本的情况。

通过Maven的版本锁定功能实现。

介绍

在maven中,可以在父工程的pom文件中通过 <dependencyManagement> 来统一管理依赖版本。

父工程:

1
2
3
4
5
6
7
8
9
10
11
<!--统一管理依赖版本-->
<dependencyManagement>
<dependencies>
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>

子工程:

1
2
3
4
5
6
7
<dependencies>
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

注意:

  • 在父工程中所配置的 <dependencyManagement> 只能统一管理依赖版本,并不会将这个依赖直接引入进来。 这点和 <dependencies> 是不同的。

  • 子工程要使用这个依赖,还是需要引入的,只是此时就无需指定 <version> 版本号了,父工程统一管理。变更依赖版本,只需在父工程中统一变更。
    我们之所以,在springboot项目中很多时候,引入依赖坐标,都不需要指定依赖的版本 <version> ,是因为在父工程 spring-boot-starter-parent中已经通过 <dependencyManagement>对依赖的版本进行了统一的管理维护。

属性配置

我们也可以通过自定义属性及属性引用的形式,在父工程中将依赖的版本号进行集中管理维护。 具体语法为:

1). 自定义属性

1
2
3
<properties>
<lombok.version>1.18.24</lombok.version>
</properties>

2). 引用属性

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>

版本集中管理之后,我们要想修改依赖的版本,就只需要在父工程中自定义属性的位置,修改对应的属性值即可。

面试题:<dependencyManagement><dependencies> 的区别是什么?

  • <dependencies> 是直接依赖,在父工程配置了依赖,子工程会直接继承下来。
  • <dependencyManagement> 是统一管理依赖版本,不会直接依赖,还需要在子工程中引入所需依赖(无需指定版本)

聚合

如果开发一个大型项目,拆分的模块很多,模块之间的依赖关系错综复杂,那此时要进行项目的打包、安装操作,是非常繁琐的(需要先把最底层的模块打成jar包 再依次往上层打)。maven的聚合就是来解决这个问题的,通过maven的聚合就可以轻松实现项目的一键构建(清理、编译、测试、打包、安装等)。

介绍

  • 聚合:将多个模块组织成一个整体,同时进行项目的构建。
  • 聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件) 【PS:一般来说,继承关系中的父工程与聚合关系中的聚合工程是同一个】
  • 作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)

实现

在maven中,我们可以在聚合工程中通过 <moudules> 设置当前聚合工程所包含的子模块的名称。我们可以在 tlias-parent中,添加如下配置,来指定当前聚合工程,需要聚合的模块:

1
2
3
4
5
6
<!--聚合其他模块-->
<modules>
<module>../tlias-pojo</module>
<module>../tlias-utils</module>
<module>../tlias-web-management</module>//指定相对路径
</modules>

那 tlias-parent 中所聚合的其他模块全部都会执行 package 指令,这就是通过聚合实现项目的一键构建(一键清理clean、一键编译compile、一键测试test、一键打包package、一键安装install等)。

继承与聚合对比

  • 作用

    • 聚合用于快速构建项目

    • 继承用于简化依赖配置、统一管理依赖

  • 相同点:

    • 聚合与继承的pom.xml文件打包方式均为pom,通常将两种关系制作到同一个pom文件中

    • 聚合与继承均属于设计型模块,并无实际的模块内容

  • 不同点:

    • 聚合是在聚合工程中配置关系,聚合可以感知到参与聚合的模块有哪些

    • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己

私服

用来解决同一项目组中不同模块设计组需要共用一些jar包的问题 要与之前的自定义starter做出区分 那个针对的是第三方提供的程序 是不肯更改的工具类集 而私服是为了解决项目内部所产生的程序的共享 一般是一些自定义的属性类或者自定义的工具模块 是可更改的
介绍

  • 私服:是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用来代理位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题。
  • 依赖查找顺序:
    • 本地仓库
    • 私服仓库
    • 中央仓库
  • 注意事项:私服在企业项目开发中,一个项目/公司,只需要一台即可(无需我们自己搭建,会使用即可)。

资源上传与下载

步骤分析

资源上传与下载,我们需要做三步配置,执行一条指令。

第一步配置:在maven的配置文件中配置访问私服的用户名、密码。

第二步配置:在maven的配置文件中配置连接私服的地址(url地址)。

第三步配置:在项目的pom.xml文件中配置上传资源的位置(url地址)。

配置好了上述三步之后,要上传资源到私服仓库,就执行执行maven生命周期:deploy。

私服仓库说明:

  • RELEASE:存储自己开发的RELEASE发布版本的资源。
  • SNAPSHOT:存储自己开发的SNAPSHOT发布版本的资源。
  • Central:存储的是从中央仓库下载下来的依赖。

项目版本说明:

  • RELEASE(发布版本):功能趋于稳定、当前更新停止,可以用于发行的版本,存储在私服中的RELEASE仓库中。
  • SNAPSHOT(快照版本):功能不稳定、尚处于开发中的版本,即快照版本,存储在私服的SNAPSHOT仓库中。

具体操作

1.设置私服的访问用户名/密码(在自己maven安装目录下的conf/settings.xml中的servers中配置)

1
2
3
4
5
6
7
8
9
10
11
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>

<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>

2.设置私服依赖下载的仓库组地址(在自己maven安装目录下的conf/settings.xml中的mirrors、profiles中配置)

1
2
3
4
5
<mirror>
<id>maven-public</id>
<mirrorOf>*</mirrorOf>
<url>http://192.168.150.101:8081/repository/maven-public/</url><!-- 使用公司提供的 -->
</mirror>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<profile>
<id>allow-snapshots</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>maven-public</id>
<url>http://192.168.150.101:8081/repository/maven-public/</url><!-- 使用公司提供的 -->
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>

3.IDEA的maven工程的pom文件中配置上传(发布)地址(直接在tlias-parent中配置发布地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
<distributionManagement>
<!-- release版本的发布地址 -->
<repository>
<id>maven-releases</id>
<url>http://192.168.150.101:8081/repository/maven-releases/</url><!-- 使用公司提供的 -->
</repository>

<!-- snapshot版本的发布地址 -->
<snapshotRepository>
<id>maven-snapshots</id>
<url>http://192.168.150.101:8081/repository/maven-snapshots/</url><!-- 使用公司提供的 -->
</snapshotRepository>
</distributionManagement>

配置完成之后,我们就可以在tlias-parent中执行deploy生命周期,将项目发布到私服仓库中。