注意
有*和--,有注意点详看
蓝色注意,黄色更注意
导学
Web开发-介绍
访问地址,向前端服务器发送请求,响应基础页面(无数据)
在请求后端服务器,后端服务器访问数据库服务器,返回数据,填充到基础页面。
此课程所学内容有:
浏览器
所有的内核,都会符合一个标准Web标准
前端课程安排
超文本标记语言
由浏览器来解析出来。
javaScript
跨平台、面向对象的脚本语言。脚本语言的意思就是写完的代码可以直接浏览器解析使用,而不需要编译。
js核心内容如下
函数:相当于java的方法
两个核心对象 : bom与dom
js事件监听:例如点击事件
vscode插件
Day01-04. HTML-VSCode安装_哔哩哔哩_bilibili
有md文档
h5 css js文档
JS
运算符== 与===
==会转换后在进行比较。
对象
主要分为三类:
Array String JSON 都是基础对象,bom是对浏览器操作的对象,dom文档对象模型 。
h5的一个标签,都是封为了一个dom对象,例如:<a>
自定义对象
js中自定义对象
以前有错误理解,以为这就是json对象。其实这是自定义js对象。
json对象
json对象
json是通过javascript对象标记法书写的文本,他的本质是文本,以键值对形式存在,key必须由双引号包围。
数字类型直接写数字 20 , 数组类型直接写 【】,字符串类型用" ",详情如下。
基础概念
示例:理解parse和stringify方法
bom对象
bom 主要封为5个对象
java程序员 需了解window对象(窗口对象)和location对象(地址栏对象)
window对象
alert就是调用了window对象的方法。确认取消按钮提示框也是window对象的confirm方法。
location对象
可以获取,也可以设置地址。
dom对象
事件监听挂钩,可以操作dom
复习一下:document是整个文档对象,element是元素对象例如每个标签,attribute是属性标签例如href,text是文本对象 例如传智教育。
操作dom
返回值是对象,如果是数组就是[XXX] 要认识。
获取后如何操作dom对象
查文档
1
2
3
js事件监听
js中事件绑定的两种方式
常见事件
监听事件案例
聚焦与失去焦点进行大小写转换
Vue
免除原生js操作dom
生命周期
mounted挂在完成后,发送请求,加载数据。
插值表达式
插值表达式可以是以上几种形式。
常用指令
v-on是为标签绑定事件的 也就是@
Ajax介绍
就是XMLHttpRequest的简写,代表所有的异步请求
Axios介绍
对原生ajax进行了封装
axios的使用
请求方式别名
response.data.data
response.data是返回的整个json数据
一般情况,我们只需要拿到json中的data的数组数据,所以要书写为 response.data.data
前后端分离开发
API接口管理平台
Yapi不好用就用apifox
前端工程化
操作流程
工程化开发依赖环境Node.js就相当于java程序依赖jdk
资料中有md的安装文档
Nodejs需要配置变量(前端要以管理员运行)
1.安装Nodejs,配置变量 (在md文档中)
2.npm install -g @vue/cli 需要联网,安装脚手架
执行vue --version可以查看是否安装成功
3. 安装镜像
Vue创建
有两种方式。
目录结构
修改端口号 8080->7000
在vue.config文件中
Vue组件库Element
Element - The world's most popular Vue UI framework
快速入门
1. npm install element-ui@2.15.3
2.Element - The world's most popular Vue UI framework
有步骤,在main.js中导入element-ui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
组件中的细节
log后显示[Object object]时,怎么办?
object时一个js对象,这时只需要将他转化为字符串即可
JSON.stringify(object) 就可以输出出来了。
当组件中的属性不清楚时,可以查看文档,拉到最下面查看。
Element案例
主页面访问
实在App.vue中
1.引入emp-view组件
布局页面
在组件库中,有对应的布局组件(布局容器),找到相应的。
继续添加表格表单组件。
完成组件实现
vue中使用axios 异步加载
yarn add axios
性别展示
拿到信息后,性别是数字,而不是男女
去element找到复杂的表格
scope.row拿到了这一行的数据,此处需要拿到gender字段
使用三元运算符来展示男女
图片展示
需要展示的是图片,而此处却是地址 。
解决方法同理,使用插槽。
优化细节
以前没边框
只需给aside添加border: 1px solid #eee
Vue路由
hash对应不同组件。
介绍
原理
使用方式
1.安装
yarn add vue-router@3.5.1
2.定义路由表
两种导入方式都可以。
3.添加路由出口
4.添加展示位置
优化细节
路由首次访问时,会是默认为 /
让他重定向到 dept http://localhost:8080/#/dept
不配置会找不到/,就会显示空白页。
打包部署 **
1.打包
npm run build
yarn run build
打包后,就会出现dist文件。
2.部署
nginx要放在没中文没空格的目录下。
注意
操作步骤
将dist下的文件,复制到html文件下。
运行nginx.exe。查看任务管理器-详细信息,查看是否启动成功
如果没成功,操作如下
没成功,查看log文件夹下的日志。
80端口被占用了,所以没启动成功
4是进程id
去任务管理器,查看是谁占用
解决方法
1.停止进程
2.改端口号
启动成功后,直接访问localhost:端口号 即可
Maven介绍
是一款构建工具
不用手动导入jar包文件,直接就可以联网下载。
统一目录结构。
跨平台
进行编译
在target目录下生成字节码文件
Maven概述
介绍
顺序:想查看本地仓库是否存在,再查找私服,如果没有,去中央仓库下载(国外很慢),下载到私服。
我们配置的镜像,就是远程仓库阿里云的私服,就会很快。
仓库:
在仓库中找到对应位置
Maven安装
springboot自带,也要自己配置仓库。
操作步骤
配置本地仓库存放位置
配阿里云私服
配置环境变量
配置完成后,在任意目录下,都可以运行maven指令。
测试是否安装成功
输入mvn -v.
注意:maven的运行是依赖jdk的,采用的是jdk11版本。
maven-idea集成配置使用
配置Maven环境
操作步骤
项目结构下
setting下
jre版本
字节码版本
idea创建Maven项目
坐标
idea导入Maven项目
操作步骤
方法1-------------------------------:
打开文件资源管理器,就是文件位置。
将项目粘贴进来。
选择新导入进来项目02的pom文件。
方法2-------------------------------:
在选择pom文件,即可。
依赖配置
依赖
我们仓库中,有这个依赖时,就会有提示信息。
如果没有提示信息,可以到网址查看。
mvnrepository.com
依赖传递
查看依赖传递的方法
排除依赖
A依赖B ,B依赖JAR。如果A不需要JAR,就可以将其断掉。
案例
因为依赖传递,A项目中是依赖junit的。
已无junit依赖。
依赖范围
Maven生命周期
只需关注5个阶段
运行install时,clean是 不会运行的。因为需要在同一套声明周期中。
使用方式
如果没有target文件夹,可以这样操作。
执行clean后,会清除以前的字节码文件,target文件就会删除。
执行 package 后,就会打成jar包,install命令会把jar包导入到本地仓库中。别的模块需要用到此模块,只需导入对应的坐标依赖即可。
总结
Web入门
SpringBootWeb入门
jdk与springboot版本一定要对应上。
新建模块。
启动springboot项目时报错
报错
java: 无法访问org.springframework.web.bind.annotation.RequestMapping
错误的类文件: /D:/A_develop/maven/maven-repository/org/springframework/spring-web/6.0.18/spring-web-6.0.18.jar!/org/springframework/web/bind/annotation/RequestMapping.class
类文件具有错误的版本 61.0, 应为 55.0
请删除该文件或确保该文件位于正确的类路径子目录中。
就是springboot与jdk版本不对应。
jdk11 springboot 2.7.17.
web入门-http协议-概述
登录时请求,与进入管理模块时的请求无法共享数据,所以就不知道当前用户是否登录。java已经通过别的方式实现解决了。
http协议-请求协议
请求行就是第一行,请求体只有post请求有,get是拼接。
请求头常见参数
Content-Type是参数的数据类型。json和http形式字符串
响应协议
常见状态码
协议解析
响应数据,浏览器都自带解析工具。
Tomcat这些服务器提供了http协议的解析工具。
Tomcat介绍
Tomcat基本使用
常见问题
冲突也可以修改Tomcat的端口号
注意
Tomcat的部署
SpringBootWeb-入门程序解析
starts 是起步依赖
创建项目后,pom中的Web依赖,因为依赖传递,所以内嵌了Tomcat
内嵌了Tomcat。 原理是循环依赖,就是依赖传递
请求响应-概述
Tomcat无法识别我们写的java,Controller代码,但是Tomcat可以识别javaEE规范中的技术,servlet ,Tomcat也是servlet容器。SpringBoot的DispatcherServlet继承于Servlet。
httpServletRequest与httpServletResponse两个对象负责 获取 和 响应 规定(符合http协议)的数据 。
请求响应-请求
postman软件的使用。
简单参数
原始方式
代码:在git中A1
package com.neuedu.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class simpleParam {
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest httpServletRequest){
String name = httpServletRequest.getParameter("name");
String ageStr = httpServletRequest.getParameter("age");
int age = Integer.parseInt(ageStr);
System.out.println("name:"+name+" age:"+age);
return "ok";
}
}
springboot方式
post请求,参数在请求体中携带,所以选择body
对于简单参数,不管get还是post请求,只要传输与接收的参数名字对应上 就可以解析成功。
参数名不一致的情况
需要用到RequestParam注解。
注意
required 默认为true,如果不传会报错。
控制台会输出:Required request parameter 'name' for method parameter type String is not present]
总结
实体参数
代码在git
简单实体参数
复杂实体参数
address.city的形式传参
数组集合参数
参数名要保持一致
参数名要一样。数组和集合区分,加RequestParam注解。
数组参数
例如复选框,可以选多个值。
sout(hobby) -> [Ljava.lang.String;@4c284867
Arrays.toString(hobby) 输出数组 ->[java, C, PHP]
集合参数
多个参数名一样,默认会封成数组,所以要加@RequestParam来进行区分
总结
日期参数
因为日期参数多种多样,所以要在服务端声明格式类型
json参数
json参数是要放在请求体中传输的
键名与对象属性名要一致。json格式要求key必须"" 。
路径参数
参数总结
请求响应
RestController = Controller + ResponseBody
他会先将数据转换为json,再响应回去。
统一处理
不同的接口,返回的数据都不同。例如字符串、json格式数据、json数组 等等,这不方便前端统一管理维护。
统一封装起来,方便维护。
总结
响应数据-案例
类路径指的就是当前的resources路径。
前端页面路径是 localhost:8080/emp.html 就行 static路径不用加,因为他就是用来存放静态资源的。
完成结果:
技巧***---------------------
动态加载路径
利用类加载器获取文件。
分层解耦-三层架构
细节基础知识:因为dao的数据,有数据库查出来的,有别的接口返回的,有xml解析出来的,不固定,所以要面向接口编程。
类似,为了增加灵活性,service层也要创建接口。
面向接口编程
分层解耦***----------概念理解
private EmpService empService = new EmpServiceImplA();
这个过程已经耦合了,如果重新写了一个 EmpServiceImplB,这时候Controller中就需要重新new出B,service改动了,controller也就会发生改动,这就是耦合。
不再去new对象,实现解耦。
把实现类对象交给容器来管理起来 控制反转(不在new,交给容器)
类型相同的可以直接拿到用 依赖注入(拿来直接用)
IOC&DI入门
当去掉new 对象时,程序会报空指针异常,因为没有赋值。这时候就需要交给容器来管理,成为Bean对象,加上Component注解。 AutoWired实现注入。
细节基本知识
接口的实现类有A 和 B。如果想用B,就在B上添加Component注解,把B交给容器管理。
IOC详解***
component多用于工具类中。controller、service、repository都对component进行了封装。
value属性可以指定名字。
DI详解***
当实现类A和B,都加上Service注解,都交给容器管理时。再自动装配时,就会报错
Autowired默认是按照 数据类型装配的。
解决方案 3种
Autowired是spring提供的,Resource是jdk提供的,而且默认是名字装配。
总结
MySql课程简介
Mysql概述-安装
参考网盘资料
或Day06-02. MySQL-概述-安装配置_哔哩哔哩_bilibili
mysql- 数据模型&SQL介绍
data文件夹存放的就是MySQL数据库的数据。
数据库的操作
DDL-数据库操作
select database() 查看当前正在使用的数据库。
show schema s 原来有s+s。
DDL-图形化操作
在资料中有操作文档。
idea集成的也可以使用。
DDL-表结构操作-创建
DDL-表结构操作-数据类型*
定义无符号,需要在类型后方添加 unsigned。
根据格式来选择合适的类型。
时间类型,根据需求,根据格式,来选择合适的。
对于金钱,要求精准,就需要decimal类型。
price decimal(8, 2) not null comment '套餐价格',
DDL-表结构操作-创建***------
代码
CREATE TABLE `tb_emp` (
`id` int NOT NULL AUTO_INCREMENT,
`column_name` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(32) DEFAULT '123456' COMMENT '密码',
`name` varchar(10) DEFAULT NULL COMMENT '员工姓名',
`gender` tinyint unsigned NOT NULL COMMENT '性别 1男 2女',
`image` varchar(300) DEFAULT NULL COMMENT '图像url',
`job` tinyint unsigned DEFAULT NULL COMMENT '职位 1班主任 2讲师 3 学工主管 4教研主管',
`entrydate` date DEFAULT NULL COMMENT '入职日期',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='员工表';
注意
性别gender 用 tinyint类型 ,且可以添加unsigned 无符号。有很多可以添加无符号
DDL-表结构操作-查询
DML-添加数据-insert
DML-更新数据-update
DML-删除数据-delete
总结
DQL-基本查询
DQL-条件查询*
__表示查询2个字的员工(两个杠)
DQL-聚合函数*
聚合函数不对null进行运算 。聚合函数是对数据纵向进行处理 。
推荐使用count(*),数据库底层对*有优化处理。
DQL-分组查询*
分组后,查询的字段,一般为聚合函数和分组字段,其他字段无意义。
having是groupby 的条件,就是分组后的过滤条件。
where不能对聚合函数进行判断。
DQL-排序查询
DQL-分页查询
DQL-案例*
案例1
案例2
if函数,gengder查询出来的数据是 1 和 2
参数1条件,参数2 为true的数据,参数3 为false的数据
案例3
运用到 case when 语法 。最后end结束
总结
多表设计1对多
多表设计1对多 外键*
场景在线
部门表和员工表。
部门有 1 2 3 4,员工去关联部门,当直接删除部门为1的行数据,此时员工里关联部门为1的数据依然存在。此时数据就会有矛盾,就会不完整。 此时只是我们逻辑上进行关联,物理上并没有关联。
所以外键的作用就是保持,数据的一致性。有了外键后,当员工有关联部门=1的,此时再删除部门为1的数据,就会删除失败。
推荐使用逻辑外键,就是删除外键,在代码层面上进行查询。
多表设计1对1 多对多*
真是开发中,物理外键基本不用
1对1
将大表拆分为两种小表
将用户表,拆分为 基本信息表 和 身份信息表。提高效率(场景:基本信息查询很多,但是身份信息查询就比较少,此时就需要进行拆分提高效率。)
此时外键还加了unique唯一约束,这样 就不会有重复值,保证1对1
多对多
多对多时,就需要借助第三张表来处理。例如选课,师生。
第三张表中,创建2个外键来分别关联两张表的主键。
多表设计案例 关系分析
多表设计案例 表结构
-- 瑞吉点餐页面原型 , 设计表结构
-- 分类表
create table category(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(20) not null unique comment '分类名称',
type tinyint unsigned not null comment '类型 1 菜品分类 2 套餐分类',
sort tinyint unsigned not null comment '顺序',
status tinyint unsigned not null default 0 comment '状态 0 禁用,1 启用',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '更新时间'
) comment '菜品及套餐分类' ;
-- 菜品表
create table dish(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(20) not null unique comment '菜品名称',
category_id int unsigned not null comment '菜品分类ID',
price decimal(8, 2) not null comment '菜品价格',
image varchar(300) not null comment '菜品图片',
description varchar(200) comment '描述信息',
status tinyint unsigned not null default 0 comment '状态, 0 停售 1 起售',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '更新时间'
) comment '菜品';
-- 套餐表
create table setmeal(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(20) not null unique comment '套餐名称',
category_id int unsigned not null comment '分类id',
price decimal(8, 2) not null comment '套餐价格',
image varchar(300) not null comment '图片',
description varchar(200) comment '描述信息',
status tinyint unsigned not null default 0 comment '状态 0:停用 1:启用',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '更新时间'
)comment '套餐' ;
-- 套餐菜品关联表
create table setmeal_dish(
id int unsigned primary key auto_increment comment '主键ID',
setmeal_id int unsigned not null comment '套餐id ',
dish_id int unsigned not null comment '菜品id',
copies tinyint unsigned not null comment '份数'
)comment '套餐菜品关系';
unsigned tinyint unique decimal(8, 2)8位2位小数 datetime 的使用。
总结
MYSQL-多表查询-内连接
查询出交集的数据,没有关联的数据,不会查出来,例如 员工表中部门为null的数据。部门表中还没有null。
MYSQL-多表查询-外连接*
可以查询出基表的所有数据。因为有时候需要全部数据。
这时候使用内连接,就会查不到如下数据。
查询出基表的数据 & 交集的数据
MYSQL-多表查询-子查询(标量、列)
标量子查询
列子查询
MYSQL-多表查询-子查询(行、表)*
行子查询
表子查询
没事看看
MYSQL-多表查询-案例1和2***
没有特殊的说明,就使用内连接 。详看5,自己想复杂了。
技巧
使用工具,将表结构关系列出来。写语句前,先进行分析用什么表,什么关联,在书写。
代码:
-- 1. 查询价格低于 10元 的菜品的名称 、价格 及其 菜品的分类名称 .
-- 表:dish , category
select d.name 名称, d.price 价格, c.name 分类名称
from dish d
inner join category c
on c.id = d.category_id
where d.price < 10;
-- 2. 查询所有价格在 10元(含)到50元(含)之间 且 状态为'起售'的菜品名称、价格 及其 菜品的分类名称 (即使菜品没有分类 , 也需要将菜品查询出来).
-- 表:dish , category
select d.name 名称, d.price 价格, c.name 分类名称
from dish d
left join category c
on c.id = d.category_id
where d.price between 10 and 50 and d.status = 1;
-- 3. 查询每个分类下最贵的菜品, 展示出分类的名称、最贵的菜品的价格 .
-- 表:dish , category
select (case category_id when 1 then '酒水饮料' when 2 then '传统主食'
when 3 then '人气套餐'when 4 then '商务套餐'
when 5 then '经典川菜'when 6 then '新鲜时蔬'else '汤类' end) 分类名称
,max(price) 价格
from dish
group by category_id
having max(price);
-- 4. 查询各个分类下 状态为 '起售' , 并且 该分类下菜品总数量大于等于3 的 分类名称 .
select (case d.category_id when 1 then '酒水饮料' when 2 then '传统主食'
when 3 then '人气套餐'when 4 then '商务套餐'
when 5 then '经典川菜'when 6 then '新鲜时蔬'else '汤类' end) 分类名称
, count(*)
from dish d
join category c on c.id = d.category_id
where c.status = 1
group by d.category_id
having count(*) >= 3;
-- 5. 查询出 "商务套餐A" 中包含了哪些菜品 (展示出套餐名称、价格, 包含的菜品名称、价格、份数).
-- 表:dish , setmeal , setmeal_dish
-- 方法1想复杂了
-- (1.1)先查询出套餐的基本配置(套餐名称、菜品id、价格、份数)
select sd.dish_id , s.name , s.price , sd.copies from setmeal s
join setmeal_dish sd
on s.id = sd.setmeal_id
where s.name = '商务套餐A';
-- (1.2)将上表作为基表,在与菜品进行关联,将dish_id转换为汉字
select d.name 菜品名称 , 套餐名称 ,价格 , 份数 from (select sd.dish_id , s.name as '套餐名称', s.price as '价格', sd.copies as '份数'from setmeal s
join setmeal_dish sd
on s.id = sd.setmeal_id
where s.name = '商务套餐A') as jb
left join dish d
on jb.dish_id = d.id;
-- 方法2(视频方法)***
select s.name , s.price , d.name , d.price , sd.copies
from dish d , setmeal_dish sd , setmeal s
where d.id = sd.dish_id and sd.setmeal_id = s.id
and s.name = '商务套餐A';
-- 6. 查询出低于菜品平均价格的菜品信息 (展示出菜品名称、菜品价格).
-- 表:dish
select avg(price)
from dish;
select d.name , d.price
from dish d
where d.price < (select avg(price)
from dish);
总结:
MYSQL-事务-介绍与操作*
事务控制
代码如下:(事务相对隔离)
-- 开启事务
start transaction ;
-- 删除部门
delete from tb_dept where id = 3;
-- 删除部门下的员工
delete from tb_emp where dept_id = 3;
-- 提交事务
commit ;
-- 回滚事务
rollback ;
select * from tb_dept;
select * from tb_emp;
这整个控制台也相当于一个事务。当没有提交前,执行下面两个查询,数据已经伪删除
打开新的控制台,查看数据
数据依然存在。
如果执行提交,将真正进行执行,进行删除。如果一条语句成功一条失败,则可以进行回滚恢复。
MYSQL-事务-四大特性*
MYSQL-索引-介 绍*
原理
索引就相当于一本书的目录。
优缺点*
他是一种数据结构,这种二叉树查询很快。
没有索引时:就是直接插入即可。
存在索引时:就需要将插入的数据维护进二叉树这个数据结构中,保持平衡(插入、修改、删除,同理)。
MYSQL-索引 -结构*
因为大数据情况下,二叉搜索树和红黑树,层级深,不利于查询。
结构
理解**(图片)
最终形成的是一个矮胖的树。第三层为叶子节点(存储真诚数据),前两层是非叶子节点(存储索引数据)。
MYSQL-索引-操作语法 ****
-- 创建 : 为tb_emp表的name字段建立一个索引 .
create index idx_emp_name on tb_emp(name);
-- 查询 : 查询 tb_emp 表的索引信息 .
show index from tb_emp;
-- 删除: 删除 tb_emp 表中name字段的索引 .
drop index idx_emp_name on tb_emp;
注意
创建主键,默认就会创建主键索引 ,主键索引时性能最高的。
字段添加唯一约束时 unique,也会创建唯一索引。
总结:
MyBatis-入门-课程介绍
MyBatis-入门-快速入门程序
准备工作
代码:
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/web02
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=root
package com.neuedu.mapper;
import com.neuedu.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper//在运行时,会自动生成该接口的实现类对象(代理对象),并且将该对象交给IOC容器管理
public interface UserMapper {
//查询全部信息
@Select("select * from user")
public List<User> list();
}
MyBatis-入门-配置SQL提示*
配置sql提示
MyBatis-入门-JDBC
在使用spring.datasource数据源时,mybatis会把jdbc这种连接对象封装到数据库连接池中,使用的时候直接拿,就不需要向jdbc还需要手动释放了。
只需关注两点
MyBatis-入门-数据库连接池
springboot默认的 时 Hikari追光者
如何切换连接池
只需要引入druid的起步依赖,配置的连接信息不用动,都是统一的。当然也可以使用德鲁伊的方式
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
总结:
自己写一个数据库连接池,就需要实现接口DataSource。
MyBatis-入门-lombok工具包
lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
Mybatis-基础操作-环境准备*
注意点
数据库 java 的 对应关系
date LocalDate
datetime LocalDateTime
LocalDate类型对应数据表中的date类型
LocalDateTime类型对应数据表中的datetime类型
Mybatis-基础操作-删除 (--基于注解方式--)
当只有一个参数时,#{}中的形参可以随便写,因为只有这1个参数,放在这里。
代码
package com.itheima.mapper;
import org.apache.ibatis.annotations.*;
/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {
//根据id删除数据
@Delete("delete from emp where id = #{id}")
public int delById(Integer id);
}
package com.itheima;
import com.itheima.mapper.EmpMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
EmpMapper empMapper;
@Test
public void test123_1(){
int i = empMapper.delById(17);
System.out.println(i);
}
}
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id;
private String username;
private String password;
private String name;
private Short gender; //1男 2 女
private String image;
private Short job; //1班主任 2 讲师 3 学工
private LocalDate entrydate; // 入职日期 年月日 LocalDate类型对应数据表中的date类型
private Integer deptId;
private LocalDateTime createTime; //创建日期 年月日 时分秒 LocalDateTime类型对应数据表中的datetime类型
private LocalDateTime updateTime;
}
Mybatis-基础操作-删除(预编译)*
添加日志myabtis
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
Mybatis-基础操作-新增
#{}中的值,是属性名,小驼峰和实体类中的一样。不是数据库_的形式。
代码:
//新增员工
@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 insertOne(Emp emp);
@Test
public void test125_1(){//测试添加数据
//创建员工对象
Emp emp = new Emp();
emp.setUsername("tom");
emp.setName("汤姆");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntrydate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
empMapper.insertOne(emp);
}
Mybatis-基础操作-新增(主键返回)*
特定业务场景,添加完数据后,主键要返回回来。
例如这里的套餐,先创建套餐信息,在添加套餐与餐品关联表(这里需要用到套餐id)
操作:添加Options注解
//会自动将生成的主键值,赋值给emp对象的id属性
@Options(useGeneratedKeys = true,keyProperty = "id")
代码:
执行完添加,直接就可以拿到id了。
Mybatis-基础操作-更新
代码
//更新操作
@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 updateOne(Emp emp);
@Test
public void test127_1(){//测试更新数据
Emp emp = new Emp();
emp.setId(18);
emp.setUsername("songdaxia");
emp.setPassword(null);
emp.setName("老宋");
emp.setImage("2.jpg");
emp.setGender((short)1);
emp.setJob((short)2);
emp.setEntrydate(LocalDate.of(2012,1,1));
emp.setCreateTime(null);
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(2);
empMapper.updateOne(emp);
}
Mybatis-基础操作-查询(根据ID)*
因为名称不一致导致没有自动封装成功。
数据展示(后几个属性没有封装成功)
Emp(id=1, username=jinyong, password=123456, name=金庸, gender=1, image=1.jpg, job=4, entrydate=2000-01-01, deptId=null, createTime=null, updateTime=null)
解决方案
//查询操作
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, " +
"create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);
//方法1:取别名,让别名与实体类属性一致
// @Select("select id, username, password, name, gender, image, job, entrydate, " +
// "dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
// "from emp " +
// "where id=#{id}")
// public Emp getById(Integer id);
//方法2:手动映射封装
// @Results({
// @Result(column = "dept_id", property = "deptId"),
// @Result(column = "create_time", property = "createTime"),
// @Result(column = "update_time", property = "updateTime")
// })
// @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
// public Emp getById(Integer id);
//方法3:开启mybatis的驼峰命名自动映射开关--->在配置文件中配置
// mybatis.configuration.map-underscore-to-camel-case=true
总结 3种方式
_&驼峰映射
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true
Mybatis-基础操作-查询(条件查询)**
字符串中是不能用#{}的,但是可以用${}.
${}和#{} 拼接与预编译
推荐方式
@param
在Springboot1.x版本中,编译时,不会编译形参,形参是以var1,var2形式存在这样与sql语句中的#{参数} 对应不上,所以要使用param注解。
在springboot2.x版本中,有内置编译的插件,就可以解决这个问题。
代码
//条件查询
//方式1:使用${}存在注入风险
// @Select("select * FROM emp " +
// "where name like '%${name}%'" +
// "and gender = #{gender} " +
// "and entrydate between #{begin} and #{end} " +
// "order by update_time desc")
// public List<Emp> list(String name , Short gender , LocalDate begin , LocalDate end);
//方式2:使用mysql的concat()函数 拼接
@Select("select * FROM emp " +
"where name like concat('%',#{name},'%') " +
"and gender = #{gender} " +
"and entrydate between #{begin} and #{end} " +
"order by update_time desc")
public List<Emp> list(String name , Short gender , LocalDate begin , LocalDate end);
@Test
public void test129_1(){//条件查询
// empMapper.list("张",1,"2000-01-01","2024-01-01");
List<Emp> list = empMapper.list("张", (short) 1, LocalDate.of(2000, 1, 1), LocalDate.of(2024, 1, 1));
list.stream().forEach(e->{
System.out.println(e);
});
入职日期是LocalDate类型,所以要用LocalDate.of静态方法来设置值,不能用字符串。
Mybatis-基础操作-XML映射文件(--基于XML方式--)**
resultType:单条记录所封装的类型
遵守3条规范
注解就不用担心,因为执行注解那,语句与方法都在一起。
xml,需要找到EmpMaaper接口的list的方法。规范就是建立连接。
注意
在resources下创建包,要以 / 分隔,如果以.分隔,在文件中 会变成一个目录。
代码
//方式3:使用xml
public List<Emp> list(String name , Short gender , LocalDate begin , LocalDate end);
<?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">
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- resultType:单条记录所封装的类型-->
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp
where name like concat('%',#{name},'%')
and gender = #{gender}
and entrydate between #{begin} and #{end}
order by update_time desc
</select>
</mapper>
上面的约定直接从官网复制。xml要遵守3条规范
@Test
public void test129_1(){//条件查询
// empMapper.list("张",1,"2000-01-01","2024-01-01");
List<Emp> list = empMapper.list("张", (short) 1, LocalDate.of(2000, 1, 1), LocalDate.of(2024, 1, 1));
list.stream().forEach(e->{
System.out.println(e);
});
}
Mybatis-基础操作-动态SQL-if*
where标签可以去除and/or
为什么使用if标签***
一行数据10个字段,如果只封装3个字段,那么其他字段就会是null,这样直接修改的时候,就会将其他字段修改为null。
<where>标签,可以去除多余的and/or,并且where中有条件成立,才会添加where,无满足条件的就不会动态渲染where。
代码
<?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">
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- resultType:单条记录所封装的类型-->
<select id="list" resultType="com.itheima.pojo.Emp">
select *
from emp
<where>
<if test="name != null">
name like concat('%', #{name}, '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>
//方式3:使用xml
public List<Emp> list(String name , Short gender , LocalDate begin , LocalDate end);
@Test
public void test129_1(){//条件查询
// List<Emp> list = empMapper.list("张", (short) 1, LocalDate.of(2000, 1, 1), LocalDate.of(2024, 1, 1));
List<Emp> list = empMapper.list("张", (short) 1, null,null);
System.out.println(list);
}
Mybatis-基础操作-动态SQL-if - 案例
遇到语法错误,找near。框架报错,从后网前看。
set标签可以去除,
总结
Mybatis-基础操作-动态SQL-foreach*****
foreach标签一般用于批量操作。
批量删除
出现了一个小问题
删除时,永远删除的都是id为 0 1 2 的数据。
原因:
写成index 了,正确时item。
Mybatis-基础操作-动态SQL-include
sql中出现重复代码。
抽离出来,方便复用。
代码
<sql id="commonSelect">
id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
</sql>
<!-- resultType:单条记录所封装的类型-->
<select id="list" resultType="com.itheima.pojo.Emp">
select
<include refid="commonSelect"/>
from emp
<where>
<if test="name != null">
name like concat('%', #{name}, '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
总结
案例-准备工作
就是员工和部门的增删改查。
基础操作
知识*********************
log记录日志
//创建log对象,记录日志 或者使用 @slf4j 注解
private static Logger log = LoggerFactory.getLogger(DeptController.class);
或者在类上添加@Slf4j注解
log.info("添加部门:{}"+dept);
要查看参数,要添加{}占位。 要不然不会输出出来。
@RequestParam注解
@RequestParam(defaultValue = "1") 设置默认值
package com.neuedu.controller;
import com.neuedu.pojo.Emp;
import com.neuedu.pojo.PageBean;
import com.neuedu.pojo.Result;
import com.neuedu.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/emps")
@Slf4j
public class EmpController {
@Autowired
EmpService empService;
@GetMapping
public Result listEmp(@RequestParam(defaultValue = "1") Integer page ,
@RequestParam(defaultValue = "10") Integer pageSize,
String name , Short gender,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
log.info("分页查询:page:{},pageSize:{},name:{},gender:{},begin:{},end:{}",page,pageSize,name,gender,begin,end);
PageBean pageBean = empService.listEmp(page, pageSize,name,gender,begin,end);
return Result.success(pageBean);
}
}
参数判断
name不填写时,传进来的是“”空字符串,这样的话,在xml中就需要 双层判断 !=null & !=''
案例-部门管理-查询
查看页面原型
查看接口文档
自己敲代码,代码在git中保存了。tlias-web-management模块。
apifox接口测试,也完成了。
案例部门管理-前后端联调
直接访问http://localhost:90/,查看接口是否返回数据成功。
案例-部门管理-删除
案例-部门管理-更新
问题
代码
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'create_time = '2024-04-06 16:22:17'
up' at line 5
### The error may exist in com/neuedu/mapper/DeptMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: update dept SET name = ? create_time = ? update_time = ? WHERE id = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'create_time = '2024-04-06 16:22:17'
up' at line 5
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'create_time = '2024-04-06 16:22:17'
up' at line 5] with root cause
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'create_time = '2024-04-06 16:22:17'
up' at line 5
以前没加 ,
案例-部门管理-新增
问题
Data truncation: Data too long for column 'name' at row 1
数据库varchar太小
总结**
优化
抽离出来。
案例-员工管理-分页查询-分析
看接口文档与页面原型
总记录数可能有点多,所以实体中定义Long类型
package com.neuedu.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageBean {
private Long total;
private List rows;
}
属性 与接口文档中对应
案例-员工管理-分页查询-实现
手搓分页
PageHelper、@RequestParam(defaultValue = "1") 设置默认值
代码在git,已经自己手写完毕
案例-员工管理-分页查询插件-PageHelper**
Mybatis插件分页
先加载依赖
两步走:1.设置分页参数。2.封装结果集
分页插件,会自动执行count(*) 和 limit。
分页插件,已经有封装的Page对象,数据要放在Page中。
所以我们只需要正常的执行查询就行。
代码
package com.neuedu.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.neuedu.mapper.EmpMapper;
import com.neuedu.pojo.Emp;
import com.neuedu.pojo.PageBean;
import com.neuedu.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
EmpMapper empMapper;
@Override
public PageBean listEmp(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
// 无分页插件时
// Integer start = (page - 1) * pageSize;
// List<Emp> emps = empMapper.listEmp(start, pageSize);
// Long total = this.listCount();
// return new PageBean(total,emps);
//使用PageHelper
PageHelper.startPage(page,pageSize);
List<Emp> emps = empMapper.listEmp(page, pageSize,name,gender,begin,end);
Page<Emp> p = (Page<Emp>) emps;
return new PageBean(p.getTotal(),p.getResult());
}
@Override
public Long listCount() {
return empMapper.listCount();
}
}
<?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">
<mapper namespace="com.neuedu.mapper.EmpMapper">
<!-- select * from emp limit #{start},#{pageSize}; 无分页插件时-->
<select id="listEmp" resultType="com.neuedu.pojo.Emp">
select * from emp
<where>
<if test="name != null and name != '' ">
name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entryDate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>
name不填写时,传进来的是“”空字符串,这样的话,在xml中就需要 双层判断 !=null & !=''
案例-员工管理-删除员工
@DeleteMapping("/{ids}")
public Result delEmp(@PathVariable List<Integer> ids){
log.info("删除员工:{}",ids);
empService.delEmp(ids);
return Result.success();
}
批量删除。
案例-员工管理-新增员工
查看需求文档与页面原型开发
<insert id="addEmp">
insert into emp (username,name,gender,image,dept_id,entrydate,job,create_time,update_time)
values (#{username},#{name},#{gender},#{image},#{deptId},#{entrydate},#{job},#{createTime},#{updateTime})
</insert>
@PostMapping
public Result addEmp(@RequestBody Emp emp){
log.info("新增员工:{}",emp);
empService.addEmp(emp);
return Result.success();
}
数据库密码有默认约束为123456,所以代码上不用设置值为123456.
案例-文件上传-简介***
前端操作
前端页面3要素
post请求,传输大小比较大,type类型是file,enctype是multipart/form-data(提交方式)是上传大的二进制文件。
enctype属性**
1.正常是application/x-www-form-urlencoded
他的解析传输形式
2.文件形式multipart/form-data
他的传输形式,会携带名称类型内容。
可以采用火狐浏览器查看。此处用的谷歌,没有看到内容(谷歌对数据进行了抓包包装,已经看不到了)。
火狐效果:
只有文本文件才能看到这个文字效果。图片视频什么的,看到的是二进制乱码。
服务端操作
操作
提交
进入断点
image下有个localtion,直接复制值。
C:\Users\ayu\AppData\Local\Temp\tomcat.8080.14935340758480073979\work\Tomcat\localhost\ROOT
在本地打开
就会有3个临时文件,正好就是传过来的3个参数,相对应。
只要请求响应完毕,临时文件就会消失,所以接下来,就要对临时文件进行处理。
案例-文件上传-本地存储***
@PostMapping("/upload")
public Result upload(String username , Integer age , MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//转存的文件信息,都在MultipartFile image对象中
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//将文件存储在服务器的磁盘目录中E:uploadfile heima
image.transferTo(new File("E:\\uploadfile\\heima\\"+originalFilename));
return Result.success();
}
转存的文件信息,都在MultipartFile image对象中
存在问题:文件名
相同的文件名上传会进行覆盖。
接下来用uuid来确定唯一性。
新出问题
上传文件超过1M
配置一下即可解决。
#配置单个文件上传大小限制 记住multipart
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大大小的限制(1次请求中是可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
本地存储最终代码
@PostMapping("/upload")
public Result upload(String username , Integer age , MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//转存的文件信息,都在MultipartFile image对象中
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//构建新的文件名 uuid+原文件尾椎
String fileName = this.createFileName(originalFilename);
//将文件存储在服务器的磁盘目录中E:uploadfile heima
image.transferTo(new File("E:\\uploadfile\\heima\\"+fileName));
return Result.success();
}
public String createFileName(String originalFilename){
//生成uuid并去除'-'
String uuid = UUID.randomUUID().toString().replace("-","");
//获取文件.的索引
int index = originalFilename.lastIndexOf(".");
//截取.索引以后的内容
String suffix = originalFilename.substring(index);
return uuid+suffix;
}
如果上传多个文件,就和批量删除一个道理,用集合来接收,在进行操作。
常用方法**
存储在本地有很多弊端,前端也无法直接访问资源,基本不会存储在本地。可以选择存储在云服务器上,百度云阿里云......
案例-文件上传-阿里云OSS-准备
基础流程
案例-文件上传-阿里云OSS-入门***
安装OSS Java SDK_对象存储(OSS)-阿里云帮助中心
参考官方文档
这些代码需要改动,剩下的就是核心代码了(不需要改动)
sdk地址,就是开发工具包,java代码
上传文件、图片、视频等资源到OSS_对象存储(OSS)-阿里云帮助中心
endpoint
进入创建的bucket -概览-往下找
accessKeyId&accessKeySecret
在创建的时候需要保存。
bucketName
自己创建的bucket
sdk新旧有差异
视频中的,旧版
String accessKeyId = "LTAI4GCH1vX6DKqJWxd6nEuW";
String accessKeySecret = "yBshYweHOpqDuhCArrVHwIiBKpyqSL";
是直接暴露的。
package com.itheima;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.FileInputStream;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "LTAI4GCH1vX6DKqJWxd6nEuW";
String accessKeySecret = "yBshYweHOpqDuhCArrVHwIiBKpyqSL";
// 填写Bucket名称,例如examplebucket。
String bucketName = "web-tlias";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "1.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "C:\\Users\\Administrator\\Pictures\\Camera Roll\\1.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
新版
需要配置环境变量,和jdk一样。
package com.neuedu;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.File;
//https://help.aliyun.com/zh/oss/user-guide/simple-upload?spm=a2c4g.11186623.0.0.34f25168FSwzPj
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "web-hmgzy";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "1.png";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
String filePath= "E:\\uploadfile\\heima\\1.png";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传文件。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
运行,查看 上传成功
上传的文件,会分配地址,直接浏览器就可以访问。
案例-文件上传-阿里云OSS-集成**
代码
package com.neuedu.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyuncs.exceptions.ClientException;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Component //交给IOC容器管理
public class AliOSSUtils {
private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// private String accessKeyId = "LTAI4GCH1vX6DKqJWxd6nEuW";
// private String accessKeySecret = "yBshYweHOpqDuhCArrVHwIiBKpyqSL";
private String bucketName = "web-hmgzy";
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws Exception {
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString().replace("-","") + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
//采用配置变量的形式,获取accessKeyId&accessKeySecret
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
package com.neuedu.controller;
import com.aliyuncs.exceptions.ClientException;
import com.neuedu.pojo.Result;
import com.neuedu.util.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@RestController
@Slf4j
public class UploadController {
//本地存储
// @PostMapping("/upload")
// public Result upload(String username , Integer age , MultipartFile image) throws IOException {
// log.info("文件上传:{},{},{}",username,age,image);
// //转存的文件信息,都在MultipartFile image对象中
// //获取原始文件名
// String originalFilename = image.getOriginalFilename();
//
// //构建新的文件名 uuid+原文件尾椎
// String fileName = this.createFileName(originalFilename);
//
// //将文件存储在服务器的磁盘目录中E:uploadfile heima
// image.transferTo(new File("E:\\uploadfile\\heima\\"+fileName));
//
// return Result.success();
// }
//
// public String createFileName(String originalFilename){
// //生成uuid并去除'-'
// String uuid = UUID.randomUUID().toString().replace("-","");
// //获取文件.的索引
// int index = originalFilename.lastIndexOf(".");
// //截取.索引以后的内容
// String suffix = originalFilename.substring(index);
// return uuid+suffix;
// }
@Autowired
AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws Exception {
log.info("文件上传,文件名:{}",image.getOriginalFilename());
//调用aliyun工具类进行文件上传
String url = aliOSSUtils.upload(image);
log.info("文件上传完成,url:{}",url);
return Result.success(url);
}
}
案例-修改员工-查询回显&修改员工
看页面原型与接口文档。
@GetMapping("/{id}")
public Result getOne(@PathVariable Integer id){
log.info("查询单条信息:{}",id);
Emp emp = empService.getOne(id);
return Result.success(emp);
}
@PutMapping
public Result updateEmp(@RequestBody Emp emp){
log.info("修改数据:{}",emp);
empService.updateEmp(emp);
return Result.success();
}
update操作,要修改更新时间。
<update id="updateEmp">
update emp
<set>
<if test="username !=null and username != ''">
username = #{username},
</if>
<if test="name != null and name != '' ">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="entrydate != null">
entrydate = #{entrydate},
</if>
<if test="deptId != null">
dept_id = #{deptId},
</if>
<if test="job != null">
job = #{job},
</if>
</set>
<where>
id = #{id}
</where>
</update>
案例-配置文件-参数配置化***
解决办法
技巧alt+鼠标左键,一起操作
已经注入成功
总结
案例-配置文件-yml配置文件
案例-配置文件-@ConfigurationProperties*
创建一个新的实体来保存 配置的属性。再将此新实体注入到所需的地方,调用get方法获取值。
代码
yml配置文件,配置参数
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: web-hmgzy
新建实体封装
package com.neuedu.util;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(value = "aliyun.oss")
public class AliyunProperties {
// private String accessKeyId = "LTAI4GCH1vX6DKqJWxd6nEuW";
// private String accessKeySecret = "yBshYweHOpqDuhCArrVHwIiBKpyqSL";
private String endpoint;
private String bucketName;
}
使用
package com.neuedu.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyuncs.exceptions.ClientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Component //交给IOC容器管理
public class AliOSSUtils {
//原始写法
// private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// private String accessKeyId = "LTAI4GCH1vX6DKqJWxd6nEuW";
// private String accessKeySecret = "yBshYweHOpqDuhCArrVHwIiBKpyqSL";
// private String bucketName = "web-hmgzy";
//配置文件 注入
// @Value("${aliyun.oss.endpoint}")
// private String endpoint;
// @Value("${aliyun.oss.bucketName}")
// private String bucketName;
//再次优化 使用@ConfigurationProperties注解,实体封装参数,利用get方法获得。
@Autowired
AliyunProperties aliyunProperties;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws Exception {
//获取阿里云OSS参数
String endpoint = aliyunProperties.getEndpoint();
String bucketName = aliyunProperties.getBucketName();
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString().replace("-","") + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
//采用配置变量的形式,获取accessKeyId&accessKeySecret
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
在实体中,会出现这个,并不影响程序的运行。
他只是需要引入依赖,就会出现相应的提示
这项依赖的作用,就是输入的时候有提示。
总结
属性少用value。
基础登录功能
根据需求接口文档编写登录。
package com.neuedu.controller;
import com.neuedu.pojo.Emp;
import com.neuedu.pojo.Result;
import com.neuedu.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class UserController {
@Autowired
UserService userService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("登录emp:{}",emp);
Emp e = userService.login(emp);
return e != null?Result.success():Result.error("用户名或密码错误");
}
}
--------理解-------
登录校验-概述*
http协议是无状态的,意思是,每次请求都是相对独立的。
登录成功后,会有登录标记,每次请求都去统一拦截,判断是否有标记,才可放行。
登录认证-登录校验-会话技术*
一次会话可以包含多次请求,直到浏览器或服务器 一方关闭,会话结束。
进入浏览器访问域名,这时候就建立会话了。
3次会话,3个浏览器。
会话跟踪
判断请求是否来自同一个浏览器,这样方便共享数据。
例如:
验证码展示,向服务端发送请求
当输入验证码登录时,又是一次请求,这就涉及到会话跟踪,进行共享数据。
会话跟踪技术
因为http是无状态,每次请求相对独立,无法携带状态,这也保证了,他的效率高。那怎么携带状态?就需要用到会话跟踪技术。
会话跟踪方案对比-Cookie 注重理解**
Cookie (Headers) - HTTP 中文开发手册 - 开发者手册 - 腾讯云开发者社区-腾讯云
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
访问服务端后,服务端把cookie相应给浏览器,存储在浏览器。
以后的请求中, cookie会跟随请求携带到服务端。
代码
package com.neuedu.controller;
import com.neuedu.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@Slf4j
public class SessionController {
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("user_name","itg"));
return Result.success();
}
@GetMapping("c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies){
if (cookie.getName().equals("user_name")){
System.out.println("user_name"+cookie.getValue());
}
}
return Result.success();
}
}
在响应头头中,可以看到服务器响应的cookie。 Set-Cookie是设置cookie
请求头内容, cookie随请求携带。
优缺点
cookie跨域了,就不能使用了。
会话跟踪方案对比-Session 注重理解**
session的底层,就是基于cookie实现的。
如果我们现在要基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
Set-Cookie的名字JSESSIONID 是固定的,传输的是 session的id。
接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。
接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
代码
//网session中设置值
@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());
//如果能取出来,就证明可以在1次会话中共享数据了。
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
向服务器发送请求,服务器给出响应Set-Cookie,存储session的id
已经存储在浏览器中了。
浏览器又会把cookie携带到服务器,服务器进行查找对应的id,这样就可以共享数据了。
优缺点
集群环境下无法直接使用session。
首次访问时,有负载均衡到第一台服务器,生产session。返回给浏览器,下次访问时,不知是哪台服务器存储的session了,这样就无法共享数据。
会话跟踪方案对比-令牌技术 注重理解**
本质就是一个字符串
服务端生成令牌,相应给客户端,保存在浏览器,后续每次请求都会携带令牌,服务端来校验令牌有效性。
(想象生活中,上班打卡)入职,录入人脸,每天上班打卡机打卡。
如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。
接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。
接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。
此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。
--------理解-------
登录校验-JWT令牌-介绍
登录校验-JWT令牌-生成&校验**
前两个部分都是base64编码,可以解码解析出来,第三部分是算法算出来的,不能解析出来。
细节:
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
代码
生成jwt
@Test
public void testGenJwt(){
Map<String, Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","gzy");
String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(负载)
.signWith(SignatureAlgorithm.HS256, "ithG") //设置签名算法(官网查看)和自定义的签名秘钥(解析的时候用)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //过期时间(有效期) 单位:毫秒
.compact();
System.out.println(jwt);
}
秘钥要大于3位,要不然生成时会报错
这就是签名算法
解析令牌jwt
/**
* 解析令牌
*/
@Test
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("ithG") 指定签名密钥(必须保证和生成令牌时使用相同的签名密钥) 上面的itG
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzEyNTc0MjYwLCJ1c2VybmFtZSI6Imd6eSJ9.M-Lp6UHFB1Hx-oEBBEvRaA-v_8Ecmbzm2dOc2rQdriU") //jwt字符串
.getBody();
System.out.println(claims);
}
exp为过期时间。id和username为传输的内容。
输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。
网上的base64解码,也可以解析出来。第三部分是算法,不是编码,不可解析。
登录校验-JWT令牌-登录下发令牌**
根据接口文档,在登录时,成功生成令牌,失败返回提示信息。
操作
1.导入工具类
package com.neuedu.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
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))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
2.成功生成jwt
package com.neuedu.controller;
import com.neuedu.pojo.Emp;
import com.neuedu.pojo.Result;
import com.neuedu.service.UserService;
import com.neuedu.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Autowired
UserService userService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("登录emp:{}",emp);
Emp e = userService.login(emp);
//登录成功,生成令牌。
if (e != null){
//存储信息info
Map<String, Object> info = new HashMap<>();
info.put("id",e.getId());
info.put("username",e.getUsername());
info.put("name",e.getName());
//调用工具类生成jwt令牌
String jwt = JwtUtils.generateJwt(info);
return Result.success(jwt);
}
//失败,返回提示信息。
return Result.error("用户名或密码错误");
}
}
前端会进行存储,存储在local storage中。JWT令牌存储在浏览器的本地存储空间local storage中了。 local storage是浏览器的本地存储,在移动端也是支持的。
演示
登录成功,jwt已经返回。
已经存储到local storage中。
我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。
登录校验-filter入门*
在Filter类上要添加@WebFilter注解,来配置拦截的路径
Filter是javaWeb的三大组件,不是springboot提供的的,如果想在springboot中使用javaWe的三大组件就要在启动类上添加注解@ServletComponentScan(注解表示,当前项目是支持javaweb3大组件的)
两步走,1.定义Filter 2.配置Filter
过滤器中有非常重要的一步,就是放行。
代码
package com.neuedu.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
//定义一个类,实现一个标准的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 销毁方法执行了-------------------------");
}
}
在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求
package com.neuedu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
支持使用servlet组件
登录校验-Filter详解-执行流程
执行流程
请求 - filter - 访问controller相关逻辑 - 拿到响应 - filter - 返回给浏览器
拦截路径
登录校验-Filter详解-过滤器链
-------------重要------------
登录校验过滤器*********
因为登录的时候,还没有令牌。
判断是否登录,判断令牌是否存在,判断解析是否通过.
基于上面的业务流程,我们分析出具体的操作步骤:
-
获取请求url
-
判断请求url中是否包含login,如果包含,说明是登录操作,放行
-
获取请求头中的令牌(token)
-
判断令牌是否存在,如果不存在,返回错误结果(未登录)
-
解析token,如果解析失败,返回错误结果(未登录)
-
放行
代码:详看,盲打
<!-- 转换为json字符串-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
package com.neuedu.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.neuedu.pojo.Result;
import com.neuedu.util.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter("/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
//强转
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resq = (HttpServletResponse) servletResponse;
//1 获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2 判断请求url中是否包含login,如果包含,说明是登录操作,放行
boolean contain = url.contains("login");
if (contain) {
log.info("登录操作,放行...");
filterChain.doFilter(req, resq);
//放行后,不在继续往下操作。
return;
}
//3 获取请求头中的令牌(token)
String token = req.getHeader("token");
//4 判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(token)){//spring提供的StringUtils
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");//接口文档规定的是NOT_LOGIN,返回给前端
//在controller中,RestController会自动将对象转换为json传输。这里需要手动传输,借助阿里巴巴fastJson
//先去引入pom,在使用。
String notLogin = JSONObject.toJSONString(error);
//响应给浏览器,调用Writer输出流
resq.getWriter().write(notLogin);
return;
}
//5 解析token,如果解析失败,返回错误结果(未登录)
try {
//ctrl + alt + t 快捷键 捕获异常
JwtUtils.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json,阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
//响应给浏览器,调用Writer输出流
resq.getWriter().write(notLogin);
return;
}
//6 放行
log.info("令牌合法,放行");
filterChain.doFilter(req, resq);
}
}
细节
1.FilterChain主要是进行放行操作的。
2.强转,因为tomcat中使用的是ServletRequest/response,传输需要时httpServletRequest/Response。
3.在controller中,RestController会自动将对象转换为json传输。这里需要手动传输,借助阿里巴巴fastJson
4.手动响应给浏览器,调用Writer输出流 resq.getWriter().write(notLogin);
5.校验解析令牌是否成功,就看是否有异常?try catch进行捕获。
5.判断String是否是空,StringUtils.hasLength(token) spring提供的。
测试
测试登录。
登录成功,返回令牌。
执行查询操作,将token放在请求头中。
最终请求成功,返回正确结果。
尝试修改令牌,测试错误token。
解析失败,返回NOT_LOGIN。
测试查询,不携带令牌。
token判断为不存在,返回NOT_LOGIN。
-------------重要------------
登录校验-interceptor-入门**
定义拦截器,就要实现拦截器的接口,成为他,就得实现它。然后把他交给容器来管理。
@configuration 标明这是一个配置类
代码
package com.neuedu.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {//ctrl + o 调出重写页面
@Override//目标资源方法(controller)执行前执行。 返回true:放行 返回false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle..");
return 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..");
}
}
交给IOC容器管理。
package com.neuedu.config;
import com.neuedu.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
LoginCheckInterceptor loginCheckInterceptor;
@Override //注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
实现WebMvcConfigurer接口,并重写addInterceptors方法
将拦截器注入到配置类中,再进行注册拦截器。
细节
ctrl+o调出界面,重写方法
目标资源方法就是controller
preHandle方法,返回true,是放行,然后才会执行controller。
拦截器,拦截所以,路径是/** 两个星星。过滤器是一个星
-------------重要------------
登录校验-interceptor-详解***
/* 和 /** 一级和两级路径,注意。
package com.neuedu.config;
import com.neuedu.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
LoginCheckInterceptor loginCheckInterceptor;
@Override //注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
//excludePathPatterns 排除路径 . /* 是一级,只能拦截1级的 /emp /dept, /dept/1 这样就拦截不了
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
excludePathPatterns 排除路径 . /* 是一级,只能拦截1级的 /emp /dept, /dept/1 这样就拦截不了
执行流程 ***重要!重要!重要!
filter是tomcat的servlet组件,interceptor是spring提供的组件。filter可以拦截所有的请求,而interceptor只能拦截进去spring的请求。因为tomcat无法识别contorller的请求,所以有dispatcherServlet中央调度器,去对接controller。
-
当我们打开浏览器来访问部署在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环境中的资源。
当filter和interceptor同时都开启后,执行流程和图片一样。
-------------重要------------
登录校验-interceptor-登录校验*
和filter的流程一样,只不过换成interceptor。
代码
逻辑一样,所以将filter中代码,直接复制到Interceptor的preHandle方法中,进行修改就行
true放行, false不放行
package com.neuedu.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.neuedu.pojo.Result;
import com.neuedu.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {//ctrl + o 调出重写页面
@Override//目标资源方法(controller)执行前执行。 返回true:放行 返回false:不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle..");
//1 获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2 判断请求url中是否包含login,如果包含,说明是登录操作,放行
boolean contain = url.contains("login");
if (contain) {
log.info("登录操作,放行...");
//true 为放行
return true;
}
//3 获取请求头中的令牌(token)
String token = req.getHeader("token");
//4 判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(token)){//spring提供的StringUtils
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");//接口文档规定的是NOT_LOGIN,返回给前端
//在controller中,RestController会自动将对象转换为json传输。这里需要手动传输,借助阿里巴巴fastJson
//先去引入pom,在使用。
String notLogin = JSONObject.toJSONString(error);
//响应给浏览器,调用Writer输出流
resp.getWriter().write(notLogin);
//令牌不存在,不能放行 返回false
return false;
}
//5 解析token,如果解析失败,返回错误结果(未登录)
try {
//ctrl + alt + t 快捷键 捕获异常
JwtUtils.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json,阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
//响应给浏览器,调用Writer输出流
resp.getWriter().write(notLogin);
//不能放行,false
return false;
}
//6 放行
log.info("令牌合法,放行");
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..");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
异常处理
添加部门
页面没有变化
进行添加部门,因为name有唯一约束,所以报错了。
所以前端页面并没有变化,而后台已经报错了。就因为前端不发识别spring给出的响应。(controller继续向上抛,抛给了spring,这是spring的处理方式。)
再抛给全局异常处理器,进行处理返回Result(前后端规定好的对象)这样前端就可以解析出来。
@RestContorllerAdvice 意思是这是一个全局异常处理器。传输时是一个json字符串,那么Result是怎么转换的呢,是因为 注解底层 有@ResponseBody注解 + @ControllerAdvice注解。
@ExceptionHandler 表明 处理什么类型的异常。
总结
两个注解
事务管理-事务回顾
@Transactional
spring事务管理 。出现运行时异常,事务会自动进行回滚。
事务管理配置文件
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
代码
@Override
public void delById(Integer id) { //删除部门和部门员工
deptMapper.delById(id);
//伪造异常
int i = 1/0;
empMapper.delByDeptId(id);
}
1号部门,学工部被删除。
员工表却没有正常删除。
所以要添加事务,保证数据的完整性。
@Transactional
@Override
public void delById(Integer id) {
deptMapper.delById(id);
//伪造异常
int i = 1/0;
empMapper.delByDeptId(id);
}
事务管理-事务进阶-rollbackFor***
1/0 伪造异常,他是一个运行时异常,运行时异常是可以正常回滚的。
rollbackFor就是指定哪样的异常可以回滚。
默认情况下,只有出现运行时异常才能回滚。
设置注解属性 rollbackFor=Exception.class 表明所有异常都回滚。
操作
手动抛出1个异常
异常下面的empMapper.xxx方法有提示,让删除。是因为上面出现异常,那么下面的代码都不会执行了。所以加个判断即可。
在service中向上抛出一个Exception编译时异常之后,由于是controller调用service,所以在controller中要有异常处理代码,此时我们选择在controller中继续把异常向上抛。
启动程序,开始测试。
部门id为1的员工,并没有删除掉。 @Transactional事务并没有回滚数据,失效了。
接下来恢复数据
使用rollbackFor来指定处理异常的范围,来解决问题
@Transactional(rollbackFor = Exception.class) //指定Exception范围,就是所有异常,都会进行处理。
@Override
public void delById(Integer id) throws Exception {
deptMapper.delById(id);
//伪造异常
// int i = 1/0;
//手动抛出一个异常,来伪造异常
if (true){
throw new Exception("出错了...");
}
empMapper.delByDeptId(id);
}
没用rollbackFor,以前使用的1/0操作时,因为是运行时异常,所以好使,可以回滚。注解的作用范围默认是运行时异常。
@Transactional(rollbackFor = Exception.class)
重启服务,测试
事务生效了,并没有删除掉,执行了回滚操作。
事务管理-事务进阶-propagation***
插件 :控制台过滤插件
Grep Console 可以对控制台日志实行高亮。
查看事务管理的日志
主要内容
主需要关注两个
案例需求
下面这个是有问题的,想一想???
insert的事务是默认值,默认值是有则加入,无则创建。他有啊,所以就会加入到delete的事务中。所以delete方法里的代码都共用一个事务。虽然记录日志的代码在finally中,会执行,因为共用1个事务,try中代码出现异常,那么整个事务中的代码都会回滚,所以日志也会被回滚掉。
将insert事务设置为REQUIRES_NEW ,意思是需要一个新的事物,无论调用方是否有事务,他都会创建1个新事物。所以当delete方法中出现异常,并不会影响insert的提交。
以为异常,所以进行了回滚。
数据库已没有插进数据,也给回滚了。
@Transactional(propagation = Propagation.REQUIRES_NEW) 给insert加入传播行为xxxnew
挂起:将当前正在执行的事务暂停执行,直接执行新创建的事务
数据加入成功
代码
DeptServiceImpl
/**
* 删除部门
* @param id
*/
@Transactional(rollbackFor = Exception.class) //指定Exception范围,就是所有异常,都会进行处理。
@Override
public void delById(Integer id) throws Exception {
try {
deptMapper.delById(id);
//伪造异常
// int i = 1/0;
//手动抛出一个异常,来伪造异常
if (true){
throw new Exception("出错了...");
}
empMapper.delByDeptId(id);
} finally {
DeptLog log = new DeptLog();
log.setCreateTime(LocalDateTime.now());
log.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
logService.insert(log);
}
}
DeptLogServiceImpl
package com.neuedu.service.impl;
import com.neuedu.mapper.DeptLogMapper;
import com.neuedu.pojo.DeptLog;
import com.neuedu.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
总结
AOP基础-快速入门
概述
粉色:模版方法
快速入门
@component交给spring容器管理,@Aspect 切面AOP类 , @Around 指定特定方法根据业务编程。 参数里的 两个 * 分别是 类/接口 和 方法。
代码
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))") //切入点表达式
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//1.记录开始时间
long startTime = System.currentTimeMillis();
//2.调用原始方法运行
Object result = proceedingJoinPoint.proceed(); //原始方式也会有返回值,要返回回去
//3.记录结束时间,计算方法耗时时间
long endTime = System.currentTimeMillis();
//proceedingJoinPoint.getSignature()拿到方法的签名
log.info(proceedingJoinPoint.getSignature()+"方法执行耗时:{}ms",endTime-startTime);
return result;
}
}
proceedingJoinPoint.getSignature()拿到方法的签名
proceedingJoinPoint.proceed() 调用原始方法运行
切入点表达式,先了解一下即可,后续详说。
使用场景
-
事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
优势
-
代码无侵入:没有修改原始的业务方法,就已经对原始的业务方法进行了功能的增强或者是功能的改变
-
减少了重复代码
-
提高开发效率
-
维护方便
AOP基础-核心概念***
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
在入门程序中是需要统计各个业务方法的执行耗时的,此时我们就需要在这些业务方法运行开始之前,先记录这个方法运行的开始时间,在每一个业务方法运行结束的时候,再来记录这个方法运行的结束时间。
但是在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。
共性的内容,要执行的逻辑,封为1个方法。
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
在通知当中,我们所定义的共性功能到底要应用在哪些方法上?此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。
在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点(后面会有详解)。
假如:切入点表达式改为DeptServiceImpl.list(),此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。
这个方法统称为切面 通知+切入点
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
5. 目标对象:Target,通知所应用的对象
目标对象指的就是通知所应用的对象,我们就称之为目标对象。
执行流程
执行流程。找到目标对象,根据切面代码逻辑,去生成代理对象,这个代理对象具备目标对象的内容,在内容上下添加切面的逻辑。最终把代理对象注入到controller中。
Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
测试
访问部门管理。
controller中注入的是CGLIB代理对象。
代理对象开始,有个代理类,将切面中的代码进行封装。这都是自动的,只是我们看不见。
执行完毕,返回结果。
一但使用aop开发,那么注意,注入的对象不是原来的对象,而是代理对象。
AOP进阶-通知类型***
5种通知类型&@Pointcut注解
@AfterReturning @AfterThrowing 互斥。一个正常执行,一个异常执行。
环绕通知比较特殊,是ProceedingJoinPoint类型,需自己调用proceed方法来执行。
而且环绕通知必须指定返回值类型。
代码
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") //第一个*是返回值 代码任意
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
//后置通知
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
环绕和前置 通知执行了。
返回后通知,后置,环绕 通知 都执行了。 异常后通知没执行。
伪造异常
异常通知执行,返回后通知没执行 互斥。
抽取接入点
execution(* com.itheima.service.impl.DeptServiceImpl.*(..)) 抽取出来复用。
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pd(){}
//前置通知
@Before("pd()") //第一个*是返回值 代码任意
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pd()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
//后置通知
@After("pd()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pd()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pd()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pd(){}
如果是public修饰,那么别的切面类也可以使用pd()。
总结
AOP进阶-通知顺序
插件细节-过滤*
内容
和类名字母顺序排序有关。
@Order
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Slf4j
@Component
@Aspect
public class MyAspect4 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pd(){}
//前置通知
@Before("pd()") //第一个*是返回值 代码任意
public void before(JoinPoint joinPoint){
log.info("before4 ...");
}
//后置通知
@After("pd()")
public void after(JoinPoint joinPoint){
log.info("after4 ...");
}
}
AOP进阶-切入点表达式execution
类似模糊查询匹配。
切入点表达式示例
-
省略方法的修饰符号
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替返回值类型execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替包名(一层包使用一个*
)execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
-
使用
..
省略包名execution(* com..DeptServiceImpl.delete(java.lang.Integer))
-
使用
*
代替类名execution(* com..*.delete(java.lang.Integer))
-
使用
*
代替方法名execution(* com..*.*(java.lang.Integer))
-
使用
*
代替参数execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
-
使用
..
省略参数execution(* com..*.*(..))
注意事项:
-
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
事例
使用切入点表达式,通配list 和 delete 方法。
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect5 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) ||" +
"execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void pp(){}
@Before("pp()")
public void testListAndDel(JoinPoint joinPoint){
log.info(joinPoint.getSignature()+"切面执行...");
}
}
尽量基于接口描述,而不是实现类,增强扩展性。
使用 || 可以写多个表达式。
总结
AOP进阶-切入点表达式-@annotation**
基于注解的方式来描述。可以自定义注解来实现。
代码
package com.itheima.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) //作用目标->方法上
@Retention(RetentionPolicy.RUNTIME) //作用范围 运行时
public @interface MyZDY {
}
自定义注解。
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect5 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) ||" +
"execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void pp(){}
// @Before("pp()")
@Before("@annotation(com.itheima.aop.MyZDY)") //添加MyZDY注解的,就会生效
public void testListAndDel(JoinPoint joinPoint){
log.info(joinPoint.getSignature()+"切面执行...");
}
}
AOP切面类。
@MyZDY
@Override
public List<Dept> list() {
// int i = 1/0;
List<Dept> deptList = deptMapper.list();
return deptList;
}
@MyZDY
@Override
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
ServiceImpl实现类,添加注解,要aop编程的方法。
总结
注解比较灵活,但是需要自定义注解。当不好匹配时,可以使用注解。
AOP进阶-连接点**
代码
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 测试连接点
*/
@Slf4j
@Component
@Aspect
public class MyAspect7 {
@Pointcut("@annotation(com.itheima.aop.MyZDY)")
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;
}
}
before通知,拿不到参数,因为主要逻辑代码还没有执行。
-
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
-
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
AOP案例-记录操作日志********
代码
<!-- AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
引入aop依赖
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
建日志表
package com.neuedu.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
建实体
package com.neuedu.mapper;
import com.neuedu.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
记录日志的插入语句。
package com.neuedu.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) //作用的范围
@Retention(RetentionPolicy.RUNTIME) //什么时候有效
public @interface Log { //自定义日志注解
}
自定义注解
package com.neuedu.aop;
import com.alibaba.fastjson.JSONObject;
import com.neuedu.mapper.OperateLogMapper;
import com.neuedu.pojo.OperateLog;
import com.neuedu.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Component
@Aspect
public class LogAspect {
@Autowired //注入操作日志mapper
OperateLogMapper mapper;
@Autowired
HttpServletRequest request;
@Around("@annotation(com.neuedu.aop.Log)")
public Object insertLog(ProceedingJoinPoint pjp) throws Throwable {
OperateLog operateLog = new OperateLog();
//记录开始时间
long start = System.currentTimeMillis();
//操作放行
Object result = pjp.proceed();
//记录结束时间
long end = System.currentTimeMillis();
//操作人id
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);//解析令牌内容
Integer userId = (Integer) claims.get("id");
operateLog.setOperateUser(userId);
//操作时间
operateLog.setOperateTime(LocalDateTime.now());
//操作类名
operateLog.setClassName(pjp.getTarget().getClass().getName());
//操作方法名
operateLog.setMethodName(pjp.getSignature().getName());
//操作方法参数
operateLog.setMethodParams(Arrays.toString(pjp.getArgs()));
//操作方法返回值
operateLog.setReturnValue(JSONObject.toJSONString(result));
//操作耗时
operateLog.setCostTime(end-start);
mapper.insert(operateLog);
return result;
}
}
定义切面类 。
拿到操作人id:
通过jwt令牌,以前实在controller中 方法参数列表中,声明一个HttpServletRequest req,在aop中可以直接注入HttpServletRequest。
返回值:
返回值是一个Object类型,插入到数据库中需要的是String,需要转换为json字符串 阿里巴巴
package com.neuedu.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.neuedu.aop.Log;
import com.neuedu.mapper.EmpMapper;
import com.neuedu.pojo.Emp;
import com.neuedu.pojo.PageBean;
import com.neuedu.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
EmpMapper empMapper;
@Override
public PageBean listEmp(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
// 无分页插件时
// Integer start = (page - 1) * pageSize;
// List<Emp> emps = empMapper.listEmp(start, pageSize);
// Long total = this.listCount();
// return new PageBean(total,emps);
//使用PageHelper
PageHelper.startPage(page,pageSize);
List<Emp> emps = empMapper.listEmp(page, pageSize,name,gender,begin,end);
Page<Emp> p = (Page<Emp>) emps;
return new PageBean(p.getTotal(),p.getResult());
}
@Override
public Long listCount() {
return empMapper.listCount();
}
@Log
@Override
public void delEmp(List<Integer> ids) {
empMapper.delEmp(ids);
}
@Log
@Override
public void addEmp(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.addEmp(emp);
}
@Override
public Emp getOne(Integer id) {
return empMapper.getOne(id);
}
@Log
@Override
public void updateEmp(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.updateEmp(emp);
}
}
给方法添加自定义注解
注意
记录的类名,看自定义注解加在什么位置,controller和impl,就是切入点的位置,从哪个位置切入。
因为impl的返回值就是null,所以是空。加在controller的自定义注解,那么返回值就是Result。
操作方法 获取属性
//操作类名
String className = joinPoint.getTarget().getClass().getName();
@Autowired
private HttpServletRequest request;
//操作人ID - 当前登录员工ID
//获取请求头中的jwt令牌, 解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");
//操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
------原理篇要理解-------
springboot-配置优先级
优先级:命令行参数>java系统属性
打包上线的项目,如何修改
如何修改 命令行参数&java系统属性
演示:
执行生命周期的package。
到target文件中,找打jar包。
在当前路径进行cmd,打开窗口,输入java,看提示命令
options 是 java系统属性 args 是命令行参数。
执行 java -jar springboot-web-config-0.0.1-SNAPSHOT.jar
没指定,启动默认是8080
执行 java -jar -Dserver.port=9000 springboot-web-config-0.0.1-SNAPSHOT.jar
指定了系统属性,启动的就是9000
执行 java -jar -Dserver.port=9000 springboot-web-config-0.0.1-SNAPSHOT.jar --server.port=10010
同时指定了, 命令行参数优先级高,启动的是10010.
注意
基于官方骨架创建,都会有
配置优先级
bean的管理-bean的获取
代码
package com.itheima;
import com.itheima.controller.DeptController;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {
//springboot的IOC容器对象
@Autowired
ApplicationContext applicationContext;
//获取bean对象
@Test
public void testGetBean(){
// getBean有很多重载方法
//根据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);
}
//bean的作用域
@Test
public void testScope(){
}
//第三方bean的管理
@Test
public void testThirdBean() throws Exception {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(this.getClass().getClassLoader().getResource("1.xml"));
Element rootElement = document.getRootElement();
String name = rootElement.element("name").getText();
String age = rootElement.element("age").getText();
System.out.println(name + " : " + age);
}
}
一个bean没有声明名称,那就是类名首字母小写。
默认是单例。
bean的管理-bean的作用域
使用@Scope注解来指定。
IOC容器当中,默认bean对象是单例模式(只有一个实例对象)。那么如何设置bean对象为非单例呢?需要设置bean的作用域。
重点是前两种。
默认情况下,项目启动时,就会实例化对象,加载到容器中。
首次实例化后,以后的对象,都是拿来直接用。
当指定@Scope("prototype") 非单例的时候,就会初始化10次,每次都不一样。
实例化了10次。
当指定@Lazy 懒加载时。
启动,控制台并没有输出构造方法的内容,还没有加载初始化。
当进入断点,首次调用getBean的时候,进行了加载。
总结
bean的管理-第三方bean*
第三方是通过pom引入的,都是只读的,无妨使用@component注解,交给spring容器来管理。这时候就需要使用@Bean注解。
演示
传统方法来解析xml文件,获取里面的值。
交给spring容器管理后,一样可以。
代码
package com.itheima.config;
import org.dom4j.io.SAXReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //配置类
public class CommonConfig {
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
public SAXReader saxReader(){
return new SAXReader();
}
}
要单独定义一个类,放在config配置包下,来统一管理第三方bean。
@Test
public void testBean2(){
Object bean = applicationContext.getBean("saxReader");
System.out.println(bean);
}
这里的saxReader就是上面代码中的方法名称。
-
通过@Bean注解的name或value属性可以声明bean的名称,如果不指定,默认bean的名称就是方法名。
可以拿到bean对象
package com.itheima.config;
import com.itheima.service.DeptService;
import org.dom4j.io.SAXReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //配置类
public class CommonConfig {
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
//通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名
public SAXReader saxReader(DeptService deptService){
System.out.println(deptService);
//给第三方bean注入对象,无法使用@AutoWired,但可以使用形参的方式给第三方bean注入。类似构造
return new SAXReader();
}
}
给第三方bean注入对象,无法使用@AutoWired,但可以使用形参的方式给第三方bean注入。类似构造
-
如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。
在启动的过程中,已经注入成功,输出出来了。默认是通过类型装配,就DeptService类型。
springboot原理-起步依赖*
springboot就是简化sping框架的。
SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。
假如我们没有使用SpringBoot,用的是Spring框架进行web程序的开发,此时我们就需要引入web程序开发所需要的一些依赖。
那么这些依赖都需要手动去引入,而且版本要对应,不对应就会出现问题。
如果使用springboot
只需要引入对应依赖,一个依赖就行。 其他依赖会通过maven的依赖传递,自动传递下来。
A需要B,B需要C,C需要D。那么引入A,BCD就会传递下载下来。
原理
很简单,起步依赖的原理就是 maven的依赖传递
springboot原理-自动配置-概述
spring是如何将bean加入到IOC容器中的。
大家会看到有两个CommonConfig,在第一个CommonConfig类中定义了一个bean对象,bean对象的名字叫reader。
在第二个CommonConfig中它的bean名字叫commonConfig,为什么还会有这样一个bean对象呢?原因是在CommonConfig配置类上添加了一个注解@Configuration,而@Configuration底层就是@Component
在IOC容器中除了我们自己定义的bean以外,还有很多配置类,这些配置类都是SpringBoot在启动的时候加载进来的配置类。这些配置类加载进来之后,它也会生成很多的bean对象。
比如:配置类GsonAutoConfiguration里面有一个bean,bean的名字叫gson,它的类型是Gson。
com.google.gson.Gson是谷歌包中提供的用来处理JSON格式数据的。
当我们想要使用这些配置类中生成的bean对象时,可以使用@Autowired就自动注入了:
import com.google.gson.Gson;
import com.itheima.pojo.Result;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AutoConfigurationTests {
@Autowired
private Gson gson;
@Test
public void testJson(){
String json = gson.toJson(Result.success());
System.out.println(json);
}
}
添加断点,使用debug模式运行测试类程序:
问题:在当前项目中我们并没有声明谷歌提供的Gson这么一个bean对象,然后我们却可以通过@Autowired从Spring容器中注入bean对象,那么这个bean对象怎么来的?
答案:SpringBoot项目在启动时通过自动配置完成了bean对象的创建。
体验了SpringBoot的自动配置了,下面我们就来分析自动配置的原理。其实分析自动配置原理就是来解析在SpringBoot项目中,在引入依赖之后是如何将依赖jar包当中所定义的配置类以及bean加载到SpringIOC容器中的。
springboot原理-自动配置-方案****
文件导入,不是真的导入,要把项目结构都导入进来import。
itheima这个项目模拟第三方依赖。使用的是springboot-web-config2演示。
在springboot-web-config2的pom文件中,引入itheima-utils的依赖。
<!-- 引入第三方提供依赖-->
<dependency>
<groupId>com.example</groupId>
<artifactId>itheima-utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
都是一些自定义的类,类上也加了@Component的注解。
bean并没有获取到。为什么???
思考
思考:引入进来的第三方依赖当中的bean以及配置类为什么没有生效?
-
原因在我们之前讲解IOC的时候有提到过,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。(没有扫描到组件)
-
SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。(两个项目,测试类这个扫描,扫不到引入的@Component)
-
当前包:com.itheima, 第三方依赖中提供的包:com.example(扫描不到)
解决办法
那么如何解决以上问题的呢?
-
方案1:@ComponentScan 组件扫描
-
方案2:@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)
方案1*简单
@ComponentScan只要指定参数,那么原来就会被覆盖掉,所以也要扫描当前项目。
value是个数组,所以要加{ }
因为@SpringBootApplication注解中包含@ComponentScan,添加@ComponentScan相当于重写,所以原来的会失效。重写后,就可以拿到bean了。
在springboot的自动配置,并没有采用这种方案,如果引入很多第三方依赖
就会写的很长,使用繁琐,性能也低。
联想:Mybatis、MP....第三方,就没有这么配置,就是因为没有采用这种方式。
方案2****
第四种方式,就是springboot所采用的方式。
这种导入@Import的方式,及时类上没有@Component注解,也好使,他是直接将类加载到容器中。
@EnableXxxx注解,内部就封装了@Import。
普通类:就是自定义的基础类。
package com.example;
import org.springframework.stereotype.Component;
@Component
public class TokenParser {
public void parse(){
System.out.println("TokenParser ... parse ...");
}
}
配置类:配置类中,封装了一些自定义的基础类。(就是封装了很懂普通类)
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HeaderConfig { //配置类
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
}
@Bean
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
}
}
ImportSelector接口实现类:可以对 配置类普通类进行封装。
package com.example;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//返回值字符串数组(数组中封装了全限定名称的类)
return new String[]{"com.example.HeaderConfig"};
}
}
返回值字符串数组(数组中封装了全限定名称的类)
package com.itheima;
import com.example.HeaderConfig;
import com.example.MyImportSelector;
import com.example.TokenParser;
import com.github.pagehelper.dialect.helper.MySqlDialect;
import org.dom4j.io.SAXReader;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@SpringBootApplication
//第一种方式
//@ComponentScan(value = {"com.itheima","com.example"})
//第二种方式
//@Import(TokenParser.class) //1.导入普通类
//@Import(HeaderConfig.class) //2.导入配置类
//@Import(MyImportSelector.class) //3.导入ImportSelector接口实现类
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}
主启动类
package com.itheima;
import com.example.HeaderParser;
import com.example.TokenParser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
@SpringBootTest
public class AutoConfigurationTests {
@Autowired
ApplicationContext applicationContext;
@Test
public void testTokenParser(){
TokenParser tokenParserBean = applicationContext.getBean(TokenParser.class);
System.out.println(tokenParserBean);
}
@Test
public void testHeaderParser(){
HeaderParser headerParser = applicationContext.getBean(HeaderParser.class);
System.out.println(headerParser);
}
}
测试类
思考**
思考:如果基于以上方式完成自动配置,当要引入一个第三方依赖时,是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象?
-
答案:是的。 (对程序员来讲,很不友好,而且比较繁琐)
思考:当我们要使用第三方依赖,依赖中到底有哪些bean和配置类,谁最清楚?
-
答案:第三方依赖自身最清楚。
结论:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。
怎么让第三方依赖自己指定bean对象和配置类?
-
比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxxx开头的注解,注解中封装的就是@Import注解
package com.example;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class) //指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig {
}
第三方提供的注解@EnableXxx.:这里封装了要导入那些依赖。
测试,一切正常。
-------看讲义理解-------
springboot原理-自动配置-原理分析-源码跟踪
看讲义,复现吧,讲义中很全。自己B站中,有视频,加强巩固
自动配置源码小结
自动配置原理源码入口就是@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容器管理。
springboot原理-自动配置-原理分析-@Conditional
@ConditoinalOnProperty注解,使用场景,当需要第三方框架时,需判断配置文件中是否配置,配置了,才会生成bean。
@Conditional注解:
-
作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。
-
位置:方法、类
-
@Conditional本身是一个父注解,派生出大量的子注解:
-
@ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
-
@ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。
-
@ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
-
前置操作
测试HeaderParser的bean是否存在。
主类使用 @EnableHeaderConfig 自定义配置注解
使用的是接口实现类。
接口实现类中,指定的是 配置类的类名。
配置类创建了2个bean,HeaderParser 和 HeaderGenerator
所以测试类HeaderParser 肯定存在。
正式操作-测试三个注解
-
@ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
-
@ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。
-
@ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
1.演示@ConditionalOnClass 拿Jwts这个类做演示。
修改配置类中的代码,当Jwts字节码文件存在,才能注册bean到IOC容器中。
成功拿到。
是因为我们已经在pom中引入了对应的依赖,所以存在
将其注释掉,刷新。重新测试。
因为依赖已经注释掉,现在环境中没有Jwt这个类,所以就不会注册到IOC容器中。
2.@ConditionalOnMissingBean//不存在该类型的bean,才会将该bean加入IOC容器,指定类型(value属性) 或 名称(name属性)
场景:别人自己声明了bean,就用他自己的,如果别人没有声明创建,就会用当前的为默认。
测试成功。
3.@ConditionalOnProperty (这个注解和配置文件当中配置的属性有关系)
场景:springboot整合第三方技术时,配置文件中配置了配置项,才会注册bena。
先在application.yml配置文件中添加如下的键值对:
配置文件中,配置键值对
配置类。
测试。
测试成功。
如果修改配置文件和注解@ConditionalOnProperty(name = "name", havingValue = "gzy") name havingValue 对应不上,就会失败。
springboot原理-自动配置-案例-自定义starter*
看讲义,详情
视频:Day14-11. SpringBoot原理-自动配置-案例(自定义starter实现)_哔哩哔哩_bilibili
在SpringBoot项目中,一般都会将这些公共组件封装为SpringBoot当中的starter,也就是我们所说的起步依赖。
需要将autoconfigure引入到starter中。
以Mybatis为例
Mybatis提供了配置类,并且也提供了springboot会自动读取的配置文件。当SpringBoot项目启动时,会读取到spring.factories配置文件中的配置类并加载配置类,生成相关bean对象注册到IOC容器中。
结果:我们可以直接在SpringBoot程序中使用Mybatis自动配置的bean对象。
需求
<groupId>com.example</groupId> 组id是包的路径
过程出现的问题***
测试运行启动时,就显示 包不存在,找不到。
autocinfig的模块,可以编译成功。但是starter编译不成功,test也编译不成功。
解决办法:运行maven生命周期 clean + install。将autocinfig模块重新清理install。
点击clean
会清理构建输出,而点击install
会编译、测试、打包项目,并将结果安装到本地仓库。这两个操作通常用于确保构建环境的清洁(使用clean
)和将项目作为依赖在其他项目中使用(使用install
)。
注意
封装阿里云utils时,老师讲解的是 传递4个参数的。
而现在的版本,是id和密码都存在配置变量中了。
-------看讲义理解-------
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持久层架构操作数据库等技术。
我们在学习这些web后端开发技术的时候,我们都是基于主流的SpringBoot进行整合使用的。而SpringBoot又是用来简化开发,提高开发效率的。像过滤器、拦截器、IOC、DI、AOP、事务管理等这些技术到底是哪个框架提供的核心功能?
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高级-分模块设计与开发
git有代码。也可以参考讲义,说明的很清楚。
主工程是基于springboot骨架的工程,我们需要创建的是maven工程。只需要在主工程里引入模块坐标就行。
在管理模块,直接引入实体 与 工具 两个模块就可以了。
分模块开发,拆分pojo与utils
操作
引入实体的坐标
controller中将不再报错。
拆分utils时,pom文件中就需要引入所有工具所依赖的坐标,可能比较多。
maven高级-继承与聚合-继承关系
就是把共用的依赖放入到父工程的pom文件中。其他子工程都继承父工程,实现传递。
我们可以再创建一个父工程 tlias-parent ,然后让上述的三个模块 tlias-pojo、tlias-utils、tlias-web-management 都来继承这个父工程 。 然后再将各个模块中都共有的依赖,都提取到父工程 tlias-parent中进行配置,只要子工程继承了父工程,依赖它也会继承下来,这样就无需在各个子工程中进行配置了。
创建父工程,打包方式要设置为pom,代表该模块不写代码,仅进行依赖管理。
因为原来management模块的父工程,已经继承了spring-boot-starter-parent。所以我们需要把创建的父工程来继承spring-boot-starter-parent。management再继承创建的父工程。传递下去
在子工程中pom中设置,继承tlias-parent工程。
操作步骤
父工程pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<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.neuedu</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
</project>
父工程引入共有的依赖,并设置打包方式为pom。
子工程的pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.neuedu</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../tlias-parent/pom.xml</relativePath>
</parent>
<artifactId>tlias-pojo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
子工程导入父工程的坐标并指定父的pom文件的相对位置,并且groupId可以继承下来省略(一样的话)
其他子工程操作类似一样。
因为学习的缘故,正常情况下父工程是在最外层。两种都行,第二种很清晰而已。
maven高级-继承与聚合-版本锁定
就是父工程统一管理版本
父工程只负责管理版本,不会引入真正的依赖。子工程引入的时候,就不需要引入版本号了。
演示
父工程的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<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.neuedu</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
<!-- 父工程统一管理版本(不是共用的依赖).管理了jwt、oss、web-->
<dependencyManagement>
<dependencies>
<!-- springboot web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version>
</dependency>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<!-- oss JAXB相关依赖-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
dependencyManagement标签来管理,并不会真正的引入。
子工程pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.neuedu</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../tlias-parent/pom.xml</relativePath>
</parent>
<artifactId>tlias-util</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- springboot web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!-- oss JAXB相关依赖-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
</project>
不再需要指定版本号了,idea也没有提示版本号标签version。
思考
当父工程的pom文件中,依赖很多,不方便查找时,不方便维护,该怎么办?
该用到自定义属性,来维护依赖 与 版本的关系 ,统一维护。
自定义属性
父工程代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<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.neuedu</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 自定义-->
<lombok.version>1.18.30</lombok.version>
<springweb.version>2.7.5</springweb.version>
<oss.version>3.15.1</oss.version>
<jaxb.runtime.version>2.3.3</jaxb.runtime.version>
<jjwt.version>0.9.1</jjwt.version>
<activation.version>1.1.1</activation.version>
<jaxb.api.version>2.3.1</jaxb.api.version>
</properties>
<dependencies>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
<!-- 父工程统一管理版本(不是共用的依赖).管理了jwt、oss、web-->
<dependencyManagement>
<dependencies>
<!-- springboot web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springweb.version}</version>
</dependency>
<!-- oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${oss.version}</version>
</dependency>
<!-- oss JAXB相关依赖-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.api.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${activation.version}</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.runtime.version}</version>
</dependency>
<!-- jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
web的起步依赖,其实不用维护,因为 springboot的起步依赖中已经配置了。
点进去查看
继续点击
已经管理了版本。此处就是springboot的版本。
总结
dependencyManagement和dependencies的区别**。
maven高级-继承与聚合-聚合实现
直接打包management模块,就会失败,因为管理模块依赖pojo和util和parent。而本地仓库中没有并没有pojo和util和parent。此时就需要对这三个模块执行maven生命周期install(所依赖的模块,必须先安装到本地仓库,才能对所需模块进行打包),进行打包注册。再对管理模块进行打包。
父工程pom
<!--聚合其他模块-->
<modules>
<module>../tlias-pojo</module>
<module>../tlias-util</module>
<module>../tlias-web-management</module>
</modules>
接下来直接操作父工程的生命周期,就可以了。
总结
maven高级-私服
如果有需要,看详细资料文档md。资料不全,查看百度网盘。
介绍
中央仓库:全球只有一个,不是能随便上传的。
首先会查找自己本地仓库是否存在utils这个jar包,然后去中央仓库查找。发现都没有。没办法去访问别人本地仓库的utilsjar包的。
如果需要mybaits最新版本依赖,这时候,私服是没有的,就会去中央仓库去查找。
查找顺序:本地->私服->中央仓库
资源上传与下载
central就是从中央仓库下载下来的。
与我们idea的pom文件相对应。只要不加SNAPSHOT,都会上传到RELEASE中。
步骤
配置私服仓库的用户名密码
配置上传到私服的路径地址
配置私服下载地址 仓库组地址(说白了就是三个仓库的上层文件夹 )
profiles配置
SNAPSHOT不稳定,一般不允许下载的。
注意
上传的id要对应仓库。
执行生命周期 deploy就是上传到私服仓库。
模拟私服