SpringBoot---CRUD测试案例:

1. 概述

本次案例模拟公司后端人员开发场景:

  • 当前案例的前端工程,前端人员已经帮我们开发好了,我们只需要关注服务端接口的开发。

1.1 页面原型

  • 产品经理绘制的的页面原型的展示效果:
    在这里插入图片描述
  • 成品展示:完成部门管理和员工管理的所有功能。
    在这里插入图片描述

2. 准备工作

2.1 需求说明

在这里插入图片描述

2.2 环境搭建:

在这里插入图片描述

2.2.1 准本数据库表

  1. 创建数据库tlias:略(使用SQLyog)
  2. 在数据库中创建部门和员工表:在询问窗口中执行sql脚本即可
    在这里插入图片描述
-- 部门管理
create table 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 '部门表';

insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()), (4,'就业部',now(),now()),(5,'人事部',now(),now());



-- 员工管理(带约束)
create table 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 教研主管, 5 咨询师',
  entrydate date comment '入职时间',
  dept_id int unsigned comment '部门ID',
  create_time datetime not nul`dept``emp`l comment '创建时间',
  update_time datetime not null comment '修改时间'
) comment '员工表';

INSERT INTO emp
	(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
	(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
	(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
	(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
	(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
	(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
	(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
	(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
	(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
	(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
	(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
	(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
	(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
	(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
	(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
	(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
	(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
	(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());

  1. 创建好的2张表效果:一对多
    在这里插入图片描述

2.2.2 创建工程,引入依赖

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2.3 编写配置文件,实体类

在这里插入图片描述

  • 配置文件
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=root

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

#开启mybatis的驼峰命名自动映射开关 a_column ------> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true

  • 实体类:
    • dept:
package com.cn.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

/**
 * 部门实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
    private Integer id; //ID
    private String name; //部门名称
    private LocalDateTime createTime; //创建时间
    private LocalDateTime updateTime; //修改时间
}

  • emp:
package com.cn.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; //ID
    private String username; //用户名
    private String password; //密码
    private String name; //姓名
    private Short gender; //性别 , 1 男, 2 女
    private String image; //图像url
    private Short job; //职位 , 1 班主任 , 2 讲师 , 3 学工主管 , 4 教研主管 , 5 咨询师
    private LocalDate entrydate; //入职日期
    private Integer deptId; //部门ID
    private LocalDateTime createTime; //创建时间
    private LocalDateTime updateTime; //修改时间
}

2.2.4 编写三层架构

  • 控制层,Service层,Mapper层
    在这里插入图片描述

2.3 开发规范

  1. 前后端分离开发,依照接口文档进行开发,一般是后端人员编写开发文档。
    在这里插入图片描述
    在这里插入图片描述

  2. 前后端基于RestFul风格的接口进行交互
    在这里插入图片描述
    在这里插入图片描述

  3. 统一响应结果:静态方法是为了更加方便的调用
    在这里插入图片描述
    在这里插入图片描述

package com.cn.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应信息 描述字符串
    private Object data; //返回的数据

    //增删改 成功响应
    public static Result success(){
        return new Result(1,"success",null);
    }
    //查询 成功响应
    public static Result success(Object data){
        return new Result(1,"success",data);
    }
    //失败响应
    public static Result error(String msg){
        return new Result(0,msg,null);
    }
}

2.4 思考开发流程

在这里插入图片描述

3. 部门管理

3.1 查询部门

3.1.1 需求分析&开发思路

  • 查看页面原型明确需求:
    在这里插入图片描述
  • 查看接口文档
    在这里插入图片描述
  • 查询部门思路分析
    在这里插入图片描述

3.1.2 接口开发

在这里插入图片描述

  • Controller:
/**
 *部门
 */
@RestController
@Slf4j
public class DeptController {

    @Autowired
    private DeptService deptService;

    //@RequestMapping(value = "/depts",method = RequestMethod.GET)//指定请求方式为get
    @GetMapping("/depts")
    public Result list(){

        //项目开发中最好不要使用System输出日志,不够专业,可以使用日志框架
        //System.out.println("查询全部部门数据");
        log.info("查询全部部门数据");

        //调用service查询部门数据
        List<Dept> list= deptService.list();
        return Result.success(list);
    }
}
  • Service:
/**
 * 部门
 */
public interface DeptService {

    //查询全部门数据
    List<Dept> list();
}
  • Serviceimpl:
/**
 *部门
 */
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Override
    public List<Dept> list() {
        return deptMapper.list();
    }
}

  • Mapper:
/**
 *部门
 */
@Mapper
public interface DeptMapper {

    //查询全部门数据
    @Select("select * from dept")
    List<Dept> list();
}
  • 运行程序使用Postman进行测试
    在这里插入图片描述

3.1.3 前后端联调

  • 将前端工程和后端工程都启动起来,访问前端工程通过前端工程来访问服务端程序,进而进行调试。
  • 前端工程已经提供好了,我们只需要启动测试即可。
    • 前端程序一般都是部署在nginx服务器中,在资料当中的nginx压缩包已经部署好这个前端程序
      在这里插入图片描述

    • 步骤:

      • 解压到一个没有中文和空格的目录并启动
        在这里插入图片描述
      • 查看任务管理器,启动成功
        在这里插入图片描述
    • 访问测试:http://localhost:90/,ngnix占用的是90端口号
      在这里插入图片描述
      在这里插入图片描述

3.2 删除部门

3.2.1 需求分析

  • 原型需求分析:
    在这里插入图片描述
  • 接口文档:
    在这里插入图片描述
  • 删除部门----思路分析
    在这里插入图片描述

3.2.2 接口开发

在这里插入图片描述

  • Controller:
/**
 *部门
 */
@RestController
@Slf4j
public class DeptController {

    @Autowired
    private DeptService deptService;

   /**
     * 删除部门
     * @return
     */
    @DeleteMapping("/depts/{id}")
    public Result delete(@PathVariable Integer id){
        log.info("根据id删除部门:{}",id);
        //调用service删除部门
        deptService.delete(id);
        return Result.success();
    }
}
  • Service:
    /**
     * 删除部门
     * @param id
     */
    void delete(Integer id);
  • Serviceimpl:
    @Override
    public void delete(Integer id) {
        deptMapper.deleteById(id);
    }
  • Mapper:
    /**
     * 根据ID删除部门
     * @param id
     */
    @Delete("delete from dept where id = #{id}")
    void deleteById(Integer id);
  • 运行程序使用Postman进行测试
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.2.3 前后端联调

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3 新增部门

3.3.1 需求分析&开发思路

  • 查看页面原型明确需求:
    在这里插入图片描述

  • 新增接口文档

    • post请求
    • 前端发送的请求参数类型为json类型(后端使用实体类+@RequestBody注解接收
    • 后端返回类型为json类型(方法使用@responseBody注解标识)
      在这里插入图片描述
  • 新增部门思路分析

    • 补充基础属性:添加部门时页面传递的参数只有一个 部门名称,在往数据库表中插入数据时还需要插入创建时间修改时间参数,可以设置为当前时间,在代码中或者sql语句中都可以。
      在这里插入图片描述
      在这里插入图片描述

3.3.2 接口开发

在这里插入图片描述

  • Controller:
/**
 *部门
 */
@RestController
@Slf4j
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 新增部门
     * @return
     */
    @PostMapping("/depts")
    public Result add(@RequestBody Dept dept){
        log.info("新增部门: {}" , dept);
        //调用service新增部门
        deptService.add(dept);
        return Result.success();
    }
}
  • Service:
    /**
     * 新增部门
     * @param dept
     */
    void add(Dept dept);
  • Serviceimpl:
    @Override
    public void add(Dept dept) {
        dept.setCreateTime(LocalDateTime.now());
        dept.setUpdateTime(LocalDateTime.now());

        deptMapper.insert(dept);
    }
  • Mapper:
    /**
     * 新增部门
     * @param dept
     */
    @Insert("insert into dept(name, create_time, update_time) values(#{name},#{createTime},#{updateTime})")
    void insert(Dept dept);
  • 运行程序使用Postman进行测试
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.3.3 前后端联调

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.4 代码优化

在这里插入图片描述

4. 员工管理

4.1 分页查询(不带条件)

4.1.1 需求分析&开发思路

  • 查看页面原型明确需求:完成分页查询功能(不带查询条件的分页)
    在这里插入图片描述
    在这里插入图片描述

    • 分页查询服务端返回的数据:服务端需要返回给前端的数据
    • 每页显示的记录数:前端页面在预设好的选项供用户来选择的,并不需要服务端返回
    • 总记录数:数据库表结构中符合条件的总记录数 ,需要服务端查询返回
    • 总页数不需要服务端返回,前端只需要拿到总记录数可以自己计算出总页数 总页数=总记录数/每页展示的记录数(向上取整)
      • eg:500/10=50条、 501/10=51条
    • 前端传递给后台的参数:当前页码值、每页显示的记录数
    • 服务端响应给前端的数据:数据列表(使用list集合封装)、总记录数(使用变量接收)
      • 问题:服务端响应给前端的数据就是控制层方法的返回值,一个方法只能有一个返回值,现在需要返回给前端两项数据并且类型还不一样,那么此时该如何返回呢???
      • 解决:使用Map集合或者实体类(常用)把两项数据进行封装后返回。
  • 接口文档:
    在这里插入图片描述

  • 思路分析
    在这里插入图片描述

4.1.2 接口开发

  • 封装分页查询数据和总记录数的实体类
    在这里插入图片描述
package com.cn.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 分页查询结果封装类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {

    private Long total; //总记录数,可能比较多选择long类型
    private List rows; //数据列表

}


  • Controller:
    在这里插入图片描述
/**
 * 员工管理Controller
 */
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    /**
     * 查看接口文档可知,2个参数要求有默认值
     *     page:分页查询的页码,如果未指定,默认为1
     *     pageSize:分页查询的每页记录数,如果未指定,默认为10
     * 解决:使用@RequestParam(defaultValue = "xxx")注解,设置默认值
     */
    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page,
                       @RequestParam(defaultValue = "10") Integer pageSize){
                       
        //{} {} 传递2个参数使用2个占位符,不使用占位符后面参数代表的值无法输出
        log.info("分页查询, 参数: {},{}",page,pageSize);

        //调用service分页查询
        PageBean pageBean = empService.page(page,pageSize);
        return Result.success(pageBean);
    }



}
  • Service:
    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    PageBean page(Integer page, Integer pageSize);
  • Serviceimpl:
    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean page(Integer page, Integer pageSize) {
        //1. 获取总记录数
        Long count = empMapper.count();

        //2. 获取分页查询结果列表
        Integer start = (page - 1) * pageSize;
        List<Emp> empList = empMapper.page(start, pageSize);

        //3. 封装PageBean对象
        PageBean pageBean = new PageBean(count, empList);
        return pageBean;
    }
  • Mapper:
    /**
     * 查询总记录数
     * @return
     */
    @Select("select count(*) from emp")
    public Long count();

    /**
     * 分页查询,获取列表数据
     * @param start
     * @param pageSize
     * @return
     */
    @Select("select * from emp limit #{start},#{pageSize}")
    public List<Emp> page(Integer start, Integer pageSize);
  • 运行程序使用Postman进行测试
    在这里插入图片描述
    在这里插入图片描述

4.1.3 前后端联调

在这里插入图片描述

4.1.4 分页插件PageHelper

  • 使用背景:项目中的很多功能(员工信息的分页查询、订单信息的分页查询、商品信息的分页查询)都会进行分页查询,步骤固定且代码繁琐。
  • 解决:使用分页插件,MyBatis框架中最为流行的就是PageHelper分页插件。
    在这里插入图片描述

改造代码步骤

  • 引入依赖
    在这里插入图片描述
  <!--PageHelper分页插件-->
   <dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
       <version>1.4.2</version>
   </dependency>
  • 改造Mapper接口:只需要执行正常的查询即可,所有与分页插件有关的操作都是由分页插件来完成的。 把原先手动查询总记录和手动查询分页结果的方式注释掉。
    在这里插入图片描述
    /**
     * 员工信息查询
     * @return
     */
    @Select("select * from emp")
    public List<Emp> list();
  • 改造Service层实现类:通用把原始的查询总记录数和查询分页结果的方式注释掉。
    在这里插入图片描述
    @Override
    public PageBean page(Integer page, Integer pageSize) {

        //1. 设置分页参数 :当前页码值  每页显示的记录数
        PageHelper.startPage(page,pageSize);

        //2. 执行查询
        List<Emp> empList = empMapper.list();
        //把查询的结果强转为Page<Emp>类型,之后才可以调用Page的方法
        Page<Emp> p = (Page<Emp>) empList;

        //3. 封装PageBean对象 p.getTotal():获取总记录数   p.getResult():获取结果列表
        PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
        return pageBean;
    }
  • 运行测试
    在这里插入图片描述
    在这里插入图片描述

4.2 分页查询(带条件)

  • 在使用分页插件的基础上改造代码,进行带条件的查询。

4.2.1 需求分析&开发思路

  • 需求分析
    在这里插入图片描述
  • 实例分析
    在这里插入图片描述

4.2.2 接口开发----改造代码

在这里插入图片描述

  • Controller:
    在这里插入图片描述
/**
 * 员工管理Controller
 */
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    /**
     * 查看接口文档可知,2个参数要求有默认值
     *     page:分页查询的页码,如果未指定,默认为1
     *     pageSize:分页查询的每页记录数,如果未指定,默认为10
     * 解决:使用@RequestParam(defaultValue = "xxx")注解,设置默认值
     */
    @GetMapping
    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){
        //{} {} 传递2个参数使用2个占位符,不使用占位符后面参数代表的值无法输出
        log.info("分页查询, 参数: {},{},{},{},{},{}",page,pageSize,name,gender,begin,end);

        //调用service分页查询
        PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);
        return Result.success(pageBean);
    }



}
  • Service:
    在这里插入图片描述
    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    PageBean page(Integer page, Integer pageSize,String name, Short gender,LocalDate begin,LocalDate end);
  • Serviceimpl:
    在这里插入图片描述
    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean page(Integer page, Integer pageSize,String name, Short gender,LocalDate begin,LocalDate end) {

        //1. 设置分页参数 :当前页码值  每页显示的记录数
        PageHelper.startPage(page,pageSize);

        //2. 执行查询
        List<Emp> empList = empMapper.list(name, gender, begin, end);
        //把查询的结果强转为Page<Emp>类型,之后才可以调用Page的方法
        Page<Emp> p = (Page<Emp>) empList;

        //3. 封装PageBean对象 p.getTotal():获取总记录数   p.getResult():获取结果列表
        PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
        return pageBean;
    }
  • Mapper:
    在这里插入图片描述
    /**
     * 员工信息查询
     * @return
     */
    //@Select("select * from emp")  使用动态sql编写分页条件查询
    public List<Emp> list(String name, Short gender,LocalDate begin,LocalDate end);
  • 配置文件:添加映射文件位置
    在这里插入图片描述
mybatis.mapper-locations=classpath:mapper/*.xml
  • 映射文件
    在这里插入图片描述
<?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.cn.mapper.EmpMapper">

    <!--条件查询
        模糊匹配:想要使用#{}占位符生成预编译的Sql,可以借助mysql中的字符串拼接函数concat进行拼接
              即:参数是写死的情况下不需要,如果是占位符需要通过concat进行拼接-->
    <select id="list" resultType="com.cn.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>

4.2.3 前后端联调

在这里插入图片描述
在这里插入图片描述

4.3 删除员工

4.3.1 需求分析&开发思路

  • 页面原型需求分析:删除一个、批量删除,可以只开发一个批量删除的接口,因为删除单个也可以看成是一个特殊的批量删除功能。
    在这里插入图片描述
  • 开发文档
    在这里插入图片描述
  • 开发思路
    在这里插入图片描述

4.3.2 接口开发

在这里插入图片描述

  • Controller:
    @DeleteMapping("/{ids}")
    public Result delete(@PathVariable List<Integer> ids){
        log.info("批量删除操作, ids:{}",ids);
        empService.delete(ids);
        return Result.success();
    }
  • Service:
    /**
     * 批量删除
     * @param ids
     */
    void delete(List<Integer> ids);
  • Serviceimpl:
    @Override
    public void delete(List<Integer> ids) {
        empMapper.delete(ids);
    }
  • Mapper:
    /**
     * 批量删除
     * @param 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>

4.3.3 前后端联调

在这里插入图片描述
在这里插入图片描述

4.4 新增员工

4.4.1 需求分析&开发思路

  • 原型分析
    在这里插入图片描述
    暂时不考虑图片
    在这里插入图片描述

  • 接口文档
    在这里插入图片描述

  • 开发思路
    在这里插入图片描述

4.4.2 接口开发

在这里插入图片描述

  • Controller:
    @PostMapping
    public Result save(@RequestBody Emp emp){
        log.info("新增员工, emp: {}",emp);
        empService.save(emp);
        return Result.success();
    }
  • Service:
    /**
     * 新增员工
     * @param emp
     */
    void save(Emp emp);
  • Serviceimpl:
    @Override
    public void save(Emp emp) {
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.insert(emp);
    }
  • Mapper:
    /**
     * 新增员工
     * id:设置的主键自增,不需要插入
     * password:设置的有默认约束,不需要插入
     * @param 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);

4.4.3 前后端联调

在这里插入图片描述
在这里插入图片描述

4.5 文件上传----阿里云OSS集成

4.5.1 需求分析&开发实录

  • 原型分析:阿里云返回的url,在浏览器中输入+回车是下载,把地址写在html中的image标签上是显示图片。
    • 流程:
      • 文件上传:新增员工时服务端通过MultipartFile 类型来接收文件,之后调用OSS的API把文件保存在阿里云OSS中,阿里云OSS会为每个文件设置一个url,我们需要在服务端获取这个url,最终将这个url返回给前端。
      • 回显图像:把url地址返回给前端后,前端会自动向阿里云OSS发送请求获取到图片,在页面中展示出来
      • 保存参数到数据库:在页面表单填写完参数后进行保存,服务端会调用新增方法,把包括图片url在内的参数保存在数据库的员工表中,这样添加的员工就和图片关联起来了

在这里插入图片描述

  • 接口文档
    在这里插入图片描述

4.5.2 接口开发

  • 步骤

    • 引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)
    • 上传图片接口开发
  • 添加依赖
    在这里插入图片描述

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.15.1</version>
        </dependency>
  • 工具类
    在这里插入图片描述
package com.cn.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;

/**
 * 阿里云 OSS 工具类
 */

@Component
public class AliOSSUtils {

    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";//OSS对象存储服务的地址
    private String accessKeyId = "LTAI5tSHskmRrsoA2C1sikMm";//miyao(。。。违反社区规所以这样写)
    private String accessKeySecret = "d6WvJcYYhfsdq36Pl4jAt72wkxGO1N";//米妖(。。。)
    private String bucketName = "web-wenjian-shangchuan";//工作空间名

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

        // 避免文件覆盖 :uuid生成的前缀+原始文件名的后缀
        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

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

        //文件访问路径:OSS地址前缀(//前的内容)+存储空间+OSS地址后缀(//后的内容)+文件名
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
        // 关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }

}

  • 控制层方法
    在这里插入图片描述
package com.cn.controller;

import com.cn.pojo.Result;
import com.cn.utils.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;

@Slf4j
@RestController
public class UploadController {

    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws IOException {

        log.info("文件上传, 文件名: {}", image.getOriginalFilename());
        /**
         * 调用阿里云OSS工具类,将上传来的文件保存在阿里云中:
         *    这个提供的阿里云OSS工具类方法并不是一个静态方法,所以调用这个方法区还需要new一个工具类对象,
         *    现在使用的都是Spring环境,所以建议把工具类交给Spring容器管理(在工具类上添加@Component),
         *    在注入即可
         */
        String url = aliOSSUtils.upload(image);
        log.info("文件上传完成,文件访问的url: {}", url);

        return Result.success(url);//将图片上传后的url返回,用于浏览器回显展示。
    }
}

  • 运行测试:
    在这里插入图片描述
    在这里插入图片描述
  • 查看阿里云OSS的工作空间:已经保存了图片。
    在这里插入图片描述
  • 查看数据库:图片的url地址已经保存到数据库的表中
    在这里插入图片描述

4.6 修改员工

4.6.1 查询回显

1)需求分析&开发思路
  • 页面原型分析:
    在这里插入图片描述
  • 开发文档
    在这里插入图片描述
  • 思路分析
    在这里插入图片描述
2)接口开发

在这里插入图片描述

  • Controller层
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        log.info("根据ID查询员工信息, id: {}",id);
        Emp emp = empService.getById(id);
        return Result.success(emp);
    }
  • Service层接口
    /**
     * 根据ID查询员工
     * @param id
     * @return
     */
    Emp getById(Integer id);
  • Service层实现类
    @Override
    public Emp getById(Integer id) {
        return empMapper.getById(id);
    }
  • Mapper层
    /**
     * 根据ID查询员工
     * @param id
     * @return
     */
    @Select("select * from emp where  id = #{id}")
    Emp getById(Integer id);
3)前后端联调

在这里插入图片描述

4.6.2 提交修改

1)需求分析&开发思路
  • 页面原型分析:更新的条件只能是id,其它字段都有可能发生变化
    在这里插入图片描述

  • 开发文档
    在这里插入图片描述

  • 思路分析:

    • 使用动态sql,字段传递了就更新,字段没有传递就不更新。
    • 因为更新操作数据已经回显过了,所以不用动态Sql也可以。
      在这里插入图片描述
2)接口开发

在这里插入图片描述

  • Controller层
    @PutMapping
    public Result update(@RequestBody Emp emp){
        log.info("更新员工信息 : {}", emp);
        empService.update(emp);
        return Result.success();
    }
  • Service层接口
    /**
     * 更新员工
     * @param emp
     */
    void update(Emp emp);
  • Service层实现类
    @Override
    public void update(Emp emp) {
        emp.setUpdateTime(LocalDateTime.now());

        empMapper.update(emp);
    }
  • Mapper层
    /**
     * 更新员工
     * @param emp
     */
    void update(Emp emp);
  • 映射文件
    <!--更新员工:使用动态Sql标签,如果最后一个条件不成立会多一个逗号,
    可以使用Set标签代替set关键字(去掉set关键字,之后使用Set标签把需要更新的字段全部包裹起来)-->
    <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>

  • 使用Postman测试:
    • 后端代码没有使用动态Sql:Postman没有传递所有的参数,发现没有更新的参数会赋值为null。

      • 后端的sql:
        在这里插入图片描述

      • Postman编写的参数:
        在这里插入图片描述

      • 发送请求前的数据:
        在这里插入图片描述

      • 发送请求后的数据:没有修改的数据会被null值覆盖。
        在这里插入图片描述

    • 后端代码使用动态Sql:

      • 后端编写的sql文件
        在这里插入图片描述
        在这里插入图片描述

      • Postman编写的请求:
        在这里插入图片描述

      • 发送请求前的数据:
        在这里插入图片描述

      • 发送请求后的数据:提交的有参数的数据才会修改,没有提交的参数不会修改保留原始数据。
        在这里插入图片描述

3)前后端联调
  • 因为更新业务是先回显在更新,更新表单中已经回显了原始数据,所以即便后端不用动态Sql仍然可以正确插入。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. 配置文件

5.1 参数配置化(@Value)

5.1.1 背景分析

  • 当前项目中代码所存在的一些问题:完成文件上传功能时调用阿里云OSS工具类是把参数直接写死在类中。这样项目中每涉及到一个技术或者第三方的服务就将参数硬编码在java代码中,就会存在2个问题
    在这里插入图片描述

    • 如果参数发生变化,每次修改代码后都需要重新编译(.java--》.class),重新运行,这样非常的繁琐。
    • 开发一个真实的企业级项目那么这个java类可能会有很多,如果将这些参数分散的定义在各个java类中,那么此时修改一个参数值就先需要定位到这个参数的位置,然后在进行修改,重新编译 重新运行,这样项目是不方便维护和管理的。
  • 解决:把参数定义在配置文件中,之后在代码中引入即可。

    • key=value形式,k自己定义 最好做到见名知意,Value为参数值。
    • 想要修改参数值只需要找到配置文件进行修改即可,并且properties文件中的配置进行了修改不用编译
      在这里插入图片描述

5.1.2 修改代码

  • 修改项目中的文件上传功能的代码:
    • 配置文件
      在这里插入图片描述
#阿里云OSS配置
# properties配置文件中信息本身就是一个字符串所以不需要引号,不需要分号,等号两边的空格也不需要。
aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tSHskmRrsoA2C1sikMm
aliyun.oss.accessKeySecret=d6WvJcYYhfsdq36Pl4jAt72wkxGO1N
aliyun.oss.bucketName=wenjian
  • 修改工具类代码
    在这里插入图片描述
    //写的是properties文件中的key,注意是Spring家的注解
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;  //OSS对象存储服务的地址
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;  //秘钥
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret; //秘钥
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;  //工作空间名
  • 启动项目断点调试,在项目中使用添加功能可以看到后端确实获取到了参数。
    在这里插入图片描述
    在这里插入图片描述

5.2 yml配置文件

5.2.1 背景分析

  • 配置格式
    在这里插入图片描述
  • 常见的三类配置文件格式对比
    在这里插入图片描述
  • yml文件的基本语法
    在这里插入图片描述
  • yml常见的数据格式
    在这里插入图片描述
  • 修改项目中的配置,使用yml替换掉properties
    在这里插入图片描述

5.2.2 修改代码

  • 修改代码
    • 删除掉properties文件或者注释掉里面的内容
      在这里插入图片描述
    • yml文件中的内容
      在这里插入图片描述
spring:
  #数据库连接信息
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/tlias
    username: root
    password: root
  #文件上传的配置
  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
  mapper-locations: classpath:mapper/*.xml

#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    accessKeyId: LTAI5tSHskmRrsoA2C1sikMm
    accessKeySecret: d6WvJcYYhfsdq36Pl4jAt72wkxGO1N
    bucketName: web-wenjian-shangchuan

  • 运行测试:仍能访问成功。
    在这里插入图片描述
    在这里插入图片描述

5.3 @ConfigurationProperties

5.3.1 背景分析

  • 问题分析:通过@Value注入外部配置文件中的属性值,如果需要注入的属性值比较多,在每一个属性上都需要加上此注解进行注入,这样会变得非常的繁琐。
    在这里插入图片描述
  • 解决:Spring提供了一种简化的方式,可以直接将配置文件中的值自动的注入到对象的属性中。
    • 前提条件:
      • 配置文件中key的名字必须和实体类中的属性名一致。
      • 需要将实体类交给ioc容器管理,即加上@Component注解
      • 使用@ConfigurationProperties指定每个字段相同的前缀

在这里插入图片描述

  • 注入流程
    • 定义一个Bean对象,之后批量的将外部属性配置直接注入到Bean对象的属性中,在其它的类当中想要获取注入进来的属性直接注入这个Bean对象,然后调用对应的get方法就可以获取到了(实体类提供的有对应的get方法)。
    • 注意:不能直接注入到这个工具类中,因为其他类也注入这个变量时,此时2个不同的类由相同的变量,无法区分注入给谁。如果单独的只注入给一个实体类,其它类要是使用的时候只需要注入对应的Bean对象,之后调用set方法就可以区分使用了。

5.3.2 修改代码

  • 创建实体类
    在这里插入图片描述
package com.cn.utils;

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

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

  • 修改工具类:
    在这里插入图片描述
package com.cn.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
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
public class AliOSSUtils {
   /* //写的是properties文件中的key,注意是Spring家的注解
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;  //OSS对象存储服务的地址
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;  //秘钥
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret; //秘钥
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;  //工作空间名*/

    @Autowired
    private AliOSSProperties aliOSSProperties;

    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile file) throws IOException {

        //获取阿里云OSS参数
        String endpoint = aliOSSProperties.getEndpoint();
        String accessKeyId = aliOSSProperties.getAccessKeyId();
        String accessKeySecret = aliOSSProperties.getAccessKeySecret();
        String bucketName = aliOSSProperties.getBucketName();

        // 获取上传的文件的输入流
        InputStream inputStream = file.getInputStream();

        // 避免文件覆盖 :uuid生成的前缀+原始文件名的后缀
        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

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

        //文件访问路径:OSS地址前缀(//前的内容)+存储空间+OSS地址后缀(//后的内容)+文件名
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
        // 关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }

}

  • Debug调试,发现仍然可以注入成功。
    在这里插入图片描述
  • 警告信息说明:加上@ConfigurationProperties(prefix = “aliyun.oss”)注解后就会提示我们需要引入spring-boot-configuration-processor依赖,此依赖的作用是,自动的识别被@ConfigurationProperties注解标识的Bean对象,之后再配置文件中进行配置的时候,就会自动的提示与这个Bean对象的属性名相对应的配置项名字。即:在配置文件中配置阿里云OSS的配置信息时就会有对应的提示,不影响程序运行是可选的操作。
    在这里插入图片描述
    • 引入依赖
      在这里插入图片描述
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

  • 重新启动项目,查看实体类发现红色警告信息变为灰色提示信息,点击隐藏即可。
    在这里插入图片描述

5.3.3 @Value和@ConfigurationProperties的对比

在这里插入图片描述

6. 登录

  • 现在直接在浏览器地址栏中输入地址就可以直接进入到这个系统中随意操作数据,这样非常的不安全。
  • 解决:登录成功后才可以访问系统中的数据

6.1 基础登录功能

6.1.1 页面分析&接口文档

  • 页面原型分析
    • 操作的是员工表
    • 登录的时候如何校验用户名密码是否正确:根据用户名和密码查询员工,如果查询到员工则表明用户名和密码正确,如果没有查询到则表明输入的用户名和密码错误。
    • 登录的时候校验用户名和密码是否正确该如何写这条Sql:根据用户名和密码查询这条员工,如果查询到了就证明用户名和密码是正确的,没有查询到证明用户名或密码错误。
    • 思考根据这条sql查询到的员工有没有可能是多个:不可能,因为在创建员工表emp时针对这个username字段添加了唯一约束,所以username不可能重复,所以最终查询到的数据最多有一条。
      在这里插入图片描述
  • 接口文档
    在这里插入图片描述
  • 思路分析:
    登录的请求都是/login开头的,所以需要新定义一个控制层
    service层使用的都是emp员工,所以不需要创建一个新的业务层。
    在这里插入图片描述

6.1.2 接口开发

  • 控制层
    在这里插入图片描述
@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("用户名或密码错误");
    }

}
  • 业务层
    在这里插入图片描述
    /**
     * 员工登录
     * @param emp
     * @return
     */
    Emp login(Emp emp);
  • 业务层实现类
    @Override
    public Emp login(Emp emp) {
        return empMapper.getByUsernameAndPassword(emp);
    }
  • 数据层
    在这里插入图片描述
    /**
     * 根据用户名和密码查询员工
     * @param emp
     * @return
     */
    @Select("select * from emp where username = #{username} and password = #{password}")
    Emp getByUsernameAndPassword(Emp emp);
  • Postman测试
    在这里插入图片描述
    在这里插入图片描述

6.1.3 前后端联调

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.2 登录校验

6.2.1 概述

  • 问题:复制员工管理模块的访问地址,之后退出登录在浏览器输入复制的地址,发现在没有登录的时候就可以访问。
    • 复制地址
      在这里插入图片描述

    • 退出登录
      在这里插入图片描述

    • 退出后直接输入访问员工管理界面的地址,发现可以直接访问
      在这里插入图片描述

  • 正常执行流程:登录成功后才能访问,没有登录的时候访问首先要跳转到登录页面。
  • 原因:当前开发的部门管理和员工管理,在后端接口的代码中只是正常的操作数据库中的数据,完成数据的增删改查操作并没有判断用户是否登陆,所以无论用户是否登录都可以去访问部门管理和员工管理的相关数据。
    在这里插入图片描述
  • 解决:需要在后端代码中完成登录校验功能。
    • 登录校验:服务端接收客户端发送过来的请求,首先需要对请求进行校验用户是否登录,如果登录了就执行对应的业务操作,如果没有登录就不允许执行相关的业务操作,而是返回给前端一个错误的结果最终跳转到登录页面。
  • 登录校验实现思路
    • http是无状态的协议(每次请求都是独立的,下一次请求不会携带上一次请求的数据),浏览器与服务器之间的交互就是基于http协议的,即:通过浏览器访问登录接口实现了登录操作,接下来执行其它业务操作时服务器并不知道员工是否登录,因为http协议是无状态、独立的,所以它无法判断员工是否登录。
    • 在服务端想要判断员工是否登录,那么就需要在员工登录成功后存储一个登录成功的标记,之后在每一个接口方法之前进行判断员工是否登录,如果登陆成功则执行对应的业务操作,如果登陆失败返回一个错误信息给前端,前端拿到这个错误信息后会自动的跳转到登录页面。
    • 查询的接口方法、更新的接口方法、新增的接口方法等等都需要进行判断员工是否登录,每个方法写一遍判断登录的代码非常麻烦。解决:可以使用统一拦截的技术(拦截器、过滤器)拦截浏览器发送的所有请求,之后获取登录成功后的标记并且这个标记也没有问题,那么说明员工已经登录进行放行,否则响应给前端一个错误信息,前端就会自动的跳转到登录页面。

在这里插入图片描述

6.2.2 会话技术

1)会话相关概念
  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含 多次 请求和响应。
    • 总结:浏览器和服务器之间的一次连接就称为一次会话。同一个浏览器在未关闭之前请求了多次服务器,那么这多次请求属于同一个会话,关闭这个浏览器这次会话就结束了,直接把web服务器关闭了那么所有的会话都结束了。
  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间 共享数据
    • 总结:识别多次请求是否来自于同一个浏览器的过程就称为会话跟踪。如果跟踪到的多次请求来自于同一个会话,那么此时就可以在多个请求之间来共享数据了。
  • 会话跟踪方案
    • 客户端会话跟踪技术:Cookie
    • 服务端会话跟踪技术:Session
    • 令牌技术

