三、SpringBoot+SpringMVC+MyBatis-Plus_20(笔记)

文章目录

SpringBoot+SpringMVC+MyBatis-Plus

一、简介

1、Spring Boot 是 Pivotal 团队在 Spring 的基础上提供的⼀套全新的开源框架,其⽬的是为了简化 Spring 应⽤的搭建和开发过程

Spring Boot 去除了⼤量的 XML 配置⽂件,简化了复杂的依赖管理。

2、Spring Boot 具有 Spring 的⼀切优秀特性,Spring 能做的事,Spring Boot 都可以做,⽽且使⽤更加简单,功能更加丰富,性能更加稳定⽽健壮。随着近些年来微服务技术的流⾏,Spring Boot 也成了时下炙⼿可热的技术。

3、Spring Boot 集成了⼤量常⽤的第三⽅库配置,Spring Boot 应⽤中这些第三⽅库⼏乎可以是零配置的开箱即⽤(out-of-the-box),⼤部分的 Spring Boot 应⽤都只需要⾮常少量的配置代码(基于 Java 的配置),开发者能够更加专注于业务逻辑。

image-20231004132600137

二、创建 SpringBoot 项⽬

下来我们通过 Intellij IDEA 创建第⼀个 Spring Boot 项⽬。

Intellij IDEA ⼀般可以通过两种⽅式创建 Spring Boot 项⽬:

  • 使⽤ Maven 创建
  • 使⽤ Spring Initializr 创建

Maven ⽅式创建

1、⾸先,⽂件—>新建—>项⽬

image-20231004132833718

2、其次,选择名称、组名和存放路径。

image-20231004132905638

下来,引⼊ Spring Boot 的相关依赖:

<project>
 ...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>最新版本号</version>
<relativePath/>
</parent>
<dependencies>
<!--web相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
 ...
</project>

3、最后,编写 Spring Boot 启动类

创建⼀个名为 XxxApplication 主程序,⽤来启动 Spring Boot 应⽤

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

注意:⼀般 Boot 项⽬的启动名称都为 项⽬名+Application ⽅式。

创建好的项⽬结构如下:

image-20231004133110506

Spring Initializr ⽅式创建

IntelliJ IDEA ⽀持⽤户使⽤ Spring 项⽬创建向导(Spring Initializr )快速地创建⼀个 Spring Boot 项⽬

⾸先,选择项⽬创建向导,分别处理如下操作:

    1. 项⽬名称
    1. 存放位置
    1. JDK设置

image-20231004133227090

注意:如果start.spring.io⽹速不好,可以点击⻮轮按钮修改成国内镜像:https://start.aliyun.io

image-20231004133257316

三、Yaml 概述

Spring Boot 提供了⼤量的⾃动配置,极⼤地简化了 spring 应⽤的开发过程,当⽤户创建了⼀个 Spring Boot 项⽬后,即使不进⾏任何配置,该项⽬也能顺利的运⾏起来。当然,⽤户也可以根据⾃身的需要使⽤配置⽂件修改Spring Boot 的默认设置。

SpringBoot 默认使⽤以下 2 种全局的配置⽂件,其⽂件名是固定的。

  • application.properties
  • application.yml

其中,application.yml 是⼀种使⽤ YAML 语⾔编写的⽂件,它与 application.properties ⼀样,可以在 SpringBoot 启动时被⾃动读取,修改 Spring Boot ⾃动配置的默认值。

注意:当两种⽂件都同时存在时,以 application.properties 为主。

概述

YAML 全称 YAML Ain’t Markup Language,它是⼀种以数据为中⼼的标记语⾔,⽐ XML 和 JSON 更适合作为配置⽂件。

想要使⽤ YAML 作为属性配置⽂件(以 .yml 或 .yaml 结尾),需要将 SnakeYAML 库添加到 classpath 下,Spring Boot 中的 spring-boot-starter-web 或 spring-boot-starter 都对 SnakeYAML 库做了集成, 只要项⽬中引⽤了这两个 Starter 中的任何⼀个,Spring Boot 会⾃动添加 SnakeYAML 库到 classpath 下。

下⾯是⼀个简单的 application.yml 属性配置⽂件。

server:
 port: 8080

语法

YAML 的语法如下:

  • 使⽤缩进表示层级关系。
  • 缩进时不允许使⽤ Tab 键,只允许使⽤空格。
  • 缩进的空格数不重要,但同级元素必须左侧对⻬。
  • ⼤⼩写敏感。
  • Key的冒号和值中间有空格隔开。

例如:

server:
 port: 8082
spring:
 datasource:
 driver-class-name: com.mysql.cj.jdbc.Driver
 url: jdbc:mysql://localhost:3306/guanwei
 type: com.alibaba.druid.pool.DruidDataSource
 username: root
 password: guanwei
mybatis-plus:
 configuration:
 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 map-underscore-to-camel-case: false

数据结构

YAML ⽀持以下三种数据结构:

  • 字⾯量:单个的、不可拆分的值

  • 对象:键值对的集合

  • 数组:⼀组按次序排列的值

1、字⾯量写法

字⾯量是指单个的,不可拆分的值,例如:数字、字符串、布尔值、以及⽇期等。

在 YAML 中,使⽤“key: value”的形式表示⼀对键值对,如 name: 关为。

字⾯量直接写在键值对的“value”中即可,且默认情况下字符串是不需要使⽤单引号或双引号的。

name: 魯迅

2、对象

在 YAML 中,对象可能包含多个属性,每⼀个属性都是⼀对键值对。

YAML 为对象提供了 2 种写法:

  • 普通写法:使⽤缩进表示对象与属性的层级关系。
  • ⾏内写法:使⽤ Map 格式表示对象中属性和值的关系。
  #对象写法
  #第一种写法
  user1:
    name: '杨康'
    id: 1
    gender: '女'
  #第二种写法
  user2:{ 'name': '王五','id': '2','gender': '男' }

3、数组

在 YAML 中,数组也和对象⼀样,存在两种写法,分别也是:普通写法和⾏内写法。

# 普通写法
array1:
 - 张三
 - 李四
 - 王五
# ⾏内写法
array2: [张三,李四,王五]

4、复合结构写法

#复合写法
classroom:
  id: 1
  name: '软件一班'
  teacher:
    id: 1
    name: '张老师'
    desc: '班级管理者'
  students:
    - id: 1
      name: '李四'
      gender: '女'
    - id: 2
      name: '薛哲'
      gender: '男'

四、Spring Boot 使⽤ MVC

1、什么是MVC

MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范。

  • 是将业务逻辑、数据、显示分离的方法来组织代码。
  • MVC主要作用是降低了视图与业务逻辑间的双向偶合。
  • MVC不是一种设计模式,MVC是一种架构模式。当然不同的MVC存在差异。

Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:Value Object(数据Dao) 和 服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。

View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。

Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器做了个调度员的工作。

最典型的MVC就是JSP + Servlet + JavaBean的模式。

image-20231004140956773

2、SpringMVC三层架构

Java SpringMVC的工程结构一般来说分为三层,自下而上是Modle层(模型,数据访问层)、Cotroller层(控制,逻辑控制层)、View层(视图,页面显示层),其中Modle层分为两层:dao层、service层,MVC架构分层的主要作用是解耦。

采用分层架构的好处,普遍接受的是系统分层有利于系统的维护,系统的扩展。就是增强系统的可维护性和可扩展性。

对于Spring这样的框架,(View\Web)表示层调用控制层(Controller),控制层调用业务层(Service),业务层调用数据访问层(Dao)。

image-20231004135900023

Service层:业务层,用来实现业务逻辑。能调用dao层或者service层,返回数据对象DO或者业务对象BO,BO通常由DO转化、整合而来,可以包含多个DO的属性,也可以是只包含一个DO的部分属性。通常为了简便,如果无需转化,service也可以直接返回DO。外部调用(HTTP、RPC)方法也在这一层,对于外部调用来说,service一般会将外部调用返回的DTO转化为BO。是专注业务逻辑,对于其中需要的数据库操作,都通过Dao去实现。主要去负责一些业务处理,比如取得连接、关闭数据库连接、事务回滚,一些复杂的逻辑业务处理就放到service层。

DAO层:负责访问数据库进行数据的操作,取得结果集,之后将结果集中的数据取出封装到VO类对象之后返回给service层。数据层,直接进行数据库的读写操作,返回数据对象DO,DO与数据库表一一对应。Dao的作用是封装对数据库的访问:增删改查,不涉及业务逻辑,只是达到按某个条件获得指定数据的要求。

Cotroller层:叫做控制层,主要的功能是处理用户发送的请求。主要处理外部请求。调用service层,将service层返回的BO/DO转化为DTO/VO并封装成统一返回对象返回给调用方。如果返回数据用于前端模版渲染则返回VO,否则一般返回DTO。不论是DTO还是VO,一般都会对BO/DO中的数据进行一些转化和整合,比如将gender属性中的0转化“男”,1转化为“女”等。controller的功能主要有5点:参数校验、调用service层接口实现业务逻辑、转换业务/数据对象、组装返回对象、异常处理。

View层:叫做显示层,主要是负责现实数据

在实际开发中dao层要先定义出自己的操作标准即标准接口,就是为了解耦合。

image-20231004140300552

1、稍微小一点的公司一般就3层:

dao->数据层

service -> 业务实现

web -> web 接口

2、稍微大一点的公司一般最多就7层:

(日志接入,对查问题有帮助的地方才接入日志)

  • common—>公共方法,公共类,工具类,只有关键地方加日志

  • dao->数据层,不加日志

  • domain->实体类,不加日志

  • rpc->调用接口,出入参加日志

  • sdk-> 给外部提供sdk,例如对外接口,只是提供接口定义,该层没有业务逻辑,真实的sdk逻辑实现是在service层,不加日志

  • service -> 业务实现逻辑,单元测试进行层,日志接入:关键地方可以加, 实现sdk层的接口方法出入参加日志

  • web -> web 接口,对外提供http 接口,一般都有逻辑,http 是直接对 c的,看到的ip 是c端用户的,出入参加日志

    SpringBoot 使⽤ MVC

虽然 SpringMVC 相较于 Servlet 在使⽤上已经有了极⼤的提升,但是在环境搭建、配置⽂件和对注解的使⽤上仍然有些繁琐,有些同学在想有没有⼀种⽅案,可以快速搭建 SpringMVC 环境?

引⼊依赖

这⾥集成了 SpringMVC、Servlet等相关依赖并设置了兼容版本。

<!--SpringMVC相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

3、spring_boot_mvc1

1、建包(controller)

2、创建类——FirstController

//如何请求和响应
// 日志注解
@Slf4j
// IoC注解
@Controller//将FirstController的对象new出来
// 访问路径注解 http://localhost:8080/first
@RequestMapping("/first")//  '/'根目录
public class FirstController {
    // http://localhost:8080/first/a
    @RequestMapping("/a")
    public String a() {//String代表的是跳转的路径/名称
        log.info("用户访问了FirstController的a方法");
        System.out.println("用户访问了FirstController的a方法");
        // /1234.html
        return "1234"; // 跳转的路径,需要和前缀后缀组合
//        return "a/1234";
    }
}

注:@RequestMapping负责访问 return +路径 负责响应

3、创建类——SpringBootMvc1Application

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

image-20230730203046753

image-20230730203641124

注:后端没错,是路径错误

4、创建如下目录

image-20230730204404841

1234.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>1234</h1>
</body>
</html>

application.yml

spring:
  mvc:
    view:
      prefix: /      # 跳转的前缀
      suffix: .html  # 跳转的后缀
顶格敲代码有提示                  

image-20230730204802642

image-20230730204835634

image-20230730204855251

  //带参数的请求
@RequestMapping("/b")
    public String b(String name, int age) {
        log.info("用户访问了FirstController的b方法,参数name是{},age是{}", name, age);//占位符比较轻松
        System.out.println("用户访问了FirstController的b方法,参数name是" + name + ",age是" + age);//字符串拼接,麻烦
        return "1234";
    }

5、直接访问会报错,没有进行赋值

image-20230730205833204

6、给html咋赋值,用? =,访问成功

image-20230730205938267

image-20230730210037075

相比于servlet而言,不仅拿到了值,还把类型转过来了

注意:image-20230730210358331

像以上这种,路径未发生改变,就跳转页面中去了, 这种方式叫做转发

7、重定向书写
//重定向
    @RequestMapping("/c")
    public String c() {
        log.info("重定向问题");
        return "redirect:/1234.html"; // 必须写完整路径
    }

注:相当于将前缀、后缀、字符串响应效果结合起来,拼接到一起去了(必须写完整路径)

image-20230730211052333

image-20230730211107598

8、多参数封装问题
 // 多参数封装问题
    @RequestMapping("/d1")
    public String d1(String ename, String job, int mgr, String hireDate, double sal, double comm, int deptNo) {
        log.info("多个参数分别是:{},{},{},{},{},{},{}", ename, job, mgr, hireDate, sal, comm, deptNo);
        return "redirect:/1234.html";
    }
    
    @RequestMapping("/d2")
    public String d2(Emp emp) {
        log.info("emp:{}", emp);
        return "redirect:/1234.html";
    }

建立bean包,创建Emp类

@Data
public class Emp {
    private Integer empNo;
    private String ename;
    private String job;
    private Integer mgr;
    private String hireDate;
    private Double sal;
    private Double comm;
    private Integer deptNo;
    private Integer state;
}

改写1234.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="first/d1">
    姓名:<input name="ename"/><br/>
    职位:<input name="job"/><br/>
    上司:<input name="mgr"/><br/>
    入职时间:<input name="hireDate" type="date"/><br/>
    工资:<input name="sal"/><br/>
    奖金:<input name="comm"/><br/>
    部门ID:<input name="deptNo"/><br/>
    <button>提交</button>
</form>
<hr/>
<form action="first/d2">
    姓名:<input name="ename"/><br/>
    职位:<input name="job"/><br/>
    上司:<input name="mgr"/><br/>
    入职时间:<input name="hireDate" type="date"/><br/>
    工资:<input name="sal"/><br/>
    奖金:<input name="comm"/><br/>
    部门ID:<input name="deptNo"/><br/>
    <button>提交</button>
</form>
</body>
</html>

第一种方式:

image-20230730211821198

点击提交

image-20230730211907301

又跳转回来了

image-20230730211941958

值从页面传过来了

第二种方式:

image-20230730212214614

点击提交

image-20230730212241655

又跳转回来了

image-20230730212314903

值从页面传过来了 ,是从Emp中按属性名找,但是保证有setter方法,会调用setter方法给赋值,相比较servlet而言,springmvc简单

