三层架构

20240716|500

关于Maven

  • 方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题

只需要在maven项目的pom.xml文件中,添加一段如下图所示的配置即可实现。

maven项目的统一项目文件格式

Maven项目的目录结构:

1
2
3
4
5
6
7
8
9
maven-project01
|--- src (源代码目录和测试代码目录)
|--- main (源代码目录)
|--- java (源代码java文件目录)
|--- resources (源代码配置文件目录)
|--- test (测试代码目录)
|--- java (测试代码java目录)
|--- resources (测试代码配置文件目录)
|--- target (编译、打包生成文件存放目录)

POM配置

下面为pom.xml的内容。POM指的是项目对象模型,用来描述当前的maven项目

POM项目对象模型

title:pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- POM模型版本 -->
<modelVersion>4.0.0</modelVersion>

<!-- 当前项目坐标 -->
<groupId>com.itheima</groupId>
<artifactId>maven_project1</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 打包方式 -->
<packaging>jar</packaging>

</project>
  • <project> :pom文件的根标签,表示当前maven项目
  • <modelVersion> :声明项目描述遵循哪一个POM模型版本
    • 虽然模型本身的版本很少改变,但它仍然是必不可少的。目前POM模型版本是4.0.0
  • 坐标 :<groupId><artifactId><version>
    • 定位项目在本地仓库中的位置,由以上三个标签组成一个坐标
  • <packaging> :maven项目的打包方式,通常设置为jar或war(默认值:jar)

什么是坐标?

  • Maven中的坐标是==资源的唯一标识== , 通过该坐标可以唯一定位资源位置
  • 使用坐标来定义项目或引入项目中需要的依赖

Maven坐标主要组成

  • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
  • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
  • version:定义当前项目版本号

依赖配置

引入依赖

除了描述这个项目,我们项目中还需要引入依赖,类似于前端的npm的包

例如:在当前工程中,我们需要用到logback来记录日志,此时就可以在maven工程的pom.xml文件中,引入logback的依赖。具体步骤如下:

  1. 在pom.xml中编写<dependencies>标签
  2. <dependencies>标签中使用<dependency>引入坐标
  3. 定义坐标的 groupId、artifactId、version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<!-- 第1个依赖 : logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- 第2个依赖 : junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