在这里插入图片描述

2)Cookie介绍
  • 执行流程
    • Cookie是客户端会话跟踪技术,在浏览器第一次发起请求访问服务器的时候设置Cookie,在Cookie当中存入一些数据信息比如用户的id,服务端在给客户端响应数据的时候会自动的将Cookie响应给浏览器。
    • 浏览器接收到Cookie之后会自动的存储在浏览器本地,之后的每一次请求都会携带本地存储的Cookie自动的携带到服务端。
    • 在服务端获取到Cookie的值在进行判断这个Cookie的值是否存在。如果不存在说明用户没有登录,如果携带了证明已经登陆过了。
    • 这样就可以基于同一次会话的不同请求之间来共享数据。
  • 注意执行流程中的三个自动
    • 因为Cookie是http协议支持的技术,而各大浏览器厂商都支持了这一标准,在http协议中就给我们提供了一个响应头(Set-Cookie)和请求头(Cookie)。
      在这里插入图片描述
3)Cookie案例演示
  • 代码:
    在这里插入图片描述
package com.cn.controller;

import com.cn.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;
import javax.servlet.http.HttpSession;

/**
 * Cookie、HttpSession演示
 */
@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        //创建Cookie对象,并知客户端保存Cookie
        response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
        return Result.success();
    }

    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        //通过request获取浏览器请求头中携带的Cookie
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }
}

  • 运行测试:
    • 访问c1:可以看到浏览器拿到了响应回来的响应头Set-Cookie,之后会自动的存储到浏览器本地。
      在这里插入图片描述
      在这里插入图片描述
    • 访问c2:可以看到请求头会自动的携带Cookie到服务端。
      在这里插入图片描述
      在这里插入图片描述
