SpringBoot系列

01-SpringBoot快速入门

1. SpringBoot介绍

1.1 简介

在您第1次接触和学习Spring框架的时候,是否因为其繁杂的配置而退却了?

在你第n次使用Spring框架的时候,是否觉得一堆反复黏贴的配置有一些厌烦?

那么您就不妨来试试使用SpringBoot来让你更易上手,更简单快捷地构建Spring应用!

SpringBoot让我们的Spring应用变的更轻量化。

我们不必像以前那样繁琐的构建项目、打包应用、部署到Tomcat等应用服务器中来运行我们的业务服务。

通过SpringBoot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过java -jar命令就可以运行起来。

这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。

总结一下SpringBoot的主要优点:

  1. 为所有Spring开发者更快的入门
  2. 开箱即用,提供各种默认配置来简化项目配置
  3. 内嵌式容器简化Web项目
  4. 没有冗余代码生成和XML配置的要求
  5. 统一的依赖管理
  6. 自动装配,更易使用,更易扩展

1.2 使用版本说明

Springboot版本:使用最新的2.5.0版本

教程参考了官方文档进行制作,权威。

其他依赖版本:

1. Maven  需求:3.5+
2. JDK 需求  8+
3. Spring Framework 5.3.7以上版本
4. Tomcat 9.0
5. Servlet版本 4.0  但是可以部署到Servlet到3.1+的容器中

2. 快速入门

快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

教程使用的Idea版本:2019.3

2.1 创建基础项目

**第一步:**创建maven项目

pom.xml :


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.xiaopizhugroupId>
    <artifactId>helloSpringBootartifactId>
    <version>1.0-SNAPSHOTversion>
	
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.0version>
    parent>
project>

注意上方的parent必须加,其中定义了springboot官方支持的n多依赖,基本上常用的已经有了,所以接下来导入依赖的时候,绝大部分都可以不加版本号。

此时的工程结构为:
SpringBoot系列_第1张图片
**第二步:**添加web依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
dependencies>

添加上方的web依赖,其中间接依赖了spring-web,spring-webmvc,spring-core等spring和springmvc的包,并且集成了tomcat。

**第三步:**编写启动类

package com.xiaopizhu.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

@SpringBootApplication注解标识了HelloApp为启动类,也是Spring Boot的核心。

**第四步:**运行启动类的main方法
SpringBoot系列_第2张图片

看到如上配置,证明启动成功,tomcat端口号默认为8080。

**第五步:**如果想要修改端口号,可以在resources目录下新建application.properties

server.port=8082

**第六步:**重新运行
SpringBoot系列_第3张图片

此时的项目结构为:
SpringBoot系列_第4张图片

src/main/java : 编写java代码,注意启动类需要放在项目的根包下。

src/main/resources: 放置资源的目录,比如springboot的配置文件,静态文件,mybatis配置,日志配置等。

src/test/java: 测试代码

2.2 编写一个Http接口

**第一步:**创建HelloController类,内容如下:

package com.xiaopizhu.springboot.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("hello")
public class HelloController {
    @GetMapping("boot")
    public String hello(){
        return "hello spring boot";
    }
}

注意包名,必须在启动类所在的包名下。

**第二步:**重启程序,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot

得到结果:hello spring boot

2.3 编写单元测试用例

**第一步:**添加spring boot测试依赖

		<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

**第二步:**在src/test 下,编写测试用例

package com.xiaopizhu.springboot.controller;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
public class TestHelloController {

    private MockMvc mockMvc;

    @BeforeEach
    public void beforeEach(){
        mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }
    @Test
    public void testHello() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/hello/boot")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("hello spring boot")));
    }
}

上面的测试用例,是构建一个空的WebApplicationContext,并且在before中加载了HelloController,得以在测试用例中mock调用,模拟请求。

2.4 打包为jar运行

**第一步:**添加打包(maven构建springboot)插件

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

在idea的右侧 maven中,使用package来打包程序,打包完成后,在target目录下生成helloSpringBoot-1.0-SNAPSHOT.jar

SpringBoot系列_第5张图片

**第二步:**打开cmd:找到jar对应的目录

输入命令

java -jar helloSpringBoot-1.0-SNAPSHOT.jar

SpringBoot系列_第6张图片

**第三步:**测试,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot

得到结果:hello spring boot

2.5 查看jar包内容

jar tvf helloSpringBoot-1.0-SNAPSHOT.jar

3. 小结

  1. 通过Maven构建了一个空白Spring Boot项目,再通过引入web模块实现了一个简单的请求处理。
  2. 通过修改配置文件,更改端口号
  3. 编写了测试用例
  4. 打包jar包运行

02-SpringBoot工程结构

1. 前言

Spring Boot框架本身并没有对工程结构有特别的要求,但是按照最佳实践的工程结构可以帮助我们减少可能会遇见的坑。

尤其是Spring包扫描机制的存在,如果您使用最佳实践的工程结构,可以免去不少特殊的配置工作。

2. 典型示例

以下结构是比较推荐的package组织方式:

com
  +- example
    +- myproject
      +- Application.java
      |
      +- dao
      |  +- Customer.java
      |  +- CustomerDao.java
      |
      +- service
      |  +- CustomerService.java
      |
      +- controller
      |  +- CustomerController.java
      |

root package:com.example.myproject,所有的类和其他package都在root package之下。

**Application.java : **应用主类,该类直接位于root package下。通常我们会在应用主类中做一些框架配置扫描等配置,我们放在root package下可以帮助程序减少手工配置来加载到我们希望被Spring加载的内容。

**com.example.myproject.dao包:**用于定义实体映射关系与数据访问相关的接口和实现。

**com.example.myproject.service包:**用于编写业务逻辑相关的接口与实现。

**com.example.myproject.controller:**用于编写Web层相关的实现,比如:Spring MVC的Controller等

默认情况下,Spring Boot的应用主类会自动扫描root package以及所有子包下的所有类来进行初始化。

包的层级目录一定要搞对,如果把CustomerController放入到com.example下,则spring boot就无法扫描到CustomerController,并进行初始化了。

3. 非典型结构下的初始化

那么如果,我们一定要加载非root package下的内容怎么办呢?

比如有一些通用的工具类,比如第三方的包等等。

方法一:使用@ComponentScan注解指定具体的加载包,比如:

@SpringBootApplication
@ComponentScan(basePackages="com.example")
public class HelloApp {
    public static void main(String[] args) {
        SpringApplication.run(HelloApp.class, args);
    }
}

这种方法通过注解直接指定要扫描的包,比较直观。

方法二:使用@Bean注解来初始化,比如:

@SpringBootApplication
public class HelloApp {
    public static void main(String[] args) {
        SpringApplication.run(HelloApp.class, args);
    }
    @Bean
    public CustomerController customerController() {
        return new CustomerController();
    }
}

@Bean这种一般用于加载第三方的jar包,比如一些框架封装场景,比如Mybaits-plus等。

4. 工程结构之其他

这里讨论一下,除了controller,service,dao这种典型的package组织方式之外,还有没有别的?

com
  +- example
    +- myproject
      +- Application.java
      |
      +- dao
        +- data
    	|  +- Customer.java
        +- mapper
    	|  +- CustomerMapper.java
      |
      +- domain
        +- repository
    	|  +- CustomerRepository.java
        +- CustomerDomain.java
      |
      +- service
      |  +- CustomerService.java
      |
      +- controller
      |  +- CustomerController.java
      |

上面的结构中,多出来一层domain层,将业务逻辑操作放入到domain中进行执行,Repository和数据层打交道,service用来管理业务流程。

03-SpringBoot配置文件详解

1. 前言

在前面的入门案例中,我们轻松的实现了一个简单的RESTful API应用,体验了一下Spring Boot给我们带来的诸多优点,我们用非常少的代码量成功的实现了一个Web应用,这是传统的Spring应用无法办到的。

配置方面,使用了application.properties 这个Spring Boot可以识别的配置文件,做了tomcat监听端口的修改,其他配置暂时没有用到。

在实际的工作中,开发一个Spring Boot程序需要修改配置文件的内容来达到不同的需求,到了后期如果学习到了Spring Cloud,在Spring Cloud中,其实有大量的工作都会是针对配置文件的。

所以我们有必要深入的了解一些关于Spring Boot中的配置文件的知识,比如:它的配置方式、如何实现多环境配置,配置信息的加载顺序等。

2. 配置基础

Spring Boot的默认配置文件位置为: src/main/resources/application.properties

关于Spring Boot应用的配置内容都可以集中在该文件中了,根据我们引入的不同Starter模块,可以在这里定义诸如:容器端口名、数据库链接信息、日志级别等各种配置信息。

server.port=8888  #自定义web模块的服务端口号,指定服务端口为8888
spring.application.name=hello #指定应用名(该名字在Spring Cloud应用中会被注册为服务名)

Spring Boot的配置文件除了可以使用传统的properties文件之外,还支持现在被广泛推荐使用的YAML文件。

src/main/resources/application.yml 也可以被Spring Boot程序识别。

YAML全称是 YAML Ain’t Markup Language 。YAML是一种直观的能够被电脑识别的数据序列化格式,并且容易被人类阅读,容易和脚本语言交互,可以被支持YAML库的不同的编程语言程序导入,比如: C/C++, Ruby, Python, Java, Perl, C#, PHP 等。YML文件是以数据为核心的,比传统的xml方式更加简洁。 YAML文件的扩展名可以使用.yml或者.yaml。

YAML采用的配置格式不像properties的配置那样以单纯的键值对形式来表示,而是以类似大纲的缩进形式来表示。比如:下面的一段YAML配置信息

environments:
    dev:
        url: http://dev.xiaopizhu.com
        name: Developer Setup
    prod:
        url: http://prod.xiaopizhu.com
        name: My App

与其等价的properties配置如下。

