嘎嘎基础的JavaWeb(中)

2024-01-10 23:05:56 浏览数 (1)

9. MySQL

  • 数据库设计:
    • MySQL 概述
    • 数据库设计 - DDL
    • 多表设计
  • 数据库操作:
    • 数据库操作 - DML
    • 数据库操作 - DQL
    • 事务
    • 多表查询
  • 数据库优化
    • 索引
    • SQL 优化
    • 分库分表

企业开发使用方式:

代码语言: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

代码语言:sql复制
case 表达式 when 值1 then 结果1 when 值2 then 结果2 ... else ... end

9.3.2 分组查询

group by having

聚合函数

介绍:将一列数据作为一个整体,进行纵向计算

语法:select 聚合函数(字段列表) from 表名;

函数

功能

count

统计数量

max

最大值

min

最小值

avg

平均值

sum

求和


分组查询:

代码语言:sql复制
select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤的条件];
  • 根据性别分组,统计员工数量
    • select gender, count(*) from tb_emp group by gender;
  • where 与 having 区别:
    1. 执行时机不同:where 是分组之前进行过滤,不满足 where 条件,不参与分组;而 having 是分组之后对结果进行过滤。
    2. 判断条件不同:where 不能对聚合函数进行判断,而 having 可以。
  • 注意事项:
    1. 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义。
    2. 执行顺序: where > 聚合函数 > having


9.3.3 排序查询

order by

代码语言:sql复制
select 字段列表 from 表名 [where 条件列表] [group by 分组字段] order by 字段1 排序方式1, 字段2 排序方式2 … ;

ASC:升序(默认)

DESC:降序

  • 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序。

9.3.4 分页查询

limit

代码语言:sql复制
select 字段列表 from 表名 limit 起始索引, 查询记录数;
  • 查询记录数为每一页要展示的数据的条数
  • 注意事项:
    1. 起始索引从 0 开始,起始索引 = (查询页码 - 1)* 每页显示记录数。
    2. 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是 limit。
    3. 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 10。

常用函数

代码语言:sql复制
if (表达式, tvalue, fvalue);  -- 当表达式为true时,取tvalue;当表达式为false时,取fvalue
代码语言:sql复制
case expr when value1 then result1 [when value2 then result2 ...] [else result] end     -- 类似于switch语句

9.4 多表设计

外键

  • 物理外键:
    • 概念:使用foreign key定义外键关联另外一张表。
    • 缺点:
      1. 影像增删改的效率(需要检查外键关系)
      2. 仅用于单节点数据库,不适用于分布式、集群场景
      3. 容易引发数据库的死锁问题,消耗性能
    • -- 创建表时指定 create table 表名( 字段名 数据类型; ... [constraint] [外键名称] foreign key (外键字段名) references 主表(字段名) )
    • -- 建完表后,添加外键 alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(字段名);
  • 逻辑外键:
    • 概念:在业务逻辑中,解决外键关联
    • 通过逻辑外键,就可以很方便的解决上述问题

一对多:在多的一方添加外键关联一的一方的主键

代码语言:sql复制
-- 部门管理
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)
代码语言:sql复制
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 '用户教育信息表';

多对多:

  • 案例:学生 与 课程的关系
  • 关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择
  • 实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
代码语言:sql复制
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的交集部分数据

隐式内连接:

代码语言:sql复制
select 字段列表 from 表1, 表2 where 条件 ……;

显式内连接:

代码语言:sql复制
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
  • 分类:
    1. 标量子查询:子查询返回的结果为单个值。
    2. 列子查询:子查询返回的结果为一列。
    3. 行子查询:子查询返回的结果为一行。
    4. 表子查询:子查询返回的结果为多行多列。

9.6 事务