9、不跳转的情况,springmvc将数据返回回去(ajax)

创建SecondController类

@Slf4j//日志信息
//不跳转情况
@Controller
@RequestMapping("/second")
public class SecondController {
    @RequestMapping("/a")
   //告诉springMVC,返回内容不再是页面名称,而是响应数据
    @ResponseBody
    public String a() {//String代表:响应的数据数据内容
        log.info("SecondController`a method!");
        return "SecondController`a method!";
    }

image-20230730223551117

@ResponseBody:写啥,就显示啥

啥类型都能支持,数字串、数组、集合 、对象

    @RequestMapping("/b")
    @ResponseBody
    public String b() {
        return "1242323";
    }

image-20230730224030588

    @RequestMapping("/c")
    @ResponseBody
    public String[] c() {
        return new String[]{"aa", "bb", "cc", "dd"};
    }

image-20230730224157890

    @RequestMapping("/d")
    @ResponseBody
    public List<String> d() {
        return Arrays.asList("aaa", "bbb", "ccc");
    }

image-20230730224430944

    @RequestMapping("/e")
    @ResponseBody
    public Emp e() {
        return new Emp();
    }

image-20230730224401371

4、spring_boot_project_assort

一、准备工作

建立数据库bookstore

#创建数据库
create database bookstore;
use bookstore;
#创建表
## 分类
create table assort
(
    id int primary key auto_increment,
    name varchar(20) not null unique ,
    `desc` varchar(1000) ,
    state int default 1
);

#录入测试数据
insert into assort values(null,'小说','网络小说前沿阵地',1);
insert into assort values(null,'散文','古今中外散文合集',1);
insert into assort values(null,'诗集','唐诗三百首',1);

导依赖

<dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
    </dependencies>

注意:以前不用数据库链接池时,假如当你想查询所有用户信息的时候,首先,先建立Java与mysql的数据连接通道,java会给mysql发送一个servlet请求,mysql执行完这个请求,会把结果返回给java,java得到这个结果之后,会把连接通道关闭。

如果还有其他请求,光是建立连接和断开连接就花费大量时间,消耗资源,不划算。

在resources中编写application.yml文件

## 必须提供账号,密码,url和驱动类
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/bookstore
    driver-class-name: com.mysql.cj.jdbc.Driver
## 日志 将内容输出到控制台中去
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
二、查询功能

1.写bean包

创建Assort类

@Data
public class Assort implements Serializable {

    private Integer id;
    private String name;
    private String desc;
    private Integer state;
}

2.写mapper包

创建AssortMapper类

@Mapper
public interface AssortMapper {
    @Select("select * from assort where state=1")
    List<Assort> findAllAssort();
    }

3.写service包

创建一个接口AssortService

public interface AssortService {
    JsonResult find();
}

4.写util包

创建一个类JsonResult

// 统一返回格式
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> {
    private Boolean success;
    private Integer code;
    private String error;
    private T data;
}

4.实现类,先创建impl包,在创建AssortServiceImpl类

//先new对象
@Service
public class AssortServiceImpl implements AssortService {
    @Resource//给赋值
    private AssortMapper mapper;

    @Override
    public JsonResult find() {//将list转换成JsonResult格式
        List<Assort> list = mapper.findAllAssort();
        if (list.size() == 0) {
            return new JsonResult(false, 404, "查询不出来数据!", null);
        }
        return new JsonResult(true, 200, null, list);
    } 
}

5.写controller包

创建一个类AssortController

@RestController   // ===@Controller+@ResponseBody
@RequestMapping("/assort")
public class AssortController {
    @Resource
    private AssortService service;

    @RequestMapping("/find")
    public JsonResult find() {//JsonResult什么类型都能返回
        return service.find();
    }
    @RequestMapping("/delete")
    public JsonResult delete(int id){
        return service.delete(id);
    }
}

注:@RestController 的作用:
1、将AssortController的对象new出来
2、所有的方法的返回结果都是内容输出

关系流程:

  • ​ 1、controller是接收页面请求的(页面找controller)
  • ​ 2、(controller)接收到页面请求找service
  • ​ 3、 service 找mapper
  • ​ 4、mapper找数据库

数据传递(若是查询):

  • ​ 1、数据库将数据给mapper
  • ​ 2、mapper将数据给service
  • 3、service将数据给controller
  • ​ 4、controller将数据给页面

6.编写启动类

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

7.打开Postman软件进行测试

image-20230730233837936

8.搭建如下目录

image-20230730234053620

9.打开ELement网站,找样式

10.index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <img src="img/background.webp"/>
            </el-main>
        </el-container>
    </el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
    new Vue({
        el: '#app'
    })
</script>

11.assort.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <el-table :data="tableData">
                    <el-table-column prop="id" label="编号" width="140">
                    </el-table-column>
                    <el-table-column prop="name" label="分类名" width="140">
                    </el-table-column>
                    <el-table-column prop="desc" label="备注" width="120">
                    </el-table-column>
                    <el-table-column prop="state" label="状态">
                    </el-table-column>
                </el-table>
            </el-main>
        </el-container>
    </el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
    new Vue({
        el: '#app',
        data() {
            return {
                //这里定义变量
                tableData: []
            }
        },
        methods: {
            loadAssortData() {
                let _this = this
                // 从后端获取到数据,把数据给tableData赋值
                axios.get('assort/find')
                    .then((response) => {
                        // 获取到后端返回的数据
                        let d = response.data
                        _this.tableData = d.data
                    })
            }
        },
        created(){
            // 页面加载后执行
            this.loadAssortData()
        }
    })
</script>

总结:

前端的书写步骤:

1、引入Vue;

4、在一个div标签中加入样式

5、new Vue对象

​ 1、确立绑定