environments.dev.url=http://dev.xiaopizhu.com
environments.dev.name=Developer Setup
environments.prod.url=http://prod.xiaopizhu.com
environments.prod.name=My App

通过YAML的配置方式,我们可以看到配置信息利用阶梯化缩进的方式,其结构显得更为清晰易读,同时配置内容的字符量也得到显著的减少。

注意:yml文件冒号后面有一个空格

除此之外,YAML还可以在单个文件中通过使用spring.config.activate.on-profile属性来定义多个不同的环境配置。

例如下面的内容:

在指定为test环境时,server.port将使用8882端口;

而在prod环境,server.port将使用8883端口;

如果没有指定环境,server.port将使用8881端口。

server:
  port: 8881
---
spring:
  config:
    activate:
      on-profile: test
server:
  port: 8882
---
spring:
  config:
    activate:
      on-profile: prod
server:
  port: 8883

注意:YAML目前还有一些不足,它无法通过@PropertySource注解来加载配置。但是,YAML加载属性到内存中保存的时候是有序的,所以当配置文件中的信息需要具备顺序含义时,YAML的配置方式比起properties配置文件更有优势。

如果properties配置文件和yml配置文件同时存在,优先加载properties文件,在加载yml文件,有同名的属性以properties为主。

2.1 自定义参数

我们除了可以在Spring Boot的配置文件中设置各个Starter模块中预定义的配置属性,也可以在配置文件中定义一些我们需要的自定义属性。比如在application.properties中添加:

book.name=SpringBoot
book.author=god_coder

然后,在应用中我们可以通过@Value注解来加载这些自定义的参数,比如:

@Component
public class Book {
    @Value("${book.name}")
    private String name;
    @Value("${book.author}")
    private String author;
    // 省略getter和setter
}

@Value注解加载属性值的时候可以支持表达式来进行配置:

  • PlaceHolder方式:格式为 ${...},大括号内为PlaceHolder,上面示例中使用的方式

yml格式:

book:
  name: SpringBoot
  author: god_coder

2.2 参数引用

application.properties中的各个参数之间,我们也可以直接通过使用PlaceHolder的方式来进行引用,就像下面的设置:

book.name=SpringBoot
book.author=god_coder
book.desc=${book.author}  is writing《${book.name}》

book.desc参数引用了上文中定义的book.namebook.author属性,最后该属性的值就是god_coder is writing《SpringBoot》

#yml格式
book:
  name: SpringBoot
  author: god_coder
  desc: ${book.author}  is writing《${book.name}

2.3 使用随机数

在一些特殊情况下,有些参数我们希望它每次加载的时候不是一个固定的值,比如:密钥、服务端口等。在Spring Boot的属性配置文件中,我们可以通过使用${random}配置来产生随机的int值、long值或者string字符串,这样我们就可以容易的通过配置随机生成属性值,而不是在程序中通过编码来实现这些逻辑。

${random}的配置方式主要有一下几种,读者可作为参考使用。

# 随机字符串
com.xiaopizhu.blog.value=${random.value}
# 随机int
com.xiaopizhu.blog.number=${random.int}
# 随机long
com.xiaopizhu.blog.bignumber=${random.long}
# 10以内的随机数
com.xiaopizhu.blog.test1=${random.int(10)}
# 10-20的随机数
com.xiaopizhu.blog.test2=${random.int[10,20]}

该配置方式可以用于设置应用端口等场景,避免在本地调试时出现端口冲突的麻烦,比如在测试用例随机生成springboot应用启动的端口号

 	@Value("${com.xiaopizhu.blog.value}")
    private String value;
    @Value("${com.xiaopizhu.blog.number}")
    private Integer number;
    @Value("${com.xiaopizhu.blog.bignumber}")
    private Long bignumber;
    @Value("${com.xiaopizhu.blog.test1}")
    private Integer test1;
    @Value("${com.xiaopizhu.blog.test2}")
    private Integer test2;

2.4 命令行参数

在之前的快速入门案例中,我们介绍了使用命令java -jar命令来启动的方式。

该命令除了启动应用之外,还可以在命令行中来指定应用的参数,比如:

java -jar xxx.jar --server.port=8888

直接以命令行的方式,来设置server.port属性,令启动应用的端口设为8888。

在命令行方式启动Spring Boot应用时,连续的两个减号--就是对application.properties中的属性值进行赋值的标识。

所以,java -jar xxx.jar --server.port=8888命令,等价于我们在application.properties中添加属性server.port=8888

通过命令行来修改属性值是Spring Boot非常重要的一个特性,通过此特性,理论上已经使得我们应用的属性在启动前是可变的,所以其中端口号也好、数据库连接也好,都是可以在应用启动时发生改变,而不同于以往的Spring应用通过Maven的Profile在编译器进行不同环境的构建。

Spring Boot的这种方式,可以让应用程序的打包内容,贯穿开发、测试以及线上部署。

但是,如果每个参数都需要通过命令行来指定,这显然也不是一个好的方案,所以下面我们看看如果在Spring Boot中实现多环境的配置。

注意:命令行参数在需要临时改变一些参数的时候非常适用

2.5 多环境配置

我们在开发任何应用的时候,一般都会有多套环境,比如:开发、测试、生产等。

其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。

对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot也不例外,或者说更加简单。

在Spring Boot中多环境配置文件名需要满足application-{profile}.properties的格式,其中{profile}对应你的环境标识,比如:

  • application-dev.properties:开发环境
  • application-test.properties:测试环境
  • application-prod.properties:生产环境

至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应配置文件中的{profile}值。如:spring.profiles.active=test就会加载application-test.properties配置文件内容。

下面,以不同环境配置不同的服务端口为例,进行样例实验。

  • 针对各环境新建不同的配置文件application-dev.propertiesapplication-test.propertiesapplication-prod.properties
  • 在这三个文件均都设置不同的server.port属性,如:dev环境设置为1111,test环境设置为2222,prod环境设置为3333
  • application.properties中设置spring.profiles.active=dev,就是说默认以dev环境设置
  • 测试不同配置的加载
  • 执行java -jar xxx.jar,可以观察到服务端口被设置为1111,也就是默认的开发环境(dev)
  • 执行java -jar xxx.jar --spring.profiles.active=test,可以观察到服务端口被设置为2222,也就是测试环境的配置(test)
  • 执行java -jar xxx.jar --spring.profiles.active=prod,可以观察到服务端口被设置为3333,也就是生产环境的配置(prod)

按照上面的实验,可以如下总结多环境的配置思路:

  • application.properties中配置通用内容,并设置spring.profiles.active=dev,以开发环境为默认配置
  • application-{profile}.properties中配置各个环境不同的内容
  • 通过命令行方式去激活不同环境的配置

2.6 加载顺序

在上面的例子中,我们将Spring Boot应用需要的配置内容都放在了项目工程中,虽然我们已经能够通过spring.profiles.active或是通过Maven来实现多环境的支持。

但是,当我们的团队逐渐壮大,分工越来越细致之后,往往我们不需要让开发人员知道测试或是生产环境的细节,而是希望由每个环境各自的负责人(QA或是运维)来集中维护这些信息。

那么如果还是以这样的方式存储配置内容,对于不同环境配置的修改就不得不去获取工程内容来修改这些配置内容,当应用非常多的时候就变得非常不方便。

同时,配置内容都对开发人员可见,本身这也是一种安全隐患。

对此,现在出现了很多将配置内容外部化的框架和工具,比如Spring Cloud Config,nacos等

springboot 官方文档链接:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

Spring Boot为了能够更合理的重写各属性的值,使用了下面这种较为特别的属性加载顺序:

  1. 命令行中传入的参数。
  2. SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。
  3. ServletConfig初始化参数 InitParameter
  4. ServletContext初始化参数 InitParameter
  5. java:comp/env中的JNDI属性。
  6. Java的系统属性,可以通过System.getProperties()获得的内容。
  7. 操作系统的环境变量
  8. 通过random.*配置的属性 RandomValuePropertySource
  9. 位于当前应用jar包之外,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  10. 位于当前应用jar包之外的application.propertiesYAML配置内容
  11. 位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  12. 位于当前应用jar包之内的application.propertiesYAML配置内容
  13. @Configuration注解修改的类中,通过@PropertySource注解定义的属性
  14. 应用默认属性,使用SpringApplication.setDefaultProperties定义的内容

优先级按上面的顺序有高到低,数字越小优先级越高。

可以看到,其中第9项和第10项都是从应用jar包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代jar包之内的配置内容。

通过这样的实现,我们的工程在配置中就变的非常干净,我们只需要在本地放置开发需要的配置即可,而其他环境的配置就可以不用关心,由其对应环境的负责人去维护即可。

3. 2.x 新特性

在Spring Boot 2.0中推出了Relaxed Binding 2.0,对原有的属性绑定功能做了非常多的改进以帮助我们更容易的在Spring应用中加载和读取配置信息。下面本文就来说说Spring Boot 2.0中对配置的改进。

3.1 配置文件绑定

3.1.1 简单类型

在Spring Boot 2.0中对配置属性加载的时候,除了像1.x版本时候那样移除特殊字符外,还会将配置均以全小写的方式进行匹配和加载。所以,下面的4种配置方式都是等价的:

  • properties格式:
spring.jpa.databaseplatform=mysql
spring.jpa.database-platform=mysql
spring.jpa.databasePlatform=mysql
spring.JPA.database_platform=mysql
  • yaml格式:
spring:
  jpa:
    databaseplatform: mysql
    database-platform: mysql
    databasePlatform: mysql
    database_platform: mysql

Tips:推荐使用全小写配合-分隔符的方式来配置,比如:spring.jpa.database-platform=mysql

3.1.2 List类型

在properties文件中使用[]来定位列表类型,比如:

