javaweb
javaweb
web前端开发(==接口文档模拟网站 yapi —可以自定义api接口供前端调用==)
==html:超文本标记语言==
1 | 1·html标签不区分大小写 |
==css:层叠样式表==
1 | 1.引入方式:行内<h1 style = ""> 内嵌<style></style> 外联.css <link rel = "stylesheet" href = "css/news.css">引入 |
==javascript(脚本语言 不用编译)==
1 | 1.引入方式:内:<script>可以出现在html中任意位置 </script> |
VUE(前端框架)
优点:免除DOM操作,简化书写
基于M-V-VM模型 实现数据双向绑定
1 | //进入vue.js文件 |
常用指令
1 | v-bind:绑定属性值 //简写为 :name="" |
生命周期(Vue从创建到完成所经历的8个阶段)
重点mounted://挂载完成时调用 一般在此时向服务器请求信息
ajax(asynchronous)(异步)
优点:可以不更新全部页面完成网页数据更新
原生ajax请求数据过程:(现在基本不用了 存在浏览器兼容性)
1 | <script> |
axios请求数据过程:(axios是对原生的ajax的封装)
1 | <script> |
前后端分离开发模式
基本理论
需求分析 ->定义接口文档-> 前后端按照接口文档分离开发-> 前后端分别测试-> 前后端联合测试
前端工程化:
用处:用于企业级开发,讲究模块化 工程化 ==组件化(页面 页面局部 单个DOM元素块均可组件化)== 规范化 自动化
环境准备:vue-cli (vue官方提供的一个脚手架 用于快速生成一个vue模板)
依赖环境:NodeJS
一些常见前端更改操作:
1更改端口号:在==vue.config.js==文件中增加
1 | devServer:{ |
vue基础语法与详细信息见官网快速入门
.vue文件基本格式:
1 | <template> |
element
美化封装的DOM元素组件
使用element组件过程:
1去官网复制对应的自己需要的组件
2粘贴并进行个性化修改
常见组件:
1 | //el-button type指定button类型 |
vue路由
使用前同axios 需要先下载导入
vue-router:路由器类router/index.js 指定url与对应的渲染组件
1 | <router-link to = "url"></router-link> |
打包部署
打包:==>形成一个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 | <groupid></groupid>//定义当前maven项目隶属组织名称(一般是域名反写) |
依赖配置:
//先在本地仓库找 没有才去远程仓库下载
//不知道依赖坐标信息可以去官网查找==mvnrepository.com==
//依赖更改后要刷新一下maven才导入成功
1 | <dependencies> |
依赖传递:
//依赖具有传递性 一个项目依赖了另一个项目 也会继承他的依赖
//依赖可以排除
1 | <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 | //原始方式 |
响应
基于@ResponseBody这个注解(@restController = @Controller+@ResponseBody)
作用:将方法返回值直接响应,如果返回值是实体对象/集合,将会转为JSON字符串格式返回
统一响应结果
return Result.success(object);//数据object又会被转为JSON格式返回
1 | /** |
有关XML文件解析
1 | //依赖注入->pom.xml |
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 | DDL:操作数据库,表 |
==DDL==
操作数据库
1 | 1.查询 show databases;#查询当前目录下的所有数据库名称 |
操作表(先进入数据库 use)
1 | 1.查询表 show tables;#展示有哪些表 |
图形化工具
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 | select 列字段 (*代表所有列)(distinct 代表去除重复项) |
外键约束
目前不推荐物理外键(foreign key)一般在业务逻辑层 通过代码实现数据库的一致性完整性
外键指向 多指向一
数据库中的五种约束:非空 主键 唯一 默认 外键一对一 任一方加外键实现 并且加上unique唯一
一对多 多的那方向一的那方加外键实现
多对多 外键+中间表实现
多表查询(笛卡尔积->要通过where条件消除无效的查询记录)
内连接 会排除掉where条件中的null属性记录
(显示(join))
(隐示)
外连接
左外连接(left join)查询左边表所有记录 不会排除掉where条件中的右边表null属性记录
右外连接(right join)查询右边表所有记录 不会排除掉where条件中的左边表null属性记录
嵌套查询(子查询)
from 子表
事务
一组操作的集合 不可分割 要么同时成功 要么同时失败
事务与事务之间是隔离的
开启事务 begin #写在执行语句前
提交事务 commit #事务没提交前只能在事务内看到执行结果 指令失败没办法提交
回滚事务 rollback #指令失败或错误可以回滚 回到事务执行前
事务的四大特性
原子性:事务是不可分割的最小单元 要么全部成功 要么全部失败
一致性:事务完成时 所有数据必须保持一致状态
隔离性:独立环境运行
持久性:事务一旦提交或回滚 他对数据的操作是永久的
索引
帮助数据库高效获取数据的数据结构
==提高查询的效率 但是降低增删改的效率==
1 | #创建索引 |
==注意==
创建主键 会默认给主键创建索引
添加唯一约束 会添加唯一索引
默认查询是全表扫描
索引默认结构是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 | #驱动类名称 |
基本mybatis操作(增删除改)
1 | //查询全部数据 |
开启驼峰命名自动映射
1 | //在application.properties加入配置项 |
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有两个优势:
- 性能更高
- 更安全(防止SQL注入)
性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)
更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。
SQL注入
SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。
参数占位符
在Mybatis中提供的参数占位符有两种:${…} 、#{…}
#{…}
- 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值
- 使用时机:参数传递,都使用#{…}
${…}
- 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
- 使用时机:如果对表名、列表进行动态设置时使用
注意事项:在项目开发中,建议使用#{…},生成预编译SQL,防止SQL注入安全。
日志输入
在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:
打开application.properties文件
开启mybatis的日志,并指定输出到控制台
1 | #指定mybatis输出日志的位置, 输出控制台 |
案例实操
Rest风格—定义接口规范
1 | //GET查询 /users/1 |
日志记录
1 |
|
抽取相同url
1 | //抽取共同路径 |
设定默认值
1 |
|
pageHelper插件依赖实现分页数据返回
1 | //注入依赖 |
文件上传
本地存储 (用的很少)(可以了解具体流程)
想要完成文件上传这个功能需要涉及到两个部分:
- 前端程序
- 服务端程序
我们先来看看在前端程序中要完成哪些代码:
1 | <form action="/upload" method="post" enctype="multipart/form-data"> |
上传文件的原始form表单,要求表单必须具备以下三点(上传文件页面三要素):
表单必须有file域,用于选择要上传的文件
1
<input type="file" name="image"/>
表单提交方式必须为POST
通常上传的文件会比较大,所以需要使用 POST 提交方式
表单的编码类型enctype必须要设置为:multipart/form-data
普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data
如果不设置 当传递file类型表单数据的时候只会传递文件的名字
后端接收数据:
UploadController代码:
1 |
|
通过后端程序控制台可以看到,上传的文件是存放在一个临时目录
表单提交的三项数据(姓名、年龄、文件),分别存储在不同的临时文件中:
当我们程序运行完毕之后,这个临时文件会自动删除。
所以,我们如果想要实现文件上传,需要将这个临时文件,要转存到我们的磁盘目录中。
本地存储到磁盘
代码实现:
- 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
- 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下
MultipartFile 常见方法:
- String getOriginalFilename(); //获取原始文件名
- void transferTo(File dest); //将接收的文件转存到磁盘文件中
- long getSize(); //获取文件的大小,单位:字节
- byte[] getBytes(); //获取文件内容的字节数组
- InputStream getInputStream(); //获取接收到的文件内容的输入流
1 |
|
优化(使上传文件不同名):保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)
1 |
|
优化:上传大文件(spring默认上传文件大小最多为1MB)
那么如果需要上传大文件,可以在application.properties进行如下配置:
1 | #配置单个文件最大上传大小 |
如果直接存储在服务器的磁盘目录中,存在以下缺点:
- 不安全:磁盘如果损坏,所有的文件就会丢失
- 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
- 无法直接访问
为了解决上述问题呢,通常有两种解决方案:
- 自己搭建存储服务器,如:fastDFS 、MinIO
- 使用现成的云服务,如:阿里云,腾讯云,华为云
阿里OSS
注册阿里云OSS账户
开创一个OSS对象
创建一个Bucket
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
拿到自己阿里云账户的密钥ID和密码(记得保存 现在不可查看了)
根据OSS的SDK开创一个工具类
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。
引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)
1 | import com.aliyun.oss.OSS; |
springboot配置文件(yml/yaml)
yml配置文件的基本语法
- 大小写敏感
- 数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
#
表示注释,从这个字符一直到行尾,都会被解析器忽略
了解完yml格式配置文件的基本语法之后,接下来我们再来看下yml文件中常见的数据格式。在这里我们主要介绍最为常见的两类:
- 定义对象或Map集合
- 定义数组、list或set集合
对象/Map集合
1 | user: |
数组/List/Set集合
1 | hobby: |
配置文件定义以及调用数据
定义数据
1 | aliyun: |
调用数据两种方式:
1 | //方法一:通过注解@value("${配置文件中的变量名}")注入 |
登录+会话技术+统一拦截
登录后端设计思路
登录服务端的核心逻辑就是:接收前端请求传递的用户名和密码 ,然后再根据用户名和密码查询用户信息,如果用户信息存在,则说明用户输入的用户名和密码正确。如果查询到的用户不存在,则说明用户输入的用户名和密码错误。
会话技术
在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。
在用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。
比如:打开了浏览器来访问web服务器上的资源(浏览器不能关闭、服务器不能断开)
- 第1次:访问的是登录的接口,完成登录操作
- 第2次:访问的是部门管理接口,查询所有部门数据
- 第3次:访问的是员工管理接口,查询员工数据
只要浏览器和服务器都没有关闭,以上3次请求都属于一次会话当中完成的。
需要注意的是:会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
服务器会接收很多的请求,但是服务器是需要识别出这些请求是不是同一个浏览器发出来的。比如:1和2这两个请求是不是同一个浏览器发出来的,3和5这两个请求不是同一个浏览器发出来的。如果是同一个浏览器发出来的,就说明是同一个会话。如果是不同的浏览器发出来的,就说明是不同的会话。而识别多次请求是否来自于同一浏览器的过程,我们就称为会话跟踪。
我们使用会话跟踪技术就是要完成在同一个会话中,多个请求之间进行共享数据。
为什么要共享数据呢?
由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据呢?此时就需要在一次会话的多次请求之间进行数据共享
会话跟踪技术有三种:
- Cookie(客户端会话跟踪技术)
- 数据存储在客户端浏览器当中
- Session(服务端会话跟踪技术)(是基于cookie的)
- 数据存储在储在服务端
前两种现在用的很少了 了解即可
- 数据存储在储在服务端
- 令牌技术
** 方案一 - 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 |
|
优缺点
- 优点: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 |
|
优缺点
- 优点: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令牌最典型的应用场景就是登录认证:
- 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
- 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
- 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。
生成和校验
首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
1 | <!-- JWT依赖--> |
我们在使用JWT令牌时需要注意:
JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。
登录下发令牌
JWT工具类
1 | //在utils包下创建工具类 |
登录成功,生成JWT令牌并返回
1 |
|
过滤器Filter
服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。
两种解决方案:
- Filter过滤器
- Interceptor拦截器
什么是Filter?
- Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
过滤器的基本使用操作:
- 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
- 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
定义过滤器
1 | //定义一个类,实现一个标准的Filter过滤器的接口 |
init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。
当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。
1 |
|
Filter详解
- 过滤器的执行流程
- 过滤器的拦截路径配置
- 过滤器链
** 执行流程**
过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。
** 拦截路径**
执行流程我们搞清楚之后,接下来再来介绍一下过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
过滤器链
所谓过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。
比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。
访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。
先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。
过滤器执行顺序:
其实是和过滤器的类名有关系。以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。
登录校验-Filter
登录校验过滤器:LoginCheckFilter
1 |
|
在上述过滤器的功能实现中,我们使用到了一个第三方json处理的工具包fastjson。我们要想使用,需要引入如下依赖:
1 | <dependency> |
拦截器Interceptor
什么是拦截器?
- 是一种动态拦截方法调用的机制,类似于过滤器。
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
拦截器的作用:
- 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
拦截器的使用步骤和过滤器类似,也分为两步:
- 定义拦截器
- 注册配置拦截器
自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
1 | //自定义拦截器 |
注意:
preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行后执行
afterCompletion方法:视图渲染完毕后执行,最后执行
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
1 |
|
** Interceptor详解**
- 拦截器的拦截路径配置
- 拦截器的执行流程
拦截路径
在配置文件中,
通过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 | //自定义拦截器 |
注册配置拦截器
1 |
|
全局异常处理器
定义全局异常处理器
- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
1 |
|
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
Spring事务
Spring事务管理
回顾:
事务的定义:一组操作的集合 不可分割 要么同时成功 要么同时失败
事务与事务之间是隔离的
开启事务 begin #写在执行语句前
提交事务 commit #事务没提交前只能在事务内看到执行结果 指令失败没办法提交
回滚事务 rollback #指令失败或错误可以回滚 回到事务执行前
事务的四大特性
原子性:事务是不可分割的最小单元 要么全部成功 要么全部失败
一致性:事务完成时 所有数据必须保持一致状态
隔离性:独立环境运行
持久性:事务一旦提交或回滚 他对数据的操作是永久的
spring对事务进行了封装 通过注解@Transaction方法或类或接口实现,一旦出错了就回滚方法等(回到方法执行前)
Transactional注解
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
- 方法
- 当前方法交给spring进行事务管理
- 类
- 当前类中所有的方法都交由spring进行事务管理
- 接口
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
开启事务管理日志
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
1 | #spring事务管理日志 |
事务进阶
@Transactional注解当中的两个常见的属性:
- 异常回滚的属性:rollbackFor
- 事务传播行为:propagation
rollbackFor
默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
1 |
|
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 | 必须没事务,否则抛异常 |
… |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
- REQUIRED(默认值)
- 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的优势:
- 减少重复代码
- 提高开发效率
- 维护方便
AOP快速入门
需求:统计各个业务层方法执行耗时。
实现步骤:
- 导入依赖:在pom.xml中导入AOP的依赖
- 编写AOP程序:针对于特定方法根据业务需要进行编程
pom.xml
1 | <dependency> |
AOP程序:TimeAspect
1 |
|
通过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进阶
- 通知类型
- 通知顺序
- 切入点表达式
- 连接点
通知类型
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1 |
|
在使用通知时的注意事项:
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
解决切入点表达重复的问题:抽取
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
1 |
|
需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:
全类名.方法名(),具体形式如下:
1 |
|
通知顺序
在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:
- 修改切面类的类名(这种方式非常繁琐、而且不便管理)
- 使用Spring提供的@Order注解
使用@Order注解,控制通知的执行顺序:
1 |
|
1 |
|
1 |
|
通知的执行顺序大家主要知道两点即可:
- 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
- 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
切入点表达式
切入点表达式:
描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution(……):根据方法的签名来匹配
@annotation(……) :根据注解匹配
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 | execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数(全类名)) throws 异常?) |
其中带?
的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
1 |
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
- 方法的访问修饰符可以省略
- 返回值可以使用
*
号代替(任意返回值类型) - 包名可以使用
*
号代替,代表任意包(一层包使用一个*
) - 使用
..
配置包名,标识此包以及此包下的所有子包 - 类名可以使用
*
号代替,标识任意类 - 方法名可以使用
*
号代替,表示任意方法 - 可以使用
*
配置参数,一个任意类型的参数 - 可以使用
..
配置参数,任意个任意类型的参数
注意事项:
- 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
1 | execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..)) |
切入点表达式的书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
@annotation
实现步骤:
1.编写自定义注解
自定义注解:MyLog
1 | //指定注解作用域--只作用于方法 |
2.在业务类要做为连接点的方法上添加自定义注解
在需要标记的方法名前加注解
1 | //自定义注解(表示:当前方法属于目标方法) |
3.在切面类中识别方法
切面类
1 |
|
- execution切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
- annotation 切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
连接点
连接点可以简单理解为可以被AOP控制的方法。
而在SpringAOP当中,连接点又特指方法的执行。
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
示例代码:
1 |
|
SpingBoot原理
配置优先级
在SpringBoot项目当中除了3种配置文件(properties,yml,yaml)外,SpringBoot为了增强程序的扩展性,除了支持配置文件的配置方式以外,还支持另外两种常见的配置方式:
- Java系统属性配置 (格式: -Dkey=value)
1 | -Dserver.port=9000 |
- 命令行参数 (格式:–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管理
- 如何从IOC容器中手动的获取到bean对象
- bean的作用域配置
- 管理第三方的bean对象
获取Bean
默认情况下,SpringBoot项目在启动的时候会自动的创建IOC容器(也称为Spring容器),并且在启动的过程当中会自动的将bean对象都创建好,存放在IOC容器当中。应用程序在运行时需要依赖什么bean对象,就直接进行依赖注入就可以了。
而在Spring容器中提供了一些方法,可以主动从IOC容器中获取到bean对象,下面介绍3种常用方式:
- 根据name获取bean
1 | Object getBean(String name) |
- 根据类型获取bean
1 | <T> T getBean(Class<T> requiredType) |
- 根据name获取bean(带类型转换)
1 | <T> T getBean(String name, Class<T> requiredType) |
- 想获取到IOC容器,直接将IOC容器对象注入进来就可以了
测试类:
1 |
|
注意事项:
- 上述所说的 【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 | //bean作用域为非单例 |
注意事项:
- 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//配置类 (在配置类当中对第三方bean进行集中的配置管理)
public class CommonConfig {
//声明第三方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框架进行项目的开发,会比较繁琐。
这个繁琐主要体现在两个地方:
- 在pom.xml中依赖配置比较繁琐,在项目开发时,需要自己去找到对应的依赖,还需要找到依赖它所配套的依赖以及对应版本,否则就会出现版本冲突问题。
- 在使用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 |
|
缺点:
- 使用繁琐
- 性能低
结论:SpringBoot中并没有采用以上这种方案。
方案二
@Import导入
- 导入形式主要有以下几种:
- 导入普通类
- 导入配置类
- 导入ImportSelector接口实现类
1). 使用@Import导入普通类:
1 | //导入的类会被Spring加载到IOC容器中 |
2). 使用@Import导入配置类:
- 配置类
1 |
|
- 启动类
1 | //导入配置类 |
3). 使用@Import导入ImportSelector接口实现类:
- ImportSelector接口实现类
1 | public class MyImportSelector implements ImportSelector { |
- 启动类
1 | //导入ImportSelector接口实现类 |
springboot中采取的依赖导入方式:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。
- 比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxxx开头的注解,注解中封装的就是@Import注解
4). 使用第三方依赖提供的 @EnableXxxxx注解
- 第三方依赖中提供的注解
1 |
|
- 在使用时只需在启动类上加上@EnableXxxxx注解即可
1 | //使用第三方依赖提供的Enable开头的注解 |
自动配置源码跟踪
源码跟踪技巧:
在跟踪框架源码的时候,一定要抓住关键点,找到核心流程。一定不要从头到尾一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上对整个流程或者整个原理有一个认识,有精力再去研究其中的细节。
要搞清楚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注解标识的方法)。
- 封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)
当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的时候,按照规范需要定义两个模块:
- starter模块(进行依赖管理[把程序开发所需要的依赖都定义在starter起步依赖中])
- 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 | <parent> |
Maven打包方式:
- jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)
- war:普通web程序打包,需要部署在外部的tomcat服务器中运行
- pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理
注意:
- 在子工程中,配置了继承关系之后,坐标中的groupId是可以省略的,因为会自动继承父工程的 。
- relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查找该工程)。
- ../ 代表的上一级目录
此时,我们已经将各个子工程中共有的依赖(lombok),都定义在了父工程中,子工程中的这一项依赖,就可以直接删除了。删除之后,我们会看到父工程中配置的依赖 lombok,子工程直接继承下来了。
版本锁定
问题:如果项目拆分的模块比较多,每一次更换版本,我们都得找到这个项目中的每一个模块,一个一个的更改。 很容易就会出现,遗漏掉一个模块,忘记更换版本的情况。
通过Maven的版本锁定功能实现。
介绍
在maven中,可以在父工程的pom文件中通过 <dependencyManagement>
来统一管理依赖版本。
父工程:
1 | <!--统一管理依赖版本--> |
子工程:
1 | <dependencies> |
注意:
在父工程中所配置的
<dependencyManagement>
只能统一管理依赖版本,并不会将这个依赖直接引入进来。 这点和<dependencies>
是不同的。子工程要使用这个依赖,还是需要引入的,只是此时就无需指定
<version>
版本号了,父工程统一管理。变更依赖版本,只需在父工程中统一变更。
我们之所以,在springboot项目中很多时候,引入依赖坐标,都不需要指定依赖的版本<version>
,是因为在父工程 spring-boot-starter-parent中已经通过<dependencyManagement>
对依赖的版本进行了统一的管理维护。
属性配置
我们也可以通过自定义属性及属性引用的形式,在父工程中将依赖的版本号进行集中管理维护。 具体语法为:
1). 自定义属性
1 | <properties> |
2). 引用属性
1 | <dependency> |
版本集中管理之后,我们要想修改依赖的版本,就只需要在父工程中自定义属性的位置,修改对应的属性值即可。
面试题:
<dependencyManagement>
与<dependencies>
的区别是什么?
<dependencies>
是直接依赖,在父工程配置了依赖,子工程会直接继承下来。<dependencyManagement>
是统一管理依赖版本,不会直接依赖,还需要在子工程中引入所需依赖(无需指定版本)
聚合
如果开发一个大型项目,拆分的模块很多,模块之间的依赖关系错综复杂,那此时要进行项目的打包、安装操作,是非常繁琐的(需要先把最底层的模块打成jar包 再依次往上层打)。maven的聚合就是来解决这个问题的,通过maven的聚合就可以轻松实现项目的一键构建(清理、编译、测试、打包、安装等)。
介绍
- 聚合:将多个模块组织成一个整体,同时进行项目的构建。
- 聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件) 【PS:一般来说,继承关系中的父工程与聚合关系中的聚合工程是同一个】
- 作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)
实现
在maven中,我们可以在聚合工程中通过 <moudules>
设置当前聚合工程所包含的子模块的名称。我们可以在 tlias-parent中,添加如下配置,来指定当前聚合工程,需要聚合的模块:
1 | <!--聚合其他模块--> |
那 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 | <server> |
2.设置私服依赖下载的仓库组地址(在自己maven安装目录下的conf/settings.xml中的mirrors、profiles中配置)
1 | <mirror> |
1 | <profile> |
3.IDEA的maven工程的pom文件中配置上传(发布)地址(直接在tlias-parent中配置发布地址)
1 | <distributionManagement> |
配置完成之后,我们就可以在tlias-parent中执行deploy生命周期,将项目发布到私服仓库中。