 	el: '#app'

​ 2、定义变量区域

data() {
        return {
            //这里定义变量
            tableData: []
        }

​ 3、定义函数

methods: {
            loadAssortData() {
                let _this = this
                // 从后端获取到数据,把数据给tableData赋值
                axios.get('assort/find')
                    .then((response) => {
                        // 获取到后端返回的数据
                        let d = response.data
                        _this.tableData = d.data
                    })
            }
        },

​ 定义一个函数:

​ 1、从后端中获取到数据,把数据赋给已定义的变量

​ 2、通过axios发送请求和返回结果

image-20231004154113316

​ 4、调用函数

 created(){
            // 页面加载后执行
            this.loadAssortData()
        }

​ 注意:所有的函数或者变量想要被调用,都必须以this.___开头

12、先看Postman好没好,确定后端有没有问题

image-20230731000707526

​ 再看浏览器,右键检查,查看报错

image-20230731001138923

image-20230731001421788

image-20230731001447948

三、删除功能

后端部分

mapper包

@Mapper
public interface AssortMapper {
    @Update("update assort set state=0 where id=#{id}")
    void deleteAssort(int id);
}

service包

public interface AssortService {
    JsonResult delete(int id);
}
@Service
public class AssortServiceImpl implements AssortService {
    @Resource
    private AssortMapper mapper;

    @Override
    public JsonResult delete(int id) {
        mapper.deleteAssort(id);
        return new JsonResult(true,200,null,null);
    }
}

controller包

@RestController   // ===@Controller+@ResponseBody
@RequestMapping("/assort")
public class AssortController {
    @Resource
    private AssortService service;

    @RequestMapping("/delete")
    public JsonResult delete(int id){
        return service.delete(id);
    }
}

前端部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <el-table :data="tableData">
                    <el-table-column prop="id" label="编号" width="60">
                    </el-table-column>
                    <el-table-column prop="name" label="分类名" width="120">
                    </el-table-column>
                    <el-table-column prop="desc" label="备注" width="190">
                    </el-table-column>
                    <el-table-column prop="state" label="状态" width="60">
                    </el-table-column>
                    <el-table-column label="操作">
                        <template slot-scope="scope">
                            <el-button
                                    size="mini" @click="showUpdateDialog(scope.row)">编辑
                            </el-button>
                            <el-button
                                    size="mini"
                                    type="danger" @click="deleteAssort(scope.row)">删除
                            </el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </el-main>
        </el-container>
    </el-container>

    <!--修改的模态框-->
    <el-dialog title="编辑分类" :visible.sync="showUpdateAssortDialog">
        {{errorMsg}}
        <el-form :model="updateForm">
            <el-form-item label="分类名称" :label-width="formLabelWidth">
                <el-input v-model="updateForm.name" autocomplete="off" @blur="checkNameIsExist"></el-input>
            </el-form-item>
            <el-form-item label="备注信息" :label-width="formLabelWidth">
                <el-input v-model="updateForm.desc" autocomplete="off"></el-input>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="showUpdateAssortDialog=false">取 消</el-button>
            <el-button type="primary" @click="updateAssort">确 定</el-button>
        </div>
    </el-dialog>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
    new Vue({
        el: '#app',
        data() {
            return {
                //这里定义变量
                tableData: [],
        },
        methods: {
            loadAssortData() {
                let _this = this
                // 从后端获取到数据,把数据给tableData赋值
                axios.get('assort/find')
                    .then((response) => {
                        // 获取到后端返回的数据
                        let d = response.data
                        _this.tableData = d.data
                    })
            },
            deleteAssort(row) {
                let _this = this
                this.$confirm('此操作将永久删除该分类, 是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                    // 点击确定了 调用后端的删除操作并传递id,成功后刷新页面
                    // axios.get('assort/delete?id='+row.id)
                    axios.get('assort/delete', {
                        params: {
                            id: row.id
                        }
                    }).then((response) => {
                        //_this.tableData = response.data.data
                        //console.log(_this.tableData)
                        _this.loadAssortData()
                        this.$message({
                            type: 'success',
                            message: '已成功删除'
                        });
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });
                });
            },
        created() {
            // 页面加载后执行
            this.loadAssortData()
        }
    })
</script>
四、修改功能

Vue与Axios的作用

1、Vue:对前端页面数据处理

2、Axios:像中介,Vue想要数据不能通过controller,而是通过axios找到数据,再给vue的某个变量赋值。

后端部分

Mapper包

@Mapper
public interface AssortMapper {

    @Update("update assort set name=#{name},`desc`=#{desc} where id=#{id}")
    void updateAssort(Assort assort);
}

service包

public interface AssortService {
   
    JsonResult update(Assort assort);
}
@Service
public class AssortServiceImpl implements AssortService {
    @Resource
    private AssortMapper mapper;

    @Override
    public JsonResult update(Assort assort) {
        mapper.updateAssort(assort);
        return new JsonResult(true, 200, null, null);
    }
}

controller包

@RestController   // ===@Controller+@ResponseBody
@RequestMapping("/assort")
public class AssortController {
    @Resource
    private AssortService service;

    @RequestMapping("/update")
    public JsonResult update(Assort assort) {
        return service.update(assort);
    }
}

前端部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <el-table :data="tableData">
                    <el-table-column prop="id" label="编号" width="60">
                    </el-table-column>
                    <el-table-column prop="name" label="分类名" width="120">
                    </el-table-column>
                    <el-table-column prop="desc" label="备注" width="190">
                    </el-table-column>
                    <el-table-column prop="state" label="状态" width="60">
                    </el-table-column>
                    <el-table-column label="操作">
                        <template slot-scope="scope">
                            <el-button
                                    size="mini" @click="showUpdateDialog(scope.row)">编辑
                            </el-button>
                            <el-button
                                    size="mini"
                                    type="danger" @click="deleteAssort(scope.row)">删除
                            </el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </el-main>
        </el-container>
    </el-container>

    <!--修改的模态框-->
    <el-dialog title="编辑分类" :visible.sync="showUpdateAssortDialog">
        {{errorMsg}}
        <el-form :model="updateForm">
            <el-form-item label="分类名称" :label-width="formLabelWidth">
                <el-input v-model="updateForm.name" autocomplete="off" @blur="checkNameIsExist"></el-input>
            </el-form-item>
            <el-form-item label="备注信息" :label-width="formLabelWidth">
                <el-input v-model="updateForm.desc" autocomplete="off"></el-input>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="showUpdateAssortDialog=false">取 消</el-button>
            <el-button type="primary" @click="updateAssort">确 定</el-button>
        </div>
    </el-dialog>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
    new Vue({
        el: '#app',
        data() {
            return {
                //这里定义变量
                tableData: [],
                showUpdateAssortDialog: false,
                formLabelWidth: '120px',
                updateForm: {
                    id: '',
                    name: '',
                    desc: ''
                },
                isExist: false,
                errorMsg: ''
            }
        },
        methods: {
            loadAssortData() {
                let _this = this
                // 从后端获取到数据,把数据给tableData赋值
                axios.get('assort/find')
                    .then((response) => {
                        // 获取到后端返回的数据
                        let d = response.data
                        _this.tableData = d.data
                    })
            },
            deleteAssort(row) {
                let _this = this
                this.$confirm('此操作将永久删除该分类, 是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                    // 点击确定了 调用后端的删除操作并传递id,成功后刷新页面
                    // axios.get('assort/delete?id='+row.id)
                    axios.get('assort/delete', {
                        params: {
                            id: row.id
                        }
                    }).then((response) => {
                        //_this.tableData = response.data.data
                        //console.log(_this.tableData)
                        _this.loadAssortData()
                        this.$message({
                            type: 'success',
                            message: '已成功删除'
                        });
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });
                });
            },
            showUpdateDialog(row) {
				//给模态框的属性赋值
                this.updateForm.name = row.name//修改后的值
                this.updateForm.desc = row.desc//修改后的值
                this.updateForm.id = row.id//修改后的值
                this.showUpdateAssortDialog = true
            },
            updateAssort() {
                if (this.isExist) {
                    let _this = this
                    axios.get('assort/update', {//修改前
                        params: {
                            id: _this.updateForm.id,
                            name: _this.updateForm.name,
                            desc: _this.updateForm.desc
                        }
                    }).then((response) => {//修改后
                        _this.loadAssortData()//重查
                        _this.showUpdateAssortDialog = false//不显示
                        this.$message({
                            type: 'success',
                            message: '已成功修改'
                        });
                    })
                }
            },
        created() {
            // 页面加载后执行
            this.loadAssortData()
        }
    })
</script>
五、修改优化

注意:在修改之前,要进行数据校验,判断能不能修改

后端部分

mapper包

@Mapper
public interface AssortMapper {

    @Select("select count(0) from assort where name=#{name}")
    int checkNameIsExist(String name);
}

service包

public interface AssortService {
    JsonResult find();

    JsonResult checkNameIsExist(String name);
}
@Service
public class AssortServiceImpl implements AssortService {
    @Resource
    private AssortMapper mapper;

    @Override
    public JsonResult checkNameIsExist(String name) {
        int row = mapper.checkNameIsExist(name);
        if (row > 0) {
            return new JsonResult(false, 500, "分类名称已被使用", null);
        }
        return new JsonResult(true, 200, null, null);
    }
}

controller包

@RestController   // ===@Controller+@ResponseBody
@RequestMapping("/assort")
public class AssortController {
    @Resource
    private AssortService service;

    @RequestMapping("/exist")
    public JsonResult exist(String name) {
        return service.checkNameIsExist(name);
    }
}

前端部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <el-table :data="tableData">
                    <el-table-column prop="id" label="编号" width="60">
                    </el-table-column>
                    <el-table-column prop="name" label="分类名" width="120">
                    </el-table-column>
                    <el-table-column prop="desc" label="备注" width="190">
                    </el-table-column>
                    <el-table-column prop="state" label="状态" width="60">
                    </el-table-column>
                    <el-table-column label="操作">
                        <template slot-scope="scope">
                            <el-button
                                    size="mini" @click="showUpdateDialog(scope.row)">编辑
                            </el-button>
                            <el-button
                                    size="mini"
                                    type="danger" @click="deleteAssort(scope.row)">删除
                            </el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </el-main>
        </el-container>
    </el-container>

    <!--修改的模态框-->
    <el-dialog title="编辑分类" :visible.sync="showUpdateAssortDialog">
        {{errorMsg}}
        <el-form :model="updateForm">
            <el-form-item label="分类名称" :label-width="formLabelWidth">
                <el-input v-model="updateForm.name" autocomplete="off" @blur="checkNameIsExist"></el-input>
            </el-form-item>
            <el-form-item label="备注信息" :label-width="formLabelWidth">
                <el-input v-model="updateForm.desc" autocomplete="off"></el-input>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="showUpdateAssortDialog=false">取 消</el-button>
            <el-button type="primary" @click="updateAssort">确 定</el-button>
        </div>
    </el-dialog>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
    new Vue({
        el: '#app',
        data() {
            return {
                //这里定义变量
                tableData: [],
                showUpdateAssortDialog: false,
                formLabelWidth: '120px',
                updateForm: {
                    id: '',
                    name: '',
                    desc: ''
                },
                isExist: false,
                errorMsg: ''
            }
        },
        methods: {
            loadAssortData() {
                let _this = this
                // 从后端获取到数据,把数据给tableData赋值
                axios.get('assort/find')
                    .then((response) => {
                        // 获取到后端返回的数据
                        let d = response.data
                        _this.tableData = d.data
                    })
            },
            deleteAssort(row) {
                let _this = this
                this.$confirm('此操作将永久删除该分类, 是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                    // 点击确定了 调用后端的删除操作并传递id,成功后刷新页面
                    // axios.get('assort/delete?id='+row.id)
                    axios.get('assort/delete', {
                        params: {
                            id: row.id
                        }
                    }).then((response) => {
                        //_this.tableData = response.data.data
                        //console.log(_this.tableData)
                        _this.loadAssortData()
                        this.$message({
                            type: 'success',
                            message: '已成功删除'
                        });
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });
                });
            },
            showUpdateDialog(row) {
				//给模态框的属性赋值
                this.updateForm.name = row.name//修改后的值
                this.updateForm.desc = row.desc//修改后的值
                this.updateForm.id = row.id//修改后的值
                this.showUpdateAssortDialog = true
            },
            updateAssort() {
                if (this.isExist) {
                    let _this = this
                    axios.get('assort/update', {//修改前
                        params: {
                            id: _this.updateForm.id,
                            name: _this.updateForm.name,
                            desc: _this.updateForm.desc
                        }
                    }).then((response) => {//修改后
                        _this.loadAssortData()//重查
                        _this.showUpdateAssortDialog = false//不显示
                        this.$message({
                            type: 'success',
                            message: '已成功修改'
                        });
                    })
                }
            },
            checkNameIsExist() {
                // 获取到名称
                let name = this.updateForm.name
                let _this = this
                // 发送给后端
                axios.get('assort/exist', {
                    params: {
                        name: name
                    }
                }).then((response) => {
                    let success = response.data.success
                    if (success) {
                        _this.isExist = true
                        _this.errorMsg = ''
                    } else {
                        _this.isExist = false
                        _this.errorMsg = response.data.error
                    }
                })
            }
        },
        created() {
            // 页面加载后执行
            this.loadAssortData()
        }
    })
</script>

测试结果

修改失败

image-20231004170954602

修改成功

image-20231004171106967

5、⽂件结构

默认情况下,Spring Boot 将从类路径或 ServletContext 的根⽬录中的名为 src/main/resources/static 的⽬录提供静态内容。

在静态内容当中我们可以放js,css样式等⽂件,除Web服务,我们还可以使⽤ Spring MVC 来提供动态 HTML 内容。Spring MVC ⽀持各种模板技术,包括Thymeleaf,FreeMarker 和 JSP。当然 SpringBoot 不推荐⽤ JSP 来作为视图层,通常情况我们把模板放在 src/main/resources/templates下。

以下⽬录就是典型的模板与静态资源⽬录结构:

image-20231004171624028

6、相关注解

image-20231004171714083

注:查询推荐使用@GetMapping,登录查询建议使用PostMapping

image-20231004175419942

7、配置⽂件

server:
 port: 8081  # 端口号
 servlet:
 context-path: /dailyblue  # 加路径
# 起服务名
spring:
 application:
 name: dailyblue

启动并访问

启动启动类,并打开浏览器输⼊路径访问:

image-20231004175118394

8、spring_boot_mvc2

项目结构

image-20231004175955053

1、导依赖,父项目中已经导入mvc依赖

2、 Controller包——>FirstController类

@Slf4j//日志
@RestController//new对象\方法的返回结果都是内容输出
@RequestMapping("/first")//二级目录,以便 查找
public class FirstController {

    // 查询使用get请求 http://localhost:8080/first
    @GetMapping//只能用一次
    public String find() {
        log.info("FirstController`find method!");
        return "FirstController`find method!";
    }

    @GetMapping("/page")//分页(第一种写法)
    public String find1(int page) {
        log.info("FirstController`find1 method!page:{}", page);
        return "FirstController`find1 method!page:{}" + page;
    }

    // http://localhost:8080/first/1
    @GetMapping("/{page}")//分页(第二种写法)
    public String find2(@PathVariable int page) {
        log.info("FirstController`find2 method!page:{}", page);
        return "FirstController`find2 method!page:{}" + page;
    }

    // http://localhost:8080/first/search/admin
    @GetMapping("/search/{name}")//模糊查询 根据名字查
    public String find3(@PathVariable String name) {
        log.info("FirstController`find3 method!name:{}", name);
        return "FirstController`find3 method!name:{}" + name;
    }

    @DeleteMapping("/{id}")//删除  @PathVariable指的是:不再是?的传递,而是拼接
    public String delete(@PathVariable int id) {//@PathVariable 从请求路径获取参数值的注解
        log.info("FirstController`delete method!id:{}", id);
        return "FirstController`delete method!id:{}" + id;
    }

    @PutMapping//只能用一次
    public String update(Student1 student) {
        log.info("FirstController`update method!student:{}", student);
        return "FirstController`update method!student:{}" + student;
    }

    @PostMapping//只能用一次
    public String save(Student1 student) {
        log.info("FirstController`save method!student:{}", student);
        return "FirstController`save method!student:{}" + student;
    }

    @GetMapping("/a")
    public String a(@RequestParam(defaultValue = "1", name = "abc") int page) {//@RequestParam指的是:防止为空,要有默认值
        log.info("FirstController`a method!page:{}", page);
        return "FirstController`a method!page:" + page;
    }
}


注意:1@PostMapping@DeleteMapping@PutMapping不能通过超链接,超链接只能是get请求
    2、post/put(Queryget(Body)

3、编写SpringBootMVC2Application

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

4.创建bean包,写Student1类

@ConfigurationProperties(prefix = "student1")
@Data
@Component
public class Student1 {
    private Integer id;
    private String name;
    private String gender;
}

写Rooml类

@Data
@Component
public class Room implements Serializable {
    private String name;
    private String address;
}

5、测试及结果

​ 1.

image-20230731193809897

image-20230731193859780

​ 2.

image-20230731193957686

​ 3.

image-20230731194155590

  1. image-20230731195508232

​ 5.

image-20230731195558983

​ 6.

image-20230731195627147

​ 7.

image-20230731195850535

springmvc 完整运行流程

image-20230731154512709

  • 中央处理器:中转和转发
  • 处理映射器:寻址
  • 处理器适配器:执行Controller
  • 视图解析器:负责解析你的视图路径

9、Web启动器

Spring MVC 是 Spring 提供的⼀个基于 MVC 设计模式的轻量级 Web 开发框架,其本身就是 Spring 框架的⼀部分,可以与 Spring ⽆缝集成,性能⽅⾯具有先天的优越性,是当今业界最主流的 Web 开发框架之⼀。

Spring Boot 是在 Spring 的基础上创建⼀款开源框架,它提供了 spring-boot-starter-web(Web 场景启动器) 来为 Web 开发予以⽀持。spring-boot-starter-web 为我们提供了嵌⼊的 Servlet 容器以及 SpringMVC 的依赖,并为 Spring MVC 提供了⼤量⾃动配置,可以适⽤于⼤多数 Web 开发场景。

Spring Boot 为 Spring MVC 提供了⾃动配置,并在 Spring MVC 默认功能的基础上添加了以下特性:

  • 引⼊了 ContentNegotiatingViewResolver 和 BeanNameViewResolver(视图解析器)

  • 对包括 WebJars 在内的静态资源的⽀持

  • ⾃动注册 Converter、GenericConverter 和 Formatter (转换器和格式化器)

  • 对 HttpMessageConverters 的⽀持(Spring MVC 中⽤于转换 HTTP 请求和响应的消息转换器)

  • ⾃动注册 MessageCodesResolver(⽤于定义错误代码⽣成规则)

  • ⽀持对静态⾸⻚(index.html)的访问

  • ⾃动使⽤ ConfigurableWebBindingInitializer

    只要我们在 Spring Boot 项⽬中的 pom.xml 中引⼊了 spring-boot-starter-web ,即使不进⾏任何配置,也可以直接使⽤ Spring MVC 进⾏ Web 开发。

    注意:由于 spring-boot-starter-web 默认替我们引⼊了核⼼启动器 spring-boot-starter,因此,当 SpringBoot 项⽬中的 pom.xml 引⼊了 spring-boot-starter-web 的依赖后,就⽆须在引⼊ spring-boot-starter 核⼼启动器的依赖了。

五、配置绑定

所谓“配置绑定”就是把配置⽂件中的值与 JavaBean 中对应的属性进⾏绑定。通常,我们会把⼀些配置信息(例如,数据库配置)放在配置⽂件中,然后通过 Java 代码去读取该配置⽂件,并且把配置⽂件中指定的配置封装到JavaBean(实体类)中。

SpringBoot 提供了以下 2 种⽅式进⾏配置绑定:

  • 使⽤ @ConfigurationProperties 注解
  • 使⽤ @Value 注解

1、@ConfigurationPreperties注解

通过 Spring Boot 提供的 @ConfigurationProperties 注解,可以将全局配置⽂件中的配置数据绑定到 JavaBean中。

下⾯演示如何通过 @ConfigurationProperties 注解进⾏配置绑定。

application.yml

创建一个包static,编写application.yml文件,添加以下⾃定义属性。

student1:
  id: 1
  name: '张三'
  gender: '男'
  age: 19
  loves: [ '足球','游戏','网球' ]
  room:
    name: '软件一班'
    address: '2#409'

实体类

@ConfigurationProperties:从yml文件中读取数据(批量读),然后将数据封装到Bean包中的Studnet类中

/**
* 将配置⽂件中配置的每⼀个属性的值,映射到这个组件中
*
* @ConfigurationProperties:告诉 SpringBoot 将本类中的所有属性和配置⽂件中相关的配置进⾏绑定
* prefix = "student":配置⽂件中哪个下⾯的所有属性进⾏⼀⼀映射
*
* 只有这个组件是容器中的组件,才能使⽤容器提供的@ConfigurationProperties功能
*/
@Component
@ConfigurationProperties(prefix = "student")
@PropertySource(value = "classpath:student.properties")
@Data
public class Student implements Serializable {
    private Integer id;
    private String name;
    private String gender;
    private String[] loves;
    private Room room;

}

注意

只有在容器中的组件,才会拥有 SpringBoot 提供的强⼤功能。如果我们想要使⽤

@ConfigurationProperties 注解进⾏配置绑定,那么⾸先就要保证该对 JavaBean 对象在 IoC 容器中,所以需要⽤到 @Component 注解来添加组件到容器中。

JavaBean 上使⽤了注解 @ConfigurationProperties(prefix = “person”) ,它表示将这个 JavaBean 中的所有属性与配置⽂件中以“person”为前缀的配置进⾏绑定。

控制层类

创建一个SecondController

@RequestMapping("/second")
@RestController
public class SecondController {
    @Resource
    private Student student;

    @GetMapping
    public Student a() {
        return student;
    }
}

启动并查看

image-20230731202724781

问题

image-20231004184252678

如何解决呢?

在 pom.xml 中引⼊如下注解:

<!--        是一个可选的依赖项,主要用于在开发阶段自动检测和处理@Configuration类中的属性。-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

2、@Value注解

当我们只需要读取配置⽂件中的某⼀个配置时,可以通过 @Value 注解获取。

创建一个ThirdController,l利用@Value注解(一次赋一个值)将yml文件中的数据读取出来

@RestController
@RequestMapping("/third")
public class ThirdController {
    @Value("${student1.id}")
    private Integer id;
    @Value("${student1.name}")
    private String name;
    @Value("${student1.room.name}")
    private String className;

    @GetMapping

    public String a() {
        return "id:" + id + ",name:" + name + ",className:" + className;
    }
}

image-20230731203319953

3、区别

@Value 和 @ConfigurationProperties 注解都能读取配置⽂件中的属性值并绑定到 JavaBean 中,但两者存在以下不同。

使⽤位置不同

@ConfigurationProperties:标注在 JavaBean 的类名上。

@Value:标注在 JavaBean 的属性上。

功能不同

@ConfigurationProperties:⽤于批量绑定配置⽂件中的配置。

@Value:只能⼀个⼀个的指定需要绑定的配置。

松散绑定⽀持不同

@ConfigurationProperties:⽀持松散绑定(松散语法),例如实体类 Student 中有⼀个属性为 firstName,那么配置⽂件中的属性名⽀持以下写法:

  • student.firstName
  • student.first-name
  • student.first_name
  • STUDENT_FIRST_NAME

@Vaule:不⽀持松散绑定。

SpEL ⽀持不同

@ConfigurationProperties:不⽀持 SpEL 表达式。

@Value:⽀持 SpEL 表达式。

复杂类型封装

@ConfigurationProperties:⽀持所有类型数据的封装,例如 Map、List、Set、以及对象等;

@Value:只⽀持基本数据类型的封装,例如字符串、布尔值、整数等类型。

应⽤场景不同

@Value 和 @ConfigurationProperties 两个注解之间,并没有明显的优劣之分,它们只是适合的应⽤场景不同⽽已。

若只是获取配置⽂件中的某项值,则推荐使⽤ @Value 注解。

若专⻔编写了⼀个 JavaBean 来和配置⽂件进⾏映射,则建议使⽤ @ConfigurationProperties 注解。

我们在选⽤时,根据实际应⽤场景选择合适的注解能达到事半功倍的效果。

4、@PropertySource 注解

  • 如果将所有的配置都集中到 application.properties 或 application.yml 中,那么这个配置⽂件会⼗分的臃肿且难以维护。
  • 因此我们通常会将与 Spring Boot ⽆关的配置(例如⾃定义配置)提取出来,写在⼀个单独的配置⽂件中,并在对应的 JavaBean 上使⽤ @PropertySource 注解指向该配置⽂件
  • 将与 student 相关的⾃定义配置移动到 src/main/resources 下的 student.properties中(注意,必须把 application.properties 或 application.yml 中的相关配置删除

1、编写上述student.properties文件

student.id=1
student.name=张三
student.sex=男
student.age=19
student.loves=足球,游戏,网球
student.room.name=软件一班
student.room.address=南2楼3楼302

2、删除yml文件

3、Student 类中引⼊@PropertySource 注解即可

@Component
@ConfigurationProperties(prefix = "student")
@PropertySource(value = "classpath:student.properties")
@Data
public class Student implements Serializable {
    private Integer id;
    private String name;
    private String gender;
    private String[] loves;
    private Room room;

}

4、启动查看

image-20230731204105524

注意

  • @PropertySource 只对 properties ⽂件可以进⾏加载,但对于 yml 或者 yaml 不能⽀持。
  • 如果读取中⽂时出现乱码,使⽤ @PropertySource(value = “classpath:student.properties”,encoding=“UTF-8”)

六、导⼊Spring配置

默认情况下,Spring Boot 中是不包含任何的 Spring 配置⽂件的,即使我们⼿动添加 Spring 配置⽂件到项⽬中,也不会被识别。

image-20231004185320681

那么 Spring Boot 项⽬中真的就⽆法导⼊ Spring 配置吗?答案是否定的。

Spring Boot 为了我们提供了以下 2 种⽅式来导⼊ Spring 配置:

  • 使⽤ @ImportResource 注解加载 Spring 配置⽂件
  • 使⽤全注解⽅式加载 Spring 配置

1、@ImportResource注解

在主启动类上使⽤ @ImportResource 注解可以导⼊⼀个或多个 Spring 配置⽂件,并使其中的内容⽣效。

创建类

在 pojo 包下创建⼀个类 DemoA ,代码如下:

@Data
public class DemoA {
private String name;
}

引⼊ spring.xml

在 resource ⽂件夹下创建 spring.xml 配置⽂件,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="da" class="com.dailyblue.java.spring.boot.pojo.DemoA">
<property name="name" value="关玉"/>
</bean>
</beans>

控制层

控制层的类注⼊刚才的 DemoA,代码如下:

@RestController
@RequestMapping("/first")
public class FirstController {
@Resource
private DemoA da;
@GetMapping("/c")
public DemoA c() {
return da;
 }
}

启动类

修改启动类,引⼊ @ImportResource 注解,代码如下:

@SpringBootApplication
@ImportResource(locations = {"classpath:/spring.xml"})
public class DailyblueBootApplication {
public static void main(String[] args) {
SpringApplication.run(DailyblueBootApplication.class, args);
 }
}

2、全注解⽅式加载Spring配置

Spring Boot 推荐我们使⽤全注解的⽅式加载 Spring 配置,其实现⽅式如下:

  1. 使⽤ @Configuration 注解定义配置类,替换 Spring 的配置⽂件。

  2. 配置类内部可以包含有⼀个或多个被 @Bean 注解的⽅法,这些⽅法会被AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类扫描,构建 bean 定义(相当于 Spring 配置⽂件中的标签),⽅法的返回值会以组件的形式添加到容器中,组件的 id 就是⽅法名。

代码部分很简单,这⾥仅仅书写配置加载类:

@Configuration
public class DemoB {
@Bean
public DemoC getDemoC(){
return new DemoC();
 }
}

七、定制Spring MVC

Spring Boot 抛弃了传统 xml 配置⽂件,通过配置类(标注 @Configuration 的类,相当于⼀个 xml 配置⽂件)以JavaBean 形式进⾏相关配置

Spring Boot 对 Spring MVC 的⾃动配置可以满⾜我们的⼤部分需求,但是我们也可以通过⾃定义配置类并实现WebMvcConfigurer 接⼝的⽅式来定制 Spring MVC 配置,例如拦截器、格式化程序、视图控制器等等。

SpringBoot 1.5 及以前是通过继承 WebMvcConfigurerAdapter 抽象类来定制 Spring MVC 配置的,但在SpringBoot 2.0 后,这个抽象类就被弃⽤了,改为实现 WebMvcConfigurer 接⼝来定制 Spring MVC 配置

WebMvcConfigurer 是⼀个基于 Java 8 的接⼝,该接⼝定义了许多与 Spring MVC 相关的⽅法,其中⼤部分⽅法都是 default 类型的,且都是空实现。因此我们只需要定义⼀个配置类实现 WebMvcConfigurer 接⼝,并重写相应的⽅法便可以定制 Spring MVC 的配置。

image-20231005105519309

在 Spring Boot 项⽬中,我们可以通过以下 2 中形式定制 Spring MVC:

  • 扩展 Spring MVC
  • 全⾯接管 Spring MVC

下⾯,我们分别对这两种定制 Spring MVC 的形式进⾏介绍。

1、扩展 Spring MVC

如果 Spring Boot 对 Spring MVC 的⾃动配置不能满⾜我们的需要,我们还可以通过⾃定义⼀个WebMvcConfigurer 类型的配置类,来扩展 Spring MVC。

这样不但能够保留 Spring Boot 对 Spring MVC 的⾃动配置,享受 Spring Boot ⾃动配置带来的便利,还能额外增加⾃定义的 Spring MVC 配置。

注意:配置类标注 @Configuration,但不标注 @EnableWebMvc 注解。

创建静态⻚⾯

在 static ⽂件夹下创建 login.html 和 index.html ⻚⾯

image-20231005105859354

配置类

创建⼀个类实现 WebMvcConfigurer 接⼝

@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//当访问 "/" 或 "/index.html" 时,都直接跳转到登陆⻚⾯
registry.addViewController("/").setViewName("login.html");
registry.addViewController("/index.html").setViewName("login.html");
 }
}

效果

image-20231005110044092

2、全⾯接管Spring MVC

在⼀些特殊情况下,我们可能需要抛弃 Spring Boot 对 Spring MVC 的全部⾃动配置,完全接管 Spring MVC。

此时我们可以⾃定义⼀个 WebMvcConfigurer 类型的配置类,并在该类上标注 @EnableWebMvc 注解,来实现完全接管 Spring MVC。

注意:完全接管 Spring MVC 后,Spring Boot 对 Spring MVC 的⾃动配置将全部失效

就修改⼀个地⽅:

@Configuration
@EnableWebMvc
public class SpringMVCConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//当访问 "/" 或 "/index.html" 时,都直接跳转到登陆⻚⾯
registry.addViewController("/").setViewName("login.html");
registry.addViewController("/index.html").setViewName("login.html");
 }
}

