9. MySQL
- 数据库设计:
- MySQL 概述
- 数据库设计 - DDL
- 多表设计
- 数据库操作:
- 数据库操作 - DML
- 数据库操作 - DQL
- 事务
- 多表查询
- 数据库优化
- 索引
- SQL 优化
- 分库分表
企业开发使用方式:
mysql -u用户名 -p密码 [-h数据库服务器IP地址 -p端口号]
数据模型:关系型数据库,建立在关键模型基础上,由多张相互连接的二维表组成的数据库
9.1 DDL - 数据库设计
用来定义数据库、表
- 查询:
- show databases; -- 查询所有数据库 select database(); -- 查询当前数据库
- 使用:
- use 数据库名();
- 创建:
- create database [if not exists] 数据库名;
- 删除:
- drop database [if exists] 数据库名;
- 上述语法中的 database 可以替换成 schema。如:create schema db01;
- 创建:
- create table 表名( 字段1 字段类型 [约束] [comment 注释], …… 字段2 字段类型 [约束] [comment 注释] ) [comment 表注释];
- 约束描述关键字非空约束限制该字段的数据不能为nullnot null唯一约束保证该字段的所有数据都是唯一、不重复的unique主键约束主键是一行数据的唯一标识,要求非空且唯一primary key(auto_increment 自增)默认约束保存数据时,如果未指定该字段的值,则采用默认值default外键约束用来让两张表的数据之间建立连接,保证数据的一致性和完整性foreign key
- 表操作:
- show tables; -- 查询当前所有表 desc 表名; -- 查询表结构 show create table 表名; -- 查询建表语句
9.2 DML - 数据库操作
- 添加数据(insert):
- 修改数据(update):
- 删除数据(delete):
9.3 DQL - 数据库查询
9.3.1 条件查询
where
比较运算符 | 功能 |
> | 大于 |
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
= | 等于 |
<> 或者 != | 不等于 |
between ... and ... | 在某个范围之内(含最小、最大值) |
in( ... ) | 在 in 之后的列表中的值,多选一 |
like 占位符 | 模糊匹配(- 匹配单个字符,% 匹配任意个字符) |
is null | 是 null |
逻辑运算符 | 功能 |
and 或 && | 并且(多个条件同时成立) |
or 或 | | | 或者(多个条件任意一个成立) |
not 或 ! | 非,不是 |
表中多个数据: 类似Java中的case
case 表达式 when 值1 then 结果1 when 值2 then 结果2 ... else ... end
9.3.2 分组查询
group by having
聚合函数
介绍:将一列数据作为一个整体,进行纵向计算
语法:select 聚合函数(字段列表) from 表名;
函数 | 功能 |
count | 统计数量 |
max | 最大值 |
min | 最小值 |
avg | 平均值 |
sum | 求和 |
分组查询:
select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤的条件];
- 根据性别分组,统计员工数量
- select gender, count(*) from tb_emp group by gender;
- where 与 having 区别:
- 执行时机不同:where 是分组之前进行过滤,不满足 where 条件,不参与分组;而 having 是分组之后对结果进行过滤。
- 判断条件不同:where 不能对聚合函数进行判断,而 having 可以。
- 注意事项:分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义。
- 执行顺序: where > 聚合函数 > having
9.3.3 排序查询
order by
select 字段列表 from 表名 [where 条件列表] [group by 分组字段] order by 字段1 排序方式1, 字段2 排序方式2 … ;
ASC:升序(默认)
DESC:降序
- 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序。
9.3.4 分页查询
limit
select 字段列表 from 表名 limit 起始索引, 查询记录数;
- 查询记录数为每一页要展示的数据的条数
- 注意事项:
- 起始索引从 0 开始,起始索引 = (查询页码 - 1)* 每页显示记录数。
- 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是 limit。
- 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 10。
常用函数
if (表达式, tvalue, fvalue); -- 当表达式为true时,取tvalue;当表达式为false时,取fvalue
case expr when value1 then result1 [when value2 then result2 ...] [else result] end -- 类似于switch语句
9.4 多表设计
外键
- 物理外键:
- 概念:使用foreign key定义外键关联另外一张表。
- 缺点:
- 影像增删改的效率(需要检查外键关系)
- 仅用于单节点数据库,不适用于分布式、集群场景
- 容易引发数据库的死锁问题,消耗性能
- -- 创建表时指定 create table 表名( 字段名 数据类型; ... [constraint] [外键名称] foreign key (外键字段名) references 主表(字段名) )
- -- 建完表后,添加外键 alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(字段名);
- 逻辑外键:
- 概念:在业务逻辑中,解决外键关联
- 通过逻辑外键,就可以很方便的解决上述问题
一对多:在多的一方添加外键关联一的一方的主键
-- 部门管理
create table tb_dept(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
-- 员工管理(带约束)
create table tb_emp (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间',
constraint fk_dept foreign key (dept_id) references tb_dept(id)
) comment '员工表';
一对一:
- 案例:用户 与 身份证信息 的关系
- 关系:一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他字段放在另一张表中,以提升效率
- 实现:在任意一方假如外键,关联另外一方的主键,并设置外键为唯一的(UNIQUE)
create table tb_user
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
age int comment '年龄',
gender char(1) comment '1: 男,2:女',
phone char(11) comment '手机号'
) comment '用户基本信息表';
create table tb_user_edu
(
id int auto_increment primary key comment '主键ID',
degree varchar(20) comment '学历',
major varchar(50) comment '专业',
primaryschool varchar(50) comment '小学',
middleschool varchar(50) comment '中学',
university varchar(50) comment '大学',
userid int unique comment '用户ID', -- 外键关联基本信息表的主键(加上了unique唯一约束)constraint fk_userid foreign key (userid) references tb_user (id)
) comment '用户教育信息表';
多对多:
- 案例:学生 与 课程的关系
- 关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择
- 实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
create table student
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';
insert into student
values (null, '黛绮丝', '2000100101'),
(null, '谢逊', '2000100102'),
(null, '韦一笑', '2000200103'),
(null, '殷天正', '2000200103'),
(null, '韦一笑', '2000200104');
create table course
(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';
insert into course
values (null, 'Java'),
(null, 'PHP'),
(null, 'MySQL'),
(null, 'Hadoop');
create table student_course
(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references course (id)
) comment '学生课程中间表';
insert into student_course value (null, 1, 1), (null, 1, 2), (null, 1, 3), (null, 2, 2), (null, 2, 3), (null, 3, 4);
9.5 多表查询
- 多表查询:指从多张表中查询数据
- 笛卡尔积:两个集合的所有组合情况(在多表查询时,需要消除无效的笛卡尔积)
9.5.1 连接查询
- 内连接:相当于查询A、B的交集部分数据
隐式内连接:
select 字段列表 from 表1, 表2 where 条件 ……;
显式内连接:
select 字段列表 from 表1 [inner] join 表2 on 连接条件 ……;
- 外连接:
- 左外连接:查询 左表 所有数据(包含两张表交集部分数据)
- select 字段列表 from 表1 left [outer] join 表2 on 连接条件 ……;
- 右外连接:查询 右表 所有数据(包含两张表交集部分数据)
- select 字段列表 from 表1 right [outer] join 表2 on 连接条件 ……;
9.5.2 子查询
- 介绍:SQL语句中嵌套 select 语句,称为嵌套查询,又称子查询。
- 形式: select * from t1 where column1 = (select column1 from t2 ...);
- 子查询外部的语句可以是 insert / update / delete / select 的任何一个,最常见的是 select
- 分类:
- 标量子查询:子查询返回的结果为单个值。
- 列子查询:子查询返回的结果为一列。
- 行子查询:子查询返回的结果为一行。
- 表子查询:子查询返回的结果为多行多列。
9.6 事务
默认MySQL的事务时自动提交的,也就是说当执行一条DML语句,MySQL会立即隐式的提交事务。
- 开启事务:
- start transaction; begin;
- 提交事务:
- commit;
- 回滚事务:
- rollback;
- 四大特性:
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
9.7 索引
- 介绍:时帮助数据库 高效获取数据的 数据结构
- 优点:
- 提高数据查询的效率,降低数据库的 IO 成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 消耗。
- 缺点:
- 索引会占用存储空间
- 索引大大提高了查询效率,同时也降低了 insert、update、delete 的效率。
- 结构:默认指定 B+Tree
- 创建索引:
- create [unique] index 索引名 on 表名 (字段名, ...);
- 查看索引:
- show index from 表名;
- 删除索引:
- drop index 索引名 on 表名;
- 注意:
- 主键字段,在建表时,会自动创建主键索引。
- 添加唯一约束时,数据库实际上会添加唯一索引。
10. Mybatis
是一款优秀的 持久层 框架,用于简化 JDBC 的开发
官网:https://mybatis.org/mybatis-3/zh/index.html
10.1 入门程序:
- 准备工作(创建 springboot )工程、数据库表User、实体类User
- 数据库表
create table user
( id int unsigned primary key auto_increment comment 'ID',
name varchar(100) comment '姓名',
age tinyint unsigned comment '年龄',
gender tinyint unsigned comment '性别, 1:男, 2:女',
phone varchar(11) comment '手机号'
) comment '用户表';
insert into user(id, name, age, gender, phone)
VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone)
VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone)
VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone)
VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone)
VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone)
VALUES (null,'光明右使',48,'1','18800000005');
@Data @NoArgsConstructor
//无参构造
@AllArgsConstructor
//全参构造
public class User {
private Integer id;
private String name;
private short age;
private short gender;
private String phone;
}
- 引入Mybatis相关依赖,配置Mybatis(数据库连接信息)
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456
- 编写SQL语句
@Mapper
//在运行时,会自动生成该接口的实现类对象(代理对象),并且将该对象交给IOC容器管理
public interface UserMapper {
//查询全部用户信息 @Select("select * from user") public List<User> list();
}
- 单元测试
@SpringBootTest
//springboot整合单元测试的注解
class SpringbootMybatisQuickstartApplicationTests {
@Autowired //依赖注入private UserMapper userMapper; @Test public void testListUser(){
List<User> userList = userMapper.list();
userList.stream().forEach(user -> {
System.out.println(user);
});
}
}
10.2 JDBC
JDBC:使用 Java 语言操作关系型数据库的一套API
- sun 公司官方定义的一套操作所有关系型数据库的规范,即接口
- 各个数据库厂商去实现这套接口,提供数据库驱动 jar 包
- 我们可以使用这套接口 (JDBC)编程,真正执行的代码是驱动 jar 包中的实现类
10.3 数据库连接池
- 数据库连接池是个容器,负责分配、、管理数据库连接( Connection )
- 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
- 释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
- 标准接口:DataSource
- 官方(sun)提供的数据库连接池接口,由第三方组织实现此接口
- 功能:获取连接
- Connection getConnection() throws SQLException;
- 常见产品:
- C3P0、DBCP、Druid、Hikari
- Druid(德鲁伊)
- Druid连接池是阿里巴巴开源的数据库连接池项目
- 功能强大,性能优秀,是Java语言最好的数据库连接池之一
- 切换:
<dependency><groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
10.4 lombok
Lombok是一个实用的Java类库,能通过注解的形式自动生成构造器、getter / setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化Java开发,提高效率
注解 | 作用 |
@Getter / @Setter | 为所有的属性提供 get / set 方法 |
@ToString | 会给类自动生成易阅读的 toString 方法 |
@EqualsAndHashCode | 根据类拥有的非静态字段自动重写 equals 方法和 hashCode 方法 |
@Data | 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @NoArgsConstructor) |
@NoArgsConstructor | 为实体类生成无参的构造器方法 |
@AllArgsConstructor | 为实体类生成除了 static 修饰的字段之外带有各参数的构造器方法 |
注意:Lombok 会再编译时自动生成 Java 代码。我们使用Lombok时,还需安装一个 lombok 的插件(idea自带)
10.5 基础操作
删除
根据主键删除:
- sql 语句
delete from emp where id = 17;
- 接口方法
//根据ID删除数据
@Delete("delete from emp where id = #{id}") //可以使用$代替#,#能预防sql注入
//根据传输进来的id动态删除表中的内容
public void delete(Integer id);//有返回值,返回值代表影响操作的记录数
- 注意:如果 mapper 接口方法形参只有一个普通类型的参数,# {...} 里面的属性名可以随便写,如:#{id}、#{value}
日志输出
- 可以再application.properies中,打开mybatis的日志,并指定输出到控制台
#配置mybatis的日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 预编译SQL优势:
- 性能更高
- 更安全(防止SQL注入)
新增
SQL语句:
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})
接口方法:
@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
"values (#{username}, #{name}, #{gender},#{image},#{job},#{entrydate},#{deptId}, #{createTime},#{updateTime})")
public void insert(Emp emp);
主键返回:
描述:在数据添加成功后,需要获取插入数据库数据的主键
@Options(useGeneratedKeys = true, keyProperty = "id") //会自动将生成的主键值,赋值给emp对象的id属性@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +"values (#{username}, #{name}, #{gender},#{image},#{job},#{entrydate},#{deptId}, #{createTime},#{updateTime})")
public void insert(Emp emp);
更新
接口方法
@Update("update emp set username = #{username}, name = #{name}, gender = #{gender}, image = #{image}, job = #{job}, entrydate = #{entrydate},dept_id = #{deptId}, update_time = #{updateTime} where id = #{id}")
public void update(Emp emp);
查询
@Select("select * from emp where id = #{id}")public Emp getById(Integer id);
数据封装
- 实体类属性名 和 数据库表查询返回的字段名一致,mybatis 会自动封装
- 如果实体类属性名 和 数据库表查询返回的字段名不一致,不能自动封装
- 起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样
@Select("select id, username, password, name, gender, image, job, entrydate, " +
"dept_id deptId, create_time createTime, update_time updateTime from emp where id = #{id}")
public Emp getById(Integer id)
- 手动结果映射:通过@Results及@Result进行手动结果映射
@Results({
@Result(column = "dept_id", property = "deptId"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")
})
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);
- 开启驼峰命名:如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射
- 需要严格的遵守数据库中的字段名是下划线分隔,实体类中的变量名是驼峰命名
- #开启mybatis的驼峰命名自动映射开关 mybatis.configuration.map-underscore-to-camel-case=true
条件查询
- 接口方法
//条件查询员工
@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
- 改进:
//条件查询员工
@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);
10.6 XML映射文件
- 规范:
- XML映射文件的名称和 Mapper 接口名称一致,并且将 XML 映射文件和 Mapper 接口放在相同包下(同包同名)
- XML映射文件的 namespace 属性为 Mapper 接口全限定名一致
- XML映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致,并保持返回类型一致
Mapper接口:
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
XML 映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper"><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>
使用 Mybatis 的注解,主要是来完成一些简单的增删改查功能,如果需要实现复杂的SQL功能,建议使用XML来排至映射的语句
官方说明:https://mybatis.net.cn/getting-started.html
10.7 动态 SQL
随着用户的输入或外部条件变化而变化的 SQL 语句
10.7.1 if
<if>
:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接
<where>
:where 元素只会在子标签有内容的情况下才插入 where 子句。而且会自动去除子句的开头的 and 或 or
<set>
:动态的在行首插入 SET 关键字,并会删掉额外的逗号(用在 update 语句中)
<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>
案例:完善更新员工功能,修改为动态更新员工数据信息
<!--动态更新员工信息-->
<update id="update2">
update emp
<set>
<if test="username != null">
username = #{username},
</if>
<if test="name != null">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="image">
image = #{image},
</if>
<if test="job != null">
job = #{job},
</if>
<if test="entrydate">
entrydate = #{entrydate},
</if>
<if test="deptId != null">
dept_id = #{deptId},
</if>
<if test="updateTime">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>
10.7.2 foreach
- 属性:
- collection:要遍历的集合
- item:遍历出来的元素
- separator:遍历出来元素的分隔符
- open:遍历开始前拼接的SQL片段
- close:遍历结束后拼接的SQL片段
- 接口方法:
//批量删除员工public void deleteByIds(List<Integer> ids);
- XML 映射文件
<delete id="deleteByIds">
delete
from emp
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
10.7.3 sql、include
<sql>
:定义可重用的 SQL 片段
<include>
:通过属性 refid,指定包含的 sql 片段
<sql id="commonSelect">
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
from emp
</sql>
<select id="list" resultType="com.itheima.pojo.Emp">
<include refid="commonSelect"/>
<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>
11. SpringBootWeb案例
11.1 准备工作
环境搭建
- 准备数据库表(dept、emp)
- 创建 springboot 工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)
- 配置文件 application.properties 中引入 mybatis 的配置信息,准备对应的实体类
- 准备对应的 Mapper、Service(接口、实现类)、Controller 基础结构
开发规范
- 案例基于当前最为主流的前后端分离开发模式
- 开发者规范--Restful:
- REST,表属性状态转换,它是一种软件架构风格
https://localhost:8080/users/1 get:查询id为1的用户
https://localhost:8080/users post:新增用户
https://localhost:8080/users put:修改用户
https://localhost:8080/users/1 delete:删除id为1的用户
- 开发流程:
- 查看页面原型明确需求
- 阅读接口文档
- 思路分析
- 接口开发
- 接口测试
- 前后端联调
11.2 部门管理
查询部门
- 前端发送请求访问到 Controller方法
@Slf4j //可以直接调用log下面的info方法来记录日志
@RestController
public class DeptController {
@Autowired //注入service对象private DeptService deptService;
//@RequestMapping(value = "/depts", method = RequestMethod.GET)@GetMapping("/depts") /*限定请求方式为 get ,post请求调用 @PostMapping*/public Result list(){
log.info("查询全部部门数据");
//调用service查询部门数据
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
}
- 方法中调用 Service 获取数据,在 Service 方法中调用 mapper 接口中的方法来查询全部的部门信息
@Service
public class DeptServiceImpl implements DeptService {
@Autowiredprivate DeptMapper deptMapper;
@Overridepublic List<Dept> list() {
return deptMapper.list();
}
}
- mapper 接口会像数据库发送 sql 语句查询全部的部门,并把查询的信息封装到 List 集合中
@Mapper
public interface DeptMapper {
/**
* 查询全部部门数据
* @return
*/@Select("select * from dept")
List<Dept> list();
}
- 最终返回给Service,Service拿到后再返回给Controller,Controller拿到后再返回给前端
前后端联调
- 将nginx解压放到没无中文无空格的目录下(develop目录下)
- 启动 nginx(打开点击nginx.exe),访问测试:http://localhost:90
删除部门
- 一个完整的请求路径,应该是类上的 @RequestMapping 的 value 属性 + 方法上的 @RequestMapping 的 value 属性
/**
* 部门管理Controller
*/
@Slf4j //可以直接调用log下面的info方法来记录日志
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired //注入service对象private DeptService deptService;
/**
* 查询部门数据
* @return Result
*///@RequestMapping(value = "/depts", method = RequestMethod.GET)@GetMapping /*限定请求方式为 get ,post请求调用 @PostMapping*/public Result list(){
log.info("查询全部部门数据");
//调用service查询部门数据List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
/**
* 删除部门
* @return Result
*/@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门:{}",id);
//调用service删除部门
deptService.delete(id);
return Result.success();
}
/**
* 新增部门
* @return Result
*/@PostMappingpublic Result add(@RequestBody Dept dept){
log.info("新增部门:{}", dept);
deptService.add(dept);
return Result.success();
}
}
11.3 员工管理
- 分页查询
- 请求参数:页码、每页展示的记录数
- 响应结果:总记录数、结果列表(PageBean)
- 注解:
@RequestParam(defaultValue="1") //设置请求参数默认值
- 分页插件
//引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
PageHelper.startPage(pageNum, pageSize);
List<Emp> list = empMapper.list();
Page<Emp> page = (Page<Emp>)list;
- 条件分页查询:
- 条件查询:动态SQL - XML 映射文件
- 分页查询:PageHelper 分页插件
@Slf4j
@RestController
public class EmpController {
@Autowiredprivate EmpService empService;
@GetMapping("/emps")public Result page(@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){
//@RequestParam:设置默认值,如果前端没设置,默认值是1
log.info("分页查询,参数:{},{},{},{},{},{}",page, pageSize, name, gender, begin, end);
//调用service分页查询
PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end);
return Result.success(pageBean);
}
}
public interface EmpService {
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
PageBean page(Integer page, Integer pageSize,String name, Short gender, LocalDate begin, LocalDate end);
}
@Service
public class EmpServiceImpl implements EmpService {
@Autowiredprivate EmpMapper empMapper;
@Overridepublic PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
//设置分页参数
PageHelper.startPage(page, pageSize);
//执行查询
List<Emp> empList = empMapper.list(name, gender, begin, end);
Page<Emp> p = (Page<Emp>) empList;
//封装PageBean对象
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
}
@Mapper
public interface EmpMapper {
/**
* 查询总记录数
* @return long
*/public List<Emp> list(@Param("name") String name, @Param("gender") Short gender, @Param("begin") LocalDate begin, @Param("end") LocalDate end);
}
<mapper namespace="com.example.mapper.EmpMapper">
<!--条件查询-->
<select id="list" resultType="com.example.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>
删除员工
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){
log.info("批量删除,ids:{}", ids);
empService.delete(ids);
return Result.success();
}
void delete(List<Integer> ids);
@Overridepublic void delete(List<Integer> ids) {
empMapper.delete(ids);
}
void delete(List<Integer> ids);
<!--批量删除 (1, 2, 3)--><delete id="delete">
delete
from emp
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach></delete>
新增员工
@PostMapping
public Result save(@RequestBody Emp emp){
log.info("新增员工, emp:{}", emp);
empService.save(emp);
return Result.success();
}
@Override
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
}
@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})")
void insert(Emp emp);
文件上传
- 简介
- 文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
- 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传
- 前端页面三要素:
- 表单项 type = "file"
- 表单提交方式 post
- 表单的 enctype 属性 multipart / formm-data
- 服务端接收文件:
- MultipartFile
@Slf4j
@RestController
public class UploadController {
public Result upload(String username, Integer age, MultipartFile image){
log.info("文件上传:{}, {}, {}", username,age, image);
return Result.success();
}
}
- 本地存储:
- 在服务端,接收到上传来的文件之后,将文件存储在本地服务器磁盘中。
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile image) throws Exception {
log.info("文件上传:{}, {}, {}", username,age, image);
//获取原始文件名String originalFilename = image.getOriginalFilename();
//构造唯一的文件名(不能重复) -- uuid(通用唯一识别码)int index = originalFilename.lastIndexOf("."); //用 . 分割String extname = originalFilename.substring(index); //截取后缀名String newFileName = UUID.randomUUID().toString() + extname; //获取新的字符串作为name
log.info("新的文件名:{}", newFileName);
/*
String newFileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
*///将文件存储在服务器的磁盘目录中
image.transferTo(new File("D:\\DeliveryOptimization\\Cache" + newFileName));
return Result.success();
}
}
- 在Springboot中,文件上传默认单个文件允许最大大小为1MB,如果需要大文件的上传,可以进行如下配置:
#配置单个文件上传大小的限制
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大大小的限制(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
阿里云
- 阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商
- 对象云存储OSS
- 注册阿里云(实名认证)
- 充值
- 开通对象存储服务(OSS)
- 创建bucket
- Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间
- 获取 AccessKey(密钥)
- 参照官方SDK编写入门程序
- SDK:软件开发工具包,包括副宗主软件开发的依赖(jar包)、代码示例等,都可以叫做SDK
- 案例集成OSS
修改员工
查询回显
@GetMapping("/{id}") //id为路径参数public Result getById(@PathVariable Integer id) {
log.info("根据id查询员工信息,id: {}", id);
Emp emp = empService.getById(id);
return Result.success();
}
@Overridepublic Emp getById(Integer id) {return empMapper.getById(id);
}
@Select("select * from emp where id = #{id}")
Emp getById(Integer id);
修改员工
@PutMapping
public Result update(@RequestBody Emp emp){
//json格式数据想封装到实体类中,需要加@RequestBody注解
log.info("更新员工信息:{}", emp);
empService.update(emp);
return Result.success();
}
@Override
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}
<!--更新员工-->
<update id="update">
update emp
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="image != null and image != ''">
image = #{image},
</if>
<if test="job != null">
job = #{job},
</if>
<if test="entrydate != null">
entrydate = #{entrydate},
</if>
<if test="deptId != null">
dept_id = #{deptId},
</if>
<if test="updateTime != null">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>
11.4 配置文件
参数配置化
- @Value 注解通常用于外部配置的属性注入,具体用法为:@Value("${配置文件中的key}")
aliyun.oss.endpoint=https://oss-cn-hhangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias
@Component
public class AliOSSUtils {
@Value("${}")private String endpoint;
@Value("${}")private String accessKeyId;
@Value("${}")private String accessKeySecret;
@Value("${}")private String bucketName;
}
yml配置文件
- 常见配置文件格式对比
- XML:
<server><port>8080</port>
<address>127.0.0.1</address>
</server>
server.port=8080 server.address=127.0.0.1
server: port: 8080 address: 127.0.0.1
- yml 基本语法:
- 大小写敏感
- 数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用 Tab 键,只能用空格(idea 中会自动将 Tab 转换为空格)
- 缩进的空格数目不重要,只要相同层级的元素左侧对其即可
- # 表示注释,从这个字符一致到行尾,都会被解析器忽略
- 对象 / Map 集合:
- user: name: Tom age: 20 address: beijing
- 数组 / List / Set 集合:
- hobby: - java - C - game - sport
spring:#数据库连接信息的配置datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/tliasusername: rootpassword: 123456
# 文件上传servlet:multipart:max-file-size: 10MBmax-request-size: 100MB
#Mybatis配置
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true
@ConfigurationProperties
- 和
@Value
相同点: - 都是用来注入外部配置的属性的
- 不同点:
- @Value注解只能一个一个的进行外部属性的注入
- @ConfigurationProperties 可以批量的将外部的属性配置注入到 bean 对象的属性中
11.5 登录认证
@Slf4j
@RestController
public class LoginController {
@Autowiredprivate EmpService empService;
@PostMapping("/login")public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
11.6 登录校验
- 在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。
- 登录标记:
- 用户登录成功之后,每一次请求中,都可以获取到该标记
- 统一拦截:
- 过滤器:Filter
- 拦截器:Interceptor
11.6.1 会话技术
- 会话:用户打开浏览器,访问 web 服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含 多次 请求和响应
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求之间 共享数据
- 会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
Cookie
- 优点:HTTP 协议中支持的技术
- 缺点:
- 移动端 APP 无法使用 Cookie
- 不安全,用户可以自己禁用Cookie
- Cookie 不能跨域
Session
- 优点:存储在服务端,安全
- 缺点:
- 服务器集群环境下无法直接使用 Session
- Cookie 的缺点
令牌技术(主流方案)
- 优点:
- 支持 PC 端、移动端
- 解决集群环境下的认证问题
- 减轻服务器端存储压力
- 缺点:
- 需要自己实现
11.6.2 JWT令牌
- 定义了一种简洁的、自包含的格式,用于在通信双方以 json 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
- 组成:
- 第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256", "type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1", "username":"Tom"}
- 第三部分:Signature(签名),防止 Token 被篡改、确保安全性。将 header、payload,并加入指定密钥,通过指定签名算法计算而来。
- 场景:登录认证
- 登录成功后,生成令牌
- 后续每个请求,都要携带 JWT 令牌,系统在每次请求处理之前,先校验令牌,通过后再处理
对应依赖:
<!--JWP令牌--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
生成:
/**
* 生成JWT
*/@Testpublic void testGenJWT(){
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "Tom");
String jwt = Jwts.builder() //构建Jwt令牌
.signWith(SignatureAlgorithm.HS256, "example")//签名算法
.setClaims(claims)//设置自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为 1h
.compact();
System.out.println(jwt);
}
解析:
/**
* 校验JWT
*/
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("example") //指定签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTY5MDAyOTI4Nn0.bxU-aSO5VpOAi7U56Uz2jazLSzj9cu0E-MSE8VkKbSo") //解析令牌
.getBody();
System.out.println(claims);
}
- 注意事项:
- JWT 校验时使用的签名密钥,必须和生成 JWT 令牌时使用的密钥是配套的
- 如果 JWT 令牌解析校验时报错,则说明 JWT 令牌被篡改 或 失效了,令牌非法。
- 思路:
- 令牌生成:登录成功后,生成JWT令牌,并返回给前端
- 令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验
- 步骤
- 引入JWT令牌操作工具类
- 登录完成后,调用工具类生成JWT令牌,并返回
@Slf4j
@RestController
public class LoginController {
@Autowiredprivate EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
/*登录成功,生成令牌并下发令牌*/if (e != null){
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("name", e.getName());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims);//jwt包含了当前登录的员工信息return Result.success(jwt);
}
/*登录失败,返回错误信息*/return Result.error("用户名或密码错误");
}
}
11.6.3 过滤器 Filter
- 概念:Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一
- 过滤器可以把对资源的请求 拦截 下来,从而实现一些特殊的功能
- 过滤器一般完成一些 通用 的操作,比如:登录校验、统一编码处理、敏感字符处理等。
定义 Filter:定义一个类,实现 Filter 接口,并重写其所有方法
配置 Filter:Filter 类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启 Servlet 组件支持
@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("拦截");
//放行
chain.doFilter(request, response);
}
@Override //销毁方法,只调用一次public void destroy() {
System.out.println("init销毁");
}
}
启动类中:
@ServletComponentScan //Filter是JavaWeb三大组件之一,想在springboot上使用JavaWeb组件必须使用注解
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
执行流程:
请求 ----> 放行前逻辑 --->放行 --> 资源 --->放行后逻辑
拦截路径
Filter 可以根据需求,配置不同的拦截资源路径:
@WebFilter(urlPatterns = "/depts")
public class DemoFilter implements Filter {
}
拦截路径 | urlPatterns | 含义 |
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问 /emps 下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
过滤器链
- 介绍:一个 web 应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
- 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序
登录校验
- 步骤:
- 获取请求url
- 判断请求url中是否包含 login, 如果包含,说明是登录操作,放行。
- 获取请求头中的令牌(token)。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析 token,如果解析失败,返回错误结果(未登录)
- 放行
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
/*获取请求url*/String url = request.getRequestURI().toString();
log.info("请求的url:{}", url);
/*判断请求url是否包含login,如果包含,说明是登录操作,放行*/if (url.contains("login")) { //如果包含了login关键字是登录请求
log.info("登录操作,放行……");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
/*获取请求头中的令牌(token)*/String jwt = request.getHeader("token");
/*判断令牌是否存在,如果不存在,返回错误结果(未登录)*/if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象 -->json ---------阿里巴巴fastJSONString notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
}
/*解析token,如果解析失败,返回错误结果(未登录)*/try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象 -->json ---------阿里巴巴fastJSONString notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return;
}
/*放行*/
log.info("令牌合法");
filterChain. doFilter(servletRequest, servletResponse);
}
}
11.6.4 拦截器 Interceptor
入门
- 概念:是一种动态拦截方法调用的机制,类似于过滤器,Spring 框架中提供的,用来动态拦截控制器方法的执行。
- 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true 放行;false 不放行public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle……");
return true;
}
//Controller方法运行
@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 {
@Autowiredprivate LoginCheckInterceptor loginCheckInterceptor;
@Overridepublic void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//拦截所有资源
}
}
详解
- 拦截器可以根据需求,配置不同的拦截路径:
@Overridepublic void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
// 需要拦截哪些资源 不需要拦截哪些资源
}
拦截路径 | 含义 | 举例 |
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配 /depts,/depts/1,/depts/1/2 |
/depts/* | /depts 下的一级路径 | 能匹配 /depts/1,不能匹配 /depts/1/2,/depts |
/depts/** | /depts 下的任意级路径 | 能匹配 /depts,/depts/1,/depts/1/2,不能匹配 /emps/1 |
- Filter 与 Interceptor
- 接口规范不同:过滤器需要实现 Filter 接口,而拦截器需要实现 HandlerInterceptor 接口
- 拦截范围不同:过滤器 Filter 会拦截所有的资源,而 Interceptor 只会拦截 Spring 环境中的资源
登录校验 - Interceptor
- 步骤:
- 获取请求url
- 判断请求url中是否包含 login, 如果包含,说明是登录操作,放行。
- 获取请求头中的令牌(token)。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析 token,如果解析失败,返回错误结果(未登录)
- 放行
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true 放行;false 不放行public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {/*获取请求url*/String url = req.getRequestURI().toString();
log.info("请求的url:{}", url);
/*判断请求url是否包含login,如果包含,说明是登录操作,放行*/if (url.contains("login")) { //如果包含了login关键字是登录请求
log.info("登录操作,放行……");
return true;
}
/*获取请求头中的令牌(token)*/String jwt = req.getHeader("token");
/*判断令牌是否存在,如果不存在,返回错误结果(未登录)*/if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象 -->json ---------阿里巴巴fastJSONString notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
/*解析token,如果解析失败,返回错误结果(未登录)*/try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象 -->json ---------阿里巴巴fastJSONString notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
/*放行*/
log.info("令牌合法");
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 ...");
}
}
11.7 异常处理
- 全局异常处理器:
@RestControllerAdvice
@ExceptionHandler(Exception.class) //捕获所有异常
系列文章
嘎嘎基础的JavaWeb(中)