# dailyblue_boot **Repository Path**: dailyblue/dailyblue_boot ## Basic Information - **Project Name**: dailyblue_boot - **Description**: Spring Boot 是 Pivotal 团队在 Spring 的基础上提供的一套全新的开源框架,其目的是为了简化 Spring 应用的搭建和开发过程。Spring Boot 去除了大量的 XML 配置文件,简化了复杂的依赖管理。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-11-07 - **Last Updated**: 2022-11-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Spring Boot 概述 Spring Boot 是 Pivotal 团队在 Spring 的基础上提供的一套全新的开源框架,其目的是为了简化 Spring 应用的搭建和开发过程。Spring Boot 去除了大量的 XML 配置文件,简化了复杂的依赖管理。 Spring Boot 具有 Spring 的一切优秀特性,Spring 能做的事,Spring Boot 都可以做,而且使用更加简单,功能更加丰富,性能更加稳定而健壮。随着近些年来微服务技术的流行,Spring Boot 也成了时下炙手可热的技术。 Spring Boot 集成了大量常用的第三方库配置,Spring Boot 应用中这些第三方库几乎可以是零配置的开箱即用(out-of-the-box),大部分的 Spring Boot 应用都只需要非常少量的配置代码(基于 Java 的配置),开发者能够更加专注于业务逻辑。 ![点击查看图片来源](img/log.png) ## 创建 SpringBoot 项目 下来我们通过 Intellij IDEA 创建第一个 Spring Boot 项目。 Intellij IDEA 一般可以通过两种方式创建 Spring Boot 项目: - 使用 Maven 创建 - 使用 Spring Initializr 创建 ### Maven 方式创建 首先,文件--->新建--->项目 ![image-20221031141352721](img/image-20221031141352721.png) 其次,选择名称、组名和存放路径。 ![image-20221031141806409](img/image-20221031141806409.png) 下来,引入 Spring Boot 的相关依赖: ```xml ... org.springframework.boot spring-boot-starter-parent 最新版本号 org.springframework.boot spring-boot-starter-web ... ``` 最后,编写 Spring Boot 启动类 创建一个名为 XxxApplication 主程序,用来启动 Spring Boot 应用 ````java @SpringBootApplication public class XxxApplication { public static void main(String[] args) { SpringApplication.run(XxxApplication.class, args); } } ```` **注意**:一般 Boot 项目的启动名称都为 项目名+Application 方式。 创建好的项目结构如下: ![image-20221031171902041](img/image-20221031171902041.png) ### Spring Initializr 方式创建 IntelliJ IDEA 支持用户使用 Spring 项目创建向导(Spring Initializr )快速地创建一个 Spring Boot 项目 首先,选择项目创建向导,分别处理如下操作: 1. 项目名称 2. 存放位置 3. 组 4. JDK设置 ![image-20221031172340220](img/image-20221031172340220.png) **注意**:如果start.spring.io网速不好,可以点击齿轮按钮修改成国内镜像:https://start.aliyun.io ![image-20221031172706772](img/image-20221031172706772.png) ## Yaml 概述 Spring Boot 提供了大量的自动配置,极大地简化了 spring 应用的开发过程,当用户创建了一个 Spring Boot 项目后,即使不进行任何配置,该项目也能顺利的运行起来。当然,用户也可以根据自身的需要使用配置文件修改 Spring Boot 的默认设置。 SpringBoot 默认使用以下 2 种全局的配置文件,其文件名是固定的。 - application.properties - application.yml 其中,application.yml 是一种使用 YAML 语言编写的文件,它与 application.properties 一样,可以在 Spring Boot 启动时被自动读取,修改 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 属性配置文件。 ```yaml server: port: 8080 ``` ### 语法 YAML 的语法如下: - 使用缩进表示层级关系。 - 缩进时不允许使用 Tab 键,只允许使用空格。 - 缩进的空格数不重要,但同级元素必须左侧对齐。 - 大小写敏感。 - Key的冒号和值中间有空格隔开。 例如: ```yaml 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 支持以下三种数据结构: - 字面量:单个的、不可拆分的值 - 对象:键值对的集合 - 数组:一组按次序排列的值 #### 字面量写法 字面量是指单个的,不可拆分的值,例如:数字、字符串、布尔值、以及日期等。 在 YAML 中,使用“key: value”的形式表示一对键值对,如 name: 关为。 字面量直接写在键值对的“value”中即可,且默认情况下字符串是不需要使用单引号或双引号的。 ```yaml name: 关为 ``` #### 对象 在 YAML 中,对象可能包含多个属性,每一个属性都是一对键值对。 YAML 为对象提供了 2 种写法: - 普通写法:使用缩进表示对象与属性的层级关系。 - 行内写法:使用 Map 格式表示对象中属性和值的关系。 ```yaml # 普通写法 user1: name: 关为 age: 18 sex: 男 # 行内写法 user2: {name: 关为,age: 18,sex: 男} ``` #### 数组 在 YAML 中,数组也和对象一样,存在两种写法,分别也是:普通写法和行内写法。 ```yaml # 普通写法 array1: - 张三 - 李四 - 王五 # 行内写法 array2: [张三,李四,王五] ``` #### 复合结构写法 ````yaml class: id: 1 name: 软件一班 headmaster: id: 1 name: 王老师 subject: 数学老师 desc: 班主任,班级管理的管家 students: - id: 1 name: 张三 age: 19 - id: 2 name: 李四 age:21 - id: 3 name: 王五 age: 20 ```` ## Spring Boot 使用 MVC 虽然 SpringMVC 相较于 Servlet 在使用上已经有了极大的提升,但是在环境搭建、配置文件和对注解的使用上仍然有些繁琐,有些同学在想有没有一种方案,可以快速搭建 SpringMVC 环境? ### 引入依赖 这里集成了 SpringMVC、Servlet等相关依赖并设置了兼容版本。 ```xml org.springframework.boot spring-boot-starter-web ``` ### 创建控制类 这里没什么特别的地方,和过去我们学习的 SpringMVC 一致。 ```java @Controller @RequestMapping("/first") public class FirstController { @GetMapping("/index") public String index() { return "index"; } @GetMapping("/json") @ResponseBody public String json() { return "json"; } } ``` ### 文件结构 默认情况下,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-20221102155213337](img/image-20221102155213337.png) ### 相关注解 | 名称 | 解释 | 属性 | | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | @Controller | 这个是一个 IoC 注解,一般用于标注控制层的类
负责产生对象。 | | | @ResponseBody | 设置当前方法返回的是数据而不是跳转页面。 | | | @RestController | 这个是一个复合注解,是@Controller+@ResponseBody
代表产生对象和指定方法返回结果是输出的内容。 | | | @RequestMapping | 映射注解,可以注解到类或者方法上
有一些子注解:
1. @GetMapping
2. @PostMapping
3. @PutMapping
4. @DeleteMapping | 1. value属性 @RequestMapping 注解的默认属性,因此如果只有 value 属性时,可以省略该属性名,如果有其它属性,则必须写上 value 属性名称。
2. path 属性和 value 属性都用来作为映射使用。path 属性支持通配符匹配,如`@RequestMapping(path="/first/*")`表示`http://localhost:8080/first/zhangsan` 或 `http://localhost:8080/first/lisi` 都能够正常访问
3. name属性相当于方法的注释,使方法更易理解。如 `@RequestMapping(value = "/first",name = "第一个备注")`
4. method 属性用于表示该方法支持哪些 HTTP 请求。如果省略 method 属性,则说明该方法支持全部的 HTTP 请求。
5. params 属性用于指定请求中**必须**包含什么名称的参数。 | | @RequestParam | 请求参数,书写到形式参数之前 | 可以手动映射参数和形参的关系,也可以设置默认值。 | | @PathVariable | Rest 风格参数传递 | | ### 配置文件 ```yaml server: port: 8081 servlet: context-path: /dailyblue spring: application: name: dailyblue ``` ### 启动并访问 启动启动类,并打开浏览器输入路径访问: ![image-20221102170421795](img/image-20221102170421795.png) ### 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,因此,当 Spring Boot 项目中的 pom.xml 引入了 spring-boot-starter-web 的依赖后,就无须在引入 spring-boot-starter 核心启动器的依赖了。 ## 配置绑定 所谓“配置绑定”就是把配置文件中的值与 JavaBean 中对应的属性进行绑定。通常,我们会把一些配置信息(例如,数据库配置)放在配置文件中,然后通过 Java 代码去读取该配置文件,并且把配置文件中指定的配置封装到 JavaBean(实体类) 中。 SpringBoot 提供了以下 2 种方式进行配置绑定: - 使用 @ConfigurationProperties 注解 - 使用 @Value 注解 ### @ConfigurationPreperties 注解 通过 Spring Boot 提供的 @ConfigurationProperties 注解,可以将全局配置文件中的配置数据绑定到 JavaBean 中。下面我们以 Spring Boot 项目 dailyblue_boot为例,演示如何通过 @ConfigurationProperties 注解进行配置绑定。 #### application.yml 在 Dailyble_boot 的全局配置文件 application.yml 中添加以下自定义属性。 ```yaml student: id: 1 name: 张三 sex: 男 age: 19 loves: [ '足球','游戏','网球' ] room: name: 软件一班 address: 南2楼3楼302 ``` #### 实体类 在 Dailyble_boot 项目的 bean 中创建一个名为 Student 的实体类,并将配置文件中的属性映射到这个实体类上,代码如下: ```java /** * 将配置文件中配置的每一个属性的值,映射到这个组件中 * * @ConfigurationProperties:告诉 SpringBoot 将本类中的所有属性和配置文件中相关的配置进行绑定 * prefix = "student":配置文件中哪个下面的所有属性进行一一映射 * * 只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能 */ @Data @ConfigurationProperties(prefix = "student") @Component public class Student implements Serializable { private Integer id; private String name; private Character sex; private Integer age; private String[] loves; private Room room; } ``` #### 注意 - 只有在容器中的组件,才会拥有 SpringBoot 提供的强大功能。如果我们想要使用 @ConfigurationProperties 注解进行配置绑定,那么首先就要保证该对 JavaBean 对象在 IoC 容器中,所以需要用到 @Component 注解来添加组件到容器中。 - JavaBean 上使用了注解 @ConfigurationProperties(prefix = “person”) ,它表示将这个 JavaBean 中的所有属性与配置文件中以“person”为前缀的配置进行绑定。 #### 控制层类 在 Dailyble_boot 项目的 controller 中创建一个名为 FirstController 的类,注入 Student 类并将它显示到页面中,代码如下: ```java @RestController @RequestMapping("/first") public class FirstController { @Resource private Student student; @GetMapping("/a") public Student a() { return student; } } ``` #### 启动并查看 启动项目,并输入 http://localhost:8080/first/a 查看效果: ![image-20221101115438561](img/image-20221101115438561.png) > **一个需要注意的问题:** > > ![image-20221101141117098](img/image-20221101141117098.png) > > 如何解决呢? > > 在 pom.xml 中引入如下注解: > > ```xml > > org.springframework.boot > spring-boot-configuration-processor > true > > ``` ### @Value 注解 当我们只需要读取配置文件中的某一个配置时,可以通过 @Value 注解获取。 以 Spring Boot 项目 dailyblue_boot 为例,新建实体类 Student2 ,使用 @Value 注解进行配置绑定,代码如下: ```java @Data @Component public class Student2 implements Serializable { @Value("${student.id}") private Integer id; @Value("${student.name}") private String name; @Value("${student.sex}") private Character sex; @Value("${student.age}") private Integer age; private String[] loves; private Room room; } ``` ### @Value 与 @ConfigurationProperties 对比 @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 注解。 我们在选用时,根据实际应用场景选择合适的注解能达到事半功倍的效果。 ### @PropertySource 注解 如果将所有的配置都集中到 application.properties 或 application.yml 中,那么这个配置文件会十分的臃肿且难以维护,因此我们通常会将与 Spring Boot 无关的配置(例如自定义配置)提取出来,写在一个单独的配置文件中,并在对应的 JavaBean 上使用 @PropertySource 注解指向该配置文件。 以 Dailyblue_Boot 为例,将与 student 相关的自定义配置移动到 src/main/resources 下的 student.properties 中(注意,必须把 application.properties 或 application.yml 中的相关配置删除),如下图: ![image-20221101144031857](img/image-20221101144031857.png) student.properties 的配置如下: ```properties student.id=1 student.name=张三 student.sex=男 student.age=19 student.loves=足球,游戏,网球 student.room.name=软件一班 student.room.address=南2楼3楼302 ``` Student 类中引入@PropertySource 注解即可,Student2 类的写法基本一致。 ![image-20221101144200786](img/image-20221101144200786.png) **注意**: - @PropertySource 只对 properties 文件可以进行加载,但对于 yml 或者 yaml 不能支持。 - 如果读取中文时出现乱码,使用 @PropertySource(value = "classpath:student.properties", encoding="UTF-8") ## 导入Spring配置 默认情况下,Spring Boot 中是不包含任何的 Spring 配置文件的,即使我们手动添加 Spring 配置文件到项目中,也不会被识别。 ![image-20221101174024557](img/image-20221101174024557.png) 那么 Spring Boot 项目中真的就无法导入 Spring 配置吗?答案是否定的。 Spring Boot 为了我们提供了以下 2 种方式来导入 Spring 配置: - 使用 @ImportResource 注解加载 Spring 配置文件 - 使用全注解方式加载 Spring 配置 ### @ImportResource 注解 在主启动类上使用 @ImportResource 注解可以导入一个或多个 Spring 配置文件,并使其中的内容生效。 我们还是使用 Dailyblue_Boot 项目为例 #### 创建类 在 pojo 包下创建一个类 DemoA ,代码如下: ```java @Data public class DemoA { private String name; } ``` #### 引入 spring.xml 在 resource 文件夹下创建 spring.xml 配置文件,代码如下: ```xml ``` #### 控制层 控制层的类注入刚才的 DemoA,代码如下: ```java @RestController @RequestMapping("/first") public class FirstController { @Resource private DemoA da; @GetMapping("/c") public DemoA c() { return da; } } ``` #### 启动类 修改启动类,引入 @ImportResource 注解,代码如下: ```java @SpringBootApplication @ImportResource(locations = {"classpath:/spring.xml"}) public class DailyblueBootApplication { public static void main(String[] args) { SpringApplication.run(DailyblueBootApplication.class, args); } } ``` ### 全注解方式加载 Spring 配置 Spring Boot 推荐我们使用全注解的方式加载 Spring 配置,其实现方式如下: 1. 使用 @Configuration 注解定义配置类,替换 Spring 的配置文件。 2. 配置类内部可以包含有一个或多个被 @Bean 注解的方法,这些方法会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类扫描,构建 bean 定义(相当于 Spring 配置文件中的标签),方法的返回值会以组件的形式添加到容器中,组件的 id 就是方法名。 代码部分很简单,这里仅仅书写配置加载类: ```java @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 的配置。 ![](img/web202211020942.png) 在 Spring Boot 项目中,我们可以通过以下 2 中形式定制 Spring MVC: - 扩展 Spring MVC - 全面接管 Spring MVC 下面,我们分别对这两种定制 Spring MVC 的形式进行介绍。 ### 扩展 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-20221102114148043](img/image-20221102114148043.png) #### 配置类 创建一个类实现 WebMvcConfigurer 接口 ```java @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-20221102114401914](img/image-20221102114401914.png) ### 全面接管 Spring MVC 在一些特殊情况下,我们可能需要抛弃 Spring Boot 对 Spring MVC 的全部自动配置,完全接管 Spring MVC。此时我们可以自定义一个 WebMvcConfigurer 类型的配置类,并在该类上标注 @EnableWebMvc 注解,来实现完全接管 Spring MVC。 **注意**:完全接管 Spring MVC 后,Spring Boot 对 Spring MVC 的自动配置将全部失效。 #### 就修改一个地方: ```java @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-20221102115740633](img/image-20221102115740633.png) #### 控制台 ![image-20221102115825089](img/image-20221102115825089.png) #### 直接访问 login.html ![image-20221102120020046](img/image-20221102120020046.png) Spring Boot 能够访问位于静态资源文件夹中的静态文件,这是在 Spring Boot 对 Spring MVC 的默认自动配置中定义的,当我们全面接管 Spring MVC 后,Spring Boot 对 Spring MVC 的默认配置都会失效,此时再访问静态资源文件夹中的静态资源就会报错了。 ## Spring Boot 拦截器 我们对拦截器并不陌生,无论是 Struts 2 还是 Spring MVC 中都提供了拦截器功能,它可以根据 URL 对请求进行拦截,主要应用于登陆校验、权限验证、乱码解决、性能监控和异常处理等功能上。Spring Boot 同样提供了拦截器功能。 在 Spring Boot 项目中,使用拦截器功能通常需要以下 3 步: 1. 定义拦截器 2. 注册拦截器 3. 指定拦截规则(如果是拦截所有,静态资源也会被拦截) ### 定义拦截器 在 Spring Boot 中定义拦截器十分的简单,只需要创建一个拦截器类,并实现 HandlerInterceptor 接口即可。 HandlerInterceptor 接口中定义以下 3 个方法,如下表。 | 返回值类型 | 方法声明 | 描述 | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | boolean | preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。 | | void | postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) | 该方法在控制器处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步修改。 | | void | afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) | 该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。 | ### 注册拦截器 创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration 注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor() 方法将自定义的拦截器注册到容器中。 在配置类 SpringMvcConfig 中,添加以下方法注册拦截器,代码如下: ```java @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(你的拦截器类对象); } } ``` ### 指定拦截规则 修改 SpringMvcConfig 配置类中 addInterceptors() 方法的代码,继续指定拦截器的拦截规则,代码如下: ```java @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:多个拦截器执行顺序优先级,值越小优先级越高。 至此,拦截器的基本功能已经完成。 ## 默认异常处理 在日常的 Web 开发中,会经常遇到大大小小的异常,此时往往需要一个统一的异常处理机制,来保证客户端能接收较为友好的提示。Spring Boot 同样提供了一套默认的异常处理机制。 ### Spring Boot 默认异常处理机制 Spring Boot 提供了一套默认的异常处理机制,一旦程序中出现了异常,Spring Boot 会自动识别客户端的类型(浏览器客户端或机器客户端),并根据客户端的不同,以不同的形式展示异常信息。 #### 对于浏览器而言 对于浏览器客户端而言,Spring Boot 会响应一个 "whitelabel" 错误视图,以 HTML 格式呈现错误信息: ![image-20221103145006116](img/image-20221103145006116.png) #### 对于机器客户端而言 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息。 ```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:用于页面上共享异常信息。 下面,我们依次对这四个组件进行详细的介绍。 #### ErrorPageCustomizer ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。 ```java @Bean public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) { return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath); } ``` ErrorPageCustomizer 通过 registerErrorPages() 方法来注册错误页面的响应规则。当系统中发生异常后,ErrorPageCustomizer 组件会自动生效,并将请求转发到 “/error”上,交给 BasicErrorController 进行处理,其部分代码如下: ```java @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); } ``` #### BasicErrorController ErrorMvcAutoConfiguration 还向容器中注入了一个错误控制器组件 BasicErrorController,代码如下。 ```java @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), errorViewResolvers.orderedStream().collect(Collectors.toList())); } ``` BasicErrorController 的定义如下。 ```java //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 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> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } Map body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } ...... } ``` Spring Boot 通过 BasicErrorController 进行统一的错误处理(例如默认的“/error”请求)。Spring Boot 会自动识别发出请求的客户端的类型(浏览器客户端或机器客户端),并根据客户端类型,将请求分别交给 errorHtml() 和 error() 方法进行处理。 | 返回值类型 | 方法声明 | 客户端类型 | 错误信息返类型 | | ----------------------------------- | ------------------------------------------------------------ | ----------------------------------------- | --------------------- | | ModelAndView | errorHtml(HttpServletRequest request, HttpServletResponse response) | 浏览器客户端 | text/html(错误页面) | | ResponseEntity> | error(HttpServletRequest request) | 机器客户端(例如安卓、IOS、Postman 等等) | JSON | > 换句话说,当使用浏览器访问出现异常时,会进入 BasicErrorController 控制器中的 errorHtml() 方法进行处理,当使用安卓、IOS、Postman 等机器客户端访问出现异常时,就进入error() 方法处理。 在 errorHtml() 方法中会调用父类(AbstractErrorController)的 resolveErrorView() 方法,代码如下。 ```java protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,Map model) { //获取容器中的所有的错误视图解析器来处理该异常信息 for (ErrorViewResolver resolver : this.errorViewResolvers) { //调用错误视图解析器的 resolveErrorView 解析到错误视图页面 ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } ``` 从上述源码可以看出,在响应页面的时候,会在父类的 resolveErrorView 方法中获取容器中所有的 ErrorViewResolver 对象(错误视图解析器,包括 DefaultErrorViewResolver 在内),一起来解析异常信息。 #### DefaultErrorViewResolver ErrorMvcAutoConfiguration 还向容器中注入了一个默认的错误视图解析器组件 DefaultErrorViewResolver,代码如下: ```java @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resources); } ``` 当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象(错误视图解析器),并分别调用它们的 resolveErrorView() 方法对异常信息进行解析,其中自然也包括 DefaultErrorViewResolver(默认错误信息解析器)。 DefaultErrorViewResolver 的部分代码如下: ```java public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map SERIES_VIEWS; static { Map 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 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 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 model) { //遍历所有静态资源文件夹 for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); //静态资源文件夹下的错误页面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.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。 2. 尝试使用模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates 目录下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html。 3. 若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流程,否则跳转到第 4 步。 4. 依次从各个静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流程,否则跳转到第 5 步。 5. 将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结束整个解析流程,否则跳转第 6 步。 6. 处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)。 #### DefaultErrorAttributes ErrorMvcAutoConfiguration 还向容器中注入了一个组件默认错误属性处理工具 DefaultErrorAttributes,代码如下: ```java @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } ``` DefaultErrorAttributes 是 Spring Boot 的默认错误属性处理工具,它可以从请求中获取异常或错误信息,并将其封装为一个 Map 对象返回,其部分代码如下: ```java public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { ...... @Override public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map 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 getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map 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路径 ## 全局异常处理 在项目开发中出现异常时很平常不过的事情,我们处理异常也有很多种方式。可能如下: ```java public int div(int a ,int b){ int c = 0; try{ c = a / b; }catch (Exception ex){ ex.printStackTrace(); } return c; } ``` 如果我们这样处理异常,代码中就会出现特别多的异常处理模块,这样代码可读性就会变得非常差,而且业务模块逻辑会夹杂特别多的非业务逻辑。但是在项目开发的过程中我们应该将主要精力放在业务模块,除了必要的异常处理模块最好不要再包含其他无关紧要的代码。那么我们如何处理项目中无处不在的异常呢? 这就引出了我们要介绍的全局异常处理方法,主要有两种方式: 1. 使用 @RestControllerAdvice 和 @ExceptionHandler 注解 2. 使用 ErrorController 类来实现 ### @RestControllerAdvice 方式 #### 全局异常处理类 书写一个全局异常处理类,代码如下: ```java // 注解@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()); } } ``` #### 控制器 书写一个控制器,代码如下: ```java @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-20221103173651819](img/image-20221103173651819.png) 控制台效果: ![image-20221103173712650](img/image-20221103173712650.png) ### 使用 ErrorController 类 系统默认的错误处理类为 BasicErrorController,当出现错误时显示默认的错误页面。这里编写一个自己的错误处理类,上面默认的处理类将不会起作用。 getErrorPath()返回的路径服务器将会重定向到该路径对应的处理类,本例中为error方法。 ```java @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-20221103174545191](img/image-20221103174545191.png) 控制台效果: ![image-20221103174603813](img/image-20221103174603813.png) ### 两者区别 1. 注解 @RestControllerAdvice 方式只能处理控制器抛出的异常。此时请求已经进入控制器中。 2. 类 ErrorController 方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误 3. 如果应用中两者共同存在,则 @RestControllerAdvice 方式处理控制器抛出的异常,类 ErrorController 方式未进入控制器的异常。 4. @RestControllerAdvice 方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常信息,自由度更大。 ## 整合 MyBatis-Plus 对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 都默认采用整合 Spring Data 的方式进行统一处理,通过大量自动配置,来简化我们对数据访问层的操作,而如果我们要使用 MyBatis-Plus,只需要引入 MyBatis-Plus 的启动器就行。 ![image-20221104112859546](img/image-20221104112859546.png) ### 概述 MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 > 愿景 > > 我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。 ![image-20221104113109625](img/image-20221104113109625.png) ### 特性 - 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑 - 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作 ### 入门案例 我们将通过一个简单的 Demo 来阐述 MyBatis-Plus 的强大功能。 #### 引入依赖 ```xml com.baomidou mybatis-plus-boot-starter 最新版本 ``` #### 配置数据源 在 `application.yml` 配置文件中添加 MySQL 数据库的相关配置: ```yaml # DataSource Config spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test type: com.alibaba.druid.pool.DruidDataSource username: root password: a123456 ``` #### 实体类 我们以经典的 `Emp` 表作为实例,进行讲解,编写对应的实体 `Emp` 类。 ```java @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; } ``` #### Mapper 类 编写 `mapper` 包下的 `EmpMapper` 接口 ```java @Mapper public interface EmpMapper extends BaseMapper{ } ``` #### 业务层 编写 `service` 包下的 `EmpService` 接口 ```java public interface EmpService extends IService{ } ``` 编写 `service.impl` 包下的 `EmpServiceImpl` 实现类 ```java @Service public class EmpServiceImpl extends ServiceImpl implements EmpService { } ``` #### 开始使用 添加测试类,进行功能测试: ```java @SpringBootTest(classes = DailyblueBootApplication.class) public class SampleTest { @Resource private EmpService empService; @Test void findTest() { List list = empService.list(); list.forEach(System.out::println); } } ``` 通过以上几个简单的步骤,我们就实现了 `Emp` 表的 CRUD 功能,甚至连 XML 文件都不用编写! 从以上步骤中,我们可以看到集成 MyBatis-Plus 非常的简单,只需要引入 starter 工程,并配置 mapper 扫描路径即可。 但 MyBatis-Plus 的强大远不止这些功能,想要详细了解 MyBatis-Plus 的强大功能?那就继续往下看吧! ### MyBatis-Plus 注解 介绍 Mybatis-Plus 注解包相关类详解(更多详细描述可点击查看源码注释) 传动门:[mybatis-plus-annotation](https://gitee.com/baomidou/mybatis-plus/tree/3.0/mybatis-plus-annotation/src/main/java/com/baomidou/mybatisplus/annotation) #### @TableName - 描述:表名注解,标识实体类对应的表 - 使用位置:实体类 ```java @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` 注解可省略,如省略对应类名的表 **属性列表:** | 属性 | 类型 | 必须指定 | 默认值 | 描述 | | :--------------- | :------- | :------- | :----- | :----------------------------------------------------------- | | value | String | 否 | "" | 表名 | | schema | String | 否 | "" | schema | | keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) | | resultMap | String | 否 | "" | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) | | autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) | | excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 | **关于 `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 - 描述:主键注解 - 使用位置:实体类主键字段 ```java @Data @TableName(value = "emp") public class Emp implements Serializable { @TableId(value = "empno",type = IdType.AUTO) private Integer empno; // 其他省略 } ``` **属性列表:** | 属性 | 类型 | 必须指定 | 默认值 | 描述 | | :---- | :----- | :------- | :---------- | :----------- | | value | String | 否 | "" | 主键字段名 | | type | Enum | 否 | IdType.NONE | 指定主键类型 | **type属性概述:** | 值 | 描述 | | :------------ | :----------------------------------------------------------- | | AUTO | 数据库 ID 自增 | | NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) | | INPUT | insert 前自行 set 主键值 | | ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口`IdentifierGenerator`的方法`nextId`(默认实现类为`DefaultIdentifierGenerator`雪花算法) | | ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口`IdentifierGenerator`的方法`nextUUID`(默认 default 方法) | | ID_WORKER | 分布式全局唯一 ID 长整型类型(please use `ASSIGN_ID`) | | UUID | 32 位 UUID 字符串(please use `ASSIGN_UUID`) | | ID_WORKER_STR | 分布式全局唯一 ID 字符串类型(please use `ASSIGN_ID`) | #### @TableField 描述:字段注解(非主键) ```java @Data @TableName(value = "emp") public class Emp implements Serializable { @TableField(value = "ename") private String ename; @TableField(value = "hiredate") private String hireDate; // 其他省略 } ``` **属性列表:** | 属性 | 类型 | 必须指定 | 默认值 | 描述 | | :--------------- | :--------------------------- | :------- | :----------------------- | :----------------------------------------------------------- | | **value** | String | 否 | "" | 数据库字段名 | | **exist** | boolean | 否 | true | 是否为数据库表字段 | | condition | String | 否 | "" | 字段 `where` 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 `%s=#{%s}`,[参考(opens new window)](https://github.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus-annotation/src/main/java/com/baomidou/mybatisplus/annotation/SqlCondition.java) | | update | String | 否 | "" | 字段 `update set` 部分注入,例如:当在version字段上注解`update="%s+1"` 表示更新时会 `set version=version+1` (该属性优先级高于 `el` 属性) | | insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL `insert into table_a(column) values (#{columnProperty})` | | updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED `update table_a set column=#{columnProperty}` | | whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY `where column=#{columnProperty}` | | fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略
1. DEFAULT:默认不处理
2. INSERT:插入时填充字段
3. UPDATE:更新时填充字段
4. DELETE:删除时填充字段 | | select | boolean | 否 | true | 是否进行 select 查询 | | keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 | | jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) | | typeHandler | Class | 否 | UnknownTypeHandler.class | 类型处理器 (该默认值不代表会按照该值生效) | | numericScale | String | 否 | "" | 指定小数点后保留的位数 | > 关于`jdbcType`和`typeHandler`以及`numericScale`的说明: > > numericScale只生效于 update 的 sql. jdbcType和typeHandler如果不配合@TableName#autoResultMap = true一起使用,也只生效于 update 的 sql. 对于typeHandler如果你的字段类型和 set 进去的类型为equals关系,则只需要让你的typeHandler让 Mybatis 加载到即可,不需要使用注解。 #### @TableLogic 描述:表字段逻辑处理注解(逻辑删除) | 属性 | 类型 | 必须指定 | 默认值 | 描述 | | :----- | :----- | :------- | :----- | :----------- | | value | String | 否 | "" | 逻辑未删除值 | | delval | String | 否 | "" | 逻辑删除值 | ### 逻辑删除 我们在实际使用中的删除操作,其实并没有删除表中的数据,仅仅是将表的数据 `隐藏` 掉,查询时不查询这些 `隐藏` 的数据。 #### 分类 - 全局方式 - 局部方式 #### 全局方式 在 `application.yml` 中进行配置: ```yaml mybatis-plus: global-config: db-config: logic-delete-field: state # 全局逻辑删除的实体字段名 logic-delete-value: 0 # 逻辑已删除值(默认为 1) logic-not-delete-value: 1 # 逻辑未删除值(默认为 0) ``` 注意:配置好后,所有实体不需额外操作,但必须保证存在 `state` 字段。 #### 局部方式 需要在每个所需实体类中引入 `@TableLogic` 注解。 ```java @TableLogic(delval='0',value='1') private Integer empState; ``` #### 说明 只对自动注入的 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() 附录: - 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。 - 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。 ### 自动填充功能 在数据表的设计中,经常需要加一些字段,如:创建时间,最后修改时间等,此时可以使用 MyBatis-Plus 来帮我们进行自动维护。 #### 填充方式 自动填充方式有两种,分别是: - 通过数据库完成自动填充 - 使用程序完成自动填充 #### 数据库自动填充 这里没什么好说的,就是正常书写就行,在添加时,有默认值的列所对应的属性不赋值,数据库会自动赋予默认值。 #### 程序自动填充 ```java public class Emp { // 注意!这里需要标记为填充字段 @TableField(.. fill = FieldFill.INSERT) private String hireDate; .... } ``` 这里就给 `hireDate` 属性设置了自动填充,如果该属性没有值,则自动填充 `Null` 。 **测试类**: ```java @Test void saveTest(){ Emp emp = new Emp(); emp.setComm(21400.3); emp.setEname("关为"); empService.save(emp); } ``` **效果**: ![image-20221105144132278](img/image-20221105144132278.png) **改进** 改进刚才的内容,我们想要在未给 `hireDate` 属性赋值时,系统赋予当前时间。 这时我们要引入自定义实现类 `MyMetaObjectHandler` ```java @Slf4j @Component public class NowDateHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.strictInsertFill(metaObject, "hireDate", () -> getNowDate(), String.class); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.strictInsertFill(metaObject, "hireDate", () -> getNowDate(), String.class); } private String getNowDate() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(new Date()); } } ``` **这时候的效果:** ![image-20221105145400066](img/image-20221105145400066.png) #### 说明 - 填充原理是直接给 `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` 不能为空,否则自动填充失效 ### 条件构造器 在Mybatis-Plus中提了构造条件的类 `Wrapper` ,它可以根据自己的意图定义我们需要的条件。Wrapper 是一个抽象类,一般情况下我们用它的子类 QueryWrapper 来实现自定义条件查询。 ![warpper](img/warpper.png) #### API | 方法名 | 说明 | 用法实例 | 等价SQL | | ----------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------- | | allEq(Map params) | 全部等于 | map.put(“id”,“3”);map.put(“user_name”,“IT可乐”);allEq(map) | user_name = “IT可乐” AND id = 3 | | **eq(R column, Object val)** | **等于 =** | **eq(“id”,“3”)** | **id = 3** | | **ne(R column, Object val)** | **不等于 <>** | **ne(“id”, “3”)** | **id <> 3** | | **gt(R column, Object val)** | **大于 >** | **gt(“user_age”,“18”)user_age > 18** | **user_age > 18** | | **ge(R column, Object val)** | **大于等于 >=** | **ge(“user_age”,“18”)** | **user_age >= 18** | | **lt(R column, Object val)** | **小于 <** | **lt(“user_age”,“18”)** | **user_age < 18** | | **le(R column, Object val)** | **小于等于 <=** | **le(“user_age”,“18”)** | **user_age <= 18** | | **between(R column, Object val1, Object val2)** | **BETWEEN 值1 AND 值2** | **between(“user_age”,“18”,“25”)** | **user_age BETWEEN 18 AND 25** | | notBetween(R column, Object val1, Object val2) | NOT BETWEEN 值1 AND 值2 | notBetween(“user_age”,“18”,“25”) | user_age NOT BETWEEN 18 AND 25 | | **like(R column, Object val)** | **LIKE ‘%值%’** | **like(“user_name”,“可乐”)** | **like ‘%可乐%’** | | **notLike(R column, Object val)** | **NOT LIKE ‘%值%’** | **notLike(“user_name”,“可乐”)** | **not like ‘%可乐%’** | | **likeLeft(R column, Object val)** | **LIKE ‘%值’** | **likeLeft(“user_name”,“可乐”)** | **like ‘%可乐’** | | **likeRight(R column, Object val)** | **LIKE ‘值%’** | **likeRight(“user_name”,“可乐”)** | **like ‘可乐%’** | | **isNull(R column)** | **字段 IS NULL** | **isNull(“user_name”)** | **user_name IS NULL** | | isNotNull(R column) | 字段 IS NOT NULL | isNotNull(“user_name”) | user_name IS NOT NULL | | in(R column, Collection value) | 字段 IN (value.get(0), value.get(1), …) | in(“user_age”,{1,2,3}) | user_age IN (?,?,?) | | notIn(R column, Collection value) | 字段 NOT IN (value.get(0), value.get(1), …) | notIn(“user_age”,{1,2,3}) | user_age NOT IN (?,?,?) | | inSql(R column, String inValue) | 字段 IN ( sql语句 ) | inSql(“id”,“select id from user”) | id IN (select id from user) | | notInSql(R column, String inValue) | 字段 NOT IN ( sql语句 ) | notInSql(“id”,“select id from user where id > 2”) | id NOT IN (select id from user where id > 2 | | **groupBy(R… columns)** | **分组:GROUP BY 字段, …** | **groupBy(“id”,“user_age”)** | **GROUP BY id,user_age** | | **orderByAsc(R… columns)** | **排序【升序】:ORDER BY 字段, … ASC** | **orderByAsc(“id”,“user_age”)** | **ORDER BY id ASC,user_age ASC** | | **orderByDesc(R… columns)** | **排序【降序】:ORDER BY 字段, … DESC** | **orderByDesc(“id”,“user_age”)** | **ORDER BY id DESC,user_age DESC** | | orderBy(boolean condition, boolean isAsc, R… columns) | ORDER BY 字段, … | orderBy(true,true,“id”,“user_age”) | ORDER BY id ASC,user_age ASC | | having(String sqlHaving, Object… params) | HAVING ( sql语句 ) | having(“sum(user_age)>{0}”,“25”) | HAVING sum(user_age)>25 | | **or()** | **拼接 OR** | **eq(“id”,1).or().eq(“user_age”,25)** | **id = 1 OR user_age = 25** | | **and(Consumerconsumer)** | **AND 嵌套** | **and(i->i.eq(“id”,1).ne(“user_age”,18))** | **id = 1 AND user_age <> 25** | | nested(Consumerconsumer) | 正常嵌套 不带 AND 或者 OR | nested(i->i.eq(“id”,1).ne(“user_age”,18)) | id = 1 AND user_age <> 25 | | apply(String applySql, Object… params) | 拼接 sql(不会有SQL注入风险) | apply(“user_age>{0}”,“25 or 1=1”) | user_age >‘25 or 1=1’ | | last(String lastSql) | 拼接到 sql 的最后,多次调用以最后一次为准(有sql注入的风险) | last(“limit 1”) | limit 1 | | exists(String existsSql) | 拼接 EXISTS ( sql语句 ) | exists(“select id from user where user_age = 1”) | EXISTS (select id from user where user_age = 1) | #### AbstractWrapper > 说明: > > QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类 > 用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件 > 注意: entity 生成的 where 条件与 使用各个 api 生成的 where 条件**没有任何关联行为** #### QueryWrapper > 说明: > > 继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件 > 及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取 ##### select ```java select(String... sqlSelect) select(Predicate predicate) select(Class entityClass, Predicate predicate) ``` - 设置查询字段 > 说明: > > 以上方法分为两类. > 第二类方法为:过滤查询字段(主键除外),入参不包含 class 的调用前需要`wrapper`内的`entity`属性有值! 这两类方法重复调用以最后一次为准 - 例: `select("id", "name", "age")` - 例: `select(i -> i.getProperty().startsWith("test"))` #### UpdateWrapper > 说明: > > 继承自 `AbstractWrapper` ,自身的内部属性 `entity` 也用于生成 where 条件 > 及 `LambdaUpdateWrapper`, 可以通过 `new UpdateWrapper().lambda()` 方法获取! ##### set ```java set(String column, Object val) set(boolean condition, String column, Object val) ``` - SQL SET 字段 - 例: `set("name", "老李头")` - 例: `set("name", "")`--->数据库字段值变为**空字符串** - 例: `set("name", null)`--->数据库字段值变为`null` ##### setSQL ```java setSql(String sql) ``` - 设置 SET 部分 SQL - 例: `setSql("name = '老李头'")` ### 使用 XML 配置 有的时候对于一些复杂 SQL 语句,尤其是动态SQL,使用注解比较麻烦,我们可以通过 XML 来书写。 #### application.yml 引入 ```yaml mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml # mapper 文件位置 ``` #### 根据接口生成xml配置文件 ```xml ``` ### application.yml ```yaml # 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 处理跨域 ### 什么是跨域 浏览器从一个域名的网页去请求另一个域名的资源时,协议、域名(主机)、端口任一不同,都是跨域。在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域访问问题。跨域出于浏览器的同源策略限制。 同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。 例如:a 页面想获取 b 页面资源,如果 a、b 页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。 **注意:**跨域限制访问,其实是浏览器的限制。理解这一点很重要!!! ![kuayu](img/kuayu.png) ### 解决方案 对于 CORS的跨域请求,主要有以下几种方式可供选择: - 返回新的CorsFilter - 重写 WebMvcConfigurer - 使用注解 @CrossOrigin - 手动设置响应头 (HttpServletResponse) - 自定web filter 实现跨域 > 注意: > > - CorFilter / WebMvConfigurer / @CrossOrigin 需要 SpringMVC 4.2以上版本才支持,对应springBoot 1.3版本以上 > - 上面前两种方式属于全局 CORS 配置,后两种属于局部 CORS配置。如果使用了局部跨域是会覆盖全局跨域的规则,所以可以通过 @CrossOrigin 注解来进行细粒度更高的跨域资源控制。 > - 其实无论哪种方案,最终目的都是修改响应头,向响应头中添加浏览器所要求的数据,进而实现跨域。 #### 返回新的 CorsFilter 在任意配置类,返回一个新的 `CorsFilter` 的对象 ,并添加映射路径和具体的 `CORS` 配置路径。 ```java @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); } } ``` #### 重写 WebMvcConfigurer ```java @Configuration public class CorsConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry){ registry.addMapping("/*/**") .allowedHeaders("*") .allowedMethods("*") .maxAge(1800) .allowedOrigins("*"); } } ``` #### 使用注解 @CrossOrigin 这个注解可以用于两个地方: - 类:表示该类的所有方法允许跨域。 - 方法:表示当前方法允许跨域。 ```java @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:准备响应前的缓存持续的最大时间(以秒为单位)。 #### 手动设置响应头 使用 HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里 Origin 的值也可以设置为 “*”,表示全部放行。 ```java @RequestMapping("/index") public String index(HttpServletResponse response) { response.addHeader("Access-Control-Allow-Origin","*"); return "index"; } ``` #### 使用自定义 filter 实现跨域 可以自定义一个多滤器,对请求的数据头进行过来,允许其他域请求访问: ```java package com.dailyblue.java.spring.boot.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; @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,content-type"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) {} public void destroy() {} } ```