默认MySQL的事务时自动提交的,也就是说当执行一条DML语句,MySQL会立即隐式的提交事务。

  • 开启事务:
    • start transaction; begin;
  • 提交事务:
    • commit;
  • 回滚事务:
    • rollback;
  • 四大特性:
    1. 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
    2. 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
    3. 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
    4. 持久性(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 入门程序:

  1. 准备工作(创建 springboot )工程、数据库表User、实体类User
    • 数据库表
代码语言:sql复制
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');
代码语言:java复制
@Data @NoArgsConstructor     
//无参构造 
@AllArgsConstructor    
//全参构造 
public class User {
     private Integer id;     
     private String name;     
     private short age;     
     private short gender;     
     private String phone; 
}
  1. 引入Mybatis相关依赖,配置Mybatis(数据库连接信息)
代码语言:yml复制
#驱动类名称 
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语句
代码语言:java复制
@Mapper   
//在运行时,会自动生成该接口的实现类对象(代理对象),并且将该对象交给IOC容器管理 
public interface UserMapper {
     //查询全部用户信息     
     @Select("select * from user")     
     public List<User> list(); 
}

  • 单元测试
代码语言:java复制
@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语言最好的数据库连接池之一
  • 切换:
代码语言:xml复制
<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 语句
代码语言:sql复制
delete from emp where id = 17;
  • 接口方法
代码语言:java复制
//根据ID删除数据
@Delete("delete from emp where id = #{id}")     //可以使用$代替#,#能预防sql注入
//根据传输进来的id动态删除表中的内容
public void delete(Integer id);//有返回值,返回值代表影响操作的记录数
  • 注意:如果 mapper 接口方法形参只有一个普通类型的参数,# {...} 里面的属性名可以随便写,如:#{id}、#{value}

日志输出

  • 可以再application.properies中,打开mybatis的日志,并指定输出到控制台
代码语言:yml复制
#配置mybatis的日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
  • 预编译SQL优势:
    • 性能更高
    • 更安全(防止SQL注入)

新增

SQL语句:

代码语言:javascript复制
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})

接口方法:

代码语言:javascript复制
    @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);

主键返回:

描述:在数据添加成功后,需要获取插入数据库数据的主键

代码语言:javascript复制
@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);

更新

接口方法

代码语言:javascript复制
@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);

查询

代码语言:javascript复制
@Select("select * from emp where id = #{id}")
    public Emp getById(Integer id);

数据封装

  • 实体类属性名 和 数据库表查询返回的字段名一致,mybatis 会自动封装
  • 如果实体类属性名 和 数据库表查询返回的字段名不一致,不能自动封装
  • 起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样
代码语言:java复制
@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进行手动结果映射
代码语言:java复制
@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

条件查询

  • 接口方法
代码语言:sql复制
//条件查询员工     
@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
  • 改进:
代码语言:java复制
//条件查询员工     
@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接口:

代码语言:javascript复制
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

XML 映射文件:

代码语言:javascript复制
<?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">
    <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 语句中)

代码语言:javascript复制
<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>

案例:完善更新员工功能,修改为动态更新员工数据信息

代码语言:javascript复制
<!--动态更新员工信息-->
    <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片段
  • 接口方法:
代码语言:java复制
//批量删除员工
    public void deleteByIds(List<Integer> ids);
  • XML 映射文件
代码语言: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 片段

代码语言:javascript复制
<sql id="commonSelect">
    select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
    from emp
</sql>
代码语言:javascript复制
<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,表属性状态转换,它是一种软件架构风格
代码语言:xml复制
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的用户
  • 开发流程:
    1. 查看页面原型明确需求
    2. 阅读接口文档
    3. 思路分析
    4. 接口开发
    5. 接口测试
    6. 前后端联调

11.2 部门管理

查询部门

  • 前端发送请求访问到 Controller方法
代码语言:java复制
@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 接口中的方法来查询全部的部门信息
代码语言:java复制
@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;
    @Override
    public List<Dept> list() {
        return deptMapper.list();
    }
}
  • mapper 接口会像数据库发送 sql 语句查询全部的部门,并把查询的信息封装到 List 集合中
代码语言:java复制
@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 属性
代码语言:java复制
/**
 * 部门管理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
     */
    @PostMapping
    public Result add(@RequestBody Dept dept){
        log.info("新增部门:{}", dept);
        deptService.add(dept);
        return Result.success();
    }
}

11.3 员工管理

  • 分页查询
    • 请求参数:页码、每页展示的记录数
    • 响应结果:总记录数、结果列表(PageBean)
  • 注解:
代码语言:java复制
@RequestParam(defaultValue="1")       //设置请求参数默认值
  • 分页插件