4)Session介绍
  • 概述
    • Session是服务端会话跟踪技术是存储在服务器端的,Session的底层是基于Cookie来实现的。
  • 执行流程
    • 浏览器第一次请求服务器的时候,就可以直接在服务器中获取到会话对象Session(两种方式获取Session),如果是第一次请求Session会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session,每一个会话对象Session都会有一个Session的Id,之后服务器端在给浏览器响应数据的时候,他会将这个Session的id通过Cookie响应给浏览器,即:在响应头中加了Set-Cookie这个响应头,这个Cookie的名字是固定的JSESSIONID
    • 之后自动的将Cookie保存在浏览器本地,在每次发送请求的时候都会自动的携带Cookie到服务端,在服务端拿到Session的id之后,就会从众多的Session当中拿到当前请求对应的会话对象Session,这样就可以在同一次会话的多次请求之间来共享数据了。

在这里插入图片描述

5)Session案例演示
  • 代码:
    在这里插入图片描述
package com.cn.controller;

import com.cn.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;
import javax.servlet.http.HttpSession;

/**
 * Cookie、HttpSession演示
 */
@Slf4j
@RestController
public class SessionController {

    /**
     * 方式一:直接在控制层方法中设置HttpSession类型的参数获取Session
     *
     * 在控制层方法中声明一个Session,之后服务器端会进行判断当前请求
     * 对应的Session是否存在,如果不存在他会创建一个新的Session,如果存在
     * 他会获取到当前请求对应的Session
     */
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());

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


    /**
     * 方式二:在方法中声明一个HttpServletRequest对象类型的参数,
     *       之后通过request对象的方法来获取当前请求对应的Session
     */
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession-s2: {}", session.hashCode());

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

  • 可以看到响应给浏览器的Cookie并且会自动的保存到浏览器本地
    在这里插入图片描述
    在这里插入图片描述

  • 每次发起请求都会携带Cookie,在服务端通过输出Session的id发现前后获取到的值是一样的,说明两次请求获取到的是同一个会话对象。
    在这里插入图片描述
    在这里插入图片描述