如果不知道依赖的坐标信息,可以到mvn的中央仓库(https://mvnrepository.com/)中搜索

依赖传递

依赖的传递性:若一个包依赖另外的包,也会把其他包一起导入

若不想使用间接依赖,可以使用排除依赖:指主动断开依赖的资源。(被排除的资源无需指定版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven-projectB</artifactId>
<version>1.0-SNAPSHOT</version>

<!--排除依赖, 主动断开依赖的资源-->
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>

A依赖C,B依赖C,如果仅在B下面排除C,但是A仍然会引入A,这个做法可以用在两个依赖版本冲突时使用。

依赖范围

在项目中导入依赖的jar包后,默认情况下,可以在任何地方使用

如果希望限制依赖的使用范围,可以通过<scope>标签设置其作用范围

scope 主程序 测试程序(test) 打包(运行) 范例
compile(默认) Y Y Y log4j
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y jdbc驱动
起步依赖

在SpringBoot的项目中,有很多的起步依赖,他们有一个共同的特征:就是以spring-boot-starter-作为开头。在以后大家遇到spring-boot-starter-xxx这类的依赖,都为起步依赖。

起步依赖有什么特殊之处呢,这里我们以入门案例中引入的起步依赖做为讲解:

  • spring-boot-starter-web:包含了web应用开发所需要的常见依赖
  • spring-boot-starter-test:包含了单元测试所需要的常见依赖

在我们之前开发的SpringBoot入门案例中,我们通过maven引入的依赖,是没有指定具体的依赖版本号的。为什么没有指定<version>版本号,可以正常使用呢?因为每一个SpringBoot工程,都有一个父工程。依赖的版本号,在父工程中统一管理。

hl:parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>SpringLearn</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringLearn</name>
<description>SpringLearn</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

生命周期

生命周期的顺序是:clean —> validate —> compile —> test —> package —> verify —> install —> site —> deploy

我们需要关注的就是:clean —> compile —> test —> package —> install

SpringBoot基础入门

第一步:创建工程

HelloSpring创建工程

分层解耦

分层解耦

  • 前端发起的请求,由Controller层接收(Controller响应数据给前端)
  • Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
  • Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
  • Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

分层解耦举个例子——

【分层】

(注:只实现了分层)
分层解耦举个例子

【解耦】

把业务类变为EmpServiceB时,需要修改controller层中的代码
当前代码的高耦合之处

为什么耦合了?
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。所以,不能在EmpController中使用new对象
不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?我们的解决思路是:

  1. 提供一个容器,容器中存储一些对象(例:EmpService对象)
  2. controller程序从容器中获取EmpService类型的对象

IOC和DI基础

Spring中的两个核心概念:

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

    对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)
    这个容器称为:IOC容器或Spring容器

  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

    程序运行时需要某个资源,此时容器就为其提供这个资源。

    例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象

IOC容器中创建、管理的对象,称之为:bean对象

【如何管理】

  • Service层及Dao层的实现类,使用Spring提供的注解:@Component,交给IOC容器管理
  • 为Controller及Service注入运行时依赖的对象,使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

【完整的三层代码】

  • Controller层:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController  
    public class EmpController {

       @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量
       private EmpService empService ;

       @RequestMapping("/listEmp")
       public Result list(){
           //1. 调用service, 获取数据
           List<Emp> empList = empService.listEmp();

           //3. 响应数据
           return Result.success(empList);
      }
    }
  • Service层:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @Component //将当前对象交给IOC容器管理,成为IOC容器的bean  
    public class EmpServiceA implements EmpService {

       @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量
       private EmpDao empDao ;

       @Override
       public List<Emp> listEmp() {
           //1. 调用dao, 获取数据
           List<Emp> empList = empDao.listEmp();

           //2. 对数据进行转换处理 - gender, job
           empList.stream().forEach(emp -> {
               //处理 gender 1: 男, 2: 女
               String gender = emp.getGender();
               if("1".equals(gender)){
                   emp.setGender("男");
              }else if("2".equals(gender)){
                   emp.setGender("女");
              }

               //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导
               String job = emp.getJob();
               if("1".equals(job)){
                   emp.setJob("讲师");
              }else if("2".equals(job)){
                   emp.setJob("班主任");
              }else if("3".equals(job)){
                   emp.setJob("就业指导");
              }
          });
           return empList;
      }
    }

Dao层:

1
2
3
4
5
6
7
8
9
10
11
@Component //将当前对象交给IOC容器管理,成为IOC容器的bean  
public class EmpDaoA implements EmpDao {
   @Override
   public List<Emp> listEmp() {
       //1. 加载并解析emp.xml
       String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
       System.out.println(file);
       List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
       return empList;
  }
}

IOC细节

pring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:

  • @Controller (标注在控制层类上)
    • @RestController
    • @RestController = @Controller + @ResponseBody
  • @Service (标注在业务层类上)
  • @Repository (标注在数据访问层dao类上)

声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。

1
2
3
4
@Component(name = "specificBeanName")
public class MyBean {
// ...
}

DI细节

我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。

入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。

那如果在IOC容器中,存在多个相同类型的bean对象,则会报错

Spring提供了以下几种解决方案:

  • @Primary:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现
    • Primary注解
  • @Qualifier:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

    • @Qualifier注解不能单独使用,必须配合@Autowired使用
    • qualifier注解
  • @Resource:使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

    • Resource注解

与Mybatis结合(参考Mybatis文档)

与Mybatis结合后的分层模型

实战

基础工作

准备

数据库准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- 部门管理
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 null 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());

实战-项目创建

实战-起始依赖加载

生成pom.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/>
</parent>
<groupId>com.itheima</groupId>
<artifactId>tlias-web-management</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tlias-web-management</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

实战-项目架构

配置文件application.properties中引入mybatis的配置信息,准备对应的实体类

  • application.properties (直接把之前项目中的复制过来)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #数据库连接  
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/tlias
    spring.datasource.username=root
    spring.datasource.password=1234

    #开启mybatis的日志输出
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

    #开启数据库表字段 到 实体类属性的驼峰映射
    mybatis.configuration.map-underscore-to-camel-case=true
  • 实体类