spring.my-example.url[0]=http://example.com
spring.my-example.url[1]=http://spring.io

也支持使用逗号分割的配置方式,上面与下面的配置是等价的:

spring.my-example.url=http://example.com,http://spring.io

而在yaml文件中使用可以使用如下配置:

spring:
  my-example:
    url:
      - http://example.com
      - http://spring.io

也支持逗号分割的方式:

spring:
  my-example:
    url: http://example.com, http://spring.io

注意:在Spring Boot 2.0中对于List类型的配置必须是连续的,不然会抛出UnboundConfigurationPropertiesException异常,所以如下配置是不允许的:

foo[0]=a
foo[2]=b

在Spring Boot 1.x中上述配置是可以的,foo[1]由于没有配置,它的值会是null

3.1.3 Map类型

Map类型在properties和yaml中的标准配置方式如下:

  • properties格式:
spring.my-example.foo=bar
spring.my-example.hello=world
  • yaml格式:
spring:
  my-example:
    foo: bar
    hello: world

注意:如果Map类型的key包含非字母数字和-的字符,需要用[]括起来,比如:

spring:
  my-example:
    '[foo.baz]': bar

3.2 环境属性绑定

简单类型

在环境变量中通过小写转换与.替换_来映射配置文件中的内容,比如:环境变量SPRING_JPA_DATABASEPLATFORM=mysql的配置会产生与在配置文件中设置spring.jpa.databaseplatform=mysql一样的效果。

List类型

由于环境变量中无法使用[]符号,所以使用_来替代。任何由下划线包围的数字都会被认为是[]的数组形式。比如:

MY_FOO_1_ = my.foo[1]
MY_FOO_1_BAR = my.foo[1].bar
MY_FOO_1_2_ = my.foo[1][2]

另外,最后环境变量最后是以数字和下划线结尾的话,最后的下划线可以省略,比如上面例子中的第一条和第三条等价于下面的配置:

MY_FOO_1 = my.foo[1]
MY_FOO_1_2 = my.foo[1][2]

3.3 系统属性绑定

简单类型

系统属性与文件配置中的类似,都以移除特殊字符并转化小写后实现绑定,比如下面的命令行参数都会实现配置spring.jpa.databaseplatform=mysql的效果:

-Dspring.jpa.database-platform=mysql
-Dspring.jpa.databasePlatform=mysql
-Dspring.JPA.database_platform=mysql

List类型

系统属性的绑定也与文件属性的绑定类似,通过[]来标识,比如:

-Dspring.my-example.url[0]=http://example.com
-Dspring.my-example.url[1]=http://spring.io

同样的,他也支持逗号分割的方式,比如:

-Dspring.my-example.url=http://example.com,http://spring.io

3.4 属性的读取

上文介绍了Spring Boot 2.0中对属性绑定的内容,可以看到对于一个属性我们可以有多种不同的表达,但是如果我们要在Spring应用程序的environment中读取属性的时候,每个属性的唯一名称符合如下规则:

  • 通过.分离各个元素
  • 最后一个.将前缀与属性名称分开
  • 必须是字母(a-z)和数字(0-9)
  • 必须是小写字母
  • 用连字符-来分隔单词
  • 唯一允许的其他字符是[],用于List的索引
  • 不能以数字开头

所以,如果我们要读取配置文件中spring.jpa.database-platform的配置,可以这样写:

this.environment.containsProperty("spring.jpa.database-platform")

而下面的方式是无法获取到spring.jpa.database-platform配置内容的:

this.environment.containsProperty("spring.jpa.databasePlatform")

注意:使用@Value获取配置内容的时候也需要这样的特点

3.5 全新的绑定API

在Spring Boot 2.0中增加了新的绑定API来帮助我们更容易的获取配置信息。下面举个例子来帮助大家更容易的理解:

例子一:简单类型

假设在properties配置中有这样一个配置:com.mszlu.foo=bar

我们为它创建对应的配置类:

@Data
@ConfigurationProperties(prefix = "com.mszlu")
public class FooProperties {
    private String foo;
}

接下来,通过最新的Binder就可以这样来拿配置信息了:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);

        Binder binder = Binder.get(context.getEnvironment());

        // 绑定简单配置
        FooProperties foo = binder.bind("com.mszlu", Bindable.of(FooProperties.class)).get();
        System.out.println(foo.getFoo());
    }
}

例子二:List类型

如果配置内容是List类型呢?比如:

com.mszlu.post[0]=Why Spring Boot
com.mszlu.post[1]=Why Spring Cloud

com.mszlu.posts[0].title=Why Spring Boot
com.mszlu.posts[0].content=It is perfect!
com.mszlu.posts[1].title=Why Spring Cloud
com.mszlu.posts[1].content=It is perfect too!

要获取这些配置依然很简单,可以这样实现:

ApplicationContext context = SpringApplication.run(Application.class, args);

Binder binder = Binder.get(context.getEnvironment());

// 绑定List配置
List<String> post = binder.bind("com.mszlu.post", Bindable.listOf(String.class)).get();
System.out.println(post);

List<PostInfo> posts = binder.bind("com.mszlu.posts", Bindable.listOf(PostInfo.class)).get();
System.out.println(posts);

4. 2.5.0新特性

  1. 支持java16
  2. spring.datasource.* 已被 spring.sql.init.* 属性替代。
  3. Flyway 和 Liquibase(数据库版本管理工具) 需要指定单独的 username / password,不再从 datasource 继承。
  4. 不再维护 spring data solr , 从此版本开始 已经开始从源码中移除。
  5. 断点 /info 不再通过 web 暴露,如果类中包含 spring security,需要安全验证。
  6. EL 语法实现由 tomcat-embed-el 替代为 jakrta-el。
  7. Error View 异常页面中不会包含 具体的错误信息,如果需要则可以通过 server.error.include-message开启。
  8. 通过 logging.register-shutdown-hook 属性可以在 jvm 退出时释放日志资源。

04-简单整合

1. SpringBoot整合MybatisPlus

1.1 准备数据库脚本

CREATE TABLE If Not Exists user
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