6.2.3 JWT令牌

1)执行流程&优缺点
  • 令牌:用户身份的标识,其实就是一个字符串。
  • 优点解释
    • 支持PC端、移动端:因为现在并不需要把令牌必须保存在Cookie中,其他任何的存储空间当中都是可以的,你只需要在客户端当中将这个令牌存储起来就可以了。
    • 解决集群环境下的认证问题:因为我在服务器端啊并不需要存储任何的数据
    • 减轻服务器端存储压力:正是因为在服务端不需要存储任何的数据,所以他也减轻了服务器端的存储压力。
  • 缺点解释
    • 需要自己实现:怎么样生成令牌,怎么样将令牌存储在客户端浏览器,怎么样将令牌携带到服务端这些都需要我们自己实现。当然在实际的开发中也需要前端的开发人员来配合实现。
  • 思考令牌存储在客户端会不会不安全,用户是否可以伪造令牌???
    • 不会,一旦令牌伪造了,那我们在服务器端校验令牌的时候就会报错,是会检测到的。
  • 执行流程
    • 浏览器发送请求在请求登录接口的时候,如果登录成功就可以生成一个令牌,之后在响应数据的时候把令牌响应给前端
    • 在前端程序中可以通过Cookie存储,也可以存储在其他的存储空间中,之后在每一次的请求中都会通过请求头token携带令牌到服务端
    • 在服务端中校验令牌的有效性。如果令牌是有效的则证明用户之前进行了登录操作,如果令牌是无效的则证明用户之前没有进行登录操作。
    • 此时想要在同一次会话的多次请求之间共享数据,就可以把共享的数据存储在领牌中就可以了。

在这里插入图片描述

2)JWT令牌介绍
  • 简介https://jwt.io/
    在这里插入图片描述

    • 即:JWT就是将原始的json数据格式进行了安全的封装,这样就可以直接基于JWT在通信双方安全的传输信息了。
    • 简洁的:指的是JWT是一个简单地字符串,可以在请求参数或者是请求头当中直接来传递这个JWT令牌
    • 自包含:JWT令牌看似是一个随机的字符串,但是可以根据自身的需要在JWT令牌中来储存自定义的内容。比如直接在JWT令牌中存储用户的相关信息。
  • 应用场景:登录认证
    在这里插入图片描述

3)JWT令牌-生成和校验(演示)
3.1)概述
  • JWT令牌-生成
    在这里插入图片描述

  • JWT令牌-校验
    在这里插入图片描述

3.2)测试
  • 引入依赖
        <!--JWT令牌-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  • 调用工具包提供的Api来完成JWT令牌的生成和校验(在测试类中进行的调试)
    在这里插入图片描述
package com.cn;

import com.cn.mapper.EmpMapper;
import com.cn.pojo.Emp;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @SpringBootTest的作用:如果想要使用注入组件功能,需要类上添加@SpringBootTest注解标识,
 * 这里显然不需要使用,所以可以把此注解注释掉。好处是在运行的时候不用加载整个Spring的运行环境,
 * 可以提高运行速度。
 */
//@SpringBootTest
class SpringBootCrudHeimaApplicationTests {



    /**
     * 生成JWT
     */
    @Test
    public void testGenJwt(){
        Map<String, Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("name","tom");

        /* Jwts.builder():构建JWT令牌,之后通过链式编程来调用里面的方法来设置gwt令牌在生成的时候所需要设置的一些参数:
         * signWith():指定签名算法、秘钥(秘钥就是一个字符串,自己设置一个)
         * setClaims():JWT令牌所存储的内容,也就是自定义的数据(载荷 第二部分数据)。参数类型为map集合,
         *             可以把自定义的数据封装到map集合中
         * setExpiration():设置令牌的有效期,在有效时间范围内生效,超出有效期就失效了。
         *                  参数类型为Date类型:拿到当前时间在加上一个小时,表示当前时间向后推迟一个小时。
         *                                   System...获取到的是当前时间的时毫秒值,所以把3600秒乘以一千转化为毫秒值后在相加。
         * compact():调用此方法后就可以拿到一个字符串类型的返回值,即生成的jwt令牌。
         */
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "itheima")
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h
                .compact();
        System.out.println(jwt);
    }

    /**
     * 解析JWT
     */
    @Test
    public void testParseJwt(){
        /* Jwts.parser():进行令牌解析
         * setSigningKey():指定之前设置的签名秘钥
         * parseClaimsJws():指定需要解析的令牌字符串
         * getBody():调用此方法就可以拿到自定义的内容,也就是jwt令牌的第二部分内容。
         * */
        Claims claims = Jwts.parser()
                .setSigningKey("itheima")
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NTk2MTY5NH0.YDNV-sLr28gKzCSYaknefgftVMAM2VvIIPaW26qGu68")
                .getBody();
              
        //解析后的结果是一个Map集合
        System.out.println(claims);
    }

}

  • 运行jwt令牌生成单元测试方法:
    在这里插入图片描述

  • 运行jwt令牌解析单元测试方法:使用的是java代码进行解析,得到自定义的数据,过期时间
    在这里插入图片描述

  • 也可以直接把生成的令牌字符串放到JWT令牌官网进行解析:
    在这里插入图片描述

  • 一旦令牌字符串被篡改了就不能在进行解析,证明jwt令牌是安全可靠的。
    在这里插入图片描述

4)JWT令牌-登录后下发令牌(运用到项目中)
4.1) 思路分析&接口文档
  • 思路:
    在这里插入图片描述

  • 接口文档:
    在这里插入图片描述

4.2) 改造登录接口代码
  • 引入JWT令牌操作工具类(前面已经演示过了基础的Api,真正在项目中使用的时候都是调用现成的工具类)
    在这里插入图片描述