代码语言:xml复制
//引入依赖
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.4.6</version>
</dependency>
代码语言:java复制
PageHelper.startPage(pageNum, pageSize);
List<Emp> list = empMapper.list();
Page<Emp> page = (Page<Emp>)list;
  • 条件分页查询:
    • 条件查询:动态SQL - XML 映射文件
    • 分页查询:PageHelper 分页插件
代码语言:javascript复制
@Slf4j
@RestController
public class EmpController {
​
    @Autowired
    private 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);
    }
}
代码语言:javascript复制
public interface EmpService {
    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    PageBean page(Integer page, Integer pageSize,String name, Short gender, LocalDate begin, LocalDate end);
}
代码语言:javascript复制
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;
    @Override
    public 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;
    }
}
代码语言:javascript复制
@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);
}
代码语言:javascript复制
<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>

删除员工

代码语言:javascript复制
@DeleteMapping("/{ids}")
    public Result delete(@PathVariable List<Integer> ids){
        log.info("批量删除,ids:{}", ids);
        empService.delete(ids);
        return Result.success();
    }
代码语言:javascript复制
void delete(List<Integer> ids);
代码语言:javascript复制
@Override
    public void delete(List<Integer> ids) {
        empMapper.delete(ids);
    }
代码语言:javascript复制
void delete(List<Integer> ids);
代码语言:javascript复制
<!--批量删除 (1, 2, 3)-->
    <delete id="delete">
        delete
        from emp
        where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

新增员工

代码语言:javascript复制
@PostMapping
    public Result save(@RequestBody Emp emp){
        log.info("新增员工, emp:{}", emp);
        empService.save(emp);
        return Result.success();
    }
代码语言:javascript复制
@Override
    public void save(Emp emp) {
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.insert(emp);
    }
代码语言:javascript复制
@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);

文件上传

  • 简介
    • 文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
    • 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传
  • 前端页面三要素:
    1. 表单项 type = "file"
    2. 表单提交方式 post
    3. 表单的 enctype 属性 multipart / formm-data
  • 服务端接收文件:
    • MultipartFile
代码语言:javascript复制
@Slf4j
@RestController
public class UploadController {
    public Result upload(String username, Integer age, MultipartFile image){
        log.info("文件上传:{}, {}, {}", username,age, image);
        return Result.success();
    }
}

  • 本地存储:
    • 在服务端,接收到上传来的文件之后,将文件存储在本地服务器磁盘中。
代码语言:javascript复制
@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,如果需要大文件的上传,可以进行如下配置:
代码语言:yml复制
#配置单个文件上传大小的限制 
spring.servlet.multipart.max-file-size=10MB ​ 
#配置单个请求最大大小的限制(一次请求可以上传多个文件) 
spring.servlet.multipart.max-request-size=100MB


阿里云

  • 阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商
  • 对象云存储OSS
    1. 注册阿里云(实名认证)
    2. 充值
    3. 开通对象存储服务(OSS)
    4. 创建bucket
      • Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间
    5. 获取 AccessKey(密钥)
    6. 参照官方SDK编写入门程序
      • SDK:软件开发工具包,包括副宗主软件开发的依赖(jar包)、代码示例等,都可以叫做SDK
    7. 案例集成OSS

修改员工

查询回显

代码语言:javascript复制
@GetMapping("/{id}")   //id为路径参数
    public Result getById(@PathVariable Integer id) {
        log.info("根据id查询员工信息,id: {}", id);
        Emp emp = empService.getById(id);
        return Result.success();
    }
代码语言:javascript复制
@Override
    public Emp getById(Integer id) {
        return empMapper.getById(id);
    }
代码语言:javascript复制
@Select("select * from emp where id = #{id}")
    Emp getById(Integer id);

修改员工

代码语言:javascript复制
@PutMapping
    public Result update(@RequestBody Emp emp){
        //json格式数据想封装到实体类中,需要加@RequestBody注解
        log.info("更新员工信息:{}", emp);
        empService.update(emp);
        return Result.success();
    }
代码语言:javascript复制
@Override
    public void update(Emp emp) {
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.update(emp);
    }
代码语言:javascript复制
<!--更新员工-->
    <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}")