replace into user (id,name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');

1.2 新建SpringBoot工程

1.2.1 依赖


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <groupId>com.mszlugroupId>
    <artifactId>unionartifactId>
    <version>1.0-SNAPSHOTversion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.0version>
    parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.3version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
         <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
    dependencies>
project>

1.2.2配置

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
#数据库 默认采用的数据源HikariDataSource

#sql脚本初始化 2.5.0
#DataSourceInitializationConfiguration 源码中得知不能写username和password,但好多文档说要写
#spring.sql.init.username=root
#spring.sql.init.password=root
spring.sql.init.schema-locations=classpath*:sql/*.sql
spring.sql.init.data-locations=classpath*:sql/data/*.sql

1.2.3 启动测试

package com.mszlu.union;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

1.3 MybatisPlus

1.3.1 Mapper接口

package com.mszlu.union.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.union.pojo.User;

public interface UserMapper extends BaseMapper<User> {}

package com.mszlu.union.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.mszlu.union.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

1.3.2 Sevice代码

package com.mszlu.union.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mszlu.union.mapper.UserMapper;
import com.mszlu.union.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Slf4j
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> findAll(){
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        List<User> users = userMapper.selectList(queryWrapper);
        return users;
    }
	//分页
    public List<User> findPage(){
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        Page page = new Page(2,2);
        Page<User> userPage = userMapper.selectPage(page,queryWrapper);
        log.info("total:{}",userPage.getTotal());
        log.info("pages:{}",userPage.getPages());
        return userPage.getRecords();
    }
}

1.3.3 测试

package com.mszlu.union.controller;

import com.mszlu.union.pojo.User;
import com.mszlu.union.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("findAll")
    public List<User> findAll(){
        return userService.findAll();
    }
    @GetMapping("findPage")
    public List<User> findPage(){
        return userService.findPage();
    }
}

2. SpringBoot整合ShardingSphere

http://shardingsphere.apache.org/index_zh.html

2.1 导入依赖

<dependency>
    <groupId>org.apache.shardingspheregroupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starterartifactId>
    <version>5.0.0-alphaversion>
dependency>

2.2 配置读写分离

##shardingsphere配置
spring.shardingsphere.datasource.common.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.common.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.common.username=root
spring.shardingsphere.datasource.common.password= root

spring.shardingsphere.datasource.names=master,slave0,slave1

# 配置第 1 个数据源
spring.shardingsphere.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.shardingsphere.datasource.master.username=root
spring.shardingsphere.datasource.master.password=root

# 配置第 2 个数据源
spring.shardingsphere.datasource.slave0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.slave0.jdbc-url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.shardingsphere.datasource.slave0.username=root
spring.shardingsphere.datasource.slave0.password=root
# 配置第 3 个数据源
spring.shardingsphere.datasource.slave1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.slave1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.slave1.jdbc-url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.shardingsphere.datasource.slave1.username=root
spring.shardingsphere.datasource.slave1.password=root

# 主数据源名称
spring.shardingsphere.rules.replica-query.data-sources.ms.primary-data-source-name=master
# 从数据源名称,多个从数据源用逗号分隔
spring.shardingsphere.rules.replica-query.data-sources.ms.replica-data-source-names=slave0,slave1 
# 负载均衡算法名称
spring.shardingsphere.rules.replica-query.data-sources.ms.load-balancer-name=round-robin

## 负载均衡算法配置
spring.shardingsphere.rules.replica-query.load-balancers.round-robin.type=ROUND_ROBIN
## 负载均衡算法属性配置
spring.shardingsphere.rules.replica-query.load-balancers.round-robin.props.workId=1
#打印sql
spring.shardingsphere.props.sql-show=true

2.3 配置分表

2.3.1 准备表

CREATE TABLE If Not Exists user_0
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);
CREATE TABLE If Not Exists user_1
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);
CREATE TABLE If Not Exists user_2
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

2.3.2 配置

## 分表
spring.shardingsphere.rules.sharding.binding-tables=user
# 标准分片表配置
# 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#ms是上面 读写分离的配置
spring.shardingsphere.rules.sharding.tables.user.actual-data-nodes=ms.user_$->{0..2}

# 用于单分片键的标准分片场景
# 分片列名称
spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-column=id
# 分片算法名称
spring.shardingsphere.rules.sharding.tables.user.table-strategy.standard.sharding-algorithm-name=table-inline

#雪花算法 分布式id
spring.shardingsphere.rules.sharding.key-generators.snowflake.type=SNOWFLAKE
spring.shardingsphere.rules.sharding.key-generators.snowflake.props.worker-id=123

spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.table-inline.props.algorithm-expression=user_$->{id % 3}

spring.shardingsphere.enabled=true

3. SpringBoot整合Redis

3.1 安装Redis并启动

#docker安装
curl -sSL https://get.daocloud.io/docker | sh
#拉取
docker pull redis:latest
#创建容器并启动
docker run -p 6379:6379 --name redis -v /usr/local/docker/redis/redis.conf:/opt/docker/redis/redis.conf -v /usr/local/docker/redis/data:/opt/docker/redis/data -d redis redis-server /opt/docker/redis/redis.conf --appendonly yes

#-p 6379:6379 端口映射:前表示主机部分,:后表示容器部分。

# --name redis  指定该容器名称,查看和进行操作都比较方便。

# -v 挂载目录,规则与端口映射相同。

# -d redis 表示后台启动redis

# redis-server /opt/docker/redis/redis.conf  #以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录/usr/local/docker/redis/redis.conf
docker start redis
#进入容器
docker exec -it redis /bin/bash
#验证
redis-cli

3.2 SpringBoot添加Redis的配置并导入依赖

<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-data-redisartifactId>
dependency>
## redis配置
spring.redis.port=6379
spring.redis.host=192.168.200.100

3.3 使用

<dependency>
  <groupId>org.apache.commonsgroupId>
  <artifactId>commons-lang3artifactId>
dependency>
<dependency>
  <groupId>com.alibabagroupId>
  <artifactId>fastjsonartifactId>
  <version>1.2.76version>
dependency>

对之前的findAll代码 做一个缓存

@Autowired
private RedisTemplate<String,String> redisTemplate;

public List<User> findAll(){
   String userListJsonStr = redisTemplate.opsForValue().get("UserService.findAll");
   if (StringUtils.isNotBlank(userListJsonStr)){
       List<User> users = JSON.parseArray(userListJsonStr, User.class);
       log.info("走了缓存~~~");
       return users;
    }else {
       //查询所有
       List<User> users = userMapper.selectList(new LambdaQueryWrapper<>());
       redisTemplate.opsForValue().set("UserService.findAll",JSON.toJSONString(users),2, TimeUnit.HOURS);
       log.info("存入缓存~~~");
       return users;
    }
}

4. SpringBoot整合RocketMQ

  • RocketMQ是由阿里捐赠给Apache的一款低延迟、高并发、高可用、高可靠的分布式消息中间件。经历了淘宝双十一的洗礼。RocketMQ既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。

4.1 安装 rocketmq

#docker 拉取
docker pull foxiswho/rocketmq:4.8.0
#启动nameserver
docker run -d -v /usr/local/rocketmq/logs:/opt/docker/rocketmq/logs \
      --name rmqnamesrv \
      -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \
      -p 9876:9876 \
      foxiswho/rocketmq:4.8.0 \
      sh mqnamesrv
#broker.conf
brokerIP1=192.168.200.100
namesrvAddr=192.168.200.100:9876
brokerName=broker_all
#启动broker
docker run -d  -v /opt/docker/rocketmq/logs:/usr/local/rocketmq/logs -v /opt/docker/rocketmq/store:/usr/local/rocketmq/store \
      -v /opt/docker/rocketmq/conf:/usr/local/rocketmq/conf \
      --name rmqbroker \
      -e "NAMESRV_ADDR=192.168.200.100:9876" \
      -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \
      -p 10911:10911 -p 10912:10912 -p 10909:10909 \
      foxiswho/rocketmq:4.8.0 \
      sh mqbroker -c /usr/local/rocketmq/conf/broker.conf
#rocketmq-console-ng
docker run --name rmqconsole --link rmqnamesrv:rmqnamesrv \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.200.100:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" \
-p 8180:8080 -t styletang/rocketmq-console-ng

#启动访问 http://192.168.200.100:8180/

4.2 整合

4.2.1 导入依赖

<dependency>
  <groupId>org.apache.rocketmqgroupId>
  <artifactId>rocketmq-clientartifactId>
  <version>4.8.0version>
dependency>
<dependency>
  <groupId>org.apache.rocketmqgroupId>
  <artifactId>rocketmq-spring-boot-starterartifactId>
  <version>2.2.0version>
dependency>

4.2.2 写配置

#rocketmq配置
rocketmq.name-server=192.168.200.100:9876
rocketmq.producer.group=springboot_group

4.2.3 代码

//生产者
@Autowired
private RocketMQTemplate rocketMQTemplate;

public void send(){
  User user = this.findById(1L);
  rocketMQTemplate.convertAndSend("topic_springboot",user);
}
//消费者
package com.mszlu.union.service.consumer;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "topic_springboot",consumerGroup = "group1")
@Slf4j
public class UserConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String msg) {
        log.info("msg:{}",msg);
    }
}
//MessageModel.CLUSTERING=集群模式,一个消息只会被一个消费者消费(默认),注意不同consumerGroup会重复消费一次
//MessageModel.BROADCASTING=广播模式,一个消息会被多个消费者消费
//一个消费者时ConsumeMode.ORDERLY(有序地接收异步传递的消息。一个队列,一个线程,对应线程队列按顺序一个一个取)
// 1.所有发送者全部发送到同一队列(hashKey相同)时,消费者只开一个线程并且按发送顺序一个一个读取
// 2.如2个发送者发送到各自队列时,则消费者会开2个线程,每个线程按队列里数据一个一个读取,
     此处只有2个队列,如果发到对应4个队列则同样开启4个线程
// 3.不指定队列,默认按一个队列放一个轮训发到默认的4个队列中,默认开启4个线程一个一个读对应线程队列的数据

//一个消费者时ConsumeMode.CONCURRENTLY(默认,同时取,并发消费模式)
// 1.默认按一个队列放一个轮训发到默认的4个队列中,每条消息都会开启一个线程读数据
// 2.所有人发送到同一队列或发送到各自队列,每条消息都会开启一个线程读数据

//一个队列只会发给一个消费者处理,不会发给多个消费者处理,多个队列可以同时由一个消费者处理,如上
//当只有一个队列时,消费的消费者挂了,会由同组(集群)的其他一个消费者继续消费,会有重复消费几个消息

producer nameServer管理broker consumer
发送者从nameServer获取broker,发消息给broker
消费者从nameServer获取broker,从broker获取消息

nameServer集群之间无状态
broker里的topic默认有4个queue
broker集群有Master(brokerId为0)和Slave(brokerId非0)通过定义相同的brokerName
producerGroup和consumerGroup名称相同既是集群

双主双重复制4个mq,配b1主b2从同brokerName,b3主b4从同brokerName,4个broker的brokerClusterName一样,namesrvAddr=4个IP地址分号隔,更多配置看笔记

消费策略

Consumer在拉取消息之前需要对TopicMessage进行负载操作,负载操作由一个定时器来完成单位,定时间隔默认20s

默认平均负载策略 AllocateMessageQueueAveragely

偶数个队列:平均分配
奇数个队列:
队列个数>消费者 排前的那个多拿一个后面平均分,4个队列3个消费者 (Q0,Q1)->C1 Q2->C2 Q3->C3
队列个数<消费者 前面平均分最后那个拿不到,2个队列3个消费者 Q1->C1 Q2->C2 C3没有队列
C3分不到消息队列,除非前面的某个Consumer挂了,20s之后,在队列重新负载的时候就能拿到MessageQueue

SpringBoot系列_第7张图片

环形平均分配 AllocateMessageQueueAveragelyByCircle

环形平均分配,这个和平均分配唯一的区别就是,再分队列的时候,平均队列是将属于自己的MessageQueue全部拿走,而环形平均则是,一人拿一个,拿到的Queue不是连续的。

SpringBoot系列_第8张图片

5. SpringBoot整合ES

5.1 安装ES

Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎

https://docs.spring.io/spring-data/elasticsearch/docs/4.2.1/reference/html/#new-features

看官方的对应版本。

#下载es的镜像
docker pull elasticsearch:7.12.1
#创建es的容器 并启动  single-node单机
docker run -d --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.12.1
#测试
http://192.168.200.100:9200/
#下载kibana的镜像
docker pull kibana:7.12.1
#准备kibana的配置文件
docker inspect es的容器id(docker ps -a 查看)
# 找到  "IPAddress": "172.17.0.2" 找出es对应的容器ip地址
# Default Kibana configuration for docker target
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://172.17.0.2:9200" ]
xpack.monitoring.ui.container.elasticsearch.enabled: true

#启动kibana
docker run -d --restart=always --log-driver json-file --log-opt max-size=100m --log-opt max-file=2 --name kibana -p 5601:5601 -v /opt/docker/es/kibana.yml:/usr/share/kibana/config/kibana.yml kibana:7.12.1
#测试
http://192.168.200.100:5601/
#命令
GET /_search #获取所有数据
PUT /ecommerce/product/1
{
      "name" : "gaolujie yagao",
      "desc" :  "gaoxiao meibai",
      "price" :  30,
      "producer" :      "gaolujie producer",
      "tags": [ "meibai", "fangzhu" ]
}
GET /ecommerce/product/1
POST /ecommerce/product/1/_update
{
    "doc" : {
        "name" : "佳洁士牙膏"
    }
}
DELETE /ecommerce/product/1

5.2 整合

5.2.1 添加依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>

5.2.2 常用注解

//标示映射到Elasticsearch文档上的领域对象
public @interface Document {
  //索引库名次,mysql中数据库的概念
    String indexName();
  //文档类型,mysql中表的概念
    String type() default "";
  //默认分片数
    short shards() default 5;
  //默认副本数量
    short replicas() default 1;
}
//表示是文档的id,文档可以认为是mysql中主键的概念
public @interface Id {}
public @interface Field {
    //文档中字段的类型
      FieldType type() default FieldType.Auto;
    //是否建立倒排索引
      boolean index() default true;
    //是否进行存储
      boolean store() default false;
    //分词器名次
      String analyzer() default "";
}
//为文档自动指定元数据类型
public enum FieldType {
      Text,//会进行分词并建了索引的字符类型
      Integer,
      Long,
      Date,
      Float,
      Double,
      Boolean,
      Object,
      Auto,//自动判断字段类型
      Nested,//嵌套对象类型
      Ip,
      Attachment,
      Keyword//不会进行分词建立索引的类型
}

5.2.3 写配置

#es的配置
spring.elasticsearch.rest.uris=http://192.168.200.100:9200
spring.data.elasticsearch.repositories.enabled=true
spring.data.elasticsearch.client.reactive.endpoints=192.168.200.100:9200

5.2.4 代码

package com.mszlu.union.domain.repository;

import com.mszlu.union.model.es.Article;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleRepository extends ElasticsearchRepository<Article,String> {

    //根据作者名称 搜索
    Page<Article> findByAuthorsName(String name, Pageable pageable);

    //搜索title字段
    Page<Article> findByTitleIsContaining(String word,Pageable pageable);
    
    Page<Article> findByTitle(String title,Pageable pageable);
}
package com.mszlu.union.model.es;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.util.List;

//注意indexName要小写
@Document(indexName = "blog")
@Data
public class Article {
    @Id
    private String id;
    private String title;
    @Field(type = FieldType.Nested, includeInParent = true)
    private List<Author> authors;

    public Article(String title) {
        this.title = title;
    }
}

public class Author {
    private String name;
    public Author(String name) {
        this.name = name;
    }
}
package com.mszlu.union.service;

import com.alibaba.fastjson.JSON;
import com.mszlu.union.domain.repository.ArticleRepository;
import com.mszlu.union.model.es.Article;
import com.mszlu.union.model.es.Author;
import org.checkerframework.checker.units.qual.A;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;

import java.util.Arrays;

import static java.util.Arrays.asList;
import static org.elasticsearch.index.query.QueryBuilders.regexpQuery;

@SpringBootTest
public class ESTest {

    @Autowired
    private ArticleRepository articleRepository;
    //新增
    @Test
    public void save(){
        Article article = new Article("Spring Data Elasticsearch");
        article.setAuthors(asList(new Author("god"),new Author("John")));
        articleRepository.save(article);

        article = new Article("Spring Data Elasticsearch2");
        article.setAuthors(asList(new Author("god"),new Author("King")));
        articleRepository.save(article);

        article = new Article("Spring Data Elasticsearch3");
        article.setAuthors(asList(new Author("god"),new Author("Bill")));
        articleRepository.save(article);
    }
    @Test
    public void queryAuthorName()  {
        Page<Article> articles = articleRepository.findByAuthorsName("chali", PageRequest.of(0,10));
        for (Article article : articles.getContent()) {
            System.out.println(article);
            for (Author author : article.getAuthors()) {
                System.out.println(author);
            }
        }
    }
    @Test
    public void update() {
        Page<Article> articles = articleRepository.findByTitle("Spring Data Elasticsearch",PageRequest.of(0,10));

        Article article = articles.getContent().get(0);
        System.out.println(article);
        System.out.println(article.getAuthors().get(0));
        Author author = new Author("chali");
        article.setAuthors(Arrays.asList(author));
        articleRepository.save(article);
    }

    @Test
    public void delete(){
        Page<Article> articles = articleRepository.findByTitle("Spring Data Elasticsearch",PageRequest.of(0,10));
        Article article = articles.getContent().get(0);
        articleRepository.delete(article);
    }

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    //使用Template进行关键字查询
    //关于正则表达式可以参考https://www.runoob.com/java/java-regular-expressions.html
    //.*data.* 可以匹配ddata, dataa等
    @Test
    void queryTileContainByTemplate()  {
        Query query = new NativeSearchQueryBuilder().withFilter(regexpQuery("title",".*elasticsearch2.*")).build();
        SearchHits<Article> articles = elasticsearchRestTemplate.search(query, Article.class, IndexCoordinates.of("blog"));
        System.out.println(JSON.toJSONString(articles));
    }

}

5.2.5 ElasticsearchRepository

关键字 使用示例 等同于的ES查询
And findByNameAndPrice {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}}
Or findByNameOrPrice {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}}
Is findByName {“bool” : {“must” : {“field” : {“name” : “?”}}}}
Not findByNameNot {“bool” : {“must_not” : {“field” : {“name” : “?”}}}}
Between findByPriceBetween {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
LessThanEqual findByPriceLessThan {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
GreaterThanEqual findByPriceGreaterThan {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}}
Before findByPriceBefore {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
After findByPriceAfter {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}}
Like findByNameLike {“bool” : {“must” : {“field” : {“name” : {“query” : “? *”,”analyze_wildcard” : true}}}}}
StartingWith findByNameStartingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “? *”,”analyze_wildcard” : true}}}}}
EndingWith findByNameEndingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,”analyze_wildcard” : true}}}}}
Contains/Containing findByNameContaining {“bool” : {“must” : {“field” : {“name” : {“query” : “?”,”analyze_wildcard” : true}}}}}
In findByNameIn(Collectionnames) {“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}}
NotIn findByNameNotIn(Collectionnames) {“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}}
True findByAvailableTrue {“bool” : {“must” : {“field” : {“available” : true}}}}
False findByAvailableFalse {“bool” : {“must” : {“field” : {“available” : false}}}}
OrderBy findByAvailableTrueOrderByNameDesc {“sort” : [{ “name” : {“order” : “desc”} }],”bool” : {“must” : {“field” : {“available” : true}}}}

6. SpringBoot整合SpringSecurity

Spring Security 是 Spring 家族中的一个安全管理框架,提供了权限的解决方案,通过一些简单的配置以及代码,就可以轻松实现。

安全:认证+授权

6.1 导入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>

6.2 启动访问接口

//启动会在控制台出现
Using generated security password: 98687887-01bf-41b5-bb6e-63009367be0f

同时访问http://localhost:8080/user/findAll ,会出现一个登陆页面

SpringBoot系列_第9张图片

这时候,用户名输入user,密码输入上方控制台打印的密码,即可登录,并且正常访问接口。

6.3 登录的用户名/密码

对登录的用户名/密码进行配置,有三种不同的方式:

  1. 在 application.properties 中进行配置

    spring.security.user.name=admin
    spring.security.user.password=mszlu
    
  2. 通过 Java 代码配置在内存中

    package com.mszlu.union.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //下面这两行配置表示在内存中配置了两个用户
            auth.inMemoryAuthentication()
                .withUser("admin")
                .roles("admin")
                .password("$2a$10$2UaOufuypWR1TASuso2S6.u6TGL7nuAGCsb4RZ5X2SMEuelwQBToO")
                .and()
                .withUser("user")
                .roles("user")
                password("$2a$10$2UaOufuypWR1TASuso2S6.u6TGL7nuAGCsb4RZ5X2SMEuelwQBToO");
        }
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        public static void main(String[] args) {
            String mszlu = new BCryptPasswordEncoder().encode("mszlu");
            System.out.println(mszlu);
        }
    }
    
  3. 通过 Java 从数据库中加载

6.4 登录配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() //开启登录认证
                .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated() // 其他所有的请求 只需要登录即可
                .and().formLogin()
                .loginPage("/login.html") //自定义的登录页面
                .loginProcessingUrl("/login") //登录处理接口
                .usernameParameter("username") //定义登录时的用户名的key 默认为username
                .passwordParameter("password") //定义登录时的密码key,默认是password
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        out.write("success");
                        out.flush();
                    }
                }) //登录成功处理器
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        out.write("fail");
                        out.flush();
                    }
                }) //登录失败处理器
        .permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
        .and().logout() //退出登录配置
        .logoutUrl("/logout") //退出登录接口
        .logoutSuccessHandler(new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write("logout success");
                out.flush();
            }
        }) //退出登录成功 处理器
        .permitAll() //退出登录的接口放行
        .and()
        .httpBasic()
        .and()
        .csrf().disable(); //csrf关闭 如果自定义登录 需要关闭

    }

6.4.1 自定义登录

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>自定义登录页面title>
    
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
head>
<body>
<div id="app">
    用户名: <el-input v-model="username" placeholder="请输入内容">el-input>
    密码:  <el-input v-model="password" placeholder="请输入内容">el-input>
           <el-button type="success" @click="login()">提交el-button>
div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">script>
<script src="https://unpkg.com/element-ui/lib/index.js">script>
<script src="https://unpkg.com/axios/dist/axios.min.js">script>

<script>
    new Vue({
        el: "#app",
        data:{
            username:"",
            password:""
        },
        methods:{
            login(){
                    axios.post("/login?username="+this.username+"&password="+this.password).then((res)=>{
                        if (res.data == "success"){
                            this.$message.success("登录成功");
                        }else{
                            this.$message.error("用户名或密码错误");
                        }
                    })
            }
        }
    });
script>
body>
html>

6.5 数据库访问认证和授权

6.5.1 登录认证

  1. 表结构

    CREATE TABLE `admin_user`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `create_time` bigint(0) NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    INSERT INTO `admin_user`(`id`, `username`, `password`, `create_time`) VALUES (1, 'admin', '$2a$10$2UaOufuypWR1TASuso2S6.u6TGL7nuAGCsb4RZ5X2SMEuelwQBToO', 1622711132975);
    
    
    package com.mszlu.union.pojo;
    
    import lombok.Data;
    
    @Data
    public class AdminUser {
    
        private  Long id;
    
        private String username;
    
        private String password;
    
        private Long createTime;
    }
    
    
    package com.mszlu.union.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.mszlu.union.pojo.AdminUser;
    
    public interface AdminUserMapper extends BaseMapper<AdminUser> {
    }
    
    
  2. 实现UserDetailService接口

    package com.mszlu.union.security;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.mszlu.union.mapper.AdminUserMapper;
    import com.mszlu.union.pojo.AdminUser;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Component
    public class SecurityUserService implements UserDetailsService {
        @Autowired
        private AdminUserMapper adminUserMapper;
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            LambdaQueryWrapper<AdminUser> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(AdminUser::getUsername,username).last("limit 1");
            AdminUser adminUser = this.adminUserMapper.selectOne(queryWrapper);
            if (adminUser == null){
                throw new UsernameNotFoundException("用户名不存在");
            }
            List<GrantedAuthority> authorityList = new ArrayList<>();
            UserDetails userDetails = new User(username,adminUser.getPassword(),authorityList);
            return userDetails;
        }
    }
    
    
  3. 在配置中添加使用UserDetailService

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.userDetailsService(securityUserService);
            //...
        }
    
  4. 测试

6.5.2 授权

  1. 权限相关表结构

    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `id` int(0) NOT NULL AUTO_INCREMENT,
      `role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `role_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `role_keyword` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES (1, '管理员', '管理员', 'ADMIN');
    INSERT INTO `role` VALUES (2, '运营', '运营部门', 'BUSINESS');
    
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT,
      `user_id` bigint(0) NOT NULL,
      `role_id` int(0) NOT NULL,
      PRIMARY KEY (`id`) USING BTREE,
      INDEX `user_id`(`user_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES (1, 1, 1);
    
    package com.mszlu.union.pojo;
    
    import lombok.Data;
    
    @Data
    public class Role {
        private Integer id;
    
        private String roleName;
    
        private String roleDesc;
    
        private String roleKeyword;
    }
    
    
    DROP TABLE IF EXISTS `permission`;
    CREATE TABLE `permission`  (
      `id` int(0) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `permission_keyword` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of permission
    -- ----------------------------
    INSERT INTO `permission` VALUES (1, '查询全部', '查询全部', 'USER_FINDALL', '/user/findAll');
    INSERT INTO `permission` VALUES (2, '年龄查询', '年龄查询', 'USER_FINDAGE', '/user/findAge');
    
    DROP TABLE IF EXISTS `role_permission`;
    CREATE TABLE `role_permission`  (
      `id` bigint(0) NOT NULL AUTO_INCREMENT,
      `role_id` int(0) NOT NULL,
      `permission_id` int(0) NOT NULL,
      PRIMARY KEY (`id`) USING BTREE,
      INDEX `role_id`(`role_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role_permission
    -- ----------------------------
    INSERT INTO `role_permission` VALUES (1, 1, 1);
    
    package com.mszlu.union.pojo;
    
    import lombok.Data;
    
    @Data
    public class Permission {
        private Integer id;
    
        private String name;
    
        private String desc;
    
        private String permissionKeyword;
    
        private String path;
    }
    
    
  2. 在UserDetailService的接口实现中,查询用户的权限

    package com.mszlu.union.security;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.mszlu.union.mapper.AdminUserMapper;
    import com.mszlu.union.mapper.PermissionMapper;
    import com.mszlu.union.mapper.RoleMapper;
    import com.mszlu.union.pojo.AdminUser;
    import com.mszlu.union.pojo.Permission;
    import com.mszlu.union.pojo.Role;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Component
    public class SecurityUserService implements UserDetailsService {
        @Autowired
        private AdminUserMapper adminUserMapper;
        @Autowired
        private RoleMapper roleMapper;
        @Autowired
        private PermissionMapper permissionMapper;
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            LambdaQueryWrapper<AdminUser> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(AdminUser::getUsername,username).last("limit 1");
            AdminUser adminUser = this.adminUserMapper.selectOne(queryWrapper);
            if (adminUser == null){
                throw new UsernameNotFoundException("用户名不存在");
            }
            List<GrantedAuthority> authorityList = new ArrayList<>();
            //查询角色和角色对应的权限 并赋予当前的登录用户,并告知spring security框架
            List<Role> roleList = roleMapper.findRoleListByUserId(adminUser.getId());
            for (Role role : roleList) {
                List<Permission> permissionList = permissionMapper.findPermissionByRole(role.getId());
                authorityList.add(new SimpleGrantedAuthority("ROLE_"+role.getRoleKeyword()));
                for (Permission permission : permissionList) {
                    authorityList.add(new SimpleGrantedAuthority(permission.getPermissionKeyword()));
                }
            }
            UserDetails userDetails = new User(username,adminUser.getPassword(),authorityList);
            return userDetails;
        }
    }
    
    
    package com.mszlu.union.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.mszlu.union.pojo.Permission;
    import com.mszlu.union.pojo.Role;
    
    import java.util.List;
    
    public interface PermissionMapper extends BaseMapper<Permission> {
    
        List<Permission> findPermissionByRole(Integer roleId);
    }
    
    
    
   package com.mszlu.union.mapper;
   
   import com.baomidou.mybatisplus.core.mapper.BaseMapper;
   import com.mszlu.union.pojo.Role;
   
   import java.util.List;
   
   public interface RoleMapper extends BaseMapper<Role> {
   
       List<Role> findRoleListByUserId(Long userId);
   }
   
   
   
   DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
   
   <mapper namespace="com.mszlu.union.mapper.PermissionMapper">
       <resultMap id="perMap" type="com.mszlu.union.pojo.Permission">
           <id column="id" property="id"/>
           <result column="name" property="name"/>
           <result column="desc" property="desc"/>
           <result column="permission_keyword" property="permissionKeyword"/>
           <result column="path" property="path"/>
       resultMap>
   
       <select id="findPermissionByRole" parameterType="int" resultMap="perMap">
           select * from permission where id in (select permission_id from role_permission where role_id=#{roleId})
       select>
   mapper>
   
   
   DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
   
   <mapper namespace="com.mszlu.union.mapper.RoleMapper">
       <resultMap id="roleMap" type="com.mszlu.union.pojo.Role">
           <id column="id" property="id"/>
           <result column="role_name" property="roleName"/>
           <result column="role_desc" property="roleDesc"/>
           <result column="role_keyword" property="roleKeyword"/>
       resultMap>
   
       <select id="findRoleListByUserId" parameterType="long" resultMap="roleMap">
           select * from role where id in (select role_id from user_role where user_id=#{userId})
       select>
   mapper>
  1. 在接口上配置权限

        @GetMapping("findAll")
        @PreAuthorize("hasAuthority('USER_FINDALL')")
        public List<User> findAll(){
            return userService.findAll();
        }
    
        @GetMapping("findAge")
        @PreAuthorize("hasAuthority('USER_FINDAGE')")
        public List<User> findAge(){
            return userService.findAge();
        }
    
        @GetMapping("findById")
        @PreAuthorize("hasRole('ADMIN')")
        public User findById(@RequestParam("id") Long id){
            return userService.findById(id);
        }
    
  2. 在配置上开启权限认证

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {}
    
  3. 测试

6.5.3 另一种方式进行授权

  1. 修改配置

      @Override
        protected void configure(HttpSecurity http) throws Exception {
    //        http.userDetailsService(securityUserService);
            http.authorizeRequests() //开启登录认证
    //                .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
                    .antMatchers("/login").permitAll()
                //使用访问控制 自己实现service处理,会接收两个参数 
                    .anyRequest().access("@authService.auth(request,authentication)")
    //                .anyRequest().authenticated() // 其他所有的请求 只需要登录即可
        }
    
  2. 编写AuthService的auth方法、

    package com.mszlu.union.service;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.mszlu.union.mapper.AdminUserMapper;
    import com.mszlu.union.mapper.PermissionMapper;
    import com.mszlu.union.mapper.RoleMapper;
    import com.mszlu.union.mapper.UserMapper;
    import com.mszlu.union.pojo.AdminUser;
    import com.mszlu.union.pojo.Permission;
    import com.mszlu.union.pojo.Role;
    import com.mszlu.union.security.MySimpleGrantedAuthority;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    @Service
    public class AuthService {
    
        @Autowired
        private AdminUserMapper adminUserMapper;
        @Autowired
        private RoleMapper roleMapper;
        @Autowired
        private PermissionMapper permissionMapper;
    
        public boolean auth(HttpServletRequest request, Authentication authentication){
            String requestURI = request.getRequestURI();
            Object principal = authentication.getPrincipal();
            if (principal == null || "anonymousUser".equals(principal)){
                //未登录
                return false;
            }
            UserDetails userDetails = (UserDetails) principal;
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                MySimpleGrantedAuthority grantedAuthority = (MySimpleGrantedAuthority) authority;
                String[] paths = StringUtils.split(requestURI, "?");
                if (paths[0].equals(grantedAuthority.getPath())){
                    return true;
                }
            }
            return false;
        }
    }
    
    
  3. 修改UserDetailService中 授权的实现,实现自定义的授权类,增加path属性

    package com.mszlu.union.security;
    
    import org.springframework.security.core.GrantedAuthority;
    
    public class MySimpleGrantedAuthority implements GrantedAuthority {
        private String authority;
        private String path;
    
        public MySimpleGrantedAuthority(){}
    
        public MySimpleGrantedAuthority(String authority){
            this.authority = authority;
        }
    
        public MySimpleGrantedAuthority(String authority,String path){
            this.authority = authority;
            this.path = path;
        }
    
        @Override
        public String getAuthority() {
            return authority;
        }
    
        public String getPath() {
            return path;
        }
    }
    
    
  4. 测试

7. SpringBoot整合Dubbo

Dubbo是阿里开源的高性能,轻量级的分布式服务框架,提供了六大核心能力:

  1. 面向接口代理的高性能RPC调用

  2. 智能容错和负载均衡

  3. 服务自动注册和发现

  4. 高度可扩展能力

  5. 运行期流量调度

  6. 可视化的服务治理与运维

https://dubbo.apache.org/zh/

使用2.7版本:

https://dubbo.apache.org/zh/docs/v2.7/user/preface/background/ 架构演进

https://dubbo.apache.org/zh/docs/v2.7/user/preface/architecture/ dubbo架构

7.1 导入依赖

        
        <dependency>
            <groupId>org.apache.dubbogroupId>
            <artifactId>dubbo-spring-boot-starterartifactId>
            <version>2.7.8version>
        dependency>

7.2 服务提供方

7.2.1 搭建工程dubbo-provider


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.mszlugroupId>
    <artifactId>dubbo-providerartifactId>
    <version>1.0-SNAPSHOTversion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.0version>
    parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.dubbogroupId>
            <artifactId>dubbo-spring-boot-starterartifactId>
            <version>2.7.8version>
        dependency>
        
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-frameworkartifactId>
            <version>5.1.0version>
        dependency>
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-recipesartifactId>
            <version>5.1.0version>
        dependency>
    dependencies>
project>

7.2.2 配置dubbo

# Spring boot application
spring.application.name=dubbo-provider
# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service
dubbo.scan.base-packages=com.mszlu.dubbo.service

# Dubbo Application
## The default value of dubbo.application.name is ${spring.application.name}
## dubbo.application.name=${spring.application.name}

# Dubbo Protocol
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

## Dubbo Registry
dubbo.registry.protocol=zookeeper
dubbo.registry.address=localhost:2181

7.2.3 编写服务

  1. 搭建通用的接口模块 dubbo-provider-service

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        <groupId>com.mszlugroupId>
        <artifactId>dubbo-provider-serviceartifactId>
        <version>1.0-SNAPSHOTversion>
    
    project>
    
  2. 编写service接口

    package com.mszlu.dubbo.service;
    
    public interface DubboUserService {
    
        void save();
    }
    
  3. 在dubbo-provider中编写实现类,前提 导入dubbo-provider-service依赖

    package com.mszlu.dubbo.service.impl;
    
    import com.mszlu.dubbo.service.DubboUserService;
    import org.apache.dubbo.config.annotation.DubboService;
    
    @DubboService(version = "1.0.0",interfaceClass = DubboUserService.class)
    public class DubboUserServiceImpl implements DubboUserService {
        @Override
        public void save() {
            System.out.println("dubbo save running.....");
        }
    }
    
  4. 启动服务

7.3 服务消费方

7.3.1 添加依赖


        <dependency>
            <groupId>org.apache.dubbogroupId>
            <artifactId>dubbo-spring-boot-starterartifactId>
            <version>2.7.8version>
        dependency>
        
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-frameworkartifactId>
            <version>5.1.0version>
        dependency>
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-recipesartifactId>
            <version>5.1.0version>
        dependency>
		 <dependency>
            <groupId>com.mszlugroupId>
            <artifactId>dubbo-provider-serviceartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>

7.3.2 添加dubbo配置

spring.application.name=union
## Dubbo Registry
dubbo.consumer.check=false
dubbo.registry.protocol=zookeeper
dubbo.registry.address=localhost:2181

7.3.3 消费方代码

 @DubboReference(version = "1.0.0")
    private DubboUserService dubboUserService;

    @GetMapping("dubbo")
    public String dubbo(){
        dubboUserService.save();
        return "success";
    }

7.3.4 测试

.antMatchers("/user/dubbo").permitAll()

8. SpringBoot整合Swagger2

强大的api文档工具,让开发人员摆脱繁杂的文档苦海。

8.1 导入依赖

<dependency>
  <groupId>com.spring4allgroupId>
  <artifactId>swagger-spring-boot-starterartifactId>
  <version>1.9.1.RELEASEversion>
dependency>

8.2 启动类添加@EnableSwagger2Doc注解

package com.mszlu.union;

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

8.2 配置

swagger.title=码神之路-swagger2
swagger.description=码神之路-swagger2 描述信息
swagger.version=2.9.2
swagger.license=Apache License, Version 2.0
swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
swagger.termsOfServiceUrl=https://github.com/dyc87112/spring-boot-starter-swagger
swagger.contact.name=码神之路
swagger.contact.url=http://blog.mszlu.com
swagger.contact.email=码神之路
swagger.base-package=com.mszlu
swagger.base-path=/**

各参数配置含义如下:

  • swagger.title:标题
  • swagger.description:描述
  • swagger.version:版本
  • swagger.license:许可证
  • swagger.licenseUrl:许可证URL
  • swagger.termsOfServiceUrl:服务条款URL
  • swagger.contact.name:维护人
  • swagger.contact.url:维护人URL
  • swagger.contact.email:维护人email
  • swagger.base-package:swagger扫描的基础包,默认:全扫描
  • swagger.base-path:需要处理的基础URL规则,默认:/**

8.3 添加文档内容

在整合完Swagger之后,在http://localhost:8081/swagger-ui.html页面中可以看到,关于各个接口的描述还都是英文或遵循代码定义的名称产生的。

这些内容对用户并不友好,所以我们需要自己增加一些说明来丰富文档内容。

如下所示,我们通过@Api@ApiOperation注解来给API增加说明、通过@ApiImplicitParam@ApiModel@ApiModelProperty注解来给参数增加说明。

package com.mszlu.union.pojo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
//User 对象 如何和 user表关联起来

@Data
//@TableName("user")
//tb_user
//@TableName("tb_user") mybatis-plus.global-config.db-config.table-prefix=tb_
@ApiModel(description="用户实体")
public class User {
    @ApiModelProperty("用户编号")
    private Long id;
    @ApiModelProperty("用户姓名")
    //@TableField("name")// 表的字段
    private String name;
    //表字段 是user_name
    //@TableField("user_name") 可以不加 _会替换为驼峰写法
    //private String userName;
    private String email;
    @ApiModelProperty("用户年龄")
    private Integer age;
}
package com.mszlu.union.controller;

import com.mszlu.dubbo.service.DubboUserService;
import com.mszlu.union.pojo.User;
import com.mszlu.union.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
@Api(tags = "用户管理")
@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("findAll")
    @ApiOperation(value = "获取用户列表")
    //@PreAuthorize("hasAuthority('USER_FINDALL')")
    public List<User> findAll(){
        return userService.findAll();
    }

    @GetMapping("findAge")
    //@PreAuthorize("hasAuthority('USER_FINDAGE')")
    public List<User> findAge(){
        return userService.findAge();
    }

    @GetMapping("findById")
    @ApiOperation(value = "查询用户信息",notes = "根据id查询用户信息")
    @ApiImplicitParam(paramType = "get传参", dataType = "Long", name = "id", value = "用户编号", required = true, example = "1")
    //@PreAuthorize("hasRole('ADMIN')")
    public User findById(@RequestParam("id") Long id){
        return userService.findById(id);
    }

    @GetMapping("findPage")
    public List<User> findPage(@RequestParam("page") Integer page,
                               @RequestParam("pageSize") Integer pageSize){
        return userService.findPage(page,pageSize);
    }
    @GetMapping("save")
    public Long findAll(@RequestParam("name") String name){
        return userService.save(name);
    }

    @GetMapping("send")
    public String send(){
         userService.send();
         return "success";
    }
    //问注册中心 要服务提供方的地址
    //发起rpc调用
    //返回的数据 进行解析
    @DubboReference(version = "1.0.0")
    private DubboUserService dubboUserService;

    @GetMapping("dubbo")
    public String dubbo(){
        dubboUserService.save();
        return "success";
    }
}

8.4 springSecurity框架忽略swagger2的相关url

 .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/swagger*/**").permitAll()
                .antMatchers("/v2/**").permitAll()
                .antMatchers("/csrf").permitAll()

05-SpringBoot原理

1. SpringBoot原理分析

1.1 @EnableAutoConfiguration

1.1.1 @Import(AutoConfigurationImportSelector.class)

使用@Import导入的类会被Spring加载到IOC容器中

@Import提供4种用法:

  1. 导入Bean

    @Import(User.class)
    
  2. 导入配置类

    @Import(UserConfig.class)
    
   @Configuration
   public class UserConfig {
       @Bean("user")
       public User user() {
           return new User();
       }
   }
  1. 导入 ImportSelector 实现类。一般用于加载配置文件中的类

    @Import(MyImportSelector.class)
    
    public class MyImportSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            return new String[]{"com.mszlu.domain.User"};
        }
    }
    
  2. 导入 ImportBeanDefinitionRegistrar 实现类。

    @Import(MyImportBeanDefinitionRegistrar.class)
    
     public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
            @Override
            public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
                AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
                registry.registerBeanDefinition("user",beanDefinition);
            }
        }
    
  3. 在resources下新建 META-INF/spring.factories 文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mszlu.union.pojo.User