package com.cn.utils;

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;  //指定过期时间为12小时

    /**
     * 生成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;
    }
}

  • 登录完成后,调用工具类生成JWT令牌,并返回。
    在这里插入图片描述
package com.cn.controller;


import com.cn.pojo.Emp;
import com.cn.pojo.Result;
import com.cn.service.EmpService;
import com.cn.utils.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;

@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){ //调用login()查询到了员工的信息说明登录成功了。
            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令牌,jwt包含了当前登录的员工信息
            return Result.success(jwt);//登陆成功后下发令牌。由接口文档可知返回的是json类型的数据
        }

        //登录失败, 返回错误信息   由接口文档可知返回的是json类型的数据
        return Result.error("用户名或密码错误");
    }

}

  • 使用Postman进行测试
    在这里插入图片描述
  • 前后端联调:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

6.2.4 过滤器Filter(令牌校验)

  • 概念Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
    在这里插入图片描述
1)快速入门

步骤:

  1. 定义Filter:定义一个类,实现Filter接口,并重写其所有方法。
  2. 配置Filter:Fiter类上加@WwebFilter注解,配置拦截资源的路径。启动类类上加@ServletComponentScan开启Servlet组件支持。

在这里插入图片描述
案例测试

  • 创建过滤器
    在这里插入图片描述
package com.cn.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*") //拦截所有请求
/*注意实现过滤器之后需要重写的3个方法,只有doFilter方法是必须要重写
的,而其他的2个方法在父接口中已经做了默认实现所以如果不用的话就不
需要重写了(因为这2个方法不经常用,所以底层使用了default标识,jdk8)*/
public class DemoFilter implements Filter {
    /*
    * 初始化方法, 只调用一次
    * web服务器启动时自动创建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);

        System.out.println("Demo 拦截到了请求...放行后逻辑");
    }

    //销毁方法, 只调用一次  关闭服务器时候调用,通常做些环境的清理以及资源的释放操作
    @Override
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}

  • 主启动类上加上@ServletComponentScan开启Servlet组件支持
    在这里插入图片描述
  • 运行测试:启动web服务器时调用初始化方法,每次发送请求都会调用doFilter方法,当关闭服务器时调用销毁方法。
    在这里插入图片描述
2)详解:执行流程、拦截路径、过滤器链
  • 执行流程
    在这里插入图片描述
  • 拦截路径
    在这里插入图片描述
  • 过滤器链
    在这里插入图片描述
3)登录校验-Filter(运用到项目中)
  • 基本流程
    在这里插入图片描述
  • 实现思路
    在这里插入图片描述
    改造代码:
    • 创建过滤器
      在这里插入图片描述
package com.cn.filter;


import com.alibaba.fastjson.JSONObject;
import com.cn.pojo.Result;
import com.cn.utils.JwtUtils;
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(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //下面用的方法都是HttpServletRequest对象中的方法,而这里重写后的方法是ServletRequest类型,所以需要强转
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        //1.获取请求url。
        String url = req.getRequestURL().toString();
        log.info("请求的url: {}",url);

        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){
            log.info("登录操作, 放行...");
            chain.doFilter(request,response);
            //判断是登录操作后放行,放行后不需要在接着执行以下业务逻辑代码,所以使用return结束方法执行。
            return;
        }

        //3.不是登录操作,获取请求头中的令牌(token)。
        String jwt = req.getHeader("token");

        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        //Spring提供的工具类,判断字符串是否有长度,如果没有长度证明为null或空串,说明令牌不存在
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");

            /*由接口文档可知未登录需要返回给前端一个错误的信息,并且是json类型,
            * 之前是在控制层方法中加上@ResponseBody注解,就会自动将方法的返回值转化为json并返回给前端,
            * 现在在过滤器中并不是在Controller中,所以需要手动的进行转换。
            *  转换步骤:引入阿里巴巴提供的json转换依赖fastJSON
            *          调用里面的工具Api方法进行转换
            * */
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            //把转化后的错误信息json对象返回给前端,注意使用的是原生的方式,因为这是过滤器中不是在控制层方法
            resp.getWriter().write(notLogin);
            //结束方法,不需要执行下面的代码
            return;
        }

        //5.如果令牌存在,解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {//jwt解析失败回报异常,所以需要进行try-catch
            e.printStackTrace();
            log.info("解析令牌失败, 返回未登录错误信息");
            //通用解析失败需要返回一个json类型的错误信息,有接口文档可知
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        //6.令牌存在且解析成功,说明登陆过了,放行。
        log.info("令牌合法, 放行");
        chain.doFilter(request, response);

    }
}

  • 主启动类上加上@ServletComponentScan开启Servlet组件支持
    在这里插入图片描述
  • 添加阿里巴巴fastJSON依赖:用于转化为json
        <!--fastJSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
  • 把之前测试功能的过滤器注释掉
    在这里插入图片描述

  • 使用Postman进行debug测试:

    • 登录功能:可以看到使用登录功能时直接放行,之后点击放行按钮可以看到返回令牌字符串。
      在这里插入图片描述
      在这里插入图片描述
    • 测试部门查询功能:在Postman中添加请求头token将令牌携带到服务端,可以看到不是登录请求会接着执行下面的代码,最终放行后查询到了部门信息
      在这里插入图片描述
      在这里插入图片描述
    • 篡改令牌测试返回错误信息:发现执行了令牌解析失败的代码,在postman中可以看到返回的错误信息。
    • 其中NOT_LOGIN是我们和前后端约定好的一个标识,前端看到这个标识后就会自动的跳转到登录页面
      在这里插入图片描述在这里插入图片描述
  • 前后端联调测试:

    • 刷新浏览器查询所有的部门信息,此时没有登录会强制我们跳转到登录页面。
      在这里插入图片描述
      在这里插入图片描述
    • 点击登录,此时已经登录后携带的有令牌会放行,所以查询成功。
      在这里插入图片描述

6.2.5 拦截器Interceptor(令牌校验)

  • 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

在这里插入图片描述

1)快速入门

步骤:

  1. 定义拦截器,实现Handlerlnterceptor接口,并重写其所有方法。
  2. 注册拦截器

在这里插入图片描述
案例测试:

  • 创建拦截器
    在这里插入图片描述
package com.cn.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 {
    @Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, 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...");
    }
}

  • 配置拦截器
    在这里插入图片描述
package com.cn.config;


import com.cn.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
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        /*注册拦截器,参数为之前创建的拦截器对象。因为拦截器交给了spring管理所以直接注入即可.
          如果没有在拦截器的类上添加@Component注解,此时使用new的方式即可。
        */
        registry.addInterceptor(loginCheckInterceptor)
                //指定需要拦截的资源,拦截所有在过滤器器是/*,在拦截器中是/**。
                .addPathPatterns("/**");

    }
}

  • 使用Postman测试登录功能:
    • 把原先的过滤器的方式注释掉
      在这里插入图片描述
    • 由于在拦截器的preHandle方法中配置的为true放行,所以会返回登陆成功后的下发令牌信息
      在这里插入图片描述
    • 控制台:目标方法和3个方法都执行了
      在这里插入图片描述
    • 设置返回值为false不放行,使用postman进行登录访问测试,查看控制台可以看到知识输出了preHandle,目标方法和之后的2个方法都没有执行。
      在这里插入图片描述
2)详解:拦截路径、过滤器拦截器执行流程
  • 拦截路径:
    在这里插入图片描述

  • 执行流程
    在这里插入图片描述

3)登录校验-Interceptor(运用到项目中)
  • 创建拦截器:
    在这里插入图片描述
package com.cn.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.cn.pojo.Result;
import com.cn.utils.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;

@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {

        //1.获取请求url。这个地方传递过来的就是HttpServletRequest对象,不需要在强转了
        String url = req.getRequestURL().toString();
        log.info("请求的url: {}",url);

        //这一步操作因为在拦截器配置类中,设置了不拦截登录请求,所以加不加都可以。
        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){
            log.info("登录操作, 放行...");
            return true;
        }

        //3.获取请求头中的令牌(token)。
        String jwt = req.getHeader("token");

        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);

            //令牌不存在说明没有登录不能放行,放行的话回去执行Controller的接口方法了
            return false;
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {//jwt解析失败
            e.printStackTrace();
            log.info("解析令牌失败, 返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);

            //同样令牌解析失败说明没有登录不能放行,放行的话回去执行Controller的接口方法了
            return false;
        }

        //6.放行。
        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...");
    }
}

  • 配置拦截器:
    在这里插入图片描述
package com.cn.config;


import com.cn.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
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        /*注册拦截器,参数为之前创建的拦截器对象。因为拦截器交给了spring管理所以直接注入即可.
          如果没有在拦截器的类上添加@Component注解,此时使用new的方式即可。
        */
        registry.addInterceptor(loginCheckInterceptor)
                //指定需要拦截的资源,拦截所有在过滤器器是/*,在拦截器中是/**。
                .addPathPatterns("/**")
                //配置不需要拦截的路径
                .excludePathPatterns("/login");

    }
}

  • 使用Postman访问测试:
    • 注册掉原先使用过滤器的方式
      在这里插入图片描述

    • 登录功能:可以看到返回值拿到了令牌
      在这里插入图片描述

    • 查询功能:添加请求头token,每次发送请求都会携带令牌信息,令牌存在且解析正确所以放行,可以看到查询成功。
      在这里插入图片描述

  • 前后端联调:之前使用过滤器已经保存了token说明已经登陆过了,所以现在想要测试需要先把token删除掉,在刷新浏览器,可以看到在没有登录的情况下跳转到登录页面。之后点击登录可以看到查询部门信息成功。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

7 异常处理-全局异常处理类

7.1 问题分析&异常执行流程&解决办法

当前项目中存在的问题

  • 问题一:没有做异常处理
    • 在部门管理模块新增销售部部门,由于数据库表中已经存储了销售部,在数据库设计部门表时,规定了部门名称字段为唯一约束,所以此时在控制台会报异常。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 问题二:异常结果的返回值不符合开发规范
    • F12打开开发者工具,查看出现异常后服务端返回给前端的数据格式是一个json格式,这个json格式并不是我们开发规范中说规定的统一响应结果Result(接口文档中规定的),而仅仅是封装了错误的相关信息。由于返回的数据不符合开发规范,所以前端并不能解析这个数据。
      在这里插入图片描述

思考:程序开发过程中不可避免的会遇到异常现象,此时在当前项目中我们并未做异常处理,那么一旦出现异常项目的流程是如何执行的呢???

  • 控制层调用业务层,业务层调用数据层,如果Mapper接口此时在操作数据库出现异常,那么这个异常会向上抛出,谁调用Mapper就抛给谁。此时抛给Service层,service层未做任何处理出现异常会继续抛给Controller,Controller也没有做任何异常处理,所以最终异常会抛给框架,框架就会给我们返回一个json类型的错误信息数据,但是这个json类型的数据并不符合我们接口文档中规定的开发规范。
    在这里插入图片描述

出现异常的解决方案

  • 方案一(不推荐):由于最终异常会抛给控制层的代码,所以只需要在每一个控制层方法中做异常处理(try-catch)就行了。

    • 缺点:在每个控制层方法中都要进行try-catch操作,这样非常麻烦而且代码臃肿
      在这里插入图片描述
  • 方案二(推荐):全局异常处理器,只要在项目中定义全局异常处理器就可以处理项目中所有的异常。

    • 优点:更加简单、优雅
    • 使用全局异常处理器执行顺序:
      • 没有出现异常:此时会走Controller中响应给前端一个正确的结果。
      • 出现异常:Mapper中出现异常不用处理抛给Service,Service出现异常不用处理抛给Controller,Controller出现异常不用处理最终交给全局异常处理器来处理,全局异常处理器处理完成后在给前端响应统一响应的结果Result,在Result中封装错误信息。

在这里插入图片描述

7.2 测试(运用到项目中)

  • 创建全局异常处理器:
    在这里插入图片描述
package com.cn.exception;