引⼊ @EnableWebMvc 注解

⻚⾯效果

image-20231005110303724

控制台

image-20231005110322087

直接访问 login.html

image-20231005110340100

Spring Boot 能够访问位于静态资源⽂件夹中的静态⽂件,这是在 Spring Boot 对 Spring MVC 的默认⾃动配置中定义的。当我们全⾯接管 Spring MVC 后,Spring Boot 对 Spring MVC 的默认配置都会失效,此时再访问静态资源⽂件夹中的静态资源就会报错了

八、Spring Boot 拦截器

我们对拦截器并不陌⽣,⽆论是 Struts 2(一个优秀、开源、免费的MVC框架) 还是 Spring MVC 中都提供了拦截器功能,它可以根据 URL 对请求进⾏拦截

主要应⽤于登陆校验、权限验证、乱码解决、性能监控和异常处理等功能上。

Spring Boot 同样提供了拦截器功能。

在 Spring Boot 项⽬中,使⽤拦截器功能通常需要以下 3 步:

  1. 定义拦截器

  2. 注册拦截器

  3. 指定拦截规则(如果是拦截所有,静态资源也会被拦截)

spring_boot_mvc3

项目结构

image-20231005133159375

过滤器(Filter)

实际上就是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。

实现方式

1、实现Filter接口

2、继承HttpFilter(简单)

controller包

@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
    //返回的数据类型和处理乱码
//    @GetMapping(produces = "application/json;charset = UTF-8")
    @GetMapping
    public String a() {
        log.info("This is FirstController`a method!");
        return "This is FirstController`a method!";
    }
}

filter包

@WebFilter("/*")
//@Component
/*
此处不能使用@Component,在SpringMVC的流程中有中央控制器(DispatcherServlet),它是servlet,
而springmvc是在servlet基础上进行的操作,Filter和Servlet是同一级别,在servlet中去控制filter做不到,
因为filter比servlet优先级更高,它是对servlet拦截,在servlet之前执行,所以不能new对象。
 */
@Slf4j
public class FirstFilter extends HttpFilter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("FirstFilter被初始化了!");
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("FirstFilter的请求方法执行了");
        chain.doFilter(request, response);//放行
        log.info("FirstFilter的响应方法执行了");
    }
}

启动类

@ServletComponentScan("com.zgh.filter")
@SpringBootApplication
public class SpringBootMVC3Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootMVC3Application.class, args);
    }
}

注意:此处如果filter没有被Tomcat加载上,就加入扫包注解@ServletComponentScan(“包路径”)

​ spring不管理对象,Tomcat进行管理(SpringBoot在启动的时候会有一个Tomcat)

效果

image-20231005121930976

image-20231005121903401

SpringMVC中也有一个类似Filter的过滤器,叫Interceptor

1、定义拦截器

在 Spring Boot 中定义拦截器⼗分的简单,只需要创建⼀个拦截器类,并实现 HandlerInterceptor 接⼝即可。

HandlerInterceptor 接⼝中定义以下 3 个⽅法,如下表。

image-20231005110954208

注:postHandle、afterCompletion的共同点和区别

相同点:都在响应之后执行

区别:postHandle:遇到异常、拦截就不会执行

​ afterCompletion:始终执行

2、注册拦截器

创建⼀个实现了 WebMvcConfigurer 接⼝的配置类(使⽤了 @Configuration 注解的类),重写addInterceptors() ⽅法,并在该⽅法中调⽤ registry.addInterceptor() ⽅法将⾃定义的拦截器注册到容器中。

在配置类 SpringMvcConfig 中,添加以下⽅法注册拦截器,代码如下:

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(你的拦截器类对象);
 }
}

3、指定拦截规则

修改 SpringMvcConfig 配置类中 addInterceptors() ⽅法的代码,继续指定拦截器的拦截规则,代码如下:

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(你的拦截器类对象).order(1).addPathPatterns("/**") //拦截所
有请求,包括静态资源⽂件
 .excludePathPatterns("/", "/login", "/index.html", "/user/login",
"/css/**", "/images/**", "/js/**", "/fonts/**"); //放⾏登录⻚,登陆操作,静态资源
 }
}