1
2
3
4
5
6
7
8
9
10
/*部门类*/  
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
   private Integer id;
   private String name;
   private LocalDateTime createTime;
   private LocalDateTime updateTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*员工类*/  
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
   private Integer id;
   private String username;
   private String password;
   private String name;
   private Short gender;
   private String image;
   private Short job;
   private LocalDate entrydate;
   private Integer deptId;
   private LocalDateTime createTime;
   private LocalDateTime updateTime;
}

第4步:准备对应的Mapper、Service(接口、实现类)、Controller基础结构

数据访问层:

  • DeptMapper
1
2
3
4
5
6
package com.itheima.mapper;  
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DeptMapper {
}
  • EmpMapper
1
2
3
4
5
6
package com.itheima.mapper;  
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpMapper {
}

业务层:

  • DeptService
1
2
3
4
5
6
7
package com.itheima.service;  

//部门业务规则
public interface DeptService {
// `DeptService` 接口会定义一些与部门(Dept)相关的业务操作方法,比如添加部门、删除部门、更新部门信息或查询部门列表等。而 `DeptServiceImpl` 类则会提供这些业务操作的具体实现。
}

  • DeptServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.itheima.service.impl;  
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    //部门业务实现类
    @Slf4j
    @Service
    public class DeptServiceImpl implements DeptService {
    }
  • EmpService

    1
    2
    3
    4
    5
    package com.itheima.service;  

    //员工业务规则
    public interface EmpService {
    }
  • EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.service.impl;  
import com.itheima.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

//员工业务实现类
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {

}

控制层:

  • DeptController
1
2
3
4
5
6
7
package com.itheima.controller;  
import org.springframework.web.bind.annotation.RestController;

//部门管理控制器
@RestController
public class DeptController {
}
  • EmpController
1
2
3
4
5
6
7
package com.itheima.controller;  
import org.springframework.web.bind.annotation.RestController;

//员工管理控制器
@RestController
public class EmpController {
}

项目工程结构:

实战-项目工程结构之2|300

编写接口

查询部门

DeptController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j  // 自动生成logger对象
@RestController
public class DeptController {
   @Autowired
   private DeptService deptService; // 自动调用deptService

   //@RequestMapping(value = "/depts" , method = RequestMethod.GET)
   @GetMapping("/depts") // get depts时
   public Result list(){
       log.info("查询所有部门数据");
       List<Dept> deptList = deptService.list(); //调用deptService的list方法
       return Result.success(deptList);
  }
}

DeptService(业务接口)

1
2
3
4
5
6
7
public interface DeptService {  
   /**
    * 查询所有的部门数据
    * @return   存储Dept对象的集合
    */
   List<Dept> list(); // list方法:方法将返回一个`List`集合,集合中包含`Dept`类型的对象
}

DeptServiceImpl(业务实现类)

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j  
@Service
public class DeptServiceImpl implements DeptService {
   @Autowired
   private DeptMapper deptMapper;
   
   @Override
   public List<Dept> list() {
       List<Dept> deptList = deptMapper.list();
       return deptList;
  }
}    

DeptMapper

1
2
3
4
5
6
@Mapper  
public interface DeptMapper {
   //查询所有部门数据
   @Select("select id, name, create_time, update_time from dept")
   List<Dept> list();
}

删除部门

DeptController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j  
@RestController
public class DeptController {
   @Autowired
   private DeptService deptService;

   @DeleteMapping("/depts/{id}")
   public Result delete(@PathVariable Integer id) {
       //日志记录
       log.info("根据id删除部门");
       //调用service层功能
       deptService.delete(id);
       //响应
       return Result.success();
  }
   
   //省略...
}

DeptService

1
2
3
4
5
6
7
8
9
10
public interface DeptService {  

   /**
    * 根据id删除部门
    * @param id   部门id
    */
   void delete(Integer id);

   //省略...
}

DeptServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j  
@Service
public class DeptServiceImpl implements DeptService {
   @Autowired
   private DeptMapper deptMapper;

   @Override
   public void delete(Integer id) {
       //调用持久层删除功能
       deptMapper.deleteById(id);
  }
   
   //省略...
}

DeptMapper