import com.cn.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器:
 * 定义一个类添加@RestControllerAdvice注解,此时就代表我们定义了一个全局异常处理器
 * @RestControllerAdvice = @ControllerAdvice + @ResponseBody  所以它会将方法的返回值转化为json在
 * 返回,符合接口文档中规定的需要返回json类型的数据格式。
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /*在全局异常处理器中定义一个方法来捕获异常,在此方法上通过@ExceptionHandler注解
     的value属性来设置需要捕获那些异常*/
    @ExceptionHandler(Exception.class)//捕获所有异常
    public Result ex(Exception ex){
        /*捕获到异常后,就可以在这个方法中处理异常,这里只是简单地输出了下异常
        的堆栈信息,然后响应一个标准的Result,在Result中封装错误的提示信息,
        最终将result响应给前端*/

        ex.printStackTrace();//输出异常的堆栈信息
        return Result.error("对不起,操作失败,请联系管理员");
    }

}

  • 运行测试:再次向部门表中添加已经存在的部门数据,可以看到页面提示出来了错误信息,控制台输出异常的堆栈信息。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 在浏览器的开发者工具中,可以看到响应回来的数据是一个标准的Result,这个结果前端是可以解析出来的,所以前端拿到这个错误信息后就直接把这个错误的提示信息提示出来了。
    在这里插入图片描述

8 事务管理

8.1 事务回顾

在这里插入图片描述

8.2 Spring事务管理-完善删除部门功能

8.2.1 删除功能存在的问题

  • 案例需求:删除部门,同时删除该部门下的员工。

  • 说明:部门解散了,我们不仅要把部门删除了,还需要把该部门下对应的员工数据删除。

  • 当前项目中的删除部门功能,只是完成了根据id删除部门数据,并没有删除该部门下的员工数据,这个逻辑是不完整的。如果仅仅是把部门删除了那么部门下的员工还会关联着这个部门,此时就会造成数据的不完整不一致。
    在这里插入图片描述

  • 改造代码:添删除部门,同时删除该部门下的员工数据的代码。

    • 部门业务层:
      在这里插入图片描述
    @Autowired
    private DeptMapper deptMapper;
    
    @Autowired
    private EmpMapper empMapper;

    @Override
    public void delete(Integer id) {
        //根据id删除部门数据
        deptMapper.deleteById(id);

        //根据部门ID删除该部门下的员工
        empMapper.deleteByDeptId(id);
    }
  • 员工数据层:
    在这里插入图片描述
    /**
     * 根据部门ID删除该部门下的员工数据
     * @param deptId
     */
    @Delete("delete  from emp where dept_id = #{deptId}")
    void deleteByDeptId(Integer deptId);
  • 运行测试:正常执行流程
    • 删除之前的部门表和对应的员工表数据:
      在这里插入图片描述
      在这里插入图片描述

    • 启动项目,删除人事部
      在这里插入图片描述

    • 删除之前的部门表和对应的员工表数据:可以看到部门数据和部门对应的员工数据都删除了。
      在这里插入图片描述
      在这里插入图片描述

8.2.2 演示不使用事务出现的问题

演示异常执行流程

  • 业务层中添加异常代码:
    在这里插入图片描述

  • 删除前的部门表和员工表中的数据:
    在这里插入图片描述
    在这里插入图片描述

  • 删除文学部:
    在这里插入图片描述

  • 删除后的部门表和员工表中的数据:发现部门表中的数据删除了,而部门对应的员工信息没有删除。
    在这里插入图片描述
    在这里插入图片描述

  • 为什么会出现这种情况呢???

    • 原因:删除部门成功后出现异常,一旦程序出现异常后面的代码就不会在执行了,所以部门对应的员工信息并未删除。(throws的方式不会执行,try-catch的方式会执行)

    • 解决:使用事务,让这两步操作处于同一个事务中,一个事务中的操作要么全部成功要么全部失败。
      在这里插入图片描述

8.2.3 使用事务

  • Spring框架已经把事务控制的代码封装好了,我们只需要使用@Transactional注解就可以完成事务的控制。
  • @Transactional注解介绍
    • 位置:业务( service)层的方法上、类上、接口上
    • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务; 成功执行完毕,提交事务; 出现异常,回滚事务
  • 说明
    • 1):一般会把这个注解添加到业务层中来控制事物,因为在业务层中,一个业务功能中可能会包含多个数据访问的操作,这样就可以将多个数据访问操作控制在一个事务范围内。
    • 2):作用在方法上表示当前方法交给Spring进行事务管理,作用在类上表示当前类中的所有方法交给Spring进行事务管理,作用在接口上表示当前接口的所有实现类中的所有实现方法交给Spring进行事务管理。
    • 3)虽然说这三个位置都可以使用这个注解,但是通常来讲我们一般会加在业务层的增删改这一类的方法上。更准确的说是加在业务层执行多次数据访问操作这一类的增删改方法上
      • 因为查询操作并不会影响数据的变更所以无需控制事务,如果在业务层只是执行了一步的增删改操作我们也不需要控制事务,因为Mysql的数据库事务是自动提交的,我们DML(数据操作语言-crud)语句执行完后事务已经自动提交了,如果执行失败数据库中的数据也不会发生变化。

运行测试:删除学工部,发现删除操作出现异常事务会进行回滚,数据库表中的数据并未被删除掉。

  • 删除学工部:
    在这里插入图片描述
  • 配置spring事务管理日志开关
    在这里插入图片描述
#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug
  • 可以看到一旦出现异常,事务进行了回滚,数据库表中的数据没有被删除。
    在这里插入图片描述
    删除之前:
    在这里插入图片描述
    删除之后:
    在这里插入图片描述

8.3 事务进阶

8.3.1 rollbackFor:异常回滚

在这里插入图片描述

8.3.2 propagation:事务传播行为

1)概述
  • 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
  • 场景:有2个方法A、B,这两个方法都加了@Transactional注解代表这2个方法都具有事务,此时在A方法中去调用B方法事务是如何管理的呢???
  • 问题:首先A方法在运行的时候会开启事务,之后在A方法中调用B方法,B方法也具有事务,那么此时B方法是加入到A方法这个事务中还是B方法在运行的时候新建一个事物????
    在这里插入图片描述
  • 解决:使用@Transactional注解的propagation属性指定传播行为。
  • 常见的事务传播行为:
    • REQUIRED(默认):b方法在运行的时候需要事务,如果有事务就加入到事务中,没有就新创建一个事务。
    • REQUIRES_NEW:需要新事物,在调用b方法的时候无论a方法是否有事务,b方法都会在一个新的事务中运行。
    • SUPPORTS:在调用b方法的时候发现有事务就加入,如果在现有的环境下没有事务就在无事务的状态下运行。
    • NOT_SUPPORTED: 不支持事务,如果在调用b方法的时候发现a已经存在事务了,那么他会先把这个事务挂起再去执行b方法,b方法运行完毕后在来执行当前事务。
    • MANDATORY: 必须有事务,否则抛出异常
    • NEWVER: 必须没有事务,否则抛出异常
      在这里插入图片描述
  • 只需要掌握2个常用的属性使用场景即可:
    在这里插入图片描述
2)案例演示:日志操作
  • 说明:解散部门是一个非常重要非常危险的操作,所以在业务当中要求每一次执行解散部门的操作时候,都需要留下痕迹 即:无论执行成功了还是失败了都需要记录日志
    在这里插入图片描述

  • 第一步的操作已经完成过了,所以我们只需要完成第二步操作即可。

  • 准备部门操作日志记录表:
    在这里插入图片描述

CREATE TABLE dept_log( 	
    id INT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
    create_time DATETIME NULL COMMENT '操作时间',
    description VARCHAR(300) NULL COMMENT '操作描述'
)COMMENT '部门操作日志表';
  • 日志pojo
    在这里插入图片描述
package com.cn.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
    private Integer id;
    private LocalDateTime createTime;
    private String description;
}

  • 部门实现类DeptServiceImpl:删除功能
    在这里插入图片描述
package com.cn.service.impl;

import com.cn.mapper.DeptMapper;
import com.cn.mapper.EmpMapper;
import com.cn.pojo.Dept;
import com.cn.pojo.DeptLog;
import com.cn.service.DeptLogService;
import com.cn.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

/**
 *部门
 */
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private DeptLogService deptLogService;

    @Transactional(rollbackFor = Exception.class) //spring事务管理
    @Override
    public void delete(Integer id) {

        /*因为要求日志操作无论程序是否执行成功,都需要记录日志,所以放到finally当中,
          又因为异常已经统一处理过了,所以不需要catch代码部分   */
        try {
            //根据id删除部门数据
            deptMapper.deleteById(id);

            //模拟异常
            int i = 1/0;

            //根据部门ID删除该部门下的员工
            empMapper.deleteByDeptId(id);
        } finally {
            //记录删除操作的日志
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
            deptLogService.insert(deptLog);
        }

    }

}

  • 日志业务层
    在这里插入图片描述
package com.cn.service;


import com.cn.pojo.DeptLog;

public interface DeptLogService {

    void insert(DeptLog deptLog);
}

  • 日志业务层实现类
    在这里插入图片描述
package com.cn.service.impl;


import com.cn.mapper.DeptLogMapper;
import com.cn.pojo.DeptLog;
import com.cn.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
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

  • 日志数据层
    在这里插入图片描述
package com.cn.mapper;

import com.cn.pojo.DeptLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DeptLogMapper {

    @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);

}

3)运行测试
  • 启动项目执行部门删除操作:发现出现异常后没有执行日志记录操作,之前要求是无论部门删除成功还是失败都会被记录日志,为此记录日志的代码放到了finally中,那么为什么日志没有记录成功呢????
    在这里插入图片描述
    在这里插入图片描述

  • 原因

    • 因为DeptServiceImpl这个部门删除的方法中使用了@Transactional注解,DeptLogServiceImpl这个记录日志的方法中也是用了@Transactional注解,也就说明这两个方法都是事务方法。
    • 日志记录的方法使用的是默认的事务传播行为属性@Transactional(propagation = Propagation.REQUIRED),表示部门删除的方法在调用日志记录的方法时候,如果A方法有事务那么B方法加入到A的事务中,如果A方法没有事务那么B方法开启一个新的事物,所以这里显然是日志记录的方法加入到了删除部门的事务方法中。
    • 删除部门的操作一旦出现异常,后续的代码不会执行,由于日志记录代码放在finallly中所以会执行,但是删除部门的操作、删除部门对应的员工、日志操作此时都在一个事务中,一旦事务中出现了异常会进行回滚,所以不但删除部门的操作会进行回滚,日志操作也跟着回滚了。
      在这里插入图片描述
      在这里插入图片描述
  • 解决:

    • 使用@Transactional(propagation = Propagation.REQUIRES_NEW):需要新事物,在调用b方法的时候无论a方法是否有事务,b方法都会在一个新的事务中运行。
    • 此时a方法和b方法分别处于不同的事务中,a方法出现异常会进行回滚,而b方法处于另个事务中不会受到影响,
    • 也就是说删除部门操作时进行了回滚,不会影响到放在finally中必定执行的日志记录代码。
  • 修改注解,再次运行测试:发现即便删除操作出现异常,仍然会进行日志记录操作。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