1.1.2 AutoConfigurationImportSelector代码分析

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
        //获取自动配置相关的信息
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
    	//读取指定的配置文件,获取配置列表 
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}
	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        //读取META-INF/spring.factories 中的配置,根据其中的配置找到对应需要加载的配置类
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				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;
	}
//这是spring-boot-autoconfigure包下的META-INF/spring.factories 其中关于Redis的配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.data.redis;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Christian Dupuis
 * @author Christoph Strobl
 * @author Phillip Webb
 * @author Eddú Meléndez
 * @author Stephane Nicoll
 * @author Marco Aust
 * @author Mark Paluch
 * @since 1.0.0
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}

1.1.3 @Conditional注解

从上方RedisAutoConfiguration的代码可以看到这些注解:

  1. @ConditionalOnMissingBean(name = “redisTemplate”)

    如果没有redisTemplate这个Bean的时候 才运行以下的代码

  2. @ConditionalOnClass(RedisOperations.class)

    如果没有RedisOperations这个类(类路径上找不到,也就是没导入相关的依赖),则不运行

  3. @ConditionalOnSingleCandidate(RedisConnectionFactory.class)

    指定的类存在,且必须注册在spring中,是单例的

上面的这些都是由@Conditional这个注解实现的:条件,满足条件执行,不满足条件就不执行