1
2
3
4
5
6
7
8
9
10
11
@Mapper  
public interface DeptMapper {
   /**
    * 根据id删除部门信息
    * @param id   部门id
    */
   @Delete("delete from dept where id = #{id}")
   void deleteById(Integer id);
 
   //省略...
}

新增部门

DeptController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j  
@RestController
public class DeptController {
   @Autowired
   private DeptService deptService;

   @PostMapping("/depts")
   public Result add(@RequestBody Dept dept){
   // @RequestBody //把前端传递的json数据填充到实体类中
       //记录日志
       log.info("新增部门:{}",dept);
       //调用service层添加功能
       deptService.add(dept);
       //响应
       return Result.success();
  }

   //省略...
}

DeptService

1
2
3
4
5
6
7
8
9
10
public interface DeptService {  

   /**
    * 新增部门
    * @param dept 部门对象
    */
   void add(Dept dept);

   //省略...
}

DeptServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j  
@Service
public class DeptServiceImpl implements DeptService {
   @Autowired
   private DeptMapper deptMapper;

   @Override
   public void add(Dept dept) {
       //补全部门数据
       dept.setCreateTime(LocalDateTime.now());
       dept.setUpdateTime(LocalDateTime.now());
       //调用持久层增加功能
       deptMapper.inser(dept);
  }

   //省略...
}

DeptMapper

1
2
3
4
5
6
7
8
9
@Mapper  
public interface DeptMapper {

   @Insert("insert into dept (name, create_time, update_time) values (#{name},#{createTime},#{updateTime})")
   void inser(Dept dept);

   //省略...
}

分页查询

先定义分页类pageBean

1
2
3
4
public class PageBean{
private Long total;
private Long rows;
}

EmpController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import com.itheima.pojo.PageBean;
import com.itheima.pojo.Result;
import com.itheima.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/emps")// 提前到此,本类的所有都基于/emps为前缀
public class EmpController {

@Autowired
private EmpService empService;

//条件分页查询
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize)
//@RequestParam(defaultValue="默认值") //设置请求参数默认值
{
//记录日志
log.info("分页查询,参数:{},{}", page, pageSize);
//调用业务层分页查询功能
PageBean pageBean = empService.page(page, pageSize);
//响应
return Result.success(pageBean);
}
}

EmpService

1
2
3
4
5
6
7
8
9
10
public interface EmpService {  
   /**
    * 条件分页查询
    * @param page 页码
    * @param pageSize 每页展示记录数
    * @return
    */
   PageBean page(Integer page, Integer pageSize);
}

EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import com.itheima.pojo.PageBean;
import com.itheima.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;

@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
   @Autowired
   private EmpMapper empMapper;

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

       //2、获取分页查询结果列表
       Integer start = (page - 1) * pageSize; //计算起始索引 , 公式: (页码-1)*页大小
       List<Emp> empList = empMapper.list(start, pageSize);

       //3、封装PageBean对象
       PageBean pageBean = new PageBean(count , empList);
       return pageBean;
  }
}

EmpMapper

1
2
3
4
5
6
7
8
9
10
@Mapper  
public interface EmpMapper {
   //获取总记录数
   @Select("select count(*) from emp")
   public Long count();

   //获取当前页的结果列表
   @Select("select * from emp limit #{start}, #{pageSize}")
   public List<Emp> list(Integer start, Integer pageSize);
}
PageHelper

现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper。

pageHelper是Mybatis的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
官网:https://pagehelper.github.io/

当使用了PageHelper分页插件进行分页,就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。

1、在pom.xml引入依赖

1
2
3
4
5
<dependency>  
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper-spring-boot-starter</artifactId>
   <version>1.4.2</version>
</dependency>

2、EmpMapper

1
2
3
4
5
6
@Mapper  
public interface EmpMapper {
   //获取当前页的结果列表
   @Select("select * from emp")
   public List<Emp> page(Integer start, Integer pageSize);
}

3、EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
@Override  
public PageBean page(Integer page, Integer pageSize) {
   // 设置分页参数
   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;
}

带条件分页

需求:

  • 姓名:模糊匹配
  • 性别:精确匹配
  • 入职日期:范围匹配

例如:

1
2
3
4
5
6
7
select * 
from emp
where
name like concat('%','张','%') -- 条件1:根据姓名模糊匹配
and gender = 1 -- 条件2:根据性别精确匹配
and entrydate = between '2000-01-01' and '2010-01-01' -- 条件3:根据入职日期范围匹配
order by update_time desc;

在原有分页查询的代码基础上进行改造:

EmpController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j  
@RestController
@RequestMapping("/emps")
public class EmpController {

   @Autowired
   private EmpService empService;

   //条件分页查询
   @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) {
       //记录日志
       log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize,name, gender, begin, end);
       //调用业务层分页查询功能
       PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end);
       //响应
       return Result.success(pageBean);
  }
}

EmpService

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface EmpService {  
   /**
    * 条件分页查询
    * @param page     页码
    * @param pageSize 每页展示记录数
    * @param name     姓名
    * @param gender   性别
    * @param begin   开始时间
    * @param end     结束时间
    * @return
    */
   PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end);
}

EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j  
@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;
  }
}

EmpMapper

1
2
3
4
5
@Mapper  
public interface EmpMapper {
   //获取当前页的结果列表
   public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}

EmpMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>
           <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>

文件上传

文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上。

可以使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法:

  • String getOriginalFilename(); //获取原始文件名
  • void transferTo(File dest); //将接收的文件转存到磁盘文件中
  • long getSize(); //获取文件的大小,单位:字节
  • byte[] getBytes(); //获取文件内容的字节数组
  • InputStream getInputStream(); //获取接收到的文件内容的输入流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+originalFilename));

return Result.success();
}

}
UUID随机生成文件名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@RestController
public class UploadController {

@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//构建新的文件名
String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+newFileName));

return Result.success();
}

}

大文件

那么如果需要上传大文件,可以在application.properties进行如下配置:

1
2
3
4
5
#配置单个文件最大上传大小  
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

使用OSS服务

使用阿里云提供的OSS等服务,需要定义一些账户密码等信息,可以写入application中

可以将参数配置在配置文件中。如下:

1
2
3
4
5
#自定义的阿里云OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias

在将阿里云OSS配置参数交给properties配置文件来管理之后,我们的AliOSSUtils工具类就变为以下形式:

1
2
3
4
5
6
7
8
9
10
@Component
public class AliOSSUtils {
/*以下4个参数没有指定值(默认值:null)*/
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;

//省略其他代码...
}

在调用时,具体用法为: @Value(“${配置文件中的key}”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class AliOSSUtils {

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;

@Value("${aliyun.oss.bucketName}")
private String bucketName;

//省略其他代码...
}

登录认证

LoginController

1
2
3
4
5
6
7
8
9
10
11
12
@RestController  
public class LoginController {

   @Autowired
   private EmpService empService;

   @PostMapping("/login")
   public Result login(@RequestBody Emp emp){
       Emp e = empService.login(emp);
   return  e != null ? Result.success():Result.error("用户名或密码错误");
  }
}

EmpService

1
2
3
4
5
6
7
8
9
10
11
public interface EmpService {  

   /**
    * 用户登录
    * @param emp
    * @return
    */
   public Emp login(Emp emp);

   //省略其他代码...
}

EmpServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j  
@Service
public class EmpServiceImpl implements EmpService {
   @Autowired
   private EmpMapper empMapper;

   @Override
   public Emp login(Emp emp) {
       //调用dao层功能:登录
       Emp loginEmp = empMapper.getByUsernameAndPassword(emp);

       //返回查询结果给Controller
       return loginEmp;
  }  
   
   //省略其他代码...
}

EmpMapper

1
2
3
4
5
6
7
8
9
10
@Mapper  
public interface EmpMapper {

   @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +
           "from emp " +
           "where username=#{username} and password =#{password}")
   public Emp getByUsernameAndPassword(Emp emp);
   
   //省略其他代码...
}

JWT与拦截器

JWT全称:JSON Web Token (官网:https://jwt.io/

定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

JWT的组成

我们在登陆的时候,由服务器生成jwt字符串,由客户端进行存储。

在后续的请求当中,客户端都会在请求头中携带JWT令牌到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。 那怎么样来统一拦截到所有的请求校验令牌的有效性呢?这里我们会学习两种解决方案:

  1. Filter过滤器
  2. Interceptor拦截器

下面对 JWT\ Filter \ Interceptor 作简单说明

JWT实现

引入JWT依赖

1
2
3
4
5
6
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

编写JWT工具类函数util

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JwtUtils {

private static String signKey = "itheima";//签名密钥
private static Long expire = 43200000L; //有效时间

/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}

/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}