9 AOP

9.1 AOP基础-概述

  • AOP:Aspect Oriented Programming (面向切面编程、面向方面编程),其实就是面向特定方法编程。

  • 场景: 在一个项目中开发了很多功能,但是有一些功能的运行速度比较慢,所以现在需要优化这个项目。那么第一步就需要先定位出执行比较耗时的业务方法,然后在针对这些业务方法进行优化。

    • 传统方式:统计项目中的每一个业务方法的耗时,可以在每一个业务方法运行之前记录这个方法的开始时间,在这个方法运行完毕后再来记录这个方法的结束时间,然后那这个方法的结束时间减去方法的开始时间就是这个方法的执行耗时。缺点:一个项目中包含很多的业务模块,每一个业务模块都会包含很多增删改查的方法,这样修改每一个业务方法的代码来记录时间,会变得非常繁琐。
      在这里插入图片描述
    • 使用AOP的方式:可以做到再不改变原始方法的基础上来针对原始方法进行编程,这个编程可以是对原始方法的增强也可以是改变原始方法的功能。此时想要统计各个方法的耗时的需求只需要定义一个模版方法,然后将记录方法执行耗时的这一部分公共的逻辑代码定义在这个模版方法当中,在这个方法开始运行之前记录方法开始运行的时间,在方法执行之后记录方法的结束时间,中间来运行原始的业务方法。
    • 使用AOP的执行流程:假如现在项目要进行查询所有的部门,此时就会调用部门管理中的list方法,但是它并不会直接执行这个原始的list方法,而是会自动执行这个模版方法,模版方法在运行的时候就会先去统计这个方法运行的开始时间,然后在来执行原始的业务方法list方法,原始方法执行完毕后在来执行方法的结束时间,结束时间减去开始时间就是原始方法的执行耗时。
      在这里插入图片描述
  • 实现:

    • 动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的
      动态代理机制,对特定的方法进行编程。
  • AOP常见的使用场景:
    在这里插入图片描述

  • 优势:
    在这里插入图片描述

9.2 AOP基础-快速入门

需求:统计各个业务层方法执行耗时
在这里插入图片描述

步骤

  • 导入依赖:在pom.xml中导入AOP的依赖
    在这里插入图片描述
  • 编写AOP程序:针对于特定方法根据业务需要进行编程
    在这里插入图片描述

案例测试:

  • 准备一个Springboot项目,把之前项目中部门管理模块的crud功能复制过来,用来演示AOP的相关知识,这样就可以保证之前项目开发的工程比较干净。
  • 在pom文件中引入AOP的相关依赖
    在这里插入图片描述
        <!--AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 编写AOP程序:
    在这里插入图片描述
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;

@Slf4j
@Component  //把当前类交给Spring容器去管理
@Aspect     //标识当前类是一个AOP类
public class TimeAspect {

    //切入点表达式:指定当前这一部分共性逻辑的代码会作用在哪些方法上
    //表示service下的所有接口或者类里面的所有方法,都会运行这个公共方法所封装的公共逻辑代码
   @Around("execution(* com.itheima.service.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1. 记录开始时间
        long begin = System.currentTimeMillis();

       /* 2. 调用原始方法运行
        *    需要借助AOP给我们提供的一个Api,在方法的行参中声明一个ProceedingJoinPoint类型
        *    的参数,之后调用proceed方法就代表运行原始方法。
        *    原始方法运行过程中可能会出现异常,所以我们需要处理,这里使用throws抛出。
        *    原始方法在运行的时候会有返回值,所以需要使用Object类型来接收。
        *    现在方法执行完毕后只是统计方法的执行耗时,所以需要把原始方法的返回值返回回去。
        * */
        Object result = joinPoint.proceed();

        //3. 记录结束时间, 计算方法执行耗时
        long end = System.currentTimeMillis();
        /*joinPoint这个对象会封装原始方法的相关信息,所以可以调用getSignature()
          方法来获取原始方法的签名,这样就可以知道是那个方法执行的耗时时间。
        */
        log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin);

        return result;
    }

}

  • 运行测试:发现只要执行部门操作中的业务,都会进行记录耗时操作。
    在这里插入图片描述
    在这里插入图片描述

9.3 AOP基础-核心概念

  • 常用术语说明
    在这里插入图片描述
  • AOP执行流程:在程序运行的时候会自动的基于动态代理技术,为这个目标对象生成一个代理对象,在这个代理对象中对目标对象进行功能的增强,之后在程序注入的时候就不再是注入目标对象了而是注入了代理对象,在调用list方法之后其实就是调用的代理对象中的list方法,而这个方法已经进行了功能的增强、
    在这里插入图片描述

9.4 AOP进阶-通知类型

9.4.1 五大类型

  • 类型:
    在这里插入图片描述
  • 注意事项:
    在这里插入图片描述

9.4.2 案例测试

没有异常执行:

  • AOP代码
    在这里插入图片描述
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
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(){
        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(){
        log.info("after ...");
    }

    @AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterReturning(){
        log.info("afterReturning ...");
    }

    @AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterThrowing(){
        log.info("afterThrowing ...");
    }
}

  • 运行测试:发现除了异常通知没有执行,其它通知都执行了。
    在这里插入图片描述
    在这里插入图片描述

有异常执行:

  • 添加异常代码:在更新功能的业务层添加
    在这里插入图片描述
  • 运行测试:可以看到异常通知执行了,后置通知、环绕后通知(出现异常代码就不会在向下执行了)的代码没有执行。
    在这里插入图片描述
    在这里插入图片描述

9.4.3 案例测试-切入点表达式优化

  • 问题:当前切面当中通知的切入点表达式都是相同的,如果将来要变动每个通知方法都要修改。
  • 解决:可以进行抽取。

测试:

  • 修改AOP代码
    在这里插入图片描述
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class MyAspect1 {
    /**
     * 相同切入点抽取:1.定义一个无返回值类型、无参、无方法体的方法,名字随意,
     *                使用@Pointcut注解描述,值为切入点表达式
     *             2.之后在具体使用的切入点表达式中引用方法名即可
     */
    @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void pt(){}

    @Before("pt()")
    public void before(){
        log.info("before ...");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

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

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

    @After("pt()")
    public void after(){
        log.info("after ...");
    }

    @AfterReturning("pt()")
    public void afterReturning(){
        log.info("afterReturning ...");
    }

    @AfterThrowing("pt()")
    public void afterThrowing(){
        log.info("afterThrowing ...");
    }
}

  • 运行测试:仍能正确执行通知
    在这里插入图片描述

9.4.4 在其它切面类中引用

说明

  • 这个抽取的切入点表达式不单单在当前切面类中引用,还可以在其它切面类中引用,前提是这个抽取切入点的方法设置为public。
    在这里插入图片描述

测试:

  • 设置抽取切入点的方法为public
    在这里插入图片描述

  • 在别的切面类中引用
    在这里插入图片描述

  • 运行测试:发现耗时功能正确执行,说明引用成功。
    在这里插入图片描述
    在这里插入图片描述

9.5 AOP进阶-通知顺序

  • 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
    在这里插入图片描述

测试:

  • 为了输出的结果更加干净,把之前测试的AOP注释掉
    在这里插入图片描述
    在这里插入图片描述
  • 添加3个切面类:在每个切面类当中所定义的通知方法基本相同,只是输出的日志标识不同,每个通知的切入点表达式相同。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 查询操作
    在这里插入图片描述
    在这里插入图片描述

9.6 AOP进阶-切入点表达式

9.6.1 分类

  • 切入点表达式:描述切入点方法的一种表达式
  • 作用:主要用来决定项目中的哪些方法需要加入通知
  • 常见形式:
    • execution(....)︰根据方法的签名来匹配
      在这里插入图片描述
    • @annotation(...)︰根据注解匹配
      在这里插入图片描述

9.6.2 execution-根据方法的签名匹配

  • 方法的参数:为目标方法参数类型的全类名
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

测试:

  • 注释掉其它切面类,为了更好的显示测试结果
    在这里插入图片描述
  • 常见的切入点表达式写法:
    在这里插入图片描述
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect6 {

    //完整写法:
    //@Pointcut("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    //省略权限修饰符:
    //@Pointcut("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    //省略权限修饰符、包名类名:  但是包名.类名不建议省略,因为匹配的范围过大影响效率
    //@Pointcut("execution(void delete(java.lang.Integer))")
    //基于接口匹配,上面的都是基于实现类匹配:
    //@Pointcut("execution(void com.itheima.service.DeptService.delete(java.lang.Integer))")

    //使用通配符:*
    //@Pointcut("execution(void com.itheima.service.DeptService.*(java.lang.Integer))")
    //@Pointcut("execution(* com.*.service.DeptService.*(*))")
    //@Pointcut("execution(* com.itheima.service.*Service.delete*(*))")

    //使用通配符:..
    //@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    //@Pointcut("execution(* com..DeptService.*(..))") com开头任意层级下的包
    //@Pointcut("execution(* com..*.*(..))") //com开头任意层级下的包中,所有类中的所有方法
    //@Pointcut("execution(* *(..))") //慎用  当前环境下的所有方法,方法的形参也是任意的

    // 使用一个切入点表达式匹配2个方法:这2个方法的返回值不同,方法名也没有相同的前缀后缀,一个是有参的一个是无参的
    // 解决:一个一个的匹配,中间使用||连接,表示满足其中一个表达式都会运行通知
    @Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
            "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
    private void pt(){}

    @Before("pt()")
    public void before(){
        log.info("MyAspect6 ... before ...");
    }

}

  • 注意:使用一个切入点表达式匹配2个不同的方法的写法
    在这里插入图片描述

9.6.3 @annotation-根据注解匹配

  • 背景:像上面匹配多个无规则的方法,使用execution描述比较繁琐,可以使用@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;

@Retention(RetentionPolicy.RUNTIME)   //注解什么时候生效
@Target(ElementType.METHOD)  //注解作用在哪些位置上
public @interface MyLog {
}

  • 在目标方法上添加此注解
    在这里插入图片描述
  • 编写切面类
    在这里插入图片描述
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect7 {

    //匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
    //@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
    @Pointcut("@annotation(com.itheima.aop.MyLog)") //指定注解的全类名,表示匹配方法上加的有MyLog这个注解的方法
    private void pt(){}

    @Before("pt()")
    public void before(){
        log.info("MyAspect7 ... before ...");
    }

}

  • 运行删除的测试方法:发现before前置通知执行了
    在这里插入图片描述

9.7 AOP进阶-连接点

  • 连接点:类中所有可以被增强的方法
  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
    • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
      在这里插入图片描述

    • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是 ProceedingJoinPoint的父类型
      在这里插入图片描述

测试:

  • 切面类:
    在这里插入图片描述
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
@Aspect
@Component
public class MyAspect8 {

    @Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){ //注意是lang包下
        log.info("MyAspect8 ... before ...");
    }
    //JoinPoint和ProceedingJoinPoint的APi都是一样的,所以以环绕通知为例
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("MyAspect8 around before ...");

        //1. 获取 目标对象的类名 .
        String className = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}", className);

        //2. 获取 目标方法的方法名 .
        String methodName = joinPoint.getSignature().getName();
        log.info("目标方法的方法名: {}",methodName);

        //3. 获取 目标方法运行时传入的参数 .
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

        //4. 放行 目标方法执行 .
        Object result = joinPoint.proceed();

        //5. 获取 目标方法运行的返回值 .
        log.info("目标方法运行的返回值: {}",result);

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

  • 运行测试方法:发现取到了目标方法的相关信息
    在这里插入图片描述
    在这里插入图片描述