1.1.3 总结 面试题

SpringBoot系列_第10张图片

  1. @EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class)来加载配置类。

  2. 配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会扫描依赖 jar 包中 spring.factories 文件,自动加载 这些配置类,初始化Bean 到spring 容器中,业务直接引用即可。

  3. 并不是所有的Bean都会被初始化,在配置类中使用@Conditional来加载满足条件的Bean。

    引入第三方jar包就相当于我本地有这jar包的代码,跟本地代码一样效果

1.2 自定义Starter封装

思路:

  1. 创建一个maven工程,创建三个个模块
  2. 一个模块为demo-app,一个模块为demo-module,一个模块为demo-module-springboot-starter
  3. demo-module中定义一个类MyModule,其中有一个save方法,读取properties配置文件中的com.mszlu.version和com.mszlu.age的属性值
  4. app模块 引入demo-module-springboot-starter模块,不需要初始化MyModule,只需要配置com.mszlu.version和com.mszlu.age就可以直接初始化MyModule并调用save方法

1.2.1 代码

  1. 新建父工程

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        <groupId>com.mszlugroupId>
        <artifactId>starter-parentartifactId>
        <version>1.0-SNAPSHOTversion>
        <modules>
            <module>demo-modulemodule>
            <module>demo-module-springboot-startermodule>
            <module>demo-appmodule>
        modules>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.5.0version>
        parent>
        <packaging>pompackaging>
    
        <properties>
            <java.version>1.8java.version>
        properties>
    project>
    
  2. 新建demo-module,编写代码

    package com.mszlu.module;
    
    public class MyModule {
    
        private String version;
    
        private Integer age;
    
        public void save(){
            System.out.println("my module save.... version:"+ version + ",age:" + age);
        }
    
        public String getVersion() {
            return version;
        }
    
        public void setVersion(String version) {
            this.version = version;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    }
    
    
  3. 新建demo-module-springboot-starter模块

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>starter-parentartifactId>
            <groupId>com.mszlugroupId>
            <version>1.0-SNAPSHOTversion>
        parent>
        <modelVersion>4.0.0modelVersion>
    
        <artifactId>demo-module-springboot-starterartifactId>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starterartifactId>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-configuration-processorartifactId>
                <optional>trueoptional>
            dependency>
            <dependency>
                <groupId>com.mszlugroupId>
                <artifactId>demo-moduleartifactId>
                <version>1.0-SNAPSHOTversion>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    project>
    
    package com.mszlu.springbootstarter.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "com.mszlu")
    public class ModuleConfig {
    
        private String version;
        private Integer age;
    
        public String getVersion() {
            return version;
        }
    
        public void setVersion(String version) {
            this.version = version;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    }
    
    
    package com.mszlu.springbootstarter;
    
    import com.mszlu.module.MyModule;
    import com.mszlu.springbootstarter.config.ModuleConfig;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @EnableConfigurationProperties(ModuleConfig.class)
    public class ModuleAutoConfiguration {
    
        @Bean
        @ConditionalOnProperty(name = {"com.mszlu.version","com.mszlu.age"})
        public MyModule myModule(ModuleConfig moduleConfig){
            MyModule myModule = new MyModule();
            myModule.setVersion(moduleConfig.getVersion());
            myModule.setAge(moduleConfig.getAge());
            return myModule;
        }
    }
    
  4. 在resources下新建META-INF/spring.factories 文件

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mszlu.springbootstarter.ModuleAutoConfiguration
    

    spring.factories文件是帮助spring-boot项目包以外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器。由于@ComponentScan注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要@EnableAutoConfiguration注解来注册项目包外的bean。而spring.factories文件,则是用来记录项目包外需要注册的bean类名。

  5. 新建demo-app模块

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>starter-parentartifactId>
            <groupId>com.mszlugroupId>
            <version>1.0-SNAPSHOTversion>
        parent>
        <modelVersion>4.0.0modelVersion>
    
        <artifactId>demo-appartifactId>
    
        <dependencies>
            <dependency>
                <groupId>com.mszlugroupId>
                <artifactId>demo-module-springboot-starterartifactId>
                <version>1.0-SNAPSHOTversion>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
        dependencies>
    project>
    
  6. 编写测试类

    package com.mszlu.app;
    
    import com.mszlu.module.MyModule;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @SpringBootApplication
    @RestController
    @RequestMapping
    public class App {
    
        public static void main(String[] args) {
            SpringApplication.run(App.class,args);
        }
    
        @Autowired
        private MyModule myModule;
    
        @GetMapping
        public String hello(){
            myModule.save();
            return "success";
        }
    }
    
    
  7. 测试

    如果没有在application.properties中配置:

    com.mszlu.version=1.0
    com.mszlu.age=20
    

    那么无法执行,如果配了可以正常执行

2. SpringBoot 启动流程分析

SpringApplication.run(App.class,args);

上面的启动类,最终会调用:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

那么问题就变为了,new SpringApplication()的时候,做了哪些操作?

run的时候做了哪些操作?

2.1 new SpringApplication()

//primarySources 启动类
public SpringApplication(Class<?>... primarySources) {
		this(null, primarySources);
	}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
    // 这可以看出来,启动类可以配置多个,放入set中去重
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //判断是否是一个web程序
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
    //从META-INF下的spring.factories文件中获取所有BootstrapRegistryInitializer
    //初始化启动注册器,用于为ApplicationContext的创建做准备工作,运行在启动期间和环境加载之后
		this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
    //从 spring.factories 加载所有 ApplicationContextInitializer 
    //用于在spring容器刷新之前初始化Spring ConfigurableApplicationContext的回调接口。
    //通常用于需要对应用程序上下文进行编程初始化的web应用程序中。例如,根据上下文环境注册属性源或激活配置文件等。
		setInitializers((Collection) 
   getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 从 spring.factories 加载所有 ApplicationListener
    //spring 事件监听与抽象类ApplicationEvent类配合来完成ApplicationContext的事件机制。 
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //推断主类
		this.mainApplicationClass = deduceMainApplicationClass();
	}

2.2 run()

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
        //spring的计时器 ms级别
		StopWatch stopWatch = new StopWatch();
        //计时开始
		stopWatch.start();
        //创建 DefaultBootstrapContext 会基于 bootstrapRegistryInitializers 初始化
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
        //属性 java.awt.headless 处理
        //Headless模式是系统的一种配置模式。在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式。
		configureHeadlessProperty();
        //springboot启动监听器 ,可以自定义 并放入META-INF下的spring.factories中 观察spring的启动流程
        //主要是就是用来发布各种 SpringApplicationEvent
		SpringApplicationRunListeners listeners = getRunListeners(args);
        //监听开始
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
             基于 args 构造 ApplicationArguments,即参数行命令
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
               /**
			 * 构造对应的 ConfigurableEnvironment 返回:
			 * 其中发布了核心事件 ApplicationEnvironmentPreparedEvent
			 * 		对应监听器完成了配置文件的读取解析
			 */
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
         	// spring.beaninfo.ignore 属性相关,值为“true”表示跳过对BeanInfo类的搜索
			configureIgnoreBeanInfo(environment);
           // 打印 banner 并返回 Banner 对象 彩蛋
			Banner printedBanner = printBanner(environment);
            // 根据 webApplicationType 创建对应的容器
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
            /**
			 * 在执行完所有 ApplicationContextInitializer 后
			 * 		发布事件 ApplicationContextInitializedEvent
			 * 在加载完所有资源类后发布事件 ApplicationPreparedEvent
			 */
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            //调用容器的 refresh 方法,单例会在该阶段后创建
			refreshContext(context);
			afterRefresh(context, applicationArguments);
            //停止计时
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
            //发布 ApplicationStartedEvent
			listeners.started(context);
            //执行所有 CommandLineRunner 和 ApplicationRunner
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
            //执行完所有 CommandLineRunner 和 ApplicationRunner 后,
            //发布事件 ApplicationReadyEvent
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}
package com.mszlu.app.listener;

import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;

public class MySpringApplicationRunListener implements SpringApplicationRunListener {

    public MySpringApplicationRunListener(SpringApplication application, String[] args) {
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("starting...项目启动中");
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        System.out.println("environmentPrepared...环境对象开始准备");
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("contextPrepared...上下文对象开始准备");
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("contextLoaded...上下文对象开始加载");
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
        System.out.println("started...上下文对象加载完成");
    }

    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println("running...项目启动完成,开始运行");
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("failed...项目启动失败");
    }
}

package com.mszlu.app.listener;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner...run");
        System.out.println(Arrays.asList(args));
    }
}

package com.mszlu.app.listener;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 当项目启动后执行run方法。
 */
@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner...run");
        System.out.println(Arrays.asList(args.getSourceArgs()));
    }
}

org.springframework.boot.SpringApplicationRunListener=com.mszlu.app.listener.MySpringApplicationRunListener

你可能感兴趣的:(SpringBoot,spring,boot,java,spring)