登录成功时,在controller层进行返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
@Slf4j
public class LoginController {
//依赖业务层对象
@Autowired
private EmpService empService;

@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
//调用业务层:登录功能
Emp loginEmp = empService.login(emp);

//判断:登录用户是否存在
if(loginEmp !=null ){
//自定义信息
Map<String , Object> claims = new HashMap<>();
claims.put("id", loginEmp.getId());
claims.put("username",loginEmp.getUsername());
claims.put("name",loginEmp.getName());

//使用JWT工具类,生成身份令牌
String token = JwtUtils.generateJwt(claims);
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}

我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。

过滤器

  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
    • 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
    • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
  • 使用方法
    • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
    • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义一个类,实现一个标准的Filter过滤器的接口
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}

@Override //拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}

@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
  • init方法: 过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
  • doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
  • destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。

在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求

hl:@WebFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )  

public class DemoFilter implements Filter {
   @Override //初始化方法, 只调用一次
   public void init(FilterConfig filterConfig) throws ServletException {
       System.out.println("init 初始化方法执行了");
  }

   @Override //拦截到请求之后调用, 调用多次
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
       System.out.println("Demo 拦截到了请求...放行前逻辑");
       //放行
       chain.doFilter(request,response);
  }

   @Override //销毁方法, 只调用一次
   public void destroy() {
       System.out.println("destroy 销毁方法执行了");
  }
}

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

1
2
3
4
5
6
7
8
9
@ServletComponentScan  
@SpringBootApplication
public class TliasWebManagementApplication {

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

}

可以定义多个filter,直接定义即可,无需其他配置。filter执行顺序按照“字典序”排序

最后,完整实现登录的filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login


//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("/login")){
chain.doFilter(request, response);//放行请求
return;//结束当前方法的执行
}


//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);


//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return;
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");

Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);
return;
}
//6.放行
chain.doFilter(request, response);
}
}

拦截器

一种动态拦截方法调用的机制,类似于过滤器。

自定义拦截器: 实现HandlerInterceptor接口,并重写其所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//自定义拦截器  
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
   //目标资源方法执行前执行。 返回true:放行   返回false:不放行
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       System.out.println("preHandle .... ");
       
       return true; //true表示放行
  }

   //目标资源方法执行后执行
   @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
       System.out.println("postHandle ... ");
  }

   //视图渲染完毕后执行,最后执行
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       System.out.println("afterCompletion .... ");
  }
}

注意:

preHandle方法:目标资源方法执行执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行执行
afterCompletion方法:视图渲染完毕后执行,最后执行

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration    
public class WebConfig implements WebMvcConfigurer {

//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@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接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
  • 时间顺序不同:先执行filter,才会执行interceptor

登录拦截器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ======================登陆配置拦截器===================
//自定义拦截器
@Component //当前拦截器对象由Spring创建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
//前置方式
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
//1.获取请求url
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行

//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");

//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return false;//不放行
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");

//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);

return false;
}

//6.放行
return true;
}

// ====================并非同一个文件=======================
// 注册配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}

错误处理

没有做错误处理时的情况

  • 方案一:在所有Controller的所有方法中进行try…catch处理
    • 缺点:代码臃肿(不推荐)
  • 方案二:全局异常处理器
    • 好处:简单、优雅(推荐)

定义全局异常处理器:定义一个类,在类上加上一个注解@RestControllerAdvice加上这个注解就代表我们定义了一个全局异常处理器。

在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestControllerAdvice  
public class GlobalExceptionHandler {