9.8 改造项目-记录操作日志

9.8.1 分析

说明:

  • 之前:在事务中记录的是解散部门的日志操作,因为解散部门是一个非常重要非常危险的操作,所以在业务当中要求每一次执行解散部门的操作时候,都需要留下痕迹。
  • 现在的业务需求
    在这里插入图片描述
  • 思路分析
    在这里插入图片描述
  • 实现步骤:
    在这里插入图片描述

9.8.2 具体实现

准备:

  • 添加依赖:在这里插入图片描述
        <!--AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 导入资料当中准备好后的数据表结构,并引入对应的实体类,Mapper接口:
    数据库文件:
    在这里插入图片描述
-- 操作日志表
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.cn.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; //操作耗时
}

Mapper文件:
在这里插入图片描述

package com.cn.mapper;

import com.cn.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.cn.anno;

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.METHOD)
public @interface Log {
}

  • 编写切面类
    在这里插入图片描述
package com.cn.aop;


import com.alibaba.fastjson.JSONObject;
import com.cn.mapper.OperateLogMapper;
import com.cn.pojo.OperateLog;
import com.cn.utils.JwtUtils;
import io.jsonwebtoken.Claims;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Component
@Aspect //切面类
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.cn.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {

        /* 1.操作人ID - 当前登录员工ID:
         *   思路分析:登陆成功后服务端会生成一个jwt令牌给前端,之后前端每次发送请求
         *           都会通过请求头token携带令牌到服务端,而令牌当中包含了员工的信息,
         *           所以我们可以通过请求头来获取令牌,然后在令牌中获取员工的id即可
         *
         *   请求头需要通过request对象获取:
         *       之前:在控制层方法中可以直接声明一个HttpServletRequest类型的参数即可
         *       现在:在切面类中获取request对象,而切面类当中通知方法的参数又不能随便定义,
         *            可以直接 @Autowired注入这个HttpServletRequest对象。
         * */
        String jwt = request.getHeader("token");//获取请求头中的令牌
        Claims claims = JwtUtils.parseJWT(jwt); //解析令牌,值为Map集合
        Integer operateUser = (Integer) claims.get("id");//根据k获取v,结果为Object类型,所以可以强转为Integer

        //2.操作时间
        LocalDateTime operateTime = LocalDateTime.now();//当前时间

        //3.操作类名
        String className = joinPoint.getTarget().getClass().getName();

        //4.操作方法名
        String methodName = joinPoint.getSignature().getName();

        //5.操作方法参数
        Object[] args = joinPoint.getArgs();//获取的结果是数组
        String methodParams = Arrays.toString(args);//pojo对象中使用的是String类型的变量接收,所以需要进行转化

        long begin = System.currentTimeMillis();
        //调用原始目标方法运行
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();

        //6.方法返回值:pojo对象中使用的是String类型接收的返回值,所以需要把获取的对象
        //           转化为json格式的字符串存储起来。---通过阿里巴巴提供的工具包fastJSON
        String returnValue = JSONObject.toJSONString(result);

        //7.操作耗时
        Long costTime = end - begin;


        //记录操作日志: 通过有参构造给对象属性赋值,之后把数据插入到日志表当中。id设置的有主键自增所 以不需要获取
        OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);

        log.info("AOP记录操作日志: {}" , operateLog);

        return result;
    }

}

  • 在增、删、改的目标方法上添加注解:指定哪些方法在运行时需要记录日志
    在这里插入图片描述
    在这里插入图片描述
  • 运行测试:点击新增,删除功能,可以看到在日志记录表中已经记录了日志操作。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/322317.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

曲线生成 | 图解贝塞尔曲线生成原理(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 贝塞尔曲线的应用2 图解贝塞尔曲线3 贝塞尔曲线的性质4 算法仿真4.1 ROS C仿真4.2 Python仿真4.3 Matlab仿真 0 专栏介绍 &#x1f525;附C/Python/Matlab全套代码&#x1f525;课程设计、毕业设计、创新竞赛必备&#xff01;详细介绍全局规划(图搜索、采样法…

CAN工具 - ValueCAN3 - 基础介绍

关注菲益科公众号—>对话窗口发送 “CANoe ”或“INCA”&#xff0c;即可获得canoe入门到精通电子书和INCA软件安装包&#xff08;不带授权码&#xff09;下载地址。 CAN/CANFD通讯广泛存在于整个车载网络中&#xff0c;几乎每一块软硬件的开发都需要用到CAN工具&#xff0c…

iOS UIViewContentMode 不同效果图文对比

一. iOS提供了的ContentMode有如下几种 其中默认mode是UIViewContentModeScaleToFill typedef NS_ENUM(NSInteger, UIViewContentMode) {UIViewContentModeScaleToFill,UIViewContentModeScaleAspectFit, // contents scaled to fit with fixed aspect. remainder is tr…

leetcode第365题:水壶问题

有两个水壶&#xff0c;容量分别为 jug1Capacity 和 jug2Capacity 升。水的供应是无限的。确定是否有可能使用这两个壶准确得到 targetCapacity 升。 如果可以得到 targetCapacity 升水&#xff0c;最后请用以上水壶中的一或两个来盛放取得的 targetCapacity 升水。 你可以&a…

使用composer构建软件包时文件(夹)权限设置

在构建软件包的时候你可能会需要对包源内文件或文件夹的权限做出相应的调整&#xff0c;以确保软件包在部署到客户端后可以正常运行。在此之前我们先来了解一下Apple文件系统内文件或文件夹的权限设定。 常见的文件或文件夹会有Owner, Group, Everyone这三种类型的所有权&#…

C++力扣题目108--有序数组转换为二叉平衡搜索树

给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输…

数据结构学习 数位dp

关键词&#xff1a;数位dp 记忆化搜索 dfs 数位dp属于比较难的题目&#xff0c;所有数位dp在leetcode都是hard。 因为没有做出jz43.里面用到了数位dp&#xff0c;所以去学习了一下&#xff0c;学习看了这位大神的基础知识。 题目基本上是跟着这位灵大哥的题单做的。 学完数…

Hive分区表实战 - 多分区字段

文章目录 一、实战概述二、实战步骤&#xff08;一&#xff09;创建学校数据库&#xff08;二&#xff09;创建省市分区的大学表&#xff08;三&#xff09;在本地创建数据文件1、创建四川成都学校数据文件2、创建四川泸州学校数据文件3、创建江苏南京学校数据文件4、创建江苏苏…

ubuntu打开epub格式的文件

Koodo Reader 是一个开源免费的电子书阅读器&#xff0c;支持多达15种主流电子书格式&#xff0c; 内置笔记、高亮、翻译功能&#xff0c;助力高效书籍阅读和学习。 官网地址 拖拽到此处即可添加图书 支持滚轮和点击翻页 菜单在这里 可以记笔记 查看笔记

大数据开发之Hive(压缩和存储)

第 9 章&#xff1a;压缩和存储 Hive不会强制要求将数据转换成特定的格式才能使用。利用Hadoop的InputFormat API可以从不同数据源读取数据&#xff0c;使用OutputFormat API可以将数据写成不同的格式输出。 对数据进行压缩虽然会增加额外的CPU开销&#xff0c;但是会节约客观…

CTFhub-phpinfo

CTFhub-Web-信息泄露-“phpinfo” 题目信息 解题过程 ctrlF搜索关键字…

【Linux驱动】设备树中指定中断 | 驱动中获得中断 | 按键中断实验

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux驱动》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 目录 &#x1f3c0;在设备树中指定中断&#x1f3c0;代码中获得中断&#x1f3c0;按键中断⚽驱动…

Python教程79:关于海龟画图,Turtle模块需要学习的知识点

1.海龟画图的基本步骤&#xff1a;A-B-C-D-E-F A.导入turtle模块&#xff1a;首先需要导入Python中的turtle模块&#xff0c;该模块提供了海龟绘图所需的基本函数和属性。 import turtle as tB.设置画布大小 使用turtle.screensize()函数。该函数可以设置画布的宽度高度背景…

中小企业如何做好信息化规划?

中小企业需不需要做信息化规划&#xff1f;什么时候做信息化规划比较好&#xff1f; 企业的信息化规划&#xff0c;一定是越早越好&#xff0c;越快越好。 因为信息化是一个过程&#xff0c;不是一个结果&#xff0c;它不是一天完成的事情&#xff0c;而是贯穿着企业经营管理…

FPGA引脚 Bank认知--FPGA 选型的一些常识

关键字 HP I/O Banks, High performance The HP I/O banks are deisgned to meet the performance requirements of high-speed memory and other chip-to-chip interface with voltages up to 1.8V. HR I/O Banks, High Range The HR I/O banks are designed to support a wid…

用python提取PDF中各类文本内容的方法

从PDF文档中提取信息&#xff0c;是很多类似RAG这样的应用第一步要处理的事情&#xff0c;这里需要做好三件事&#xff1a; 提取出来的文本要保持信息完整性&#xff0c;也就是准确性提出的结果需要有附加信息&#xff0c;也就是要保存元数据提取过程要完成自动化&#xff0c;…

FPGA四选一的多路选择器(用三元运算符?:解决)

一. 三元运算符? :用法 ?:符号通常用于条件运算符&#xff0c;表示条件判断。它类似于C语言中的三元运算符&#xff0c;用于根据条件选择不同的操作或值。 例如&#xff0c;在Verilog中&#xff0c;条件运算符?:可以用于if-else语句的简写形式。它的一般语法格式如下&#x…

市场复盘总结 20240113

仅用于记录当天的市场情况&#xff0c;用于统计交易策略的适用情况&#xff0c;以便程序回测 短线核心&#xff1a;不参与任何级别的调整&#xff0c;采用龙空龙模式 昨日主题投资 连板进级率 100% 二进三&#xff1a; 进级率低 14% 最常用的二种方法&#xff1a; 方法一&a…

IIC学习之SHT30温湿度传感器(基于STM32)

简介 附上SHT30资料和逻辑分析仪源文件&#xff0c;点击下载 关于IIC的介绍网上已经非常详尽&#xff0c;这里只说重点&#xff1a; 双线&#xff08;SDA&#xff0c;SCL&#xff09;&#xff0c;半双工采用主从结构&#xff0c;支持一主多从&#xff0c;通过地址寻址&#…

Hologres + Flink 流式湖仓建设

Hologres Flink 流式湖仓建设 1 Flink Hologres2 实时维表 Lookup 1 Flink Hologres holo在实时数仓领域非常受欢迎&#xff0c;一般搭配flinkhologres来做实时数仓&#xff0c;中间分层用holo&#xff0c;上下游一般依赖于holo的binlog来下发数据 2 实时维表 Lookup Holo…