在指定拦截器拦截规则时,调⽤了⼏个⽅法,这⼏个⽅法的说明如下:

  • addPathPatterns:该⽅法⽤于指定拦截路径,例如拦截路径为“/**”,表示拦截所有请求,包括对静态资源的请求。
  • excludePathPatterns:该⽅法⽤于排除拦截路径,即指定不需要被拦截器拦截的请求
  • order:多个拦截器执⾏顺序优先级,值越⼩优先级越⾼

⾄此,拦截器的基本功能已经完成。

4、拦截器实现

controller包

@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
    //返回的数据类型和处理乱码
//    @GetMapping(produces = "application/json;charset = UTF-8")
    @GetMapping
    public String a() {
        log.info("This is FirstController`a method!");
        return "This is FirstController`a method!";
    }

    @GetMapping("/b")
    public String b(int a) {
        log.info("This is FirstController`b method!a:" + a);
        return "This is FirstController`b method!a:" + a;
    }

    @GetMapping("/c")
    public String c(int a) {
        log.info("This is FirstController`c method!a:" + a);
        int m = 9 / a;
        return "This is FirstController`c method!a:" + a;
    }
    @GetMapping("/d")
    public String d() {
        log.info("This is FirstController`d method!");
        return "This is FirstController`d method!";
    }
}

interceptor包

@Component
@Slf4j
public class FirstInterceptor implements HandlerInterceptor {

    static {
        log.info("我是FirstInterceptor的静态块");
    }

    public FirstInterceptor() {
        log.info("我是FirstInterceptor的构造器");
    }

    // 请求方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("访问了FirstInterceptor的preHandle方法!");
        return true;
    }

    // 响应方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("访问了FirstInterceptor的postHandle方法!");
    }

    // 响应方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("访问了FirstInterceptor的afterCompletion方法!");
    }
}
@Component
@Slf4j
public class SecondInterceptor implements HandlerInterceptor {

    static {
        log.info("我是SecondInterceptor的静态块");
    }

    public SecondInterceptor() {
        log.info("我是SecondInterceptor的构造器");
    }

    // 请求方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("访问了SecondInterceptor的preHandle方法!");
        return true;
    }

    // 响应方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("访问了SecondInterceptor的postHandle方法!");
    }

    // 响应方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("访问了SecondInterceptor的afterCompletion方法!");
    }
}
@Component
@Slf4j
public class ThirdInterceptor implements HandlerInterceptor {

    static {
        log.info("我是ThirdInterceptor的静态块");
    }

    public ThirdInterceptor() {
        log.info("我是ThirdInterceptor的构造器");
    }

    // 请求方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("访问了ThirdInterceptor的preHandle方法!");
        return true;
    }

    // 响应方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("访问了ThirdInterceptor的postHandle方法!");
    }

    // 响应方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("访问了ThirdInterceptor的afterCompletion方法!");
    }
}

config包

@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {

    //注册
    @Resource
    private FirstInterceptor firstInterceptor;
    @Resource
    private SecondInterceptor secondInterceptor;
    @Resource
    private ThirdInterceptor thirdInterceptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(firstInterceptor).order(8).addPathPatterns("/first/**");
        registry.addInterceptor(secondInterceptor)
                .order(19).addPathPatterns("/first/**").excludePathPatterns("/first/d");
        registry.addInterceptor(thirdInterceptor).order(12).addPathPatterns("/**");
    }
}

启动类

@ServletComponentScan("com.zgh.filter")
@SpringBootApplication
public class SpringBootMVC3Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootMVC3Application.class, args);
    }
}

5、拦截器验证登陆

前端结构

image-20231005145401888

util包

@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> {

    private Boolean success;
    private Integer code;
    private String error;
    private T data;
}
public class ResultCode {

    public static JsonResult success() {
        return new JsonResult(true, 200, null, null);
    }

    public static JsonResult success(Object data) {
        return new JsonResult(true, 200, null, data);
    }

    public static JsonResult error(String error) {
        return new JsonResult(false, 401, error, null);
    }
}

interceptor包

@Slf4j
@Component
public class IsLoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("开始校验用户是否登陆");
        // 验证用户是否登陆
        // 获取用户传递过来的uuid
        String uuid1 = request.getHeader("uuid");
        String uuid2 = (request.getSession().getAttribute("uuid") == null)
                ? null : request.getSession().getAttribute("uuid").toString();
        log.info("用户发送过来的uuid:{}",uuid1);
        log.info("服务器中存放的uuid:{}",uuid2);
        if (uuid1 == null) {
            log.info("用户未登录");
            response.getWriter().println(ResultCode.error("用户未登录"));
            return false;
        }
        if (uuid1.equals(uuid2)) {
            log.info("用户已登陆");
            return true;
        }
        log.info("用户未登录");
        response.getWriter().println(ResultCode.error("用户未登录"));
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //response.getWriter().println(ResultCode.error("用户未登录"));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // response.getWriter().println(ResultCode.error("用户未登录"));
    }
}

controller包

@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @PostMapping("/login")
    public JsonResult login(String username, String password, HttpServletRequest request) {
        // 校验登陆是否成功
        if (!(username.startsWith("adm") && password.contains("123"))) {
            return ResultCode.error("账号或者密码错误!");
        }
        // 服务端保存用户信息
        HttpSession session = request.getSession();
        // 生成一个唯一的卡号,和你的信息绑定
        String uuid = UUID.randomUUID().toString();
        log.info("生成的uuid:{}", uuid);
        session.setAttribute("uuid", uuid);
        // 返回用户信息
        return ResultCode.success(uuid);
    }

    @GetMapping("/is")
    public JsonResult is() {
        return ResultCode.success();
    }
}

static包

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    {{errorMsg}}<br/>
    账号:<input v-model="username"/><br/>
    密码:<input v-model="password" type="password"/><br/>
    <button @click="login">登陆</button>
</div>
</body>
</html>
<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                username: '',
                password: '',
                errorMsg: ''
            }
        },
        methods: {
            login() {
                let _this = this
                let data = new URLSearchParams()
                data.append('username', this.username)
                data.append('password', this.password)
                axios({
                    method: 'post',
                    url: 'user/login',
                    data: data
                }).then((response) => {
                    if (response.data.success) {
                        // 保存服务器生成的uuid
                        let uuid = response.data.data
                        localStorage.setItem('uuid', uuid)
                        // 跳转到a.html
                        location.href = 'a.html'
                    } else {
                        _this.errorMsg = response.data.error
                    }
                })
            }
        }
    })
</script>

a.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
a.html
</body>
</html>

<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        created() {
            let _this = this
            axios.get('user/is', {
                headers: {
                    uuid: localStorage.getItem('uuid')
                }
            })
                .then((response) => {
                    if (!response.data.success) {
                        // location.href = 'login.html'
                        alert('用户未登录')
                    } else {
                        alert('用户已登录')
                    }

                })
        }
    })
</script>

b.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
b.html
</body>
</html>

c.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
c.html
</body>
</html>

流程

用户登录验证?

1、 校验登陆是否成功
2、服务端保存用户信息
3、生成一个唯一的卡号,和你的信息绑定
4、返回用户信息

用拦截器拦截用否登录实现步骤?

原因:为了避免用户在未登录的情况下,访问一些敏感的数据或信息,此时拦截器就发挥了作用。

步骤:

​ 1、通过拦截器拦截请求,得到用户的令牌信息(UUID1),这是用户在登录传递过来的;

​ 再得到浏览器中存放的令牌信息(UUID2),这是用户在登录时存入的;

​ 2、将两个UUID进行比对,若UUID1或UUID2为null,说明用户未登录;

​ 若比对成功,继续访问;若比对失败,拦截请求,并返回错误信息。

面试题

1、过滤器和拦截器的区别?

​ 1.运行顺序不同(如图):过滤器是在 Servlet 容器接收到请求之后,但在 Servlet被调用之前运行的;而拦截器则是在 Servlet 被调用之后,但在响应被发送到客户端之前运行的。

image-20231005125025197

  1. 配置方式不同:过滤器是在 web.xml 中进行配置;而拦截器的配置则是在 Spring的配置文件中进行配置,或者使用注解进行配置。

  2. Filter 依赖于 Servlet 容器,而 Interceptor 不依赖于 Servlet 容器

  3. Filter 在过滤是只能对 request 和 response 进行操作,而 interceptor 可以对request、response、handler、modelAndView、exception 进行操作。

2、过滤器和拦截器谁先执行?

Filter需要在web.xml中配置,依赖于Servlet

Interceptor需要在SpringMVC中配置,依赖于框架

Filter的执行顺序在Interceptor之前

原因:filer是与servlet一个级别,springmvc相当于一个servlet,filter是在springmvc之前执行,interceptor是springmvc的一个组件,是在springmvc之后执行,因此Filter的执行顺序在Interceptor之前。

image-20231005124006942

3、preHandle、postHandle、afterCompletion区别?

1、preHandle

调用时间:Controller方法处理之前
执行顺序:链式Intercepter情况下,Intercepter按照声明的顺序一个接一个执行
若返回false,则中断执行,注意:不会进入afterCompletion

2、postHandle

调用前提:preHandle返回true
调用时间:Controller方法处理完之后,DispatcherServlet进行视图的渲染之前,也就是说在这个方法中你可以对ModelAndView进行操作
执行顺序:链式Intercepter情况下,Intercepter按照声明的顺序倒着执行。
备注:postHandle虽然post打头,但post、get方法都能处理

3、afterCompletion

调用前提:preHandle返回true

调用时间:DispatcherServlet进行视图的渲染之后
多用于清理资源

4、登录的流程?

  1. 用户访输入网址导航到登录页面;
  2. 用户在登录页面中输入登录信息传递给前端页面;
  3. 前端实现表单验证,对用户名和密码进行校验,例如检查字段是否为空、格式是否有误等。
  4. 用户点击登录按钮后,前端发送请求给后端服务器。
  5. 后端服务器接收到登录请求,首先对用户输入的用户名进行验证,以确保用户账号的存在。
  6. 后端服务器使用安全的方式从数据库中检索存储的用户信息,包括用户名和对应的哈希密码。
  7. 后端服务器将用户输入的密码进行加密,通常会使用密码哈希算法(如MD5、SHA-256等)与一些安全盐(Salt)进行混合。
  8. 加密后的密码与从数据库中检索到的哈希密码进行比对。如果两者匹配,则表示用户输入的密码是正确的。
  9. 如果账号和密码验证不通过,后端服务器会将封装好的对应的错误信息发送回给前端,用户可以选择继续登录。
  10. 如果密码验证通过,后端服务器会生成一个令牌(Token),用于标识用户的身份认证,并在下次的请求中进行验证。
  11. 后端服务器将令牌作为响应的一部分发送回前端,前端会将该令牌保存在Cookie或LocalStorage中,以便后续的请求能够使用它进行身份验证。
  12. 用户登录成功后,可以跳转到应用的主界面或指定的页面。

5、post和get的区别?

特点不同:

GET请求的数据会附在URL之后,而POST方法提交的数据放在请求体中,所以POST方法的安全性比GET方法要高,但是执行效率却比Post方法好。

用途不同

get方式的安全性较Post方式要差些,包含机密信息的话,建议用Post数据提交方式;

​ 在做数据查询时,建议用Get方式;而在做数据添加、修改或删除时,建议用Post方式;

传输的数据量不同

GET方法传输的数据量(有限制)一般限制在2KB,而Chrome,FireFox浏览器理论上对于URL是没有限制的,它真正的限制取决于操作系统本身;

POST方法对于数据大小是无限制的,真正影响到数据大小的是服务器处理程序的能力。

九、默认异常处理

在⽇常的 Web 开发中,会经常遇到⼤⼤⼩⼩的异常,此时往往需要⼀个统⼀的异常处理机制,来保证客户端能接收较为友好的提示。Spring Boot 同样提供了⼀套默认的异常处理机制。

Spring Boot 默认异常处理机制

Spring Boot 提供了⼀套默认的异常处理机制,⼀旦程序中出现了异常,Spring Boot 会⾃动识别客户端的类型(浏览器客户端或机器客户端),并根据客户端的不同,以不同的形式展示异常信息。

对于浏览器⽽⾔

对于浏览器客户端⽽⾔,Spring Boot 会响应⼀个 “whitelabel” 错误视图,以 HTML 格式呈现错误信息:

image-20231005181251762

对于机器客户端⽽⾔

对于机器客户端(Apipost)⽽⾔,Spring Boot 将⽣成 JSON 响应,来展示异常消息。

{
"timestamp": "2022-11-02T14:49:56.885+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/dailyblue/first/d"
}

Spring Boot异常处理⾃动配置原理

Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了⾃动配置,该配置类向容器中注⼊了以下4 个组件。

  • ErrorPageCustomizer:该组件会在在系统发⽣异常后,默认将请求转发到“/error”上。
  • BasicErrorController:处理默认的“/error”请求。
  • DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
  • DefaultErrorAttributes:⽤于⻚⾯上共享异常信息。

下⾯,我们依次对这四个组件进⾏详细的介绍。

1、ErrorPageCustomizer

ErrorMvcAutoConfiguration 向容器中注⼊了⼀个名为 ErrorPageCustomizer 的组件,它主要⽤于定制错误⻚⾯的响应规则

可以指定特定的错误页面来处理不同类型的错误。例如,您可以将特定的HTTP状态代码映射到自定义的错误页面。

@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath
dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

ErrorPageCustomizer 通过 registerErrorPages() ⽅法来注册错误⻚⾯的响应规则。当系统中发⽣异常后,ErrorPageCustomizer 组件会⾃动⽣效,并将请求转发到 “/error”上,交给 BasicErrorController 进⾏处理,其部分代码如下:

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//将请求转发到 /errror(this.properties.getError().getPath())上
ErrorPage errorPage
= new
ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath
()));
// 注册错误⻚⾯
errorPageRegistry.addErrorPages(errorPage);
}

2、BasicErrorController

当系统发生异常并被转发到"/error"路径时,BasicErrorController会根据异常的类型和HTTP状态代码,返回相应的错误响应

ErrorMvcAutoConfiguration 还向容器中注⼊了⼀个错误控制器组件 BasicErrorController,代码如下:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search =
SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver>
errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

BasicErrorController 的定义如下:

//BasicErrorController ⽤于处理 “/error” 请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
 ......

    /**
     * 该⽅法⽤于处理浏览器客户端的请求发⽣的异常
     * ⽣成 html ⻚⾯来展示异常信息
     */
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse
            response) {
//获取错误状态码
        HttpStatus status = getStatus(request);
//getErrorAttributes 根据错误信息来封装⼀些 model 数据,⽤于⻚⾯显示
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request,
                        getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//为响应对象设置错误状态码
        response.setStatus(status.value());
//调⽤ resolveErrorView() ⽅法,使⽤错误视图解析器⽣成 ModelAndView 对象(包含错误⻚⾯地址和⻚⾯内容)
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error",
                model);
    }

    /**
     * 该⽅法⽤于处理机器客户端的请求发⽣的错误
     * 产⽣ JSON 格式的数据展示错误信息
     */
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request,
                getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
 ......
}

Spring Boot 通过 BasicErrorController 进⾏统⼀的错误处理(例如默认的“/error”请求)。Spring Boot 会⾃动识别发出请求的客户端的类型(浏览器客户端或机器客户端),并根据客户端类型,将请求分别交给 errorHtml() 和 error() ⽅法进⾏处理。

image-20231005182658875

换句话说,当使⽤浏览器访问出现异常时,会进⼊ BasicErrorController 控制器中的 errorHtml() ⽅法进⾏处理,当使⽤安卓、IOS、Postman 等机器客户端访问出现异常时,就进⼊error() ⽅法处理。

在 errorHtml() ⽅法中会调⽤⽗类(AbstractErrorController)的 resolveErrorView() ⽅法,代码如下:

    protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse
            response, HttpStatus status,Map<String, Object> model) {
//获取容器中的所有的错误视图解析器来处理该异常信息
        for (ErrorViewResolver resolver : this.errorViewResolvers) {
//调⽤错误视图解析器的 resolveErrorView 解析到错误视图⻚⾯
            ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
            if (modelAndView != null) {
                return modelAndView;
            }
        }
        return null;
    }

从上述源码可以看出,在响应⻚⾯的时候,会在⽗类的 resolveErrorView ⽅法中获取容器中所有的ErrorViewResolver 对象(错误视图解析器,包括 DefaultErrorViewResolver 在内),⼀起来解析异常信息。

3、DefaultErrorViewResolver

DefaultErrorViewResolver根据异常的类型、HTTP状态代码和配置的错误视图路径来决定使用哪个错误视图

可以通过自定义DefaultErrorViewResolver来定制错误视图的解析方式

ErrorMvcAutoConfiguration 还向容器中注⼊了⼀个默认的错误视图解析器组件 DefaultErrorViewResolver,代码如下:

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象(错误视图解析器),并分别调⽤它们的 resolveErrorView() ⽅法对异常信息进⾏解析,其中⾃然也包括 DefaultErrorViewResolver(默认错误信息解析器)。