   //处理异常
   @ExceptionHandler(Exception.class) //指定能够处理的异常类型
   //当控制器(Controller)中的方法抛出异常时,Spring 框架会查找匹配的异常处理器来处理该异常。
   // `Exception`:这是 Java 标准库中的一个类,它是所有“非运行时异常”(checked exceptions)的超类。
   // `.class`:这是一个语法糖,用于获取一个类的 `Class` 对象。当你使用 `.class` 语法时,编译器会生成一个指向该类的 `Class` 对象的引用。
   public Result ex(Exception e){
       e.printStackTrace();//打印堆栈中的异常信息
       //捕获到异常之后,响应一个标准的Result
       return Result.error("对不起,操作失败,请联系管理员");
  }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端

事务管理

场景:删除部门表,删除需要同时删除部门下面的所有人员信息。当删除人员信息过程中发生错误,可能导致删除部门表成功但仍然留存一部分人员仍属于这个部门。

要想保证操作前后,数据的一致性,就需要让解散部门中涉及到的两个业务操作,要么全部成功,要么全部失败

在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。

使用@Transactional注解即可开启事务

  • @Transactional注解书写位置:
    • 方法
      • 当前方法交给spring进行事务管理
      • 当前类中所有的方法都交由spring进行事务管理
    • 接口
      • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Autowired
private EmpMapper empMapper;


@Override
@Transactional //当前方法添加了事务管理
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);

//模拟:异常发生
int i = 1/0;

//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}

下面详细介绍@Transactional注解的两个重要属性

rollbackFor

默认情况下,只有出现RuntimeException(运行时异常) 才会回滚事务。比如,把代码更改为

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional
public void delete(Integer id) throws Exception {
//根据部门id删除部门信息
deptMapper.deleteById(id);

//模拟:异常发生
if(true){
throw new Exception("出现异常了~~~");
}

//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}

通过抛出异常将不会产生任何回滚效果。

假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

hl:rollbackFor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j  
@Service
public class DeptServiceImpl implements DeptService {
   @Autowired
   private DeptMapper deptMapper;

   @Autowired
   private EmpMapper empMapper;

   
   @Override
   @Transactional(rollbackFor=Exception.class)
   public void delete(Integer id){
       //根据部门id删除部门信息
       deptMapper.deleteById(id);
       
       //模拟:异常发生
       int num = id/0;

       //删除部门下的所有员工信息
       empMapper.deleteByDeptId(id);  
  }
}

propagation

这个属性是用来配置事务的传播行为的。

什么是事务的传播行为呢?就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
例如:A\B两个方法,都加了@Transactional方法。A方法调用了B方法,那么在调用的时候,是新开启事务还是直接沿用同一个事物(因为在一个事务范围内,两个操作都会被回滚)?

我们在@Transactional注解的后面指定一个属性propagation

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

只需要关注前两个

场景:解散部门的操作需要非常完整的日志记录

我们采用REQUIRES_NEW,这样即便解散失败,部门数据会回滚,但是日志记录仍然存在(如果日志操作事务成功)。若默认值,即便日志事务成功,一旦解散失败,日志也会回滚。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class DeptLogServiceImpl implements DeptLogService {

@Autowired
private DeptLogMapper deptLogMapper;

@Transactional(propagation = Propagation.REQUIRES_NEW)//事务传播行为:不论是否有事务,都新建事务
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}

AOP:面向方法编程

面向切面(方法)编程

场景:有一个监控埋点,统计某个业务花费时间。针对不同业务那么都需要写相同的代码。实际上可以简化为:开始计时->调用对应方法->结束计时与分析
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

示例程序

title:pom.xml
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

@Around("execution(* com.itheima.service.*.*(..))")
//用于声明一个环绕通知(Around Advice)。环绕通知是所有通知类型中最强大的,因为它允许你在目标方法执行之前和之后执行自定义的代码,并且可以决定是否执行目标方法以及何时执行。
/*
"execution(* com.itheima.service.*.*(..))":这是切入点表达式,它指定了通知应该应用于哪些方法。这个表达式的含义如下:
execution:这是切入点指示符,用于匹配方法执行连接点。
*:第一个 * 表示返回类型不限。
com.itheima.service:这是包名,表示这个切入点表达式只匹配 com.itheima.service 包下的类。
.*:第二个 * 表示匹配 com.itheima.service 包下的任何类。
.*:第三个 * 表示匹配任何方法。
(..):括号中的两个点 .. 表示匹配任何参数列表的方法。
*/
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();

//执行原始方法
Object result = pjp.proceed();

//记录方法执行结束时间
long end = System.currentTimeMillis();

//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

return result;
}
}
  • AOP典型应用场景:
    • 记录系统的操作日志
    • 权限控制
    • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