代码语言:javascript复制
aliyun.oss.endpoint=https://oss-cn-hhangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias
代码语言:javascript复制
@Component
public class AliOSSUtils {
    @Value("${}")
    private String endpoint;
    @Value("${}")
    private String accessKeyId;
    @Value("${}")
    private String accessKeySecret;
    @Value("${}")
    private String bucketName;
}

yml配置文件

  • 常见配置文件格式对比
    • XML:
代码语言:xml复制
<server>
     <port>8080</port>     
     <address>127.0.0.1</address> 
</server>
代码语言:xml复制
server.port=8080 server.address=127.0.0.1
代码语言:xml复制
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
代码语言:javascript复制
spring:
  #数据库连接信息的配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/tlias
    username: root
    password: 123456
#    文件上传
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
#Mybatis配置
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

@ConfigurationProperties

  • @Value相同点:
    • 都是用来注入外部配置的属性的
  • 不同点:
    • @Value注解只能一个一个的进行外部属性的注入
    • @ConfigurationProperties 可以批量的将外部的属性配置注入到 bean 对象的属性中

11.5 登录认证

代码语言:javascript复制
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private 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("用户名或密码错误");
    }
}
代码语言:javascript复制
@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,并加入指定密钥,通过指定签名算法计算而来。
  • 场景:登录认证
    1. 登录成功后,生成令牌
    2. 后续每个请求,都要携带 JWT 令牌,系统在每次请求处理之前,先校验令牌,通过后再处理

对应依赖:

代码语言:javascript复制
<!--JWP令牌-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

生成:

代码语言:javascript复制
    /**
     * 生成JWT
     */
    @Test
    public 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);
    }

解析:

代码语言:javascript复制
    /**
     * 校验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令牌,并返回
代码语言:javascript复制
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private 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 组件支持

代码语言:javascript复制
@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销毁");
    }
}

启动类中:

代码语言:javascript复制
@ServletComponentScan    //Filter是JavaWeb三大组件之一,想在springboot上使用JavaWeb组件必须使用注解
@SpringBootApplication
public class TliasWebManagementApplication {

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

}

执行流程:

请求 ----> 放行前逻辑 --->放行 --> 资源 --->放行后逻辑


拦截路径

Filter 可以根据需求,配置不同的拦截资源路径:

代码语言:javascript复制
@WebFilter(urlPatterns = "/depts")
public class DemoFilter implements Filter {

}

拦截路径

urlPatterns

含义

拦截具体路径

/login

只有访问 /login 路径时,才会被拦截

目录拦截

/emps/*

访问 /emps 下的所有资源,都会被拦截

拦截所有

/*

访问所有资源,都会被拦截


过滤器链

  • 介绍:一个 web 应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
  • 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序

登录校验

  • 步骤:
    1. 获取请求url
    2. 判断请求url中是否包含 login, 如果包含,说明是登录操作,放行。
    3. 获取请求头中的令牌(token)。
    4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
    5. 解析 token,如果解析失败,返回错误结果(未登录)
    6. 放行
代码语言:javascript复制
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public 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     ---------阿里巴巴fastJSON
            String 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     ---------阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }
        /*放行*/
        log.info("令牌合法");
        filterChain. doFilter(servletRequest, servletResponse);
    }
}

11.6.4 拦截器 Interceptor

入门

  • 概念:是一种动态拦截方法调用的机制,类似于过滤器,Spring 框架中提供的,用来动态拦截控制器方法的执行。
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
代码语言:javascript复制
@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 ...");;
    }
}
代码语言:javascript复制
@Configuration   //配置类
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//拦截所有资源
    }
}

详解

  • 拦截器可以根据需求,配置不同的拦截路径:
代码语言:java复制
@Override
    public 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

  • 步骤:
    1. 获取请求url
    2. 判断请求url中是否包含 login, 如果包含,说明是登录操作,放行。
    3. 获取请求头中的令牌(token)。
    4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
    5. 解析 token,如果解析失败,返回错误结果(未登录)
    6. 放行
代码语言:javascript复制
@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     ---------阿里巴巴fastJSON
            String 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     ---------阿里巴巴fastJSON
            String 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 异常处理

  • 全局异常处理器:
代码语言:java复制
@RestControllerAdvice
代码语言:java复制
@ExceptionHandler(Exception.class)    //捕获所有异常

0 人点赞