zoukankan      html  css  js  c++  java
  • spring boot快速入门

    1.什么是SpringBoot

    • 一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can "just run",能迅速的开发web应用,几行代码开发一个http接口。

    • 所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。

    • 是的这就是Java企业级应用->J2EE->spring->springboot的过程。

    • 随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;

    • Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。

    • 简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

    • Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。

    Spring Boot的主要优点:

    ● 能够快速创建基于Spring的应用程序

    ● 能够直接使用java main方法启动内嵌的Tomcat服务器运行SpringBoot程序,不需要部署war包文件

    ● 提供约定的starter POM来简化Maven配置,让Maven的配置变得简单

    ● 自动化配置,根据项目的Maven依赖配置,Springboot自动配置Spring、Spring mvc等

    ● 提供了程序的健康检查等功能

    ● 基本可以完全不使用XML配置文件,采用注解配置

    SpringBoot四大核心

    ● 自动配置

    针对很多Spring应用程序和常见的应用功能,SpringBoot能自动提供相关配置

    ● 起步依赖

    告诉SpringBoot需要什么功能,它就能引入需要的依赖库

    ● Actuator

    让你能够深入运行中的SpringBoot应用程序,一探SpringBoot程序的内部信息

    ● 命令行界面

    这是SpringBoot的可选特性,主要针对Groovy语言使用;

    Groovy是一种基于JVM(Java虚拟机) 的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与Java代码很好地结合,也能用于扩展现有代码,由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。

    2.第一个spring boot程序

    2.1.使用 IDEA 直接创建项目

    1、创建一个新项目

    2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现

    3、填写项目信息

    4、选择初始化的组件(初学勾选 Web 即可)

    5、填写项目路径

    6、等待项目构建成功

    image-20211108104225240

    image-20211108104244944

    img

    2.2.pom.xml 分析

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <!-- 父依赖 -->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.5.6</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>springbootOne</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>springbootOne</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <!-- web场景启动器 -->
            <!--web依赖:tomcat,dispatcherServlet,xml...-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--spring-boot-starter:所有的springboot依赖都是使用这个开头的-->
            <!-- springboot单元测试 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
    
                <!-- 剔除依赖 -->
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
        </dependencies>
    
        <build>
            <plugins>
                <!-- 打包插件 -->
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    
    • 项目元数据:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId、artifactId、version、name、description等

    • parent:继承spring-boot-starter-parent的依赖管理,控制版本与打包等内容

    • dependencies:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器。spring-boot-starter-test用于编写单元测试的依赖包。更多功能模块的使用将在后面逐步展开。

    • build:构建配置部分。默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot应用打包成JAR来直接运行。

    2.3.编写一个http接口

    1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到

    ![image-20211108111755801](https://gitee.com/wyl1924/cdn/raw/master/img/blog/image-20211108111755801.png

    image-20211108111933347

    image-20211108112414293

    2.4.将项目打成jar包

    点击 maven的 package,等待生成。

    image-20211108112805061

    如果测试用例影响到打包,可以跳过

    <!--
        在工作中,很多情况下我们打包是不想执行测试用例的
        可能是测试用例不完事,或是测试用例会影响数据库数据
        跳过测试用例执
    -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
            <!--跳过项目运行测试用例-->
            <skipTests>true</skipTests>
        </configuration>
    </plugin>
    

    打成了jar包后,就可以在任何地方运行了

    image-20211108113444005

    2.5.端口

    有没有看到我的端口被占用了

    image-20211108113910276

    2.6.生成项目模板

    为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。

    1. 打开浏览器,访问:https://start.spring.io/

    2. 根据页面提示,选择构建工具,开发语言,项目信息等。

    3. 点击 Generate the project,生成项目模板,生成之后会将压缩包下载到本地。

    image-20211108171343805

    3.运行原理探究

    我们之前写的HelloSpringBoot,到底是怎么运行的呢,我们从pom.xml文件探究起;

    3.1.父依赖

    pom.xml

    • spring-boot-dependencies:核心依赖在父工程中!
    • 我们在写或者引入一些Springboot依赖的时候,不需要指定版本,就因为有这些版本仓库

    1、其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!

    <!-- 父依赖 -->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.5.6</version>
            <relativePath/>
        </parent>
    

    2、点进去,发现还有一个父依赖

    image-20211108135229517

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.5.6</version>
    </parent>
    

    3、这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;

    4、以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了

    3.2.启动器 spring-boot-starter

    • 依赖

      <dependency>        								 <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter</artifactId>
      </dependency>
      
    • springboot-boot-starter-xxx,说白了就是Springboot的启动场景

    • 比如spring-boot-starter-web,他就会帮我们自动导入web的所有依赖

    • springboot会将所有的功能场景,都变成一个个的启动器

    • 我们要使用什么功能,就只需要找到对应的启动器就好了start

    3.3.主程序

    3.3.1.默认的主启动类

    package com.example.springbootone;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    //@SpringBootApplication 来标注一个主程序类
    //说明这是一个Spring Boot应用
    @SpringBootApplication
    public class SpringbootOneApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootOneApplication.class, args);
        }
    
    }
    
    

    3.3.2注解(@SpringBootApplication)

    • 作用:标注在某个类上说明这个类是SpringBoot的主配置

    • SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;

    • 进入这个注解:可以看到上面还有很多其他注解!

    image-20211108135746831

    @ComponentScan

    • 这个注解在Spring中很重要 ,它对应XML配置中的元素。

    • 作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中

    @SpringBootConfiguration

    • 作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;

    • 我们继续进去这个注解查看

    image-20211108135926732

    • 这里的 @Configuration,说明这是一个spring的配置类 ,配置类就是对应Spring的xml 配置文件;

    • @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!

    • 我们回到 SpringBootApplication 注解中继续看。

    @EnableAutoConfiguration

    • 开启自动配置功能

      • 以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;
      • @EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;

      点进注解接续查看:

    • @AutoConfigurationPackage :自动配置包

    image-20211108140054846

    image-20211108140207428

      • @import :Spring底层注解@import , 给容器中导入一个组件

      • Registrar.class 作用:自动配置包注册,将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

      • 这个分析完了,退到上一步,继续看

    • @Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;

      • AutoConfigurationImportSelector自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:
    // 获取所有的配置
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
    
    • 获得候选的配置

      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
          // 和下面的方法对应
          //这里的getSpringFactoriesLoaderFactoryClass()方法
          //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
          
        
          Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
          return configurations;
      }
      
      //和上面的类的方法loadFactoryNames里面的第一个参数对应
      //这里的getSpringFactoriesLoaderFactoryClass()方法
      //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
       protected Class<?> getSpringFactoriesLoaderFactoryClass() {
           return EnableAutoConfiguration.class;
       }
      
    • 这个方法getCandidateConfigurations()又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法,获取所有的加载配置

      public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
          String factoryClassName = factoryClass.getName();
          //这里它又调用了 loadSpringFactories 方法
          return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
      }
      
    • 我们继续点击查看 loadSpringFactories 方法

      • 项目资源:META-INF/spring.factories
      • 系统资源:META-INF/spring.factories
      • 从这些资源中配置了所有的nextElement(自动配置),分装成properties
      //将所有的资源加载到配置类中(将下面的抽离出来分析,第15行)
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      
      private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
          //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
          MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
          if (result != null) {
              return result;
          } else {
              try {
                  //去获取一个资源 "META-INF/spring.factories"
                  Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                  LinkedMultiValueMap result = new LinkedMultiValueMap();
                  //判断有没有更多的元素,将读取到的资源循环遍历,封装成为一个Properties
                  while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                      UrlResource resource = new UrlResource(url);
                      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                      Iterator var6 = properties.entrySet().iterator();
                      while(var6.hasNext()) {
                          Entry<?, ?> entry = (Entry)var6.next();
                          String factoryClassName = ((String)entry.getKey()).trim();
                          String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                          int var10 = var9.length;
                          for(int var11 = 0; var11 < var10; ++var11) {
                              String factoryName = var9[var11];
                              result.add(factoryClassName, factoryName.trim());
                          }
                      }
                  }
                  cache.put(classLoader, result);
                  return result;
              } catch (IOException var13) {
                  throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
              }
          }
      }
      
    • 发现一个多次出现的文件:spring.factories

    3.3.3.spring.factories

    我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!

    image-20211108140808447

    3.3.4.WebMvcAutoConfiguration

    在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    		ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {
    
    	public static final String DEFAULT_PREFIX = "";
    
    	public static final String DEFAULT_SUFFIX = "";
    
    	private static final String[] SERVLET_LOCATIONS = { "/" };
    
    	@Bean
    	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    

    可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!

    所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中

    3.3.5.结论:

    1. SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值

    2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;

    3. 以前我们需要自动配置的东西,现在springboot帮我们做了

    4. 整合JavaEE,整体解决方案和自动配置的东西都在springboot-autoconfigure的jar包中;

    5. 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中

    6. 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并自动配置,@Configuration(javaConfig) ;

    7. 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;

      img

    3.4启动

    @SpringBootApplication
    public class Springboot01HellowordApplication {
    
        public static void main(String[] args) {
           	//该方法返回一个ConfigurableApplicationContext对象
     		//参数一:应用入口的类; 参数二:命令行参数  
            SpringApplication.run(Springboot01HellowordApplication.class, args);
        }
    
    }
    

    SpringApplication.run分析

    • 分析该方法主要分两部分
    • 一是SpringApplication的实例化,
    • 二是run方法的执行;

    3.4.1.SpringApplication

    image-20211108141346936

    这个类主要做了以下四件事情:

    1. 推断应用的类型是普通的项目还是Web项目

    2. 查找并加载所有可用初始化器 , 设置到initializers属性中

    3. 找出所有的应用程序监听器,设置到listeners属性中

    4. 推断并设置main方法的定义类,找到运行的主类

    查看构造器

    image-20211108141446936

    3.4.2.run方法流程分析

    img

    4.yaml语法学习

    4.1.配置文件

    SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的

    • application.properties

      • 语法结构 :key=value
    • application.yaml

      • 语法结构 :key:空格 value

    配置文件的作用 :修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;

    比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!

    server:
      port: 8081
    

    4.2.YAML

    yaml概述

    • YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)

    • 这种语言以数据作为中心,而不是以标记语言为重点!

    • 以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml

      • 传统xml配置:

        <server>
            <port>8081<port>
        </server>
        
      • yaml配置:

        server:
          prot: 8080
        

    yaml基础语法

    说明:语法要求严格!

    1. 空格不能省略

    2. 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

    3. 属性和值的大小写都是十分敏感的。

    字面量:普通的值 [ 数字,布尔值,字符串 ]

    • 字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;k: v

      注意:

      • “ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;

        比如 :name: "wang\n jingmo" 输出 :wang换行 jingmo

      • '' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

        比如 :name: ‘wang\n jingmo’ 输出 :wang\n jingmo

    对象、Map(键值对)

    #对象、Map格式
    k: 
        v1:
        v2:
    

    在下一行来写对象的属性和值得关系,注意缩进;比如:

    student:
        name: wangyanling
        age: 3
    

    行内写法

    student: {name: wangyanling,age: 3}
    

    数组( List、set )

    用 - 值表示数组中的一个元素,比如:

    pets:
     - cat
     - dog
     - pig
    

    行内写法

    pets: [cat,dog,pig]
    

    修改SpringBoot的默认端口号

    配置文件中添加,端口号的参数,就可以切换端口;

    server:
      port: 8082
    

    4.3.注入配置文件

    yaml文件强大的地方在于可以给实体类直接注入匹配值

    4.3.1. yaml注入配置文件

    ① 在springboot项目中的resources目录下新建一个文件 application.yml

    ② 编写一个实体类 Dog

    package com.wyl.springboot.pojo;
    
    @Component  //注册bean到容器中
    public class Dog {
        private String name;
        private Integer age;
        
        //有参无参构造、get、set方法、toString()方法  
    }
    

    ③ 试着用@Value给bean注入属性值

    @Component //注册bean
    public class Dog {
        @Value("旺财")
        private String name;
        @Value("1")
        private Integer age;
    }
    

    ④ 在SpringBoot的测试类下注入并输出

    @SpringBootTest
    class DemoApplicationTests {
    
        @Autowired //将狗狗自动注入进来
        Dog dog;
    
        @Test
        public void contextLoads() {
            System.out.println(dog); //打印看下狗狗对象
        }
    
    }
    

    结果成功输出,@Value注入成功

    Dog{name='旺财', age=1}
    

    ⑤ 再编写一个复杂点的实体类

    @Component //注册bean到容器中
    public class Person {
        private String name;
        private Integer age;
        private Boolean happy;
        private Date birth;
        private Map<String,Object> maps;
        private List<Object> lists;
        private Dog dog;
        
        //有参无参构造、get、set方法、toString()方法  
    }
    

    ⑥ 使用yaml配置的方式进行注入

    写的时候注意区别和优势,首先编写一个yaml配置

    person:
      name: wjm
      age: 3
      happy: false
      birth: 2000/01/01
      maps: {k1: v1,k2: v2}
      lists:
       - code
       - girl
       - music
      dog:
        name: 旺财
        age: 1
    

    ⑦ 把对象的所有值都写好后,注入到类中

    /*
    @ConfigurationProperties作用:
    将配置文件中配置的每一个属性的值,映射到这个组件中;
    告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
    参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
    */
    @Component //注册bean
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private Integer age;
        private Boolean happy;
        private Date birth;
        private Map<String,Object> maps;
        private List<Object> lists;
        private Dog dog;
    }
    

    ⑧ IDEA 提示,springboot配置注解处理器没有找到

    Not Found
    
    The requested URL /spring-boot/docs/2.3.3.RELEASE/reference/html/configuration-metadata.html was not found on this server.
    

    查看文档(在网址中更改版本获得,如回到2.1.9),找到一个依赖

    <!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    

    ⑨ 确认以上配置都完成后,去测试类中测试

    @SpringBootTest
    class DemoApplicationTests {
    
        @Autowired
        Person person; //将person自动注入进来
    
        @Test
        public void contextLoads() {
            System.out.println(person); //打印person信息
        }
    
    }
    

    结果:所有值全部注入成功

    4.3.2. 加载指定配置文件

    @PropertySource :加载指定的配置文件;
    @configurationProperties:默认从全局配置文件中获取值

    1. 在resources目录下新建一个person.properties文件
    name=hello
    
    1. 在代码中指定加载person.properties文件
    @PropertySource(value = "classpath:person.properties")
    @Component //注册bean
    public class Person {
        @Value("${name}")
        private String name;
        ......
    }
    
    1. 再次输出测试,指定配置文件绑定成功

    4.3.3.配置文件占位符

    配置文件还可以编写占位符生成随机数

    person:
        name: wangjingmo${random.uuid} # 随机uuid
        age: ${random.int}  # 随机int
        happy: false
        birth: 2000/01/01
        maps: {k1: v1,k2: v2}
        lists:
          - code
          - girl
          - music
        dog:
          name: ${person.hello:other}_旺财
          age: 1
    

    4.3.4.回顾properties配置

    上面采用的yaml方法都是最简单的方式,也是开发中最常用的、pringboot所推荐的

    接下来看看其他的实现方式,原理都是相同的,写还是那样写

    配置文件除了yml还有之前常用的properties

    【注意】properties配置文件在写中文的时候会有乱码 , 需要去IDEA中设置编码格式为UTF-8:settings-->FileEncodings 中配置

    测试步骤

    1. 新建一个实体类User
    @Component //注册bean
    public class User {
        private String name;
        private int age;
        private String sex;
    }
    
    1. 编辑配置文件 user.properties
    user1.name=wyl
    user1.age=18
    user1.sex=男
    
    1. 在User类上使用@Value来进行注入
    @Component //注册bean
    @PropertySource(value = "classpath:user.properties")
    public class User {
        //直接使用@value
        @Value("${user.name}") //从配置文件中取值
        private String name;
        @Value("#{9*2}")  // #{SPEL} Spring表达式
        private int age;
        @Value("男")  // 字面量
        private String sex;
    }
    
    1. Springboot测试
    SpringBootTest
    class DemoApplicationTests {
    
        @Autowired
        User user;
    
        @Test
        public void contextLoads() {
            System.out.println(user);
        }
    
    }
    

    结果正常输出

    4.3.4.对比小结

    @Value使用起来并不友好!我们需要为每个属性单独注解赋值比较麻烦
    img

    1. @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
    2. 松散绑定:这个什么意思呢? 比如yml中写的last-name,这个和lastName是一样的,- 后面跟着的字母默认是大写的。这就是松散绑定
    3. JSR303数据校验 ,可以在字段是增加一层过滤器验证 , 保证数据的合法性
    4. 复杂类型封装,yml中可以封装对象 , 使用value就不支持

    结论:

    1. 配置yml和配置properties都可以获取到值 , 强烈推荐 yml;
    2. 如果在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
    3. 如果专门编写了一个JavaBean来和配置文件进行一一映射,就直接使用@configurationProperties

    5.JSR303数据校验

    Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。这里来写个注解让name只能支持Email格式

    @Component //注册bean
    @ConfigurationProperties(prefix = "person")
    @Validated //数据校验
    public class Person {
        @Email(message="邮箱格式错误") //name必须是邮箱格式
        private String name;
    }
    

    运行结果:default message [不是一个合法的电子邮件地址]

    使用数据校验,可以保证数据的正确性; 下面列出一些常见的使用

    @NotNull(message="名字不能为空")
    private String userName;
    @Max(value=120,message="年龄最大不能查过120")
    private int age;
    @Email(message="邮箱格式错误")
    private String email;
    
    空检查
    @Null 验证对象是否为null
    @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
    @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空
    格.
    @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
    Booelan检查
    @AssertTrue 验证 Boolean 对象是否为 true
    @AssertFalse 验证 Boolean 对象是否为 false
    长度检查
    @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
    @Length(min=, max=) string is between min and max included.
    日期检查
    @Past 验证 Date 和 Calendar 对象是否在当前时间之前
    @Future 验证 Date 和 Calendar 对象是否在当前时间之后
    @Pattern 验证 String 对象是否符合正则表达式的规则
    .......等等
    除此以外,我们还可以自定义一些数据校验规则
    

    5.1.多环境切换

    profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境

    5.1.1.多配置文件

    在主配置文件编写的时候,文件名可以是application-{profile}.properties/yml , 用来指定多个环境版本。例如:application-test.properties 代表测试环境配置 application-dev.properties代表开发环境配置
    但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件。但可以通过配置来选择需要激活的环境

    #比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
    #我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
    spring.profiles.active=dev
    

    5.1.2.yml的多文档块

    和properties配置文件中一样,但使用yml去实现不需要创建多个配置文件,更加方便

    server:
    	port: 8081
    #选择要激活那个环境块
    spring:
    	profiles:
    		active: prod
    ---
    server:
    	port: 8083
    spring:
    	profiles: dev #配置环境的名称
    	
    ---
    server:
    	port: 8084
    spring:
    	profiles: prod #配置环境的名称
    

    注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的

    5.1.3.配置文件加载位置

    外部加载配置文件的方式很多,一般选择最常用的即可,在开发的资源文件中进行配置

    springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件

    优先级1:项目路径下的config文件夹配置文件
    优先级2:项目路径下配置文件
    优先级3:资源路径下的config文件夹配置文件
    优先级4:资源路径下配置文件
    

    优先级由高到底,高优先级的配置会覆盖低优先级的配置;
    SpringBoot会从这四个位置全部加载主配置文件;互补配置

    4.4.4.运维小技巧

    指定位置加载配置文件

    我们还可以通过spring.config.location来改变默认的配置文件位置

    项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;

    这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高

    java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
    

    6.自动配置原理

    ----联系---- spring.factories

    SpringBoot官方文档中有大量的配置,我们无法全部记住,官网:https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/appendix-application-properties.html#core-properties

    1595493746481

    6.1.分析自动配置原理

    1. SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration

    2. @EnableAutoConfiguration 作用

      • 利用EnableAutoConfigurationImportSelector给容器中导入一些组件

      • 可以查看selectImports()方法的内容,他返回了一个autoConfigurationEnty,来自this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);这个方法我们继续来跟踪:

      • 这个方法有一个值:List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);叫做获取候选的配置 ,我们点击继续跟踪

        • SpringFactoriesLoader.loadFactoryNames()
        • 扫描所有jar包类路径下META-INF/spring.factories
        • 把扫描到的这些文件的内容包装成properties对象
        • 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
      • 在类路径下,META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值加入到容器中:

    image-20211108144552231

      • 每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;
    1. 每一个自动配置类进行自动配置功能;

    2. 我们以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理;

      //表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
      @Configuration 
         
      //启动指定类的ConfigurationProperties功能;
        //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
        //并把HttpProperties加入到ioc容器中
      @EnableConfigurationProperties({HttpProperties.class}) 
         
      //Spring底层@Conditional注解
        //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
        //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
      @ConditionalOnWebApplication(
          type = Type.SERVLET
      )
         
      //判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
      @ConditionalOnClass({CharacterEncodingFilter.class})
         
      //判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
        //如果不存在,判断也是成立的
        //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
      @ConditionalOnProperty(
          prefix = "spring.http.encoding",
          value = {"enabled"},
          matchIfMissing = true
      )
         
      public class HttpEncodingAutoConfiguration {
          //他已经和SpringBoot的配置文件映射了
          private final Encoding properties;
          //只有一个有参构造器的情况下,参数的值就会从容器中拿
          public HttpEncodingAutoConfiguration(HttpProperties properties) {
              this.properties = properties.getEncoding();
          }
         
          //给容器中添加一个组件,这个组件的某些值需要从properties中获取
          @Bean
          @ConditionalOnMissingBean //判断容器没有这个组件?
          public CharacterEncodingFilter characterEncodingFilter() {
              CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
              filter.setEncoding(this.properties.getCharset().name());
              filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
              filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
              return filter;
          }
          //。。。。。。。
      }
      

    一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!

    • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;

    • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;

    • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;

    • 配置文件能配置什么就可以参照某个功能对应的这个属性类

      //从配置文件中获取指定的值和bean的属性进行绑定
      @ConfigurationProperties(prefix = "spring.http") 
      public class HttpProperties {
          // .....
      }
      

    我们去配置文件里面试试前缀,看提示!

    1595493884773

    这就是自动装配的原理!

    6.2.总结

    1. SpringBoot启动会加载大量的自动配置类

    2. 我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;

    3. 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)

    4. 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;

      xxxxAutoConfigurartion:自动配置类;给容器中添加组件

      xxxxProperties:封装配置文件中相关属性;

    6.3.@Conditional

    了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;

    @Conditional派生注解(Spring注解版原生的@Conditional作用

    作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;

    @Conditional扩展注解 作用(判断是否满足当前指定条件)
    @ConditionalOnJava 系统的java版本是否符合要求
    @ConditionalOnJava 容器中存在指定Bean ;
    @ConditionalOnMissingBean 容器中不存在指定Bean ;
    @ConditionalOnExpression 满足SpEL表达式指定
    @ConditionalOnClass 系统中有指定的类
    @ConditionalOnMissingClass 系统中没有指定的类
    @ConditionalOnSingleCandidate 容器中只有一个指定的Bean ,或者这个Bean是首选Bean
    @ConditionalOnProperty 系统中指定的属性是否有指定的值
    @ConditionalOnResource 类路径下是否存在指定资源文件
    @ConditionalOnWebApplication 当前是web环境
    @ConditionalOnNotWebApplication 当前不是web环境
    @ConditionalOnJndi JNDI存在指定项

    那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

    6.4.自动配置类是否生效

    我们可以在application.properties通过启用 debug=true属性;

    在控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

    #开启springboot的调试类
    debug=true 
    
    • Positive matches:(自动配置类启用的:正匹配)

    • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

    • Unconditional classes: (没有条件的类)

    6.5.自定义Starter

    我们分析完毕了源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩玩!

    6.5.1.说明

    启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;

    命名归约:

    官方命名:

    • 前缀:spring-boot-starter-xxx
    • 比如:spring-boot-starter-web....

    自定义命名:

    • xxx-spring-boot-starter
    • 比如:mybatis-spring-boot-starter

    6.5.2.编写启动器

    1. 在IDEA中新建一个空项目 spring-boot-starter-diy

    2. 新建一个普通Maven模块:kuang-spring-boot-starter

    3. 新建一个Springboot模块:kuang-spring-boot-starter-autoconfigure

    4. 点击apply即可,基本结构

    5. 在我们的 starter 中 导入 autoconfigure 的依赖!

      <!-- 启动器 -->
      <dependencies>
          <!--  引入自动配置模块 -->
          <dependency>
              <groupId>com.kuang</groupId>
              <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId>
              <version>0.0.1-SNAPSHOT</version>
          </dependency>
      </dependencies>
      
    6. 将 autoconfigure 项目下多余的文件都删掉,Pom中只留下一个 starter,这是所有的启动器基本配置!

    7. 我们编写一个自己的服务

      package wyl.ss;
      
      public class HelloService {
      
          HelloProperties helloProperties;
      
          public HelloProperties getHelloProperties() {
              return helloProperties;
          }
      
          public void setHelloProperties(HelloProperties helloProperties) {
              this.helloProperties = helloProperties;
          }
      
          public String sayHello(String name){
              return helloProperties.getPrefix() + name + helloProperties.getSuffix();
          }
      
      }
      
    8. 编写HelloProperties 配置类

      package wyl.ss;
      
      import org.springframework.boot.context.properties.ConfigurationProperties;
      
      // 前缀 kuang.hello
      @ConfigurationProperties(prefix = "kuang.hello")
      public class HelloProperties {
      
          private String prefix;
          private String suffix;
      
          public String getPrefix() {
              return prefix;
          }
      
          public void setPrefix(String prefix) {
              this.prefix = prefix;
          }
      
          public String getSuffix() {
              return suffix;
          }
      
          public void setSuffix(String suffix) {
              this.suffix = suffix;
          }
      }
      
    9. 编写我们的自动配置类并注入bean,测试!

      package wyl.ss;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
      import org.springframework.boot.context.properties.EnableConfigurationProperties;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      @ConditionalOnWebApplication //web应用生效
      @EnableConfigurationProperties(HelloProperties.class)
      public class HelloServiceAutoConfiguration {
      
          @Autowired
          HelloProperties helloProperties;
      
          @Bean
          public HelloService helloService(){
              HelloService service = new HelloService();
              service.setHelloProperties(helloProperties);
              return service;
          }
      
      }
      
    10. 在resources编写一个自己的 META-INF\spring.factories

      # Auto Configure
      org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      wyl.ss.HelloServiceAutoConfiguration
      
    11. 编写完成后,可以安装到maven仓库中!

    6.5.3.新建项目测试我们自己写的启动器

    1. 新建一个SpringBoot 项目

    2. 导入我们自己写的启动器

      <dependency>
          <groupId>wyl.ss</groupId>
          <artifactId>ss-spring-boot-starter</artifactId>
          <version>1.0-SNAPSHOT</version>
      </dependency>
      
    3. 编写一个 HelloController 进行测试我们自己的写的接口!

      package wyl.ss.controller;
      
      @RestController
      public class HelloController {
      
          @Autowired
          HelloService helloService;
      
          @RequestMapping("/hello")
          public String hello(){
              return helloService.sayHello("zxc");
          }
      
      }
      
    4. 编写配置文件 application.properties

      ss.hello.prefix="ppp"
      ss.hello.suffix="sss"
      
    5. 启动项目进行测试,结果成功 !

    7.整合JDBC

    7.1.SpringData简介

    对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。

    Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

    Sping Data 官网

    数据库相关的启动器 :可以参考官方文档

    7.2.创建项目

    7.2.1.新建项目测试

    引入相应的模块:Spring Web、SQL中的JDBC API、MySql Driver

    项目建好之后,Spring Boot自动导入了启动器

    7.2.2.编写yaml配置文件,连接数据库

    新建一个 application.yml

    spring:
    datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/数据库名称?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    

    7.2.3.测试

    配置完这一些东西后就可以直接去使用了,SpringBoot已经默认进行了自动配置

    @SpringBootTest
    class SpringbootDataJdbcApplicationTests {
    
        //DI注入数据源
        @Autowired
        DataSource dataSource;
    
        @Test
        public void contextLoads() throws SQLException {
            //看一下默认数据源
            System.out.println(dataSource.getClass());
            //获得连接
            Connection connection =   dataSource.getConnection();
            System.out.println(connection);
            //关闭连接
            connection.close();
        }
    }
    

    可以看到默认配置的数据源为 class com.zaxxer.hikari.HikariDataSource, 我们并没有手动配置

    HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀

    可以使用 spring.datasource.type 指定自定义的数据源类型,值为使用的连接池实现的完全限定名

    有了数据库连接,就可以 CRUD 操作数据库了。但需要先了解对象 JdbcTemplate

    7.2.4.源码

    打开 DataSourceProperties 的源码,能配置的所有东西都在这

    public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    
    	private ClassLoader classLoader;
    	private String name;
    	private boolean generateUniqueName = true;
    	private Class<? extends DataSource> type;
    	private String driverClassName;
    	private String url;
    	private String username;
    	private String password;
        ............
    

    打开 DataSourceAutoConfiguration 源码,数据源的所有自动配置都在这里

    7.2.5.JDBCTemplate

    • 有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库
    • 即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
    • 数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
    • Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
    • JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

    JdbcTemplate主要提供以下几类方法:

    • execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
    • update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
    • query方法及queryForXXX方法:用于执行查询相关语句;
    • call方法:用于执行存储过程、函数相关语句。

    测试

    新建一个 controller 目录,在里面编写一个 JdbcController

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/jdbc")
    public class JdbcController {
        /**
         * Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
         * JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
         * 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
         */
        @Autowired
        JdbcTemplate jdbcTemplate;
        //查询employee表中所有数据
        //List 中的1个 Map 对应数据库的 1行数据
        //Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
        @GetMapping("/userList")
        public List<Map<String,Object>> userList(){
            String sql = "select * from user";
            List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
            return maps;
        }
        //新增一个用户
        @GetMapping("/add")
        public String addUser(){
            //插入语句,注意时间问题
            String sql = "insert into mybatis.user(id, name,pwd)" +
                " values (9,'Java编程思想','qqwweer987')";
            jdbcTemplate.update(sql);
            //查询
            return "addOk";
        }
        //修改用户信息
        @GetMapping("/update/{id}")
        public String updateUser(@PathVariable("id") int id){
            //插入语句
            String sql = "update mybatis.user set name=?,pwd=? where id="+id;
            //数据封装
            Object[] objects = new Object[2];
            objects[0] = "大威天龙";
            objects[1] = "qwert123";
            jdbcTemplate.update(sql,objects);
            //查询
            return "update-Ok";
        }
        //删除用户
        @GetMapping("/delete/{id}")
        public String delUser(@PathVariable("id") int id){
            //插入语句
            String sql = "delete from mybatis.user where id=?";
            jdbcTemplate.update(sql,id);
            //查询
            return "delete-Ok";
        }
    }
    

    8.集成 druid

    8.1.druid简介

    • Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。

    • Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

    • Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

    • Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。

    • Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。

    • Github地址:https://github.com/alibaba/druid/

    8.2.配置

      加入druid相关配置(.yml配置文件)

    spring:
      #数据库配置
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
        username: root
        password: root
        druid:
            # 初始连接数
            initial-size: 10
            # 最大连接池数量
            max-active: 100
            # 最小连接池数量
            min-idle: 10
            # 配置获取连接等待超时的时间
            max-wait: 60000
            # 打开PSCache,并且指定每个连接上PSCache的大小
            pool-prepared-statements: true
            max-pool-prepared-statement-per-connection-size: 20
            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # 配置一个连接在池中最小生存的时间,单位是毫秒
            min-evictable-idle-time-millis: 300000
            validation-query: SELECT 1 FROM DUAL
            test-while-idle: true
            test-on-borrow: false
            test-on-return: false
            stat-view-servlet:
                enabled: true
                url-pattern: /druid/*
            filter:
                stat:
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: false
                wall:
                    config:
                        multi-statement-allow: true
    

     com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:

    配置 缺省值 说明
    name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this)
    jdbcUrl 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
    username 连接数据库的用户名
    password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
    driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下)
    initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
    maxActive 8 最大连接池数量
    maxIdle 8 已经不再使用,配置了也没效果
    minIdle 最小连接池数量
    maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
    poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
    maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
    validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
    validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc
    Statement对象的void setQueryTimeout(int seconds)方法
    testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
    timeBetweenEvictionRunsMillis 1分钟
    ( 1.0.14 )
    有两个含义: 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
    numTestsPerEvictionRun 不再使用,一个DruidDataSource只支持一个EvictionRun
    minEvictableIdleTimeMillis 30分钟
    ( 1.0.14 )
    连接保持空闲而不被驱逐的最长时间
    connectionInitSqls 物理连接初始化的时候执行的sql
    exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
    filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
    proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

    引入druid依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.9</version>
    </dependency>
    

    项目启动后可通过show processlist查看mysql的全部线程是否和配置的initial-size一致。

    8.3.druid监控测试

    访问ip:port/druid验证即可,url中的/druid要和配置文件中的url-pattern一致

    stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
    

    效果如下:

    img

    9.整合mybatis

    9.1.导入 MyBatis 所需要的依赖

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    

    9.2.配置数据库连接信息

    spring:
      datasource:
        username: root
        password: admin
        #?serverTimezone=UTC解决时区的报错
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
    
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    

    9.3.创建实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
    

    9.4.创建mapper

    UserMapper.java

    // 这个注解表示了这是一个 mybatis 的 mapper 类
    @Mapper
    @Repository
    public interface UserMapper {
    
        List<User> queryUserList();
    
        User queryUserById(int id);
    
        int addUser(User user);
    
        int updateUser(User user);
    
        int deleteUser(int id);
    }
    
    1. 对应的Mapper映射文件

      UserMapper.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">
      <!--namespace=绑定一个对应的Dao/Mapper接口-->
      <mapper namespace="com.wyl.mapper.UserMapper">
          
          <select id="queryUserList" resultType="User">
              select * from mybatis.user;
          </select>
      
          <select id="queryUserById" resultType="User">
              select * from mybatis.user where id = #{id};
          </select>
      
          <insert id="addUser" parameterType="User">
              insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
          </insert>
      
          <update id="updateUser" parameterType="User">
              update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
          </update>
      
          <delete id="deleteUser" parameterType="int">
              delete from mybatis.user where id = #{id}
          </delete>
      </mapper>
      
    2. maven配置资源过滤问题

      <resources>
          <resource>
              <directory>src/main/java</directory>
              <includes>
                  <include>**/*.xml</include>
              </includes>
              <filtering>true</filtering>
          </resource>
      </resources>
      
    3. 编写部门的 UserController 进行测试!

      @RestController
      public class UserController {
          @Autowired
          private UserMapper userMapper;
      
          @GetMapping("/queryUserList")
          public List<User> queryUserList() {
              List<User> userList = userMapper.queryUserList();
      
              for (User user : userList) {
                  System.out.println(user);
              }
      
              return userList;
          }
          
           //添加一个用户
          @GetMapping("/addUser")
          public String addUser() {
              userMapper.addUser(new User(7,"wyl","123456"));
              return "ok";
          }
      
          //修改一个用户
          @GetMapping("/updateUser")
          public String updateUser() {
              userMapper.updateUser(new User(7,"wjm","123456"));
              return "ok";
          }
      
          @GetMapping("/deleteUser")
          public String deleteUser() {
              userMapper.deleteUser(7);
      
              return "ok";
          }
      }
      

    启动项目访问进行测试!

    9.5.分页

    添加相关依赖

    首先,我们需要在 pom.xml 文件中添加分页插件依赖包。

    pom.xml

    <!-- pagehelper -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.2.5</version>
    </dependency>
    

    添加相关配置

    然后在 application.yml 配置文件中添加分页插件有关的配置。

    application.yml

    # pagehelper   
    pagehelper:
        helperDialect: mysql
        reasonable: true
        supportMethodsArguments: true
        params: count=countSql
    

    编写分页代码

    首先,在 DAO 层添加一个分页查找方法。这个查询方法跟查询全部数据的方法除了名称几乎一样。

    SysUserMapper.java

    import java.util.List;
    import com.louis.springboot.demo.model.SysUser;
    
    public interface SysUserMapper {
        int deleteByPrimaryKey(Long id);
    
        int insert(SysUser record);
    
        int insertSelective(SysUser record);
    
        SysUser selectByPrimaryKey(Long id);
    
        int updateByPrimaryKeySelective(SysUser record);
    
        int updateByPrimaryKey(SysUser record);
        
        /**
         * 查询全部用户
         * @return
         */
        List<SysUser> selectAll();
        
        /**
         * 分页查询用户
         * @return
         */
        List<SysUser> selectPage();
    }
    

    然后在 SysUserMapper.xml 中加入selectPage的实现,当然你也可以直接用@Select注解将查询语句直接写在DAO代码,但我们这里选择写在XML映射文件,这是一个普通的查找全部记录的查询语句,并不需要写分页SQL,分页插件会拦截查询请求,并读取前台传来的分页查询参数重新生成分页查询语句。

    SysUserMapper.xml

    <select id="selectPage"  resultMap="BaseResultMap">
      select 
      <include refid="Base_Column_List" />
      from sys_user
    </select>
    

    服务层通过调用DAO层代码完成分页查询,这里统一封装分页查询的请求和结果类,从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会影响服务层以上的分页接口,起到了解耦的作用。

    SysUserService.java

    import java.util.List;
    import com.louis.springboot.demo.model.SysUser;
    import com.louis.springboot.demo.util.PageRequest;
    import com.louis.springboot.demo.util.PageResult;
    
    public interface SysUserService {
    
        /**
         * 根据用户ID查找用户
         * @param userId
         * @return
         */
        SysUser findByUserId(Long userId);
    
        /**
         * 查找所有用户
         * @return
         */
        List<SysUser> findAll();
    
        /**
         * 分页查询接口
         * 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
         * 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
         * 影响服务层以上的分页接口,起到了解耦的作用
         * @param pageRequest 自定义,统一分页查询请求
         * @return PageResult 自定义,统一分页查询结果
         */
        PageResult findPage(PageRequest pageRequest);
    }
    

    服务实现类通过调用分页插件完成最终的分页查询,关键代码是 PageHelper.startPage(pageNum, pageSize),将前台分页查询参数传入并拦截MyBtis执行实现分页效果。

    SysUserServiceImpl.java

    import java.util.List;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.louis.springboot.demo.dao.SysUserMapper;
    import com.louis.springboot.demo.model.SysUser;
    import com.louis.springboot.demo.service.SysUserService;
    import com.louis.springboot.demo.util.PageRequest;
    import com.louis.springboot.demo.util.PageResult;
    import com.louis.springboot.demo.util.PageUtils;
    
    @Service
    public class SysUserServiceImpl implements SysUserService {
        
        @Autowired
        private SysUserMapper sysUserMapper;
        
        @Override
        public SysUser findByUserId(Long userId) {
            return sysUserMapper.selectByPrimaryKey(userId);
        }
    
        @Override
        public List<SysUser> findAll() {
            return sysUserMapper.selectAll();
        }
        
        @Override
        public PageResult findPage(PageRequest pageRequest) {
            return PageUtils.getPageResult(pageRequest, getPageInfo(pageRequest));
        }
        
        /**
         * 调用分页插件完成分页
         * @param pageQuery
         * @return
         */
        private PageInfo<SysUser> getPageInfo(PageRequest pageRequest) {
            int pageNum = pageRequest.getPageNum();
            int pageSize = pageRequest.getPageSize();
            PageHelper.startPage(pageNum, pageSize);
            List<SysUser> sysMenus = sysUserMapper.selectPage();
            return new PageInfo<SysUser>(sysMenus);
        }
    }
    

    在控制器SysUserController中添加分页查询方法,并调用服务层的分页查询方法。

    SysUserController.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.louis.springboot.demo.service.SysUserService;
    import com.louis.springboot.demo.util.PageRequest;
    
    @RestController
    @RequestMapping("user")
    public class SysUserController {
    
        @Autowired
        private SysUserService sysUserService;
        
        @GetMapping(value="/findByUserId")
        public Object findByUserId(@RequestParam Long userId) {
            return sysUserService.findByUserId(userId);
        }
        
        @GetMapping(value="/findAll")
        public Object findAll() {
            return sysUserService.findAll();
        }
        
        @PostMapping(value="/findPage")
        public Object findPage(@RequestBody PageRequest pageQuery) {
            return sysUserService.findPage(pageQuery);
        }
    }
    

    分页查询请求封装类。

    PageRequest.java

    /**
     * 分页请求
     */
    public class PageRequest {
        /**
         * 当前页码
         */
        private int pageNum;
        /**
         * 每页数量
         */
        private int pageSize;
        
        public int getPageNum() {
            return pageNum;
        }
        public void setPageNum(int pageNum) {
            this.pageNum = pageNum;
        }
        public int getPageSize() {
            return pageSize;
        }
        public void setPageSize(int pageSize) {
            this.pageSize = pageSize;
        }
    }
    

    分页查询结果封装类。

    PageResult.java

    import java.util.List;
    /**
     * 分页返回结果
     */
    public class PageResult {
        /**
         * 当前页码
         */
        private int pageNum;
        /**
         * 每页数量
         */
        private int pageSize;
        /**
         * 记录总数
         */
        private long totalSize;
        /**
         * 页码总数
         */
        private int totalPages;
        /**
         * 数据模型
         */
        private List<?> content;
        public int getPageNum() {
            return pageNum;
        }
        public void setPageNum(int pageNum) {
            this.pageNum = pageNum;
        }
        public int getPageSize() {
            return pageSize;
        }
        public void setPageSize(int pageSize) {
            this.pageSize = pageSize;
        }
        public long getTotalSize() {
            return totalSize;
        }
        public void setTotalSize(long totalSize) {
            this.totalSize = totalSize;
        }
        public int getTotalPages() {
            return totalPages;
        }
        public void setTotalPages(int totalPages) {
            this.totalPages = totalPages;
        }
        public List<?> getContent() {
            return content;
        }
        public void setContent(List<?> content) {
            this.content = content;
        }
    }
    

    分页查询相关工具类。

    PageUtils.java

    import com.github.pagehelper.PageInfo;
    public class PageUtils {
    
        /**
         * 将分页信息封装到统一的接口
         * @param pageRequest 
         * @param page
         * @return
         */
        public static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
            PageResult pageResult = new PageResult();
            pageResult.setPageNum(pageInfo.getPageNum());
            pageResult.setPageSize(pageInfo.getPageSize());
            pageResult.setTotalSize(pageInfo.getTotal());
            pageResult.setTotalPages(pageInfo.getPages());
            pageResult.setContent(pageInfo.getList());
            return pageResult;
        }
    }
    

    编译测试运行

    启动应用,访问:localhost:8088/swagger-ui.html,找到对应接口,模拟测试,结果如下。

    参数:pageNum: 1, pageSize: 5

    10.事务管理

    SpringBoot 使用事务非常简单,底层依然采用的是Spring本身提供的事务管理

    • 在入口类中使用注解 @EnableTransactionManagement 开启事务支持

    • 在访问数据库的Service方法上添加注解 @Transactional 即可

    案例思路

    通过SpringBoot +MyBatis实现对数据库学生表的更新操作,在service层的方法中构建异常,查看事务是否生效;

    项目名称:springboot--transacation

    10.1.实现步骤

    10.1.1.StudentController

    @Controller
    public class SpringBootController {
    
        @Autowired
        private StudentService studentService;
        @RequestMapping(value = "/springBoot/update")
        public @ResponseBody Object update() {
            Student student = new Student();
            student.setId(1);
            student.setName("Mark");
            student.setAge(100);
            int updateCount = studentService.update(student);
            return updateCount;
        }
    }
    

    10.1.2.StudentService接口

    public interface StudentService {
    
        /**
         * 根据学生标识更新学生信息
         * @param student
         * @return
         */
        int update(Student student);
    }
    

    10.1.3.StudentServiceImpl

    接口实现类中对更新学生方法进行实现,并构建一个异常,同时在该方法上加@Transactional注解

    @Override
    @Transactional //添加此注解说明该方法添加的事务管理
    public int update(Student student) {
        int updateCount = studentMapper.updateByPrimaryKeySelective(student);
        System.out.println("更新结果:" + updateCount);
        //在此构造一个除数为0的异常,测试事务是否起作用
        int a = 10/0;
        return updateCount;
    }
    

    10.1.4.Application

    在类上加@EnableTransactionManagement开启事务支持

    @EnableTransactionManagement可选,但是@Service必须添加事务才生效

    @SpringBootApplication
    @EnableTransactionManagement //SpringBoot开启事务的支持
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    

    10.1.5.启动Application

    1.查看数据库,并没有修改数据,通过以上结果,说明事务起作用了。

    2.注释掉StudentServiceImpl上的@Transactional测试----数据库发上来改变。

    11.SpringMVC注解

    SpringBoot下的SpringMVC和之前的SpringMVC使用是完全一样的,主要有以下注解:

    1.@Controller

    Spring MVC的注解,处理http请求

    2.@RestController

    Spring4后新增注解,是@Controller注解功能的增强,是@Controller与@ResponseBody的组合注解;

    如果一个Controller类添加了@RestController,那么该Controller类下的所有方法都相当于添加了@ResponseBody注解;

    用于返回字符串或json数据。

    案例:

    • 创建MyUserController类,演示@RestController替代@Controller + @ResponseBody

    @RestController
    public class MyUserController {
        @Autowired
        private UserService userService;
    
        @RequestMapping("/user/getUser")
        public Object getUser(){
            return userService.getUser(1);
        }
    }
    

    3.@RequestMapping(常用)

    支持Get请求,也支持Post请求

    4.@GetMapping

    RequestMapping和Get请求方法的组合只支持Get请求;Get请求主要用于查询操作。

    5.@PostMapping

    RequestMapping和Post请求方法的组合只支持Post请求;Post请求主要用户新增数据。

    6.@PutMapping

    RequestMapping和Put请求方法的组合只支持Put请求;Put通常用于修改数据。

    7.@DeleteMapping

    RequestMapping 和 Delete请求方法的组合只支持Delete请求;通常用于删除数据。

    12.RESTful实现

    Spring boot开发RESTFul 主要是几个注解实现:

    @PathVariable:获取url中的数据,该注解是实现RESTFul最主要的一个注解。

    @PostMapping:接收和处理Post方式的请求

    @DeleteMapping:接收delete方式的请求,可以使用GetMapping代替

    @PutMapping:接收put方式的请求,可以用PostMapping代替

    @GetMapping:接收get方式的请求

    RESTful的优点

    • 轻量,直接基于http,不再需要任何别的诸如消息协议,get/post/put/delete为CRUD操作

    • 面向资源,一目了然,具有自解释性。

    • 数据描述简单,一般以xml,json做数据交换。

    • 无状态,在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了复杂度。

    • 简单、低耦合

    12.1.案例

    使用RESTful风格模拟实现对学生的增删改查操作

    该项目集成了MyBatis、spring、SpringMVC,通过模拟实现对学生的增删改查操作。

    1.创建RESTfulController,并编写代码

    @RestController
    public class RESTfulController {
    
        /**
         * 添加学生
         * 请求地址:http://localhost:8080/springboot-restful/springBoot/student/wyl/23
         * 请求方式:POST
         * @param name
         * @param age
         * @return
         */
        @PostMapping(value = "/springBoot/student/{name}/{age}")
        public Object addStudent(@PathVariable("name") String name,
                                 @PathVariable("age") Integer age) {
            Map retMap = new HashMap();
            retMap.put("name",name);
            retMap.put("age",age);
            return retMap;
        }
        /**
         * 删除学生
         * 请求地址:http://localhost:8080/springboot-restful/springBoot/student/1
         * 请求方式:Delete
         * @param id
         * @return
         */
        @DeleteMapping(value = "/springBoot/student/{id}")
        public Object removeStudent(@PathVariable("id") Integer id) {
    
            return "删除的学生id为:" + id;
        }
    
        /**
         * 修改学生信息
         * 请求地址:http://localhost:8080/springboot-restful/springBoot/student/2
         * 请求方式:Put
         * @param id
         * @return
         */
        @PutMapping(value = "/springBoot/student/{id}")
        public Object modifyStudent(@PathVariable("id") Integer id) {
    
            return "修改学生的id为" + id;
        }
    
        @GetMapping(value = "/springBoot/student/{id}")
        public Object queryStudent(@PathVariable("id") Integer id) {
    
            return "查询学生的id为" + id;
        }
    }
    

    12.2.RESTful原则

    • 增post请求、删delete请求、改put请求、查get请求

    • 请求路径不要出现动词

    例如:查询订单接口

    /boot/order/1021/1(推荐)

    /boot/queryOrder/1021/1(不推荐)

    • 分页、排序等操作,不需要使用斜杠传参数

    例如:订单列表接口 /boot/orders?page=1&sort=desc

    一般传的参数不是数据库表的字段,可以不采用斜杠

    13.静态资源处理

    13.1.对哪些目录映射?

    classpath:/META-INF/resources/ 
    classpath:/resources/
    classpath:/static/ 
    classpath:/public/
    /:当前项目的根路径
    

    就我们在上面五个目录下放静态资源(比如:a.png等),可以直接访问(http://localhost:8080/a.png),类似于以前web项目的webapp下;放到其他目录下无法被访问。

    优先级:resources > static(默认) > public

    13.2.源码分析

    SpringBoot自动配置的WebMvcAutoConfiguration.java

    • SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;

    • 我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

    • 有一个方法:addResourceHandlers 添加资源处理

      @Override
      public void addResourceHandlers(ResourceHandlerRegistry registry) {
          if (!this.resourceProperties.isAddMappings()) {
              // 已禁用默认资源处理
              logger.debug("Default resource handling disabled");
              return;
          }
          // 缓存控制
          Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
          CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
          // webjars 配置
          if (!registry.hasMappingForPattern("/webjars/**")) {
              customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                                   .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                                   .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
          }
          // 静态资源配置
          String staticPathPattern = this.mvcProperties.getStaticPathPattern();
          if (!registry.hasMappingForPattern(staticPathPattern)) {
              customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                                   .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                                   .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
          }
      }
      

      读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;

    13.2.1.什么是webjars 呢?

    Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。

    13.2.2.第一种静态资源映射规则

    使用SpringBoot需要使用Webjars,我们可以去搜索一下:

    网站:https://www.webjars.org

    要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!

    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.4.1</version>
    </dependency>
    

    导入完毕,查看webjars目录结构,并访问Jquery.js文件!

    访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

    1595506019658

    13.2.3.第二种静态资源映射规则

    1、那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;

    1595516976999

    2、我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:

    // 进入方法
    public String[] getStaticLocations() {
        return this.staticLocations;
    }
    // 找到对应的值
    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
    // 找到路径
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
        "classpath:/META-INF/resources/",
      	"classpath:/resources/", 
        "classpath:/static/", 
        "classpath:/public/" 
    };
    

    3、ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。

    4、所以得出结论,以下四个目录存放的静态资源可以被我们识别:

    "classpath:/META-INF/resources/"
    "classpath:/resources/"
    "classpath:/static/"
    "classpath:/public/"
    

    5、我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;

    1595517831392

    6、比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;

    1595517869049

    13.2.4.引用

    不用写static 路径

     <link rel="stylesheet" href="/layui/css/layui.css">
     <link rel="stylesheet" href="/easyui/default/easyui.css">
     <script src="/layui/jquery-1.10.2.min.js" type="text/javascript"></script>
     <script src="/easyui/jquery.easyui.min.js" type="text/javascript"></script>
    

    13.3.自定义静态资源路径

    首先,自定义会覆盖默认!所以没十足把握的情况下,不建议覆盖,但可以添加。
    两种方式

    13.3.1.配置类代码实现

    @Configuration
    public class WebMvcConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {         
        	registry.addResourceHandler("/**").addResourceLocations("file:F:/AppFiles/");
        }
    }
    

    13.3.2.配置文件

    spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,\
      classpath:/static/,classpath:/public/,file:c:/appfiles/
    
    

    13.3.3.相对路径配置

    以上的情况均是绝对路径,受限于环境,开发生产LinuxWin等。这种变化情况多,建议理清思路,再决定是否适用。

    解决方案

    String gitPath = path.getParentFile().getParentFile().getParent() 
    	+ File.separator + "logistics" + File.separator + "uploads" 
    	+ File.separator;
    

    工具类

    spring框架自带的ResourceUtils,或者结合第三方工具

    13.3.4. 欢迎页与图标

    image-20211108162103407

    14.国际化

    国际化(Internationalization 简称 I18n,其中“I”和“n”分别为首末字符,18 则为中间的字符数)是指软件开发时应该具备支持多种语言和地区的功能。换句话说就是,开发的软件需要能同时应对不同国家和地区的用户访问,并根据用户地区和语言习惯,提供相应的、符合用具阅读习惯的页面和数据,例如,为中国用户提供汉语界面显示,为美国用户提供提供英语界面显示。

    在 Spring 项目中实现国际化,通常需要以下 3 步:

    1. 编写国际化资源(配置)文件;
    2. 使用 ResourceBundleMessageSource 管理国际化资源文件;
    3. 在页面获取国际化内容。

    14.1. 编写国际化资源文件

    在 Spring Boot 的类路径下创建国际化资源文件,文件名格式为:基本名_语言代码_国家或地区代码,例如 login_en_US.properties、login_zh_CN.properties。

    以 spring-boot-springmvc-demo1为例,在 src/main/resources 下创建一个 i18n 的目录,并在该目录中按照国际化资源文件命名格式分别创建以下三个文件,

    • login.properties:无语言设置时生效
    • login_en_US.properties :英语时生效
    • login_zh_CN.properties:中文时生效
      以上国际化资源文件创建完成后,IDEA 会自动识别它们,并转换成如下的模式:

    img

    打开任意一个国际化资源文件,并切换为 Resource Bundle 模式,然后点击“+”号,创建所需的国际化属性,如下图。

    国际化配置文件

    14.2.配置文件生效探究

    Spring Boot 已经对 ResourceBundleMessageSource 提供了默认的自动配置。

    Spring Boot 通过 MessageSourceAutoConfiguration 对 ResourceBundleMessageSource 提供了默认配置,其部分源码如下。

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @Conditional(org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition.class)
    @EnableConfigurationProperties
    public class MessageSourceAutoConfiguration {
    
        private static final Resource[] NO_RESOURCES = {};
    
        // 将 MessageSourceProperties 以组件的形式添加到容器中
        // MessageSourceProperties 下的每个属性都与以 spring.messages 开头的属性对应
        @Bean
        @ConfigurationProperties(prefix = "spring.messages")
        public MessageSourceProperties messageSourceProperties() {
            return new MessageSourceProperties();
        }
    
        //Spring Boot 会从容器中获取 MessageSourceProperties
        // 读取国际化资源文件的 basename(基本名)、encoding(编码)等信息
        // 并封装到 ResourceBundleMessageSource 中
        @Bean
        public MessageSource messageSource(MessageSourceProperties properties) {
            ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
            //读取国际化资源文件的 basename (基本名),并封装到 ResourceBundleMessageSource 中
            if (StringUtils.hasText(properties.getBasename())) {
                messageSource.setBasenames(StringUtils
                        .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
            }
            //读取国际化资源文件的 encoding (编码),并封装到 ResourceBundleMessageSource 中
            if (properties.getEncoding() != null) {
                messageSource.setDefaultEncoding(properties.getEncoding().name());
            }
            messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
            Duration cacheDuration = properties.getCacheDuration();
            if (cacheDuration != null) {
                messageSource.setCacheMillis(cacheDuration.toMillis());
            }
            messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
            messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
            return messageSource;
        }
        ...
    }
    

    从以上源码可知:
    Spring Boot 将 MessageSourceProperties 以组件的形式添加到容器中;
    MessageSourceProperties 的属性与配置文件中以“spring.messages”开头的配置进行了绑定;
    Spring Boot 从容器中获取 MessageSourceProperties 组件,并从中读取国际化资源文件的 basename(文件基本名)、encoding(编码)等信息,将它们封装到 ResourceBundleMessageSource 中;
    Spring Boot 将 ResourceBundleMessageSource 以组件的形式添加到容器中,进而实现对国际化资源文件的管理。

    查看 MessageSourceProperties 类,其代码如下。

    public class MessageSourceProperties {
        private String basename = "messages";
        private Charset encoding;
        @DurationUnit(ChronoUnit.SECONDS)
        private Duration cacheDuration;
        private boolean fallbackToSystemLocale;
        private boolean alwaysUseMessageFormat;
        private boolean useCodeAsDefaultMessage;
    
        public MessageSourceProperties() {
            this.encoding = StandardCharsets.UTF_8;
            this.fallbackToSystemLocale = true;
            this.alwaysUseMessageFormat = false;
            this.useCodeAsDefaultMessage = false;
        }
        ...
    }
    

    通过以上代码,我们可以得到以下 3 点信息:

    • MessageSourceProperties 为 basename、encoding 等属性提供了默认值;
    • basename 表示国际化资源文件的基本名,其默认取值为“message”,即 Spring Boot 默认会获取类路径下的 message.properties 以及 message_XXX.properties 作为国际化资源文件;
    • 在 application.porperties/yml 等配置文件中,使用配置参数“spring.messages.basename”即可重新指定国际化资源文件的基本名。

    通过以上源码分析可知,Spring Boot 已经对国际化资源文件的管理提供了默认自动配置,我们这里只需要在 Spring Boot 全局配置文件中,使用配置参数“spring.messages.basename”指定我们自定义的国际资源文件的基本名即可,代码如下(当指定多个资源文件时,用逗号分隔)。

    spring.messages.basename=i18n.login
    

    14.3. 获取国际化内容

    由于页面使用的是 Tymeleaf 模板引擎,因此我们可以通过表达式 #{...} 获取国际化内容。

    以 spring-boot-adminex 为例,在 login.html 中获取国际化内容,代码如下。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <meta name="description" content="">
        <meta name="author" content="ThemeBucket">
        <link rel="shortcut icon" href="#" type="image/png">
        <title>Login</title>
        <!--将js css 等静态资源的引用修改为 绝对路径-->
        <link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
        <link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
    
        <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
        <!--[if lt IE 9]>
    
        <script src="js/html5shiv.js" th:src="@{/js/html5shiv.js}"></script>
        <script src="js/respond.min.js" th:src="@{/js/respond.min.js}"></script>
        <![endif]-->
    </head>
    
    <body class="login-body">
    <div class="container">
    
        <form class="form-signin" th:action="@{/user/login}" method="post">
            <div class="form-signin-heading text-center">
                <h1 class="sign-title" th:text="#{login.btn}">Sign In</h1>
                <img src="/images/login-logo.png" th:src="@{/images/login-logo.png}" alt=""/>
            </div>
            <div class="login-wrap">
                <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
                <input type="text" class="form-control" name="username" placeholder="User ID" autofocus
                       th:placeholder="#{login.username}"/>
                <input type="password" class="form-control" name="password" placeholder="Password"
                       th:placeholder="#{login.password}"/>
                <label class="checkbox">
                    <input type="checkbox" value="remember-me" th:text="#{login.remember}">
                    <span class="pull-right">
                        <a data-toggle="modal" href="#myModal" th:text="#{login.forgot}"> </a>
                    </span>
                </label>
                <button class="btn btn-lg btn-login btn-block" type="submit">
                    <i class="fa fa-check"></i>
                </button>
    
                <div class="registration">
                    <!--Thymeleaf 行内写法-->
                    [[#{login.not-a-member}]]
                    <a class="" href="/registration.html" th:href="@{/registration.html}">
                        [[#{login.signup}]]
                    </a>
                    <!--thymeleaf 模板引擎的参数用()代替 ?-->
                    <br/>
                    <a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>|
                    <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
                </div>
            </div>
    
            <!-- Modal -->
            <div aria-hidden="true" aria-labelledby="myModalLabel" role="dialog" tabindex="-1" id="myModal"
                 class="modal fade">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <div class="modal-header">
                            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                            <h4 class="modal-title">Forgot Password ?</h4>
                        </div>
                        <div class="modal-body">
                            <p>Enter your e-mail address below to reset your password.</p>
                            <input type="text" name="email" placeholder="Email" autocomplete="off"
                                   class="form-control placeholder-no-fix">
    
                        </div>
                        <div class="modal-footer">
                            <button data-dismiss="modal" class="btn btn-default" type="button">Cancel</button>
                            <button class="btn btn-primary" type="button">Submit</button>
                        </div>
                    </div>
                </div>
            </div>
            <!-- modal -->
        </form>
    </div>
    <!-- Placed js at the end of the document so the pages load faster -->
    <!-- Placed js at the end of the document so the pages load faster -->
    <script src="js/jquery-1.10.2.min.js" th:src="@{/js/jquery-1.10.2.min.js}"></script>
    <script src="js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}"></script>
    <script src="js/modernizr.min.js" th:src="@{/js/modernizr.min.js}"></script>
    </body>
    </html>
    

    14.4.区域信息解析器自动配置

    我们知道,Spring MVC 进行国际化时有 2 个十分重要的对象:

    • Locale:区域信息对象
    • LocaleResolver:区域信息解析器,容器中的组件,负责获取区域信息对象

    我们可以通过以上两个对象对区域信息的切换,以达到切换语言的目的。

    Spring Boot 在 WebMvcAutoConfiguration 中为区域信息解析器(LocaleResolver)进行了自动配置,源码如下。

        @Bean
        @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
        @SuppressWarnings("deprecation")
        public LocaleResolver localeResolver() {
            if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
                return new FixedLocaleResolver(this.webProperties.getLocale());
            }
            if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
                return new FixedLocaleResolver(this.mvcProperties.getLocale());
            }
            AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
            Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale()
                    : this.mvcProperties.getLocale();
            localeResolver.setDefaultLocale(locale);
            return localeResolver;
        }
    

    从以上源码可知:

    • 该方法默认向容器中添加了一个区域信息解析器(LocaleResolver)组件,它会根据请求头中携带的“Accept-Language”参数,获取相应区域信息(Locale)对象。
    • 该方法上使用了 @ConditionalOnMissingBean 注解,其参数 name 的取值为 localeResolver(与该方法注入到容器中的组件名称一致),该注解的含义为:当容器中不存在名称为 localResolver 组件时,该方法才会生效。换句话说,当我们手动向容器中添加一个名为“localeResolver”的组件时,Spring Boot 自动配置的区域信息解析器会失效,而我们定义的区域信息解析器则会生效。

    手动切换语言

    1. 修改 login.html 切换语言链接,在请求中携带国际化区域信息,代码如下。
    <!--thymeleaf 模板引擎的参数用()代替 ?-->
    <a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>|
    <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
    
    1. 创建一个 component 包,并在该包中创建一个区域信息解析器 MyLocalResolver,代码如下。
    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.LocaleResolver;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Locale;
    //自定义区域信息解析器
    public class MyLocalResolver implements LocaleResolver {
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            //获取请求中参数
            String l = request.getParameter("l");
            //获取默认的区域信息解析器
            Locale locale = Locale.getDefault();
            //根据请求中的参数重新构造区域信息对象
            if (StringUtils.hasText(l)) {
                String[] s = l.split("_");
                locale = new Locale(s[0], s[1]);
            }
            return locale;
        }
        @Override
        public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
        }
    }
    

    为了让我们的区域化信息能够生效,我们需要再配置一下这个组件!在我们自己的MvcConofig下添加bean

    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocalResolver();
    }
    

    15.swagger

    15.1.添加依赖

    <!-- swagger -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    

    15.2.添加配置类

    添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类。

    SwaggerConfig.java

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    
        @Bean
        public Docket createRestApi(){
            return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                    .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any()).build();
        }
    
        private ApiInfo apiInfo(){
            return new ApiInfoBuilder()
                    .title("Kitty API Doc")
                    .description("This is a restful api document of Kitty.")
                    .version("1.0")
                    .build();
        }
    
    }
    

    15.3.添加控制器

    添加一个控制器,在工程下新建 controller包并添加一个 HelloController控制器。

    HelloController.java

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.annotations.ApiParam;
    
    /* 类注解 */
    @Api(value = "desc of class")
    @RestController
    public class HelloController {
    
        /* 方法注解 */
        @ApiOperation(value = "desc of method", notes = "")
        @GetMapping(value="/hello")
        public Object hello( /* 参数注解 */ @ApiParam(value = "desc of param" , required=true ) @RequestParam String name) {
            return "Hello " + name + "!";
        }
    }
    

    运行即可

    15.4.常用注解说明

    swagger 通过注解接口生成文档,包括接口名,请求方法,参数,返回信息等。

    @Api: 修饰整个类,用于controller类上

    @ApiOperation: 描述一个接口,用户controller方法上

    @ApiParam: 单个参数描述

    @ApiModel: 用来对象接收参数,即返回对象

    @ApiModelProperty: 对象接收参数时,描述对象的字段

    @ApiResponse: Http响应其中的描述,在ApiResonse中

    @ApiResponses: Http响应所有的描述,用在

    @ApiIgnore: 忽略这个API

    @ApiError: 发生错误的返回信息

    @ApiImplicitParam: 一个请求参数

    @ApiImplicitParam: 多个请求参数

    更多使用说明,参考 Swagger 使用手册

    15.5.添加请求参数

    在很多时候,我们需要在调用我们每一个接口的时候都携带上一些通用参数,比如采取token验证逻辑的往往在接口请求时需要把token也一起传入后台,接下来,我们就来讲解一下如何给Swagger添加固定的请求参数。

    修改SwaggerConfig配置类,替换成如下内容,利用ParameterBuilder构成请求参数。

    SwaggerConfig.java

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    
        @Bean
        public Docket createRestApi(){
            // 添加请求参数,我们这里把token作为请求头部参数传入后端
            ParameterBuilder parameterBuilder = new ParameterBuilder();  
            List<Parameter> parameters = new ArrayList<Parameter>();  
            parameterBuilder.name("token").description("令牌")
                .modelRef(new ModelRef("string")).parameterType("header").required(false).build();  
            parameters.add(parameterBuilder.build());  
            return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                    .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                    .build().globalOperationParameters(parameters);
    //        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
    //                .select()
    //                .apis(RequestHandlerSelectors.any())
    //                .paths(PathSelectors.any()).build();
        }
    
        private ApiInfo apiInfo(){
            return new ApiInfoBuilder()
                    .title("Swagger API Doc")
                    .description("This is a restful api document of Swagger.")
                    .version("1.0")
                    .build();
        }
    
    }
    

    完成之后重新启动应用,再次查看hello接口,可以看到已经支持发送token请求参数了。

    15.6.配置API分组

    如果没有配置分组,默认是default。通过groupName()方法即可配置分组

    @Bean
    public Docket docket(Environment environment) {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
          .groupName("group1") // 配置分组
           ....
    }
    

    如何配置多个分组?配置多个分组只需要配置多个docket即可:

    @Bean
    public Docket docket1(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
    }
    @Bean
    public Docket docket2(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
    }
    @Bean
    public Docket docket3(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
    }
    

    16.拦截器

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

    1. 定义拦截器;

    2. 注册拦截器;

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

    16.1.定义拦截器

    在 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) 该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @Slf4j
    public class LoginInterceptor implements HandlerInterceptor {
        /**
         * 目标方法执行前
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            Object loginUser = request.getSession().getAttribute("loginUser");
            if (loginUser == null) {
                //未登录,返回登陆页
                request.setAttribute("msg", "您没有权限进行此操作,请先登陆!");
                request.getRequestDispatcher("/index.html").forward(request, response);
                return false;
            } else {
                //放行
                return true;
            }
        }
    
        /**
         * 目标方法执行后
         *
         * @param request
         * @param response
         * @param handler
         * @param modelAndView
         * @throws Exception
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            log.info("postHandle执行{}", modelAndView);
        }
    
        /**
         * 页面渲染后
         *
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("afterCompletion执行异常{}", ex);
        }
    }
    

    16.2.注册拦截器

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

    在配置类 MyMvcConfig 中,添加以下方法注册拦截器,代码如下。

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        ......
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor());
        }
    }
    

    16.3.指定拦截规则

    在使用 registry.addInterceptor() 方法将拦截器注册到容器中后,我们便可以继续指定拦截器的拦截规则了,代码如下

    @Slf4j
    @Configuration
    public class MyConfig implements WebMvcConfigurer {
        ......
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            log.info("注册拦截器");
            registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //拦截所有请求,包括静态资源文件
                    .excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); //放行登录页,登陆操作,静态资源
        }
    }
    

    在指定拦截器拦截规则时,调用了两个方法,这两个方法的说明如下:

    • addPathPatterns:该方法用于指定拦截路径,例如拦截路径为“/**”,表示拦截所有请求,包括对静态资源的请求。
    • excludePathPatterns:该方法用于排除拦截路径,即指定不需要被拦截器拦截的请求。

    至此,拦截器的基本功能已经完成,接下来,我们先实现 spring-boot-adminex 的登陆功能,为验证登陆拦截做准备。

    17.异常处理

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

    1. 对于浏览器客户端而言,Spring Boot 会响应一个“ whitelabel”错误视图,以 HTML 格式呈现错误信息

    image-20211108174302407

    1. 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息。
    {
        "timestamp": "2021-07-12T07:05:29.885+00:00",
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/m1ain.html"
    }
    

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

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

    • ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。

    • BasicErrorController:处理默认的“/error”请求。

    • DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。

    • DefaultErrorAttributes:用于页面上共享异常信息。

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

    ErrorPageCustomizer

    ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。

    @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);
    }
    

    BasicErrorController

    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 页面来展示异常信息
         * @param request
         * @param response
         * @return
         */
        @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 格式的数据展示错误信息
         * @param request
         * @return
         */
        @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() 方法进行处理。

    返回值类型 方法声明 客户端类型 错误信息返类型
    ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) 浏览器客户端 text/html(错误页面)
    ResponseEntity<Map<String, Object>> error(HttpServletRequest request) 机器客户端(例如安卓、IOS、Postman 等等) JSON

    换句话说,当使用浏览器访问出现异常时,会进入 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 在内),一起来解析异常信息。

    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/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,代码如下。

    @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路径

    18.全局异常处理

    我们知道 Spring Boot 已经提供了一套默认的异常处理机制,但是 Spring Boot 提供的默认异常处理机制却并不一定适合我们实际的业务场景,因此,我们通常会根据自身的需要对 Spring Boot 全局异常进行统一定制,例如定制错误页面,定制错误数据等。

    定制错误页面

    我们可以通过以下 3 种方式定制 Spring Boot 错误页面:

    • 自定义 error.html
    • 自定义动态错误页面
    • 自定义静态错误页面

    自定义 error.html

    我们可以直接在模板引擎文件夹(/resources/templates)下创建 error.html ,覆盖 Spring Boot 默认的错误视图页面(Whitelabel Error Page)。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>自定义 error.html</title>
    </head>
    <body>
    <h1>自定义 error.html</h1>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    </body>
    </html>
    

    如果 Sprng Boot 项目使用了模板引擎,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)就会解析模板引擎文件夹(resources/templates/)下 error 目录中的错误视图页面。

    精确匹配

    我们可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的动态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在模板引擎文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据其错误状态码精确匹配到对应的错误页面上。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <h1>自定义动态错误页面 404.html</h1>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    </body>
    </html>
    

    匹配

    我们还可以使用 4xx.html 和 5xx.html 作为动态错误页面的文件名,并将它们存放在模板引擎文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以“4”开头的所有异常,都会解析到动态错误页面 4xx.html 上。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <h1>自定义动态错误页面 4xx.html</h1>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    </body>
    </html>
    

    自定义静态错误页面

    若 Sprng Boot 项目没有使用模板引擎,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)则会解析静态资源文件夹下 error 目录中的静态错误页面。

    精确匹配

    我们可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的静态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在静态资源文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据错误状态码精确匹配到对应的错误页面上。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <h1>自定义静态错误页面 404.html</h1>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    </body>
    </html>
    

    模糊匹配

    我们还可以使用 4xx.html 和 5xx.html 作为静态错误页面的文件名,并将它们存放在静态资源文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以“4”开头的所有错误,都会解析到静态错误页面 4xx.html 上。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <h1>自定义静态错误页面 4xx.html</h1>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    </body>
    </html>
    

    错误页面优先级

    以上 5 种方式均可以定制 Spring Boot 错误页面,且它们的优先级顺序为:自定义动态错误页面(精确匹配)>自定义静态错误页面(精确匹配)>自定义动态错误页面(模糊匹配)>自定义静态错误页面(模糊匹配)>自定义 error.html。

    当遇到错误时,Spring Boot 会按照优先级由高到低,依次查找解析错误页,一旦找到可用的错误页面,则直接返回客户端展示。

    定制错误数据

    我们知道,Spring Boot 提供了一套默认的异常处理机制,其主要流程如下:

    1. 发生异常时,将请求转发到“/error”,交由 BasicErrorController(Spring Boot 默认的 Error 控制器) 进行处理;
    2. BasicErrorController 根据客户端的不同,自动适配返回的响应形式,浏览器客户端返回错误页面,机器客户端返回 JSON 数据。
    3. BasicErrorController 处理异常时,会调用 DefaultErrorAttributes(默认的错误属性处理工具) 的 getErrorAttributes() 方法获取错误数据。

    我们还可以定制 Spring Boot 的错误数据,具体步骤如下。

    1. 自定义异常处理类,将请求转发到 “/error”,交由 Spring Boot 底层(BasicErrorController)进行处理,自动适配浏览器客户端和机器客户端。
    2. 通过继承 DefaultErrorAttributes 来定义一个错误属性处理工具,并在原来的基础上添加自定义的错误数据。

    1. 自定义异常处理类

    被 @ControllerAdvice 注解的类可以用来实现全局异常处理,这是 Spring MVC 中提供的功能,在 Spring Boot 中可以直接使用。

    创建一个名为 UserNotExistException 的异常类,代码如下

    /**
    * 自定义异常
    */
    public class UserNotExistException extends RuntimeException {
        public UserNotExistException() {
            super("用户不存在!");
        }
    }
    

    在 IndexController 添加以下方法,触发 UserNotExistException 异常,代码如下

    @Controller
    public class IndexController {
        ......
        @GetMapping(value = {"/testException"})
        public String testException(String user) {
            if ("user".equals(user)) {
                throw new UserNotExistException();
            }
            //跳转到登录页 login.html
            return "login";
        }
    }
    

    创建一个名为 MyExceptionHandler 异常处理类,代码如下

    import com.wyl.Exception.UserNotExistException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Map;
    
    @ControllerAdvice
    public class MyExceptionHandler {
        @ExceptionHandler(UserNotExistException.class)
        public String handleException(Exception e, HttpServletRequest request) {
            Map<String, Object> map = new HashMap<>();
            //向 request 对象传入错误状态码
            request.setAttribute("javax.servlet.error.status_code",500);
            //根据当前处理的异常,自定义的错误数据
            map.put("code", "user.notexist");
            map.put("message", e.getMessage());
            //将自定的错误数据传入 request 域中
            request.setAttribute("ext",map);
            return "forward:/error";
        }
    }
    

    2. 自定义错误属性处理工具

    1)在 net.biancheng.www.componet 包内,创建一个错误属性处理工具类 MyErrorAttributes(继承 DefaultErrorAttributes ),通过该类我们便可以添加自定义的错误数据,代码如下。

    import org.springframework.boot.web.error.ErrorAttributeOptions;
    import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.WebRequest;
    
    import java.util.Map;
    //向容器中添加自定义的储物属性处理工具
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
            //添加自定义的错误数据
            errorAttributes.put("company", "www.biancheng.net");
            //获取 MyExceptionHandler 传入 request 域中的错误数据
            Map ext = (Map) webRequest.getAttribute("ext", 0);
            errorAttributes.put("ext", ext);
            return errorAttributes;
        }
    }
    

    在 templates/error 目录下,创建动态错误页面 5xx.html,代码如下。

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>自定义 error.html</title>
    </head>
    <body>
    <p>status:<span th:text="${status}"></span></p>
    <p>error:<span th:text="${error}"></span></p>
    <p>timestamp:<span th:text="${timestamp}"></span></p>
    <p>message:<span th:text="${message}"></span></p>
    <p>path:<span th:text="${path}"></span></p>
    <!--取出定制的错误信息-->
    <h3>以下为定制错误数据:</h3>
    <p>company:<span th:text="${company}"></span></p>
    <p>code:<span th:text="${ext.code}"></span></p>
    <p>path:<span th:text="${ext.message}"></span></p>
    </body>
    </html>
    
  • 相关阅读:
    类的加载过程
    算法模板之基数排序
    算法模板之Dijkstra
    算法模板之SPFA
    算法模板之树状数组
    算法模板之排序
    深入JVM-自动内存管理(读书笔记)
    VMware Fault-Tolerant Virtual Machine 论文总结
    深入JVM--高效并发(读书笔记)
    欧拉素数筛
  • 原文地址:https://www.cnblogs.com/wyl1924/p/15527426.html
Copyright © 2011-2022 走看看