核心概念

1. 连接点:JoinPoint

可以被AOP控制的方法(暗含方法执行时的相关信息)

例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

2. 通知:Advice

指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

例如:上面示例程序的recordTime就是一类通知Advice

  • Spring中AOP的通知类型:
    • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
    • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
    • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
    • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
    • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");

}

//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
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.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
// 执行顺序around before-before-after ruturning-after-around after

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了(执行位置与AfterReturning相同)
  • @Around环绕通知中原始方法调用时有异常,通知中的环绕后(around after) 的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)

3. 切入点:PointCut

匹配连接点的条件,通知仅会在切入点方法执行时被应用

即@Around括号内的内容,限制哪些方法受限于这个AOP方法。假如:切入点表达式改为DeptServiceImpl.list(),此时就代表仅仅只有list这一个方法是切入点。只有list()方法在运行的时候才会应用通知。

execution

若对同一切入点有多个方法调用,可以使用@PointCut统一切入点,就不用写的重复。(专业名字叫抽取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j
@Component
@Aspect
public class MyAspect1 {

//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){

}

//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
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(JoinPoint joinPoint){
log.info("after ...");
}

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}

当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:全类名.方法名();比如@Before("com.itheima.aspect.MyAspect1.pt()")

annotation

若定义execution复杂,可以自行编写注解,然后为每个目标方法添加对应注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 自定义注解MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}


// 业务类
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}


@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}

@Override
public Dept getById(Integer id) {
return deptMapper.getById(id);
}

@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}

// 切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知

//前置通知
@Before("@annotation(com.itheima.anno.MyLog)")// 使用annotation指明注解
public void before(){
log.info("MyAspect6 -> before ...");
}

//后置通知
@After("@annotation(com.itheima.anno.MyLog)")// 使用annotation指明注解
public void after(){
log.info("MyAspect6 -> after ...");
}
}

4.切面:Aspect

描述通知与切入点的对应关系(通知+切入点)

切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)

AOP切面

多个切面类同时存在,调用顺序为——默认按照切面类的类名字母排序

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行

5. 目标对象:Target

通知所应用的对象

案例

  • 自定义注解@Log
1
2
3
4
5
6
7
8
/**  
* 自定义Log注解
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
  • 修改业务实现类,在增删改业务方法上添加@Log注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j  
@Service
public class EmpServiceImpl implements EmpService {
   @Autowired
   private EmpMapper empMapper;

   @Override
   @Log // 这个方法需要进行日志记录
   public void update(Emp emp) {
       emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间

       empMapper.update(emp);
  }

   @Override
   @Log // 这个方法需要进行日志记录
   public void save(Emp emp) {
       //补全数据
       emp.setCreateTime(LocalDateTime.now());
       emp.setUpdateTime(LocalDateTime.now());
       //调用添加方法
       empMapper.insert(emp);
  }

   @Override
   @Log // 这个方法需要进行日志记录
   public void delete(List<Integer> ids) {
       empMapper.delete(ids);
  }

   //省略其他代码...
}

以同样的方式,修改EmpServiceImpl业务类

  • 定义切面类,完成记录操作日志的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j  
@Component
@Aspect //切面类
public class LogAspect {

   @Autowired
   private HttpServletRequest request;
   // 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。

   @Autowired
   private OperateLogMapper operateLogMapper;

   @Around("@annotation(com.itheima.anno.Log)")
   public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
       //操作人ID - 当前登录员工ID
       //获取请求头中的jwt令牌, 解析令牌
       String jwt = request.getHeader("token");
       Claims claims = JwtUtils.parseJWT(jwt);
       Integer operateUser = (Integer) claims.get("id");

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

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

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

       //操作方法参数
       Object[] args = joinPoint.getArgs();
       String methodParams = Arrays.toString(args);

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


       long end = System.currentTimeMillis();

       //方法返回值
       String returnValue = JSONObject.toJSONString(result);

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


       //记录操作日志
       OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
       operateLogMapper.insert(operateLog);

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

       return result;
  }

}

代码实现细节: 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。