DefaultErrorViewResolver 的部分代码如下:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
    private static final Map<HttpStatus.Series, String> SERIES_VIEWS;

    static {
        Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }
 ......

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
                                         Map<String, Object> model) {
//尝试以错误状态码作为错误⻚⾯名进⾏解析
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//尝试以 4xx 或 5xx 作为错误⻚⾯⻚⾯进⾏解析
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误模板⻚⾯,例如 error/404、error/4xx、error/500、error/5xx
        String errorViewName = "error/" + viewName;
//当模板引擎可以解析这些模板⻚⾯时,就⽤模板引擎解析
        TemplateAvailabilityProvider provider =
                this.templateAvailabilityProviders.getProvider(errorViewName,
                        this.applicationContext);
        if (provider != null) {
//在模板能够解析到模板⻚⾯的情况下,返回 errorViewName 指定的视图
            return new ModelAndView(errorViewName, model);
        }
//若模板引擎不能解析,则去静态资源⽂件夹下查找 errorViewName 对应的⻚⾯
        return resolveResource(errorViewName, model);
    }

    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍历所有静态资源⽂件夹
        for (String location : this.resources.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
//静态资源⽂件夹下的错误⻚⾯,例如error/404.html、error/4xx.html、
                error / 500. html、error / 5 xx.html
                        resource = resource.createRelative(viewName + ".html");
//若静态资源⽂件夹下存在以上错误⻚⾯,则直接返回
                if (resource.exists()) {
                    return new ModelAndView(new
                            DefaultErrorViewResolver.HtmlResourceView(resource), model);
                }
            } catch (Exception ex) {
            }
        }
        return null;
    }
 ......
}

DefaultErrorViewResolver 解析异常信息的步骤如下:

  1. 根据错误状态码(例如 404、500、400 等),⽣成⼀个错误视图 error/status,例如 error/404、

error/500、error/400。

  1. 尝试使⽤模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates ⽬录下,查找

error/status.html,例如 error/404.html、error/500.html、error/400.html。

  1. 若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流

程,否则跳转到第 4 步。

  1. 依次从各个静态资源⽂件夹中查找 error/status.html,若在静态⽂件夹中找到了该错误⻚⾯,则返回并结束

整个解析流程,否则跳转到第 5 步。

  1. 将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结

束整个解析流程,否则跳转第 6 步。

  1. 处理默认的 “/error ”请求,使⽤ Spring Boot 默认的错误⻚⾯(Whitelabel Error Page)。

4、DefaultErrorAttributes

DefaultErrorAttributes是一个错误属性类,用于在页面上共享异常信息

当处理异常时,DefaultErrorAttributes会从异常对象中提取相关属性,例如错误消息、异常堆栈等,

并将它们添加到模型中,以便将这些信息显示在错误视图上。

ErrorMvcAutoConfiguration 还向容器中注⼊了⼀个组件默认错误属性处理⼯具 DefaultErrorAttributes,代码如下:

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search =
SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}

DefaultErrorAttributes 是 Spring Boot 的默认错误属性处理⼯具,它可以从请求中获取异常或错误信息,并将其封装为⼀个 Map 对象返回,其部分代码如下:

public class DefaultErrorAttributes implements ErrorAttributes,
        HandlerExceptionResolver, Ordered {
 ......

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest,
                                                  ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = getErrorAttributes(webRequest,
                options.isIncluded(Include.STACK_TRACE));
        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }
        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }
        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") !=
                null) {
            errorAttributes.remove("message");
        }
        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }
        return errorAttributes;
    }

    private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean
            includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
 ......
}

在 Spring Boot 默认的 Error 控制器(BasicErrorController)处理错误时,会调⽤ DefaultErrorAttributes 的getErrorAttributes() ⽅法获取错误或异常信息,并封装成 model 数据(Map 对象),返回到⻚⾯或 JSON 数据中。该 model 数据主要包含以下属性:

  • timestamp:时间戳;
  • status:错误状态码
  • error:错误的提示
  • exception:导致请求处理失败的异常对象
  • message:错误/异常消息
  • trace: 错误/异常栈信息
  • path:错误/异常抛出时所请求的URL路径

十、全局异常处理

在项⽬开发中出现异常时很平常不过的事情,我们处理异常也有很多种⽅式。可能如下:

public int div(int a ,int b){
int c = 0;
try{
c = a / b;
 }catch (Exception ex){
ex.printStackTrace();
 }
return c;
}

如果我们这样处理异常,代码中就会出现特别多的异常处理模块,这样代码可读性就会变得⾮常差,⽽且业务模块逻辑会夹杂特别多的⾮业务逻辑。但是在项⽬开发的过程中我们应该将主要精⼒放在业务模块,除了必要的异常处理模块最好不要再包含其他⽆关紧要的代码。

那么我们如何处理项⽬中⽆处不在的异常呢?这就引出了我们要介绍的全局异常处理⽅法,主要有两种⽅式:

  • 使⽤ @RestControllerAdvice 和 @ExceptionHandler 注解
  • 使⽤ ErrorController 类来实现

1、@RestControllerAdvice⽅式

全局异常处理类

书写⼀个全局异常处理类,代码如下:

// 注解@RestControllerAdvice表示这是⼀个控制器增强类,当控制器发⽣异常且符合类中定义的拦截异常类,将会被拦截。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    // 对那些异常进⾏拦截
    @ExceptionHandler(Exception.class)
    public JsonResult globalException(HttpServletResponse response, Exception e) {
        log.info("进⼊了全局异常处理⽅法");
        log.info("异常信息是:{}", e.getMessage());
        return ResultTool.fail(e.getMessage());
    }
}

注:e.getMessage()是获取异常信息的方法

控制器
书写⼀个控制器,代码如下:

@RequestMapping("/second")
@RestController
public class SecondController {
    @RequestMapping("/login")
    public String login() {
        System.out.println("aaa");
        return "login";
    }
    // 这个⽅法会出现错误
    @RequestMapping("/b")
    public String b() {
        System.out.println("bbb");
        System.out.println(9/0);
        return "b";
    }
}

启动

启动并调⽤错误⽅法,⻚⾯效果:

image-20231005184441431

控制台效果:

image-20231005184453578

2、使⽤ErrorController类

系统默认的错误处理类为 BasicErrorController,当出现错误时显示默认的错误⻚⾯。这⾥编写⼀个⾃⼰的错误处理类,上⾯默认的处理类将不会起作⽤。

getErrorPath()返回的路径服务器将会重定向到该路径对应的处理类,本例中为error⽅法。

@RestController
@Slf4j
public class HttpErrorController implements ErrorController {
    private final static String ERROR_PATH = "/error";
    @RequestMapping(path = ERROR_PATH)
    public JsonResult error(HttpServletRequest request, HttpServletResponse response) {
        log.info("访问/error" + " 错误代码:" + response.getStatus());
        return ResultTool.fail();
    }
    public String getErrorPath() {
        return ERROR_PATH;
    }
}

还是⽤刚才的错误控制器⽅法,继续启动并访问,浏览器效果:

image-20231005184632977

控制台效果:

image-20231005184649790

两者区别

  1. 注解 @RestControllerAdvice ⽅式处理控制器抛出的异常或所定义的异常。此时请求已经进⼊控制器中。

  2. 类 ErrorController ⽅式可以处理所有的异常,包括未进⼊控制器的错误,⽐如404,401等错误

  3. 如果应⽤中两者共同存在,则 @RestControllerAdvice ⽅式处理控制器抛出的异常,类 ErrorController ⽅式

未进⼊控制器的异常。

  1. @RestControllerAdvice ⽅式可以定义多个拦截⽅法,拦截不同的异常类,并且可以获取抛出的异常信息,⾃

由度更⼤。

4、spring_boot_mvc4

项目结构

image-20231005192452563

第一种方式(推荐)

controller包

@RestController
@RequestMapping("/first")
public class FirstController {
    @Resource
    private FirstService service;
    @GetMapping("/a")
    public JsonResult a() {
        int a = 9 / 0;
        return ResultTool.success();
    }

    @GetMapping("/b")
    public JsonResult b() {
        return ResultTool.success();
    }
    @GetMapping("/c")
    public JsonResult c(){
        return service.a();
    }
}

handler包

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(NullPointerException.class)
    public JsonResult nullException(Exception e) {
        log.info("代码出现了异常情况!");
        log.info("异常信息是:{}", e.getMessage());
        return ResultTool.error(e.getMessage());
    }

    @ExceptionHandler(ArithmeticException.class)
    public JsonResult arithmeticException(Exception e) {
        log.info("代码出现了异常情况!");
        log.info("异常信息是:{}", e.getMessage());
        return ResultTool.error(e.getMessage());
    }

    @ExceptionHandler(ArrayIndexOutOfBoundsException.class)
    public JsonResult arrayIndexOutOfBoundsException(Exception e) {
        log.info("代码出现了异常情况!");
        log.info("异常信息是:{}", e.getMessage());
        return ResultTool.error(e.getMessage());
    }
}

service包

public interface FirstService {
    JsonResult a();
}
@Service
public class FirstServiceImpl implements FirstService {

    @Override
    public JsonResult a() {
        String[] a={"aa", "bb"};
        a[4]="bbb";
        return ResultTool.success(a);
    }
}

util包

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonResult<T> {

    private boolean success;
    private Integer code;
    private String error;
    private T data;
}
public class ResultTool {

    public static JsonResult success() {
        return new JsonResult(true, 200, null, null);
    }

    public static JsonResult success(Object data) {
        return new JsonResult(true, 200, null, data);
    }

    public static JsonResult error(String error) {
        return new JsonResult(false, 401, error, null);
    }
}

启动类

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

效果

image-20231005193451897

image-20231005193402160

image-20231005193554754

第二种方式

注:注释掉@RestControllerAdvice

//@RestControllerAdvice

handler包

@Slf4j
@RestController
@RequestMapping("/error")
public class HttpErrorController implements ErrorController {
    @GetMapping
    public JsonResult global() {
        log.info("我处理错误信息");
        return ResultTool.error("error");
    }

    public String getErrorPath() {
        return "/error";
    }
}

效果

image-20231005194211633

image-20231005194303481

image-20231005194235471

十一、整合MyBatis-Plus

对于数据访问层,⽆论是 SQL(关系型数据库) 还是 NOSQL(⾮关系型数据库),

Spring Boot 都默认采⽤整合Spring Data 的⽅式进⾏统⼀处理,通过⼤量⾃动配置,来简化我们对数据访问层的操作

⽽如果我们要使⽤MyBatis-Plus,只需要引⼊ MyBatis-Plus 的启动器就⾏。

image-20231005195221257

概述

MyBatis-Plus (opens new window)(简称 MP)是⼀个 MyBatis (opens new window)的增强⼯具,在 MyBatis的基础上只做增强不做改变,为简化开发、提⾼效率⽽⽣。

愿景

我们的愿景是成为 MyBatis 最好的搭档,就像 魂⽃罗 中的 1P、2P,基友搭配,效率翻倍。

image-20231005195310636

特性

  • ⽆侵⼊:只做增强不做改变,引⼊它不会对现有⼯程产⽣影响,如丝般顺滑
  • 损耗⼩:启动即会⾃动注⼊基本 CURD,性能基本⽆损耗,直接⾯向对象操作
  • 强⼤的 CRUD 操作:内置通⽤ Mapper、通⽤ Service,仅仅通过少量配置即可实现单表⼤部分 CRUD 操作,更有强⼤的条件构造器,满⾜各类使⽤需求
  • ⽀持 Lambda 形式调⽤:通过 Lambda 表达式,⽅便的编写各类查询条件,⽆需再担⼼字段写错
  • ⽀持主键⾃动⽣成:⽀持多达 4 种主键策略(内含分布式唯⼀ ID ⽣成器 - Sequence),可⾃由配置,完美解决主键问题
  • ⽀持 ActiveRecord 模式:⽀持 ActiveRecord 形式调⽤,实体类只需继承 Model 类即可进⾏强⼤的 CRUD 操作
  • ⽀持⾃定义全局通⽤操作:⽀持全局通⽤⽅法注⼊( Write once, use anywhere )
  • 内置代码⽣成器:采⽤代码或者 Maven 插件可快速⽣成 Mapper 、 Model 、 Service 、 Controller 层代码,⽀持模板引擎,更有超多⾃定义配置等您来使⽤
  • 内置分⻚插件:基于 MyBatis 物理分⻚,开发者⽆需关⼼具体操作,配置好插件之后,写分⻚等同于普通List 查询
  • 分⻚插件⽀持多种数据库:⽀持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执⾏时间,建议开发测试时启⽤该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可⾃定义拦截规则,预防误操作

1、spring_boot_mybatic_plus

入门案例

项目结构

image-20231005200841078

我们将通过⼀个简单的 Demo 来阐述 MyBatis-Plus 的强⼤功能。

引⼊依赖

    <dependencies>
<!--        mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
<!--        数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
<!--         mybatic_puls-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
<!--        分页-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
<!--        测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.7.2</version>
        </dependency>
    </dependencies>

配置数据源

在 application.yml 配置⽂件中添加 MySQL 数据库的相关配置:

# DataSource Config
spring:
  datasource:
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/test

实体类

我们以经典的 Emp 表作为实例,进⾏讲解,编写对应的实体 Emp 类。

@Data
public class Emp implements Serializable {
private Integer empno;
private String ename;
private String job;
private Integer mgr;
private String hiredate;
private Double sal;
private Double comm;
private Integer deptno;
private Integer empstate;
}

//添加注解后
 @TableName("emp")
@Data
public class Emp implements Serializable {
    @TableId(value = "empno", type = IdType.AUTO)
    private Integer empNo; // 自动---emp_no
    private String ename;
    private String job;
    private Integer mgr;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String hireDate;
    private Double sal;
    private Double comm;
    //@TableField("deptno")
    private Integer deptNo;
    @TableLogic(delval = "0", value = "1")
    private Integer state;
    @TableField(exist = false)
    private String dname;
}   

Mapper

编写 mapper 包下的 EmpMapper 接⼝

@Mapper
public interface EmpMapper extends BaseMapper<Emp> {

    List<Emp> find16(Emp emp);
}

业务层

编写 service 包下的 EmpService 接⼝

public interface EmpService extends IService<Emp> {
}

编写 service.impl 包下的 EmpServiceImpl 实现类

@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {
}

开始使⽤

添加测试类,进⾏功能测试:

@SpringBootTest
@Slf4j
public class App {
    @Resource
    private EmpService service;

    @Test
    public void find() {
        List<Emp> list = service.list();
        list.forEach(System.out::println);
    }
   }

启动类

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

启动后控制台输出

image-20231005202128048

问题:数据库字段和实体类中的属性对不上

解决方法:

1、通过注解

@TableName("emp")
@Data
public class Emp implements Serializable {
    @TableId(value = "empno", type = IdType.AUTO)
    private Integer empNo; // 自动---emp_no
    private String ename;
    private String job;
    private Integer mgr;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String hireDate;
    private Double sal;
    private Double comm;
    //@TableField("deptno")
    private Integer deptNo;
    @TableLogic(delval = "0", value = "1")
    private Integer state;
    @TableField(exist = false)
    private String dname;
}

2、通过配置

# 关闭字段名下划线自动转驼峰标记
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
    # 设置日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

测试效果

image-20231005202552095

总结

通过以上⼏个简单的步骤,我们就实现了 Emp 表的 CRUD 功能,甚⾄连 XML ⽂件都不⽤编写!

从以上步骤中,我们可以看到集成 MyBatis-Plus ⾮常的简单,只需要引⼊ starter ⼯程,并配置 mapper 扫描路径即可。

但 MyBatis-Plus 的强⼤远不⽌这些功能,想要详细了解 MyBatis-Plus 的强⼤功能?那就继续往下看吧!

2、MyBatis-Plus注解

介绍 Mybatis-Plus 注解包相关类详解(更多详细描述可点击查看源码注释)

传动⻔:mybatis-plus: mybatis 增强工具包,简化 CRUD 操作。 文档 http://baomidou.com 低代码组件库 http://aizuda.com - Gitee.com

@TableName

描述:表名注解,标识实体类对应的表

使⽤位置:实体类

@Data
@TableName(value = "emp")
public class Emp implements Serializable {
private Integer empno;
private String ename;
private String job;
private Integer mgr;
private String hiredate;
private Double sal;
private Double comm;
private Integer deptno;
private Integer empstate;
}

注意: @TableName 注解可省略,如省略对应类名的表

属性列表:

image-20231005203136927

关于 autoResultMap 的说明:

MP 会⾃动构建⼀个 resultMap 并注⼊到 MyBatis ⾥(⼀般⽤不上),请注意以下内容:

因为 MP 底层是 MyBatis,所以 MP 只是帮您注⼊了常⽤ CRUD 到 MyBatis ⾥,注⼊之前是动态的(根据您的

Entity 字段以及注解变化⽽变化),但是注⼊之后是静态的(等于 XML 配置中的内容)。

⽽对于 typeHandler 属性,MyBatis 只⽀持写在 2 个地⽅:

  1. 定义在 resultMap ⾥,作⽤于查询结果的封装

  2. 定义在 insert 和 update 语句的 #{property} 中的 property 后⾯(例: #

    {property,typehandler=xxx.xxx.xxx} ),并且只作⽤于当前 设置值

除了以上两种直接指定 typeHandler 的形式,MyBatis 有⼀个全局扫描⾃定义 typeHandler 包的配置,原理是

根据您的 property 类型去找其对应的 typeHandler 并使⽤。

@TableId

描述:主键注解

使⽤位置:实体类主键字段

@Data
@TableName(value = "emp")
public class Emp implements Serializable {
@TableId(value = "empno",type = IdType.AUTO)
private Integer empno;
// 其他省略
}

属性列表:

image-20231005203418862

type属性概述:

image-20231005203449596

@TableField

描述:字段注解(⾮主键)

@Data
@TableName(value = "emp")
public class Emp implements Serializable {
@TableField(value = "ename")
private String ename;
@TableField(value = "hiredate")
private String hireDate;
// 其他省略
}

属性列表:

image-20231005203604663

关于 jdbcType 和 typeHandler 以及 numericScale 的说明:

numericScale只⽣效于 update 的 sql. jdbcType和typeHandler如果不配合@TableName#autoResultMap

= true⼀起使⽤,也只⽣效于 update 的 sql. 对于typeHandler如果你的字段类型和 set 进去的类型为equals

关系,则只需要让你的typeHandler让 Mybatis 加载到即可,不需要使⽤注解。

@TableLogic

描述:表字段逻辑处理注解(逻辑删除)

image-20231005203814408

3、逻辑删除

我们在实际使⽤中的删除操作,其实并没有删除表中的数据,仅仅是将表的数据隐藏掉,查询时不查询这些隐藏的数据

分类

  • 全局⽅式
  • 局部⽅式
全局⽅式

在 application.yml 中进⾏配置:

mybatis-plus:
 global-config:
 db-config:
 logic-delete-field: state # 全局逻辑删除的实体字段名
 logic-delete-value: 0 # 逻辑已删除值(默认为 1)
 logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)

注意:配置好后,所有实体不需额外操作,但必须保证存在 state 字段。

启动后控制台输出

image-20231005205552171

注:查询的sql语句发生变化,查询只查状态为1的,删除时会将状态改为0。

test包

@Test
public void delete() {
    service.removeById(7900);
}

启动后控制台输出

image-20231005205925834

局部⽅式

需要在每个所需实体类中引⼊ @TableLogic 注解

@TableLogic(delval = "0", value = "1")
private Integer state;

启动后控制台输出

image-20231005210507748

说明

只对⾃动注⼊的 sql 起效:

  • 插⼊: 不作限制

  • 查找: 追加 where 条件过滤掉已删除数据,如果使⽤ wrapper.entity ⽣成的 where 条件也会⾃动追加该字段

  • 更新: 追加 where 条件防⽌更新到已删除数据,如果使⽤ wrapper.entity ⽣成的 where 条件也会⾃动追加该字段

  • 删除: 转变为 更新

例如:

  • 删除: update user set deleted=1 where id = 1 and deleted=0
  • 查找: select id,name,deleted from user where deleted=0

字段类型⽀持说明:

  • ⽀持所有数据类型(推荐使⽤ Integer,Boolean,LocalDateTime)
  • 如果数据库字段使⽤ datetime ,逻辑未删除值和已删除值⽀持配置为字符串null,另⼀个值⽀持配置为函数来获取值如now()

附录:

  • 逻辑删除是为了⽅便数据恢复和保护数据本身价值等等的⼀种⽅案,但实际就是删除。
  • 如果你需要频繁查出来看就不应使⽤逻辑删除,⽽是以⼀个状态去表示。

4、⾃动填充功能

在数据表的设计中,经常需要加⼀些字段,如:创建时间,最后修改时间等,此时可以使⽤ MyBatis-Plus 来帮我们进⾏⾃动维护

填充⽅式

⾃动填充⽅式有两种,分别是:

  • 通过数据库完成⾃动填充
  • 使⽤程序完成⾃动填充

注:只有添加和修改有自动填充

数据库⾃动填充

这⾥没什么好说的,就是正常书写就⾏,在添加时,有默认值的列所对应的属性不赋值,数据库会⾃动赋予默认值。

程序⾃动填充

1、添加的自动填充

// 注意!这⾥需要标记为填充字段
@TableField(fill = FieldFill.INSERT)
private String hireDate;
@TableName("emp")
@Data
public class Emp implements Serializable {
    @TableId(value = "empno", type = IdType.AUTO)
    private Integer empNo; // 自动---emp_no
    private String ename;
    private String job;
    private Integer mgr;
    @TableField(fill = FieldFill.INSERT)
    private String hireDate;
    private Double sal;
    private Double comm;
    //@TableField("deptno")
    private Integer deptNo;
    @TableLogic(delval = "0", value = "1")
    private Integer state;
    @TableField(exist = false)
    private String dname;
}

这⾥就给 hireDate 属性设置了⾃动填充,如果该属性没有值,则⾃动填充 Null 。

测试类

@Test
public void save() {
    Emp emp = new Emp();
    emp.setEname("周国豪");
    emp.setJob("页面设计");
    emp.setSal(9999.0);
    service.save(emp);
}

效果

image-20231005211600976

改进

改进刚才的内容,我们想要在未给 hireDate 属性赋值时,系统赋予当前时间。

handler包

这时我们要引⼊⾃定义实现类 DateHandler

@Component
@Slf4j
public class DateHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("开始录入当前时间");
        this.strictInsertFill(metaObject, "hireDate", this::getNowDate, String.class);
    }


    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("开始录入当前时间");
        this.strictUpdateFill(metaObject, "hireDate", this::getNowDate, String.class);
    }

    private String getNowDate() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date());
    }
}

启动后控制台输出

image-20231005212449211

2、修改的自动填充

@TableField(fill = FieldFill.INSERT_UPDATE)//修改
private String hireDate;
@TableName("emp")
@Data
public class Emp implements Serializable {
    @TableId(value = "empno", type = IdType.AUTO)
    private Integer empNo; // 自动---emp_no
    private String ename;
    private String job;
    private Integer mgr;
//    @TableField(fill = FieldFill.INSERT)//添加
    @TableField(fill = FieldFill.INSERT_UPDATE)//修改
    private String hireDate;
    private Double sal;
    private Double comm;
    //@TableField("deptno")
    private Integer deptNo;
    @TableLogic(delval = "0", value = "1")
    private Integer state;
    @TableField(exist = false)
    private String dname;
}
说明
  • 填充原理是直接给 entity 的属性设置值!!!
  • 注解则是指定该属性在对应情况下必有值,如果⽆值则⼊库会是 null
  • MetaObjectHandler 提供的默认⽅法的策略均为:如果属性有值则不覆盖,如果填充值为 null 则不填充
  • 字段必须声明 TableField 注解,属性 fill 选择对应策略,该声明告知 Mybatis-Plus 需要预留注⼊ SQL字段
  • 填充处理器 NowDateHandler 在 Spring Boot 中需要声明 @Component 或 @Bean 注⼊
  • 要想根据注解 FieldFill.xxx 和字段名以及字段类型来区分必须使⽤⽗类的 strictInsertFill 或者strictUpdateFill ⽅法
  • 不需要根据任何来区分可以使⽤⽗类的 fillStrategy ⽅法
  • update(T t,Wrapper updateWrapper) 时 t 不能为空,否则⾃动填充失效

5、条件构造器

在Mybatis-Plus中提了构造条件的类 Wrapper ,它可以根据⾃⼰的意图定义我们需要的条件。

Wrapper 是⼀个抽象类,⼀般情况下我们⽤它的⼦类 QueryWrapper 来实现⾃定义条件查询

image-20231005213027708

API

image-20231005213212082

image-20231005213237289

AbstractWrapper

说明:

QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的⽗类

⽤于⽣成 sql 的 where 条件, entity 属性也⽤于⽣成 sql 的 where 条件

注意: entity ⽣成的 where 条件与 使⽤各个 api ⽣成的 where 条件没有任何关联⾏为

QueryWrapper

说明:

继承⾃ AbstractWrapper ,⾃身的内部属性 entity 也⽤于⽣成 where 条件

及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() ⽅法获取

select
select(String... sqlSelect)
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
  • 设置查询字段

说明:

以上⽅法分为两类.

第⼆类⽅法为:过滤查询字段(主键除外),⼊参不包含 class 的调⽤前需要 wrapper 内的 entity 属性有值!

这两类⽅法重复调⽤以最后⼀次为准

  • 例: select(“id”, “name”, “age”)
  • 例: select(i -> i.getProperty().startsWith(“test”))
UpdateWrapper

说明:

继承⾃ AbstractWrapper ,⾃身的内部属性 entity 也⽤于⽣成 where 条件

及 LambdaUpdateWrapper , 可以通过 new UpdateWrapper().lambda() ⽅法获取!

set
set(String column, Object val)

set(boolean condition, String column, Object val)
  • SQL SET 字段
  • 例: set(“name”, “⽼李头”)
  • 例: set(“name”, “”) —>数据库字段值变为空字符串
  • 例: set(“name”, null) —>数据库字段值变为 null
setSQL
setSql(String sql)
  • 设置 SET 部分 SQL
  • 例: setSql(“name = ‘⽼李头’”)

基于spring_boot_mybatic_plus

service包

public interface EmpService extends IService<Emp> {

    // select * from emp where state=1 and job=?
    List<Emp> find1(String job);

    List<Emp> find2(String job, double sal);

    List<Emp> find3(String job, double sal);

    List<Emp> find4();

    List<Emp> find5(String name);

    List<Emp> find6(double a, double b);

    List<Emp> find7(String fieldName);

    Emp find8(String ename);

    List<Map<String, Object>> find9(String fieldName);

    List<Emp> find10(String... fields);

    Emp find11();

    List<Emp> find12();

    List<Emp> find13(int size);

    //更新
    void update(Emp emp);

    //分页
    PageInfo<Emp> find14(int page, int size);

    PageInfo<Emp> find15(int page, int size);

}
@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {

    @Resource
    private EmpMapper mapper;

//    步骤:
//    1、产生对象
//    2、设置条件
//    3、查询


    @Override
    public List<Emp> find1(String job) {
        // 设置条件
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.eq("job", job);// =
        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find2(String job, double sal) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.eq("job", job);// =
        wrapper.gt("sal", sal);// >
        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find3(String job, double sal) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.eq("job", job).or().gt("sal", sal);// = or >
        return mapper.selectList(wrapper);
    }


    @Override
    public List<Emp> find4() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.isNull("comm");//没有奖金的员工
        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find5(String name) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
//        wrapper.like("ename",name);//模糊
//        wrapper.likeRight("ename",name);//向右模糊
        wrapper.likeLeft("ename", name);//向左模糊
        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find6(double a, double b) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.between("sal", a, b);//工资在[a,b]之间
        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find7(String fieldName) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.orderByAsc(fieldName);//按名字升序
        wrapper.orderByDesc(fieldName);//按名字降序
        return mapper.selectList(wrapper);
    }

    @Override
    public Emp find8(String ename) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.eq("eanme", ename);//按名字查询一行数据
        return mapper.selectOne(wrapper);
    }

    @Override
    public List<Map<String, Object>> find9(String fieldName) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.groupBy(fieldName);//按名字分组
        wrapper.select(fieldName);//显示那些列
        return mapper.selectMaps(wrapper);

    }

    @Override
    public List<Emp> find10(String... fields) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select(fields);//传入哪些列,显示哪些列
        return mapper.selectList(wrapper);
    }

    @Override
    public Emp find11() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("count(0) as 'empno");//查询总的条数
        return mapper.selectOne(wrapper);

    }

    @Override
    public List<Emp> find12() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();

        //统计各个部门的最高工资
        wrapper.select("max(sal) as 'sal',job");
        wrapper.groupBy("job");

        return mapper.selectList(wrapper);
    }

    @Override
    public List<Emp> find13(int size) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.last("limit" + size);//分页查询,每页size条
        return mapper.selectList(wrapper);
    }

    @Override
    public void update(Emp emp) {
        UpdateWrapper<Emp> wrapper = new UpdateWrapper<>();
        //修改部分字段值
        wrapper.set("ename", emp.getEname());
        wrapper.set("sal", emp.getSal());
        wrapper.eq("empno", emp.getEmpNo());
        // mapper.update(emp,wrapper);
        update(wrapper); // service
    }

    @Override
    public PageInfo<Emp> find14(int page, int size) {
        //分页查询(搭配分页依赖使用)
        PageHelper.startPage(page, size);
        List<Emp> list = list();
        PageInfo<Emp> pageInfo = new PageInfo<>(list);
        return pageInfo;
    }

    @Override
    public PageInfo<Emp> find15(int page, int size) {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        //对已知字段查询的结果后进行分页
        wrapper.eq("job", "程序员");
        PageHelper.startPage(page, size);
        List<Emp> list = list(wrapper);
        return new PageInfo<>(list);
    }
}   

步骤:
1、产生对象
2、设置条件
3、查询

test包

//用于启动一个Spring Boot应用作为测试环境。
@SpringBootTest
@Slf4j
public class App {
    @Resource
    private EmpService service;

    @Test
    public void find() {
        List<Emp> list = service.list();
        list.forEach(System.out::println);
    }

    @Test
    public void delete() {
        service.removeById(7900);
    }

    @Test
    public void save() {
        Emp emp = new Emp();
        emp.setEname("赵佳旺");
        emp.setJob("页面设计");
        emp.setSal(9999.0);
        service.save(emp);
    }

    @Test
    public void find1() {
        service.find1("程序员").forEach(System.out::println);
    }

    @Test
    public void find2() {
        service.find2("程序员", 2000).forEach(System.out::println);
    }

    @Test
    public void find3() {
        service.find3("程序员", 2000).forEach(System.out::println);
    }

    @Test
    public void find4() {
        service.find4().forEach(System.out::println);
    }

    @Test
    public void find5() {
        service.find5("a").forEach(System.out::println);
    }

    @Test
    public void find6() {
        service.find6(1890, 1999).forEach(System.out::println);
    }

    @Test
    public void find7() {
        service.find7("SAL").forEach(System.out::println);
    }

    @Test
    public void find8() {
        System.out.println(service.find8("董雪"));
    }

    @Test
    public void find9() {
        service.find9("job").forEach(System.out::println);
    }

    @Test
    public void find10() {
        service.find10("job").forEach(System.out::println);
    }

    @Test
    public void find11() {
        System.out.println(service.find11());
    }

    @Test
    public void find12() {
        service.find12().forEach(System.out::println);
    }

    @Test
    public void find13() {
        service.find13(10).forEach(System.out::println);
    }

    @Test
    public void update() {
        Emp emp = new Emp();
        emp.setEmpNo(7966);
        emp.setEname("李思前");
        emp.setSal(9999.9);
        emp.setMgr(7878);
        service.update(emp);
    }

    @Test
    public void find14() {
        System.out.println(service.find14(1, 10));
    }

    @Test
    public void find15() {
        System.out.println(service.find15(2, 3));
    }
}

6、使⽤ XML配置

有的时候对于⼀些复杂 SQL 语句,尤其是动态SQL,使⽤注解⽐较麻烦,我们可以通过 XML 来书写。

application.yml 引⼊

mybatis-plus:
 mapper-locations: classpath*:/mapper/**Mapper.xml # 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.dailyblue.java.spring.boot.mapper.EmpMapper">
 <select id="list" resultType="com.dailyblue.java.spring.boot.bean.Emp">
 select * from emp where empstate=1
 </select>
</mapper>

application.yml

# MybatisPlus
mybatis-plus:
  global-config:
  db-config:
  column-underline: true # 驼峰形式
  logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
  logic-delete-value: 1 # 逻辑已删除值(默认为 1)
  logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  db-type: mysql
  id-type: assign_id # id策略
  table-prefix: t_ # 配置表的默认前缀
  mapper-locations: classpath*:/mapper/**Mapper.xml # mapper ⽂件位置
  type-aliases-package: com.dailyblue.java.mybatis.bean # 实体类别名
  config-location: classpath*:/config.xml # config.xml ⽂件位置
  configuration:
  log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # ⽇志:打印sql 语句
  map-underscore-to-camel-case: false # 驼峰⾃动转换

基于spring_boot_mybatic_plus

resources——>mapper包

<?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.zgh.mapper.EmpMapper">
    <select id="find16"  parameterType="com.zgh.bean.Emp" resultType="com.zgh.bean.Emp">
        select * from emp
        <where>
            <if test="ename!=null">
                and ename like#{ename}
            </if>
            <if test="job!=null">
                and job=#{job}
            </if>
        </where>
    </select>
</mapper>

优化

1、application.yml

mapper-locations: classpath*:/mapper/**Mapper.xml # mapper ⽂件位置
type-aliases-package: com.zgh.bean.Emp # 实体类别名

2、EmpMapper.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.zgh.mapper.EmpMapper">
    <select id="find16"  parameterType="Emp" resultType="Emp">
        select * from emp
        <where>
            <if test="ename!=null">
                and ename like#{ename}
            </if>
            <if test="job!=null">
                and job=#{job}
            </if>
        </where>
    </select>
</mapper>

mapper包

@Mapper
public interface EmpMapper extends BaseMapper<Emp> {

    List<Emp> find16(Emp emp);
}

service包

public interface EmpService extends IService<Emp> {

    List<Emp> find16(Emp emp);
}
@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {

    @Resource
    private EmpMapper mapper;

    @Override
    public List<Emp> find16(Emp emp) {
        return mapper.find16(emp);
    }
}

test包

//用于启动一个Spring Boot应用作为测试环境。
@SpringBootTest
@Slf4j
public class App {
    @Test
    public void find16() {
        Emp emp = new Emp();
        emp.setJob("程序员");
        service.find16(emp).forEach(System.out::println);
    }
}

启动后控制台输出

image-20231005224603586

十二、Spring Boot处理跨域

什么是跨域

浏览器从⼀个域名的⽹⻚去请求另⼀个域名的资源时,协议、域名(主机)、端⼝任⼀不同,都是跨域。

在前后端分离的模式下,前后端的域名是不⼀致的,此时就会发⽣跨域访问问题

跨域出于浏览器的同源策略限制

  • 同源策略是⼀种约定,它是浏览器最核⼼也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。
  • 可以说Web是构建在同源策略基础之上的浏览器只是针对同源策略的⼀种实现
  • 同源策略会阻⽌⼀个域的javascript脚本和另外⼀个域的内容进⾏交互
  • 所谓同源(即指在同⼀个域)就是两个⻚⾯具有相同的协议(protocol),主机(host)和端⼝号(port)。

例如:a ⻚⾯想获取 b ⻚⾯资源,如果 a、b ⻚⾯的协议、域名、端⼝、⼦域名不同,所进⾏的访问⾏动都是跨域的,

⽽浏览器为了安全问题⼀般都限制了跨域访问,也就是不允许跨域请求资源。

注意:跨域限制访问,其实是浏览器的限制。理解这⼀点很重要!!!

image-20231005230132352

解决⽅案

对于 CORS的跨域请求,主要有以下⼏种⽅式可供选择:

  • 返回新的CorsFilter
  • 重写 WebMvcConfigurer
  • 使⽤注解 @CrossOrigin
  • ⼿动设置响应头 (HttpServletResponse)
  • ⾃定web filter 实现跨域

注意

  • CorFilter / WebMvConfigurer / @CrossOrigin 需要 SpringMVC 4.2以上版本才⽀持,对应springBoot1.3版本以上
  • 上⾯前两种⽅式属于全局 CORS 配置,后两种属于局部 CORS配置。如果使⽤了局部跨域是会覆盖全局跨域的规则,所以可以通过 @CrossOrigin 注解来进⾏细粒度更⾼的跨域资源控制。
  • 其实⽆论哪种⽅案,最终⽬的都是修改响应头,向响应头中添加浏览器所要求的数据,进⽽实现跨域。

1、返回新的CorsFilter

在任意配置类,返回⼀个新的 CorsFilter 的对象 ,并添加映射路径和具体的 CORS 配置路径。

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
//1. 添加 CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
//放⾏哪些原始域
        config.addAllowedOrigin("*");
//放⾏哪些请求⽅式
        config.addAllowedMethod("*");
//放⾏哪些原始请求头部信息
        config.addAllowedHeader("*");
        // 添加最⼤存活时间 单位:秒
        config.setMaxAge(1800L);
//2. 添加映射路径
        UrlBasedCorsConfigurationSource corsConfigurationSource = new
                UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**",config);
//3. 返回新的CorsFilter
        return new CorsFilter(corsConfigurationSource);
    }
}

2、重写WebMvcConfigurer

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    public void addCorsMappings(CorsRegistry registry){
        registry.addMapping("/*/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .maxAge(1800)
                .allowedOrigins("*");
    }
}

3、使⽤注解 @CrossOrigin

这个注解可以⽤于两个地⽅:

  • 类:表示该类的所有⽅法允许跨域。
  • ⽅法:表示当前⽅法允许跨域。
@RestController
@CrossOrigin(origins = "*")
public class DailyblueController {
    @RequestMapping("/a")
    public String a() {
        return "This is DailyblueController`a method!";
    }
    @RequestMapping("/b")
    @CrossOrigin(origins = "http://localhost:8081") //指定具体ip允许跨域
    public String b() {
        return "This is DailyblueController`b method!";
    }
    @RequestMapping("/c")
    public String c() {
        return "This is DailyblueController`c method!";
    }
}

其中 @CrossOrigin 中的2个参数:

  • origins:允许可访问的域列表。
  • maxAge:准备响应前的缓存持续的最⼤时间(以秒为单位)。

4、⼿动设置响应头

使⽤ HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这⾥ Origin 的值也可以设置为 “*”,表示全部放⾏。

@RequestMapping("/index")
public String index(HttpServletResponse response) {
    response.addHeader("Access-Control-Allow-Origin","*");
    return "index";
}

5、使⽤⾃定义filter 实现跨域

可以⾃定义⼀个多滤器,对请求的数据头进⾏过来,允许其他域请求访问:

@Component
public class CorsFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,contenttype");
        chain.doFilter(req, res);
    }
    public void init(FilterConfig filterConfig) {}
    public void destroy() {}
}

6、spring_boot_mvc5

项目结构

image-20231005231816002

1、注解方式@CrossOrigin(推荐)

controller包

@RestController
@RequestMapping("/first")
public class FirstController {
    @CrossOrigin
    @GetMapping("/a")
    public String a() {
        return "This is FirstController`a method!";
    }

    @GetMapping("/b")
    public String b() {
        return "This is FirstController`b method!";
    }
}

启动类

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

前端部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
  <button @click="loadAxiosData1">发送1</button>
  <button @click="loadAxiosData2">发送2</button>
</div>
</body>
</html>
<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        methods: {
            loadAxiosData1() {
                axios.get('http://localhost:8080/first/a')
                .then((response)=>{
                    console.log(response)
                })
            },
            loadAxiosData2() {
                axios.get('http://localhost:8080/first/b')
                    .then((response)=>{
                        console.log(response)
                    })
            },
        }
    })
</script>

启动项目后效果

image-20231005234226358

image-20231005234306813

2、返回新的 CorsFilter

@Configuration
public class GlobalCorsConfig {
     @Bean
    public CorsFilter corsFilter() {
        //1. 添加 CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        //放⾏哪些原始域
        config.addAllowedOrigin("*");
        //放⾏哪些请求⽅式
        config.addAllowedMethod("*");
        //放⾏哪些原始请求头部信息
        config.addAllowedHeader("*");
        // 添加最⼤存活时间 单位:秒
        config.setMaxAge(1800L);
        //2. 添加映射路径
        UrlBasedCorsConfigurationSource corsConfigurationSource = new
                UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**", config);
        //3. 返回新的CorsFilter
        return new CorsFilter(corsConfigurationSource);
    }
}

image-20231005234644206

3、重写 WebMvcConfigurer

@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {
    public void addCorsMappings(CorsRegistry registry){
        registry.addMapping("/*/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .maxAge(1800)
                .allowedOrigins("*");
    }
}

image-20231005234933981

4、⼿动设置响应头

@RestController
@RequestMapping("/first")
public class FirstController {
    //@CrossOrigin
    @GetMapping("/a")
    public String a() {
        return "This is FirstController`a method!";
    }

    @GetMapping("/b")
    public String b(HttpServletResponse response) {
        //表头全通过
        response.addHeader("Access-Control-Allow-Origin", "*");
        return "This is FirstController`b method!";
    }
}

image-20231005235159827

5、使⽤⾃定义 filter 实现跨域

@Component
public class CorsFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,contenttype");
        chain.doFilter(request, response);
    }

    public void init(FilterConfig filterConfig) {
    }

    public void destroy() {
    }
}

image-20231005235722272

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
springboot:是一个基于Java开发的框架,简化了Spring应用的初始化配置和部署过程。它提供了一套开发规范和约定,帮助开发人员快速搭建高效稳定的应用程序。 mybatis-plus:是基于MyBatis的增强工具,提供了一些便捷的CRUD操作方法和代码生成功能,简化了数据库操作的开发工作。它能够轻松集成到SpringBoot应用中,提高开发效率。 springmvc:是一种基于MVC设计模式的Web框架,用于构建Web应用程序。它能够从URL中解析请求参数,并将请求分发给对应的Controller进行处理。SpringMVC提供了一套灵活的配置和注解方式,支持RESTful风格的API开发。 shiro:是一种用于身份验证和授权的框架,可以集成到SpringBoot应用中。它提供了一套简单易用的API,可以处理用户认证、角色授权、会话管理等安全相关的功能。Shiro还支持集成其他认证方式,如LDAP、OAuth等。 redis:是一种开源的内存数据库,采用键值对存储数据。Redis具有高性能、高并发和持久化等特点,常用于缓存、消息队列和分布式锁等场景。在企业级报表后台管理系统中,可以使用Redis来进行缓存数据,提高系统的响应速度和性能。 企业级报表后台管理系统:是一种用于统一管理和生成报表的系统。它通常包括用户权限管理、报表设计、报表生成、数据分析等功能。使用SpringBootMyBatis-PlusSpringMVC、Shiro和Redis等技术,可以快速搭建一个可靠、高效的报表管理系统,满足企业对数据分析和决策的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值