SpringBoot学习笔记

学习视频链接:B站 遇见狂神说

一、概述

1、什么是Spring

Spring是一个开源框架。Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

Spring是如何简化Java开发?

为了降低Java开发的复杂性,Spring采用了以下4种关键策略:

  • 基于POJO的轻量级和最小侵入性编程;
  • 通过IOC,依赖注入(DI)和面向接口实现松耦合;
  • 基于切面(AOP)和惯例进行声明式编程;
  • 通过切面和模板减少样式代码

2、什么是SpringBoot

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

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

SpringBoot的特性

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

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

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

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

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

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

SpringBoot四大核心

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

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

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

  • 命令行界面:这是SpringBoot的可选特性,主要针对Groovy语言使用;

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

Spring Boot的主要优点:

  • 为所有Spring开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

二、微服务

1、什么是微服务

微服务是一种架构风格,它要求在开发一个应用时,这个应用必须构建成一系列小服务的组合,可以通过http的方式进行互通。

2、单体应用架构

所谓单体应用架构(all in one)是指,将一个应用中的所有应用服务都封装在一个应用中。

无论是ERP、CRM或其他系统,都把数据库访问、web访问…各个功能放到一个war包内。

  • 优点:易于开发和测试,方便部署;当需要扩展时,只需要将war复制多份,然后放到多个服务器上,再做负载均衡即可。
  • 缺点:哪怕要修改一个非常小的地方,都需要停掉整个服务,重新打包、部署这个应用war包。特别是对于一个大型应用,不可能把所有内容都放在一个应用里面,这样如何维护、如何分工合作都是问题。

3、微服务架构

all in one的架构方式,把所有的功能单元放在一个应用里面。然后把整个应用部署到服务器上。如果负载能力不行,将整个应用进行水平复制,进行扩展,然后在负载均衡。

所谓微服务架构,就是打破之前all in one的架构方式,把每个功能元素独立出来。把独立出来的功能元素动态组合,需要的功能元素才去拿来组合,需要多一些时间时可以整个多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。

优点:

  • 节省了调用资源。
  • 每个功能元素的服务都是一个可替换的、可独立升级的软件代码。

三、第一个SpringBoot程序

Spring官方提供了非常方便的工具让我们快速构建应用

Spring Initializr:https://start.spring.io/

**项目创建方式一:**使用Spring Initializr 的 Web页面创建项目

1、打开 https://start.spring.io/

2、填写项目信息

3、点击”Generate Project“按钮生成项目;下载此项目

4、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。

5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。

**项目创建方式二:**使用 IDEA 直接创建项目

1、创建一个新项目

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

3、填写项目信息

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

5、填写项目路径

6、等待项目构建成功

项目结构分析:

通过上面步骤完成了基础项目的创建。就会自动生成以下文件。

1、程序的主启动类

2、一个 application.properties 配置文件

3、一个 测试类

4、一个 pom.xml

resources目录

  • static:存放静态资源,如图片、CSS、JavaScript等。

  • templates:存放web页面的模板文件。

  • application.properties/application.yml:用于存放程序的各种依赖模块的配置信息,比如服务端口、数据库连接配置等。

四、运行原理

1、pom.xml

1-1、父依赖

spring-boot-dependencies:核心依赖在父工程中。

在写或者引入一些SpringBoot依赖的时候,不需要指定版本,因为有这些版本仓库。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

键盘按住CTRL点击spring-boot-starter-parent,发现还有一个父依赖

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

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

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

1-2、启动器

spring-boot-starter

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

启动器:简单来说,就是SpringBoot的启动场景。

比如spring-boot-starter-web,会自动导入web环境所有的依赖。

SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 需要用什么功能就导入什么样的场景启动器即可 。

1-3、主程序

//@SpringBootApplication:标注这个类是一个springboot的应用
@SpringBootApplication
public class Springboot01Application {

    public static void main(String[] args) {
        //springboot项目代码必须放在放到SpringbootApplication类所在的同级目录或下级目录
        //将springboot应用启动
        SpringApplication.run(Springboot01Application.class, args);
    }

}

@SpringBootApplication注解

作用:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用。

进入该注解,看到还有其它注解:

  • @ComponentScan

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

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

  • @SpringBootConfiguration

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

    继续进去这个注解查看:

    • @Configuration说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件
      • @Component 说明启动类本身也是Spring中的一个组件而已,负责启动应用
  • @EnableAutoConfiguration开启自动配置功能

    进入该注解可以看到一下注解:

    • @AutoConfigurationPackage :自动配置包

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

        Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器

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

      AutoConfigurationImportSelector :自动配置导入选择器

      AutoConfigurationImportSelector.class

      1、这个类中有一个这样的方法

      // 获得候选的配置
      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
          //这里的getSpringFactoriesLoaderFactoryClass()方法
          //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
          List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
          Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
          return configurations;
      }
      

      2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法

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

      3、继续点击查看 loadSpringFactories 方法

      private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
          //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
          MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
          if (result != null) {
              return result;
          } else {
              try {
                  //去获取一个资源 "META-INF/spring.factories"
                  Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                  LinkedMultiValueMap result = new LinkedMultiValueMap();
      
                  //将读取到的资源遍历,封装成为一个Properties
                  while(urls.hasMoreElements()) {
                      URL url = (URL)urls.nextElement();
                      UrlResource resource = new UrlResource(url);
                      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                      Iterator var6 = properties.entrySet().iterator();
      
                      while(var6.hasNext()) {
                          Entry<?, ?> entry = (Entry)var6.next();
                          String factoryClassName = ((String)entry.getKey()).trim();
                          String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                          int var10 = var9.length;
      
                          for(int var11 = 0; var11 < var10; ++var11) {
                              String factoryName = var9[var11];
                              result.add(factoryClassName, factoryName.trim());
                          }
                      }
                  }
      
                  cache.put(classLoader, result);
                  return result;
              } catch (IOException var13) {
                  throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
              }
          }
      }
      

      Properties properties = PropertiesLoaderUtils.loadProperties(resource);所有资源加载到配置类中。

      4、发现一个多次出现的文件:spring.factories

      META-INF/spring.factories:自动配置的核心文件

      在这里插入图片描述

      在该文件自动配置类中随便点击一个看看,例如:WebMvcAutoConfiguration

      可以发现里面一个个的都是JavaConfig配置类,而且都注入了一些Bean。

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

在这里插入图片描述

思考:这么多自动配置为什么有些没有生效,需要导入对应的start才能有作用。

自动配置类中的核心注解:@ConditionalOnXXX => 如果这里面的条件都满足才会生效。

结论:

springboot所有自动配置都是在启动的时候扫描并加载,spring.factories所有的自动配置类都在这里面,但是不一定生效,要判断条件是否成立。只要导入了对应的start,就有对应的启动器了。有了启动器,自动装配就会生效,然后就配置成功。

  1. SpringBoot在启动的时候,从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值;
  2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
  3. 以前需要自动配置的东西,现在springboot帮助做了;
  4. 整合JavaEE,解决方案和自动配置都在spring-boot-autoconfigure-2.3.4.RELEASE.jar这个jar包中;
  5. 它会把所有需要导入的组件,以类名的方式返回 ,这些组件就会被添加到容器;
  6. 容器中也会存在非常多的xxxAutoConfiguration的文件(@Bean),就是这些类给容器中导入了这个场景需要的所有组件,并自动配置,@ConfigurationJavaConfig
  7. 有了自动配置类 , 免去了手动编写配置注入功能组件等的工作。

2、SpringApplication

//@SpringBootApplication:标注这个类是一个springboot的应用:启动类下的所有资源被导入
@SpringBootApplication
public class Springboot01Application {

    public static void main(String[] args) {
        //将springboot应用启动
        SpringApplication.run(Springboot01Application.class, args);
    }

}

2-1、SpringApplication

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

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

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

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

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

查看构造器:

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    // ......
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances();
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

2-2、run方法流程分析

SpringApplication.run分析

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

在这里插入图片描述

五、yaml配置注入

1、配置文件

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

  • application.properties

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

    • 语法结构 :key: value

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

如果application.propertiesapplication.yml同时存在,两个文件都有效,但是application.properties的优先级会比application.yml高。

2、yaml概述

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

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

3、yaml基础语法

说明:语法要求严格!

1、空格不能省略

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

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

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

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

k: v

注意:

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

    比如 :name: “kuang \n shen” 输出 :kuang 换行 shen

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

    比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

对象、Map(键值对)

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

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

student:
    name: 张三
    age: 3

行内写法:

student: {name: 张三,age: 3}

数组( List、set )

- 值表示数组中的一个元素。例如:

pets:
 - cat
 - dog
 - pig

行内写法:

pets: [cat,dog,pig]

修改SpringBoot的默认端口号

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

server:
  port: 8081

4、注入配置文件

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

4-1、yaml注入配置文件

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

2、编写一个实体类 Dog;

package com.lskj.pojo;

import org.springframework.stereotype.Component;

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

3、使用注解@Value给bean注入属性值的 ;

@Component  //注册bean到容器中
public class Dog {
    @Value("小黑")
    private String name;
    @Value("2")
    private Integer age;
    //...有参、无参构造,get、set、toString()方法
}

4、在SpringBoot的测试类下注入Dog并输出一下;

@SpringBootTest
class Springboot01ApplicationTests {

    @Autowired
    private Dog dog;

    @Test
    void contextLoads() {
        System.out.println(dog);
    }

}

5、再编写一个复杂一点的实体类:Person 类

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

6、使用yaml配置的方式进行注入,编写一个yaml配置;

person:
  name: lskj
  age: 18
  happy: true
  birth: 2020/09/21
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - play
    - music
  dog:
    name: 小黑
    age: 2

7、注入到Person类中;

@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
}

@ConfigurationProperties作用:将配置文件中配置的每一个属性的值,映射到这个组件中;告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定。

参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应。

只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能。

8、IDEA 提示,springboot配置注解处理器没有找到。可以查看文档,找到一个依赖!(打开网页后,版本改成2.1.9.RELEASE,依赖在最后一部分开头哪里)

在这里插入图片描述

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<!--解决使用@ConfigurationProperties注解出现警告问题-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

重启过后,可能上面的依赖会报红,刷新一下Maven。

9、在测试类中测试一下

@SpringBootTest
class Springboot01ApplicationTests {

    @Autowired
    //private Dog dog;
    private Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }
}

4-2、加载指定的配置文件

**@PropertySource **:加载指定的配置文件;

@configurationProperties:默认从全局配置文件中获取值;

1、在resources目录下新建一个person.properties文件。

name=lskj

2、在Person中指定加载person.properties文件。

@Component
//@ConfigurationProperties(prefix = "person")
//加载指定的配置文件
@PropertySource(value = "classpath:person.properties")
public class Person {
    //SPEL表达式取出配置文件的值
    @Value("${name}")
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
    //...
}

properties配置文件在写中文的时候,会有乱码 , 需要去IDEA中设置编码格式为UTF-8

3、在测试类进行测试。

5、配置文件占位符

person:
  name: lskj${random.uuid} #随机uuid
  age: ${random.int} #随机int
  happy: true
  birth: 2020/09/21
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - play
    - music
  dog:
    name: ${person.hello:other}_小黑
    age: 2

需注释掉加载指定文件中的@PropertySource@Value,去掉@ConfigurationProperties的注释。

在测试类中进行测试。

6、对比

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

@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SpEL不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持

1、@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加

2、松散绑定:这个什么意思呢? 比如yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。

3、JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性

4、复杂类型封装,yml中可以封装对象 , 使用value就不支持

结论:

  • 配置yml和配置properties都可以获取到值 , 强烈推荐 yml;

  • 如果在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value

  • 如果说编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties

六、JSR303数据校验及多环境切换

1、JSR303数据校验

Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。

写个注解让name只支持Email格式

@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated  //数据校验
public class Person {

    @Email(message="邮箱格式错误") //name必须是邮箱格式
    private String name;
}

@Email注解报红需导入依赖。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

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

使用数据校验,可以保证数据的正确性;

常见参数

@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;

空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.
    
Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  
    
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) string is between min and max included.

日期检查
@Past       验证 DateCalendar 对象是否在当前时间之前  
@Future     验证 DateCalendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

2、多环境切换

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

2-1、多配置文件

在实际开发的过程中,我们的项目会经历很多的阶段(开发->测试->上线),每个阶段的配置也会不同,例如:端口、上下文根、数据库等,那么这个时候为了方便在不同的环境之间切换,SpringBoot提供了多环境配置。

在主配置文件编写时,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本。

  • application-test.properties 代表测试环境配置
  • application-dev.properties 代表开发环境配置
  • application-ready.properties代表准生产环境配置
  • application-product.properties代表生产环境配置

但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件

需要通过一个配置来选择需要激活的环境(在总配置文件application.properties进行环境的激活):

#激活开发环境
#spring.profiles.active=dev

#激活测试环境
#spring.profiles.active=test

#激活生产环境
spring.profiles.active=product

2-2、yaml的多文档块

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

server:
  port: 8081
#选择要激活那个环境块
spring:
  profiles:
    active: prod

---
server:
  port: 8083
spring:
  profiles: dev #配置环境的名称


---

server:
  port: 8084
spring:
  profiles: prod  #配置环境的名称

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

2-3、配置文件加载位置

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

官方外部配置文件说明参考文档:

Config locations are searched in reverse order. By default, the configured locations are classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/. The resulting search order is the following:

file:./config/

file:./config/*/

file:./

classpath:/config/

classpath:/

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

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

优先级由高到底,高优先级的配置会覆盖低优先级的配置;

SpringBoot会从这四个位置全部加载主配置文件;互补配置;

在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题:

#配置项目的访问路径
server.servlet.context-path=/kuang

指定位置加载配置文件,还可以通过spring.config.location来改变默认的配置文件位置。

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高。

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

七、自动配置原理

SpringBoot官方文档

1、分析自动配置原理

以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理。

//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration 

/*
    启动指定类的ConfigurationProperties功能;
    进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
    并把HttpProperties加入到ioc容器中
*/
@EnableConfigurationProperties({HttpProperties.class}) 

/*
    Spring底层@Conditional注解
    根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
    这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
*/
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})

/*
    判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
    如果不存在,判断也是成立的
    即使我们配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的。
*/
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)

public class HttpEncodingAutoConfiguration {
    //他已经和SpringBoot的配置文件映射了
    private final Encoding properties;
    //只有一个有参构造器的情况下,参数的值就会从容器中拿
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }
    
    //给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @Bean
    @ConditionalOnMissingBean //判断容器没有这个组件?
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
    //。。。。。。。
}

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

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http") 
public class HttpProperties {
    // .....
    /*
    	去配置文件里面试试前缀,会有提示。例如:spring.http.encoding.
    	这就是自动装配的原理。
    */
}

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

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

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

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

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

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

2、@Conditional

自动配置类必须在一定的条件下才能生效。

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

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

在这里插入图片描述

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

怎么知道哪些自动配置类生效?

可以通过启用 debug=true属性。来让控制台打印自动配置报告。

这样,就可以很方便的知道,哪些自动配置类生效。

Debug,默认是false
True,表示开启Spring boot的debug模式。

#开启SpringBoot debug模式
debug=true
server.port=8081

spring.http.encoding.enabled=true
spring.http.encoding.charset=utf-8
spring.http.encoding.force=true

打印信息

Positive matches
匹配成功
表示,启用的自动配置类

Negative matches
没有匹配成功
表示,没有启用的自动配置类

Positive matches:(自动配置类启用的:正匹配)

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

Unconditional classes: (没有条件的类)

八、自定义starter

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

官方命名:

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

自定义命名:

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

1、编写启动器

1、新建一个空项目spring-boot-starter-diy。

2、新建一个普通Maven模块test-spring-boot-starter。

3、新建一个SpringBoot模块test-spring-boot-starter-autoconfigure。

4、在starter(test-spring-boot-starter)中导入autoconfigure依赖。

<!-- 启动器 -->
<dependencies>
    <!--  引入自动配置模块 -->
    <dependency>
        <groupId>com.lskj</groupId>
        <artifactId>test-spring-boot-starter-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

5、将atuoconfigure项目下多余的文件删除。

6、编写一个service。

package com.lskj;

public class TestService {
    TestProperties testProperties;

    public TestProperties getTestProperties() {
        return testProperties;
    }

    public void setTestProperties(TestProperties testProperties) {
        this.testProperties = testProperties;
    }

    public String test(String name){
        return testProperties.getPrefix() + name + testProperties.getSuffix();
    }
}

7、编写配置类。

package com.lskj;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "test.test")
public class TestProperties {
    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}

8、编写自动配置类并注入bean。

package com.lskj;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnWebApplication  //wev应用生效
@EnableConfigurationProperties(TestProperties.class)
public class TestServiceAutoConfiguration {

    @Autowired
    TestProperties testProperties;

    @Bean
    public TestService testService(){
        TestService service = new TestService();
        service.setTestProperties(testProperties);
        return service;
    }
}

9、在resources下编写META-INF/spring-factories。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lskj.TestServiceAutoConfiguration

10、编写完成后,将其安装到maven仓库中。

  • 点击右侧边栏的 Maven
  • 点击test-spring-boot-starter和autoconfigure下的Lifecycle/install

新建项目测试自定义的启动器

1、新建一个SpringBoot项目starter-test(勾选上web模块)。

2、导入自定义的启动器。

<dependency>
    <groupId>com.lskj</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3、编写controller,进行测试自定义的接口。

package com.lskj.controller;

import com.lskj.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @Autowired
    TestService testService;

    @RequestMapping("/test")
    public String test(){
        return testService.test("DiyStarter Test!");
    }
}

4、编写配置文件application.properties。

test.test.prefix="This is Perfix!"
test.test.suffix="This is SUffix!"

5、启动项目进行测试。

访问http://localhost:8080/test

"This is Perfix!"DiyStarter Test!"This is SUffix!"

九、Web开发静态资源处理

1、静态资源处理

静态资源映射规则

SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类中。

其中的addResourceHandlers方法:添加资源处理。

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

所有的 /webjars/** , 都需要去classpath:/META-INF/resources/webjars/找对应的资源。

webjars:本质就是以jar包的方式引入静态资源 。

使用SpringBoot需要使用Webjars,如使用jQuery,只要引入jQuery对应版本的pom依赖即可。

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

导入的jQuery.js文件在Maven:org.webjars:jquery:3.5.1/jquery-3.5.1/META-INF/resources/webjars/jquery/3.5.1下。

如需访问,只要是静态资源,SpringBoot就会去对应的路径寻找资源,例如访问jquery.js文件。

http://localhost:8080/webjars/jquery/3.5.1/jquery.js

第二种静态资源映射规则

导入自己需要的静态资源,应去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类。

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

ResourceProperties 可以设置和我们静态资源有关的参数。

上述代码中的数组指向了它会去寻找资源的文件夹。

故以下四个目录存放的静态资源可以被识别:

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

在resources根目录下新建对应的文件夹,都可以存放需要使用的静态文件。

比如访问http://localhost:8080/test.js,它就可以去以上文件夹中寻找对应的静态资源文件。

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

自定义静态资源路径

一旦自定义了静态文件夹的路径,原来的自动配置都会失效。

application.properties

spring.resources.static-locations=classpath:/test/,classpath:/lskj/

2、首页处理

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService,
                                                           ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}


private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    // ::是java8 中新引入的运算符
    // Class::function的时候function是属于Class的,应该是静态方法。
    // this::function的funtion是属于这个对象的。
    // 简而言之,就是一种语法糖而已,是一种简写
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。

比如访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html

十、Thymeleaf模板引擎

SpringBoot项目默认推荐使用的前端引擎是thymeleaf。

SpringBoot使用JSP框架

如果需要使用jsp,需要引入springboot内嵌Tomcat对jsp的解析依赖。使用springboot集成jsp,需手动指定jsp最后编译的路径,而且springboot集成jsp编译jsp的路径是springboot规定好的位置META-INF/resources

pom.xml

<dependencies>
	<!--引入Spring Boot内嵌的Tomcat对JSP的解析包,不加解析不了jsp页面-->
    <!--如果只是使用JSP页面,可以只添加该依赖-->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>

    <!--如果要使用servlet必须添加该以下两个依赖-->
    <!-- servlet依赖的jar包-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>

    <!-- jsp依赖jar包-->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.1</version>
    </dependency>

    <!--如果使用JSTL必须添加该依赖-->
    <!--jstl标签依赖的jar包start-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>
</dependencies>

<build>
	<resources>
    	<resource>
        	<!--源文件夹-->
            <!--webapp文件夹是自定义创建来存放jsp,创建后打开项目结构-Modules-项目-Web-通过+号进行添加Web Resource Directory,并点击Create Artifact-应用-->
            <directory>src/main/webapp</directory>
            <!--指定编译到META—INF/resources-->
            <targetPath>META-INF/resources</targetPath>
            <!--指定源文件夹中的哪个资源需要进行编译-->
            <includes>
            	<include>*.*</include>
            </includes>
        </resource>
    </resources>
</build>

application.properties配置文件中配置视图解析器。

#配置视图解析器
spring.mvc.view.perfix=/
spring.mvc.view.suffix=.jsp

1、认识Thymeleaf

Thymeleaf是一个流行的模板引擎,该模板引擎采用Java语言开发。

模板引擎是一个技术名词,是跨领域平台的概念。模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想

Thymeleaf 对网络环境不存在严格的要求,既能用于Web环境下,也能用于非Web环境下。在非Web环境下,他能直接显示模板上的静态数据;在Web环境下,它能像Jsp一样从后台接收数据并替换掉模板上的静态数据。它是基于HTML的,以HTML标签为载体,Thymeleaf要寄托在HTML标签下实现。

Spring Boot 集成了Thymeleaf模板技术,并且Spring Boot官方也推荐使用Thymeleaf来替代JSP技术,Thymeleaf是另外的一种模板技术,它本身并不属于Spring Boot,Spring Boot只是很好地集成这种模板技术,作为前端页面的数据展示,在过去的Java Web开发中,我们往往会选择使用Jsp去完成页面的动态渲染,但是jsp需要翻译编译运行,效率低。

Thymeleaf的官方网站:http://www.thymeleaf.org

Thymeleaf官方手册:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

2、引入Thymeleaf

Thymeleaf ->Github:https://github.com/thymeleaf/thymeleaf

Spring官方文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

Spring官方文档中找到对应的版本,在Starters下找到spring-boot-starter-thymeleaf依赖后,pom.xml文件中引入依赖。

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

在application.properties中设置thymeleaf参数。

#设置thymeleaf页面缓存失效
spring.thymeleaf.cache=false

#thymeleaf模版前缀,默认值,可选项
spring.thymeleaf.prefix=classpath:/templates/
#thymeleaf模版后缀,默认值,可选项
spring.thymeleaf.suffix=.html

3、Thymeleaf分析

ThymeleafProperties自动配置类

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
}

上述代码中可看到默认的前缀和后缀。而我们只需要把html页面放在类路径下的templates目录下,thymeleaf就可以自动渲染了。

总结:使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可。

4、测试

1.编写一个TestController。

package com.lskj.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

//在templates目录下的页面,只能通过controller来跳转
//这个需要模板引擎的支持,thymeleaf
@Controller
public class TestController {
    @GetMapping("/test")
    public String index(){
        return "test";
    }
}

2.在templates目录下编写一个测试页面test.html。

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

    <h1>Test</h1>

</body>
</html>

3.启动项目,http://localhost:8080/test请求测试。

5、Thymeleaf语法

1、使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
</html>

所有的html元素都可以被thymeleaf替换接管(th:元素名)

2、修改测试请求,增加数据传输。

@GetMapping("/test")
public String index(Model model){
    model.addAttribute("text","test text!");
    return "test";
}

3、test.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>

    <h1 th:text="${text}"></h1>

</body>
</html>

4、启动,访问测试。

thymeleaf常见属性

thymeleaf大部分属性和html的一样,只不过是其前面多了一个th:前缀。

使用到的静态资源存放在static文件夹下。

<script type="text/javascript" th:src="@{/js/jquery-1.7.2.min.js}"></script>

<img th:src="@{/image/test.jpg}">

<span th:id="${test}"></span>

<input th:type="text" th:id="test" th:name="test"/>

<input type="hidden" id="id" name="id" th:value="${id}"/>
<!--
	th:atrr属性也是给HTML中某元素的某属性赋值
	使用该属性的好处是可以给html中没有定义的属性动态的赋值
-->
<input type="hidden" id="id" name="id" th:attr="value=${id}"/>
<!--thymeleaf没有对应的th标签,所以${user.id}不能被识别-->
<span zhangsan=${user.id}></span>
<!--通过th:attr对自定义的属性赋值-->
<span th:attr="zhangsan=${user.id}"></span>

<!--
	值不是从后台获取的,可以不使用thymeleaf属性
-->
<form id="register" th:action="@{'/register"th:method="post">
    用户姓名:<input type="text" id="username" th:name="username"/><br/>
    用户密码:<input type="text" th:id="password" th:name="password"/><br/>
    <input type="submit" value="submit"/>
</form>

<button th:onclick="test()"></button>

<!--
	th:object属性用于数据对象绑定,通常用于选择变量表达式(星号表达式)
-->
<div th:object="${user}">
    <sapn th:text="*{id}"></sapn>
    <span th:text="*{name}"></span>
</div>

<!--
	th:style设置样式
-->
<span th:style="'color:red'">一个红色的span标签</span>

<!--
	th:each既可以循环遍历集合,也可以循环遍历数组和Map

	user:当前循环的对象变量名称
	userStat:当前循环对象状态的变量,通过该变量可以获取以下信息
		index: 当前迭代对象的index(从0开始计算)
        count: 当前迭代对象的个数(从1开始计算)这两个用的较多
        size: 被迭代对象的大小
        current: 当前迭代变量
        even/odd: 布尔值,当前循环是否是偶数/奇数(从0开始计算)
        first: 布尔值,当前循环是否是第一个
        last: 布尔值,当前循环是否是最后一个
	循环体信息xxxStat也可以不定义,则默认采用迭代变量加上Stat后缀
	${userList}:当前循环的集合
-->
<div th:each="user,userStat:${userList}">
    <span th:text="userStat.index"></span>
    <span th:text="${user.id}"></span>
    <span th:text="${user.name}"></span>
</div>

<div th:each="map:${userMap}">
    <span th:text="${map.key}"></span>
    <span th:text="${map.value}"></span>
    <sapn th:text="${map.value.id}"></sapn>
    <span th:text="${map.value.name}"></span>
</div>

<div th:if="${sex eq 1}"></div>
<div th:if="${sex eq 0}"></div>

<!--
	th:unless:与th:if相反,即条件判断取反
-->
<div th:unless="${sex ne 1}"></div>

<!--
	一旦某个case判断值为true,剩余的case则都当做false,“*”表示默认的case,前面的case都不匹配时候,执行默认的case
-->
<div th:switch="${testType}">
    <span th:case="0">0</span>
    <span th:case="1">1</span>
    <span th:case="*">2</span>
</div>

<!--
	th:inline 有三个取值类型 (text, javascript 和 none)

		内敛文本=> th:inline="text" 可以让Thymeleaf表达式不依赖于html标签,直接使用内敛表达式[[表达式]]即可获取动态数据,要求在父级标签上加th:inline = “text”属性

		内敛脚本=> th:inline="javascript" 在js代码中获取后台的动态数据
-->
标准变量表达式用户数据的展示:<br>
<span th:text="${user.id}"></span>
<span th:text="${user.name}"></span>
<br>
<!--以上代码可以使用内敛文本代替-->
内敛表达式 用户数据的展示:<br>
<span th:inline="text">
    [[${user.id}]]
    [[${user.name}]]
</span>
<br>

<button type="button" onclick="test()">点击弹窗</button>
<script type="text/javascript" th:inline="javascript">
	function test(){
        alert("欢迎"+[[${user.name}]]+"用户!")
    }
</script>
功能属性
片段包含th:insert
th:replace
遍历th:each
条件判断th:if
th:unless
th:swith
th:case
声明变量th:object
th:with
任意属性修改th:attr
th:attrprepend
th:attrappend
修改制定属性默认值th:value
th:href
th:src
修改标签体内容th:text(转义特殊字符)
th:utext(不转义特殊字符)
声明片段th:fragment
片段删除th:remove

表达式

标准变量表达式${...},标准变量表达式用于访问容器(tomcat)上下文环境中的变量,功能和EL中的 ${} 相同。Thymeleaf 中的变量表达式使用 ${变量名} 的方式获取Controller中model其中的数据。

选择变量表达式*{...},也叫星号变量表达式,使用th:object属性来绑定对象,不推荐使用。选择变量表达式*{...}是另一种类似于标准变量表达式${...}表示变量的方法;选择变量表达式在执行时是在选择的对象上求解,而${...}是在上下文的变量Model上求解,这种写法比标准变量表达式繁琐。

URL表达式@{...},主要用于链接、地址的展示。

<span th:text="${data}"></span>

<!--
	th:object属性用于数据对象绑定,通常用于选择变量表达式(星号表达式)
-->
<div th:object="${user}">
    <sapn th:text="*{id}"></sapn>
    <span th:text="*{name}"></span>
</div>

<div>
    <sapn th:text="*{user.id}"></sapn>
    <span th:text="*{user.name}"></span>
</div>

<a th:href="@{http://localhost:8080/test}"></a>

<a th:href="@{'/test?username='+${username}+'&password='+${password}}"></a>

<a th:href="@{/test(username=${username},password=${password})}"></a>

<a th:href="@{'/test/'+${id}+'/'+${username}}">请求路径为RESTful风格</a>
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象:#18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.

    3)、内置的一些工具对象:
      #execInfo : information about the template being processed.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any).
      #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
==================================================================================

  Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
  Message Expressions: #{...}:获取国际化内容
  Link URL Expressions: @{...}:定义URL;
  Fragment Expressions: ~{...}:片段引用表达式

Literals(字面量)
      Text literals: 'one text' , 'Another one!' ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…
      
Text operations:(文本操作)
    String concatenation: +
    Literal substitutions: |The name is ${name}|
    
Arithmetic operations:(数学运算)
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
    
Boolean operations:(布尔运算)
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
    
Comparisons and equality:(比较运算)
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
    
Conditional operators:条件运算(三元运算符)
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
    
Special tokens:
    No-Operation: _

字面量

字面量:对应数据类型的合法取值,可以在html页面直接使用,不需要后台传递。

1、文本字面量

用单引号包围的字符串为文本字面量。

<a th:href="@{'/user/'+${user.id}}">查看用户</a>

<span th:text="test"></span>

2、数字字面量

现在是<span th:text="2021"></span>

1+1=<span th:text="1+1"></span>

3、boolean字面量

<sapn th:if="${isOk == true}">显示OK</sapn>

4、null字面量

<span th:if="${isOk == null}">isOk为空</span>
<span th:if="${isOk != null}">isOk不为空</span>

字符串拼接

thymeleaf字符串拼接

<!--一种是字面量使用加号拼接-->
<span th:text="'当前是第'+${currentPage}+'页 ,共'+${total}+'页'"></span>

<!--另一种更优雅的方式,使用“|”减少了字符串的拼接的加号-->
<span th:text="|当前是第${currentPage}页,共${total}页|"></span>

运算符

算术运算:+ ,-, * , / , %

关系比较: > , < , >= , <= ( gt , lt , ge , le )

相等判断:== ,!=( eq , ne )

<span th:text="${sex eq 1 ? '男' : '女'}"></span>
<span th:text="${sex == 1 ? '男' : '女'}"></span>

表达式基本对象与功能对象

模板引擎提供了一组内置的对象,这些内置的对象可以直接在模板中使用,这些对象由#号开始引用、比较常用的内置对象。

#request,相当于httpServletRequest对象,这是3.x版本,若是2.x版本使用#httpServletRequest

  • 在页面获取应用的上下文根,一般在js中请求路径中加上,避免404

    ${#request.getContextPath()}
    
  • 如果后台将数据传到request中,可以通过该方式在页面上获取

    ${#request.getAttribute("xxx")}
    
<script type="text/javascript" th:inline="javascript">
    var contextPath =[[${#request.getContextPath()}]];
    var url = contextPath + "/user/userInfo";
    alert(url);
</script>

#session,相当于HttpSession对象,这是3.x版本,若是2.x版本使用#httpSession

  • 在后台ThymeleafController中的方法中向session中放数据
session.setAttribute("site","http://www.baidu.com");

requtst.getSession().setAttribute("site","http://www.baidu.com");
  • 在前端页面中从session中取数据
<span th:text="${#session.getArrtibute('site')}"></span>
<!--从session中获取值-->
<sapn th:text="${#session.getAttribute('data')}"></sapn>
<sapn th:text="${#httpSession.getAttribute('data')}"></sapn>
<span th:text="${session.data}"></span>

<script type="text/javascript" th:inline="javascript">
	//获取协议名称
    var scheme = [[${#{request.getScheme}}]];
    //获取服务器名称
    var serverName = [[${#{request.getServerName()}}]];
    //获取服务器端口
    var serverPort = [[${#{request.getServerPort()}}]];
    //获取上下文根
    var contextPath = [[${#request.getContextPath()}]];
                   
    var requestURL = [[${#httpServletRequest.requestURL}]];
    //请求的参数
    var queryString == [[${#httpServletRequest.queryString}]];
                   
    var requestAddress = requestURL + "?" + queryString;               
</script>

Thymeleaf表达式功能对象

模板引擎提供的一组功能性内置对象,可以在模板中直接使用这些对象提供的功能方法;

工作中常使用的数据类型,如集合,时间,数值,可以使用Thymeleaf的提供的功能性对象来处理它们;

内置功能对象前都需要加#号,内置对象一般都以s结尾;

官方手册:http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

  • #dates: java.util.Date对象的实用方法;

    <span th:text="${time}"></span>
    <span th:text="${#dates.format(time,'yyyy-MM-dd HH:mm:ss')}"></span>
    
  • #calendars: 和dates类似, 但是 java.util.Calendar 对象;

  • #numbers: 格式化数字对象的实用方法;

  • #strings: 字符串对象的实用方法: contains, startsWith, prepending/appending等;

  • #objects: 对objects操作的实用方法;

  • #bools: 对布尔值求值的实用方法;

  • #arrays: 数组的实用方法;

  • #lists: list的实用方法,比如#sets: set的实用方法;#maps: map的实用方法;#aggregates: 对数组或集合创建聚合的实用方法。

十一、MVC自动配置原理

官方文档地址:https://docs.spring.io/spring-boot/docs/2.3.4.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

11-1、内容协商视图解析器

ContentNegotiatingViewResolver 内容协商视图解析器

自动配置了ViewResolver,就是SpringMVC的视图解析器,即根据方法的返回值取得视图对象(view),然后由视图对象决定如何渲染(转发,重定向)。

源码:WebMvcAutoConfiguration->ContentNegotiatingViewResolver

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver uses all the other view resolvers to locate---->ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
    // a view so it should have a high precedence
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}

点进ContentNegotiatingViewResolver类,找到对应的解析视图代码。

@Nullable //注解说明:@Nullable 即参数可为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
    if (requestedMediaTypes != null) {
        //获取候选的试图对象
        List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
        //选择一个最适合的视图对象,然后把这个对象返回
        View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }
    //...
}

getCandidateViews方法中是把所有的视图解析器拿来进行while循环,挨个解析!

Iterator var5 = this.viewResolvers.iterator();

ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的

给容器中添加一个视图解析器,这个类就会自动的将它组合进来。

1、写一个视图解析器。

package com.lskj.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Locale;

//全面扩展 SpringMVC  DispatchServlet
@Configuration
//再加上@EnableWebMvc该注释,全面接管SpringMVC
public class MvcConfig implements WebMvcConfigurer {
    //ViewResolver实现了视图解析器接口的类,可以把它看做视图解析器
    @Bean
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }
    //自定义一个视图解析器
    //静态内部类,视图解析器需要实现ViewResolver接口
    public static class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }
}

2、观察自定义的视图解析器是否起作用。

给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中。

3、启动项目,访问http://localhost:8080,看Debug信息。

找到this->(viewResolvers)视图解析器,可看到自定义的视图解析器。

综上,如果想要使用自定义的东西,只需要给容器中添加这个组件即可。

11-2、转换器和格式化器

格式转化器(在WebMvcAutoConfiguration类中):

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    Format format = this.mvcProperties.getFormat();
    WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()                                                               .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
    addFormatters(conversionService);
    return conversionService;
}

点进getDateTime()

/**
		 * Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
		 */
private String dateTime;

public String getDateTime() {
    return this.dateTime;
}

在Properites文件中,刻进行自动配置。

如果配置了自定义格式化方式,就会注册到Bean中生效,可以在配置文件中配置日期格式化的规则。

#自定义的配置日期格式化
spring.mvc.format.date=

11-3、修改SpringBoot的默认配置

SpringBoot在自动配置很多组件时,先看容器中是否有用户自己配置的(如果用户自己配置@bean),若存在则使用用户配置的,若不存在就使用自动配置的。

如果有些组件可以存在多个,例如视图解析器,就将用户配置的与默认的组合起来。

扩展使用SpringMVC

编写一个@Configuration注解类,并且类型为WebMvcConfigurer(不标注@EnableWebMvc注解)

package com.lskj.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    //视图跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/lskj").setViewName("test");
    }
}

启动测试,访问http://localhost:8080/lskj

要扩展SpringMVC,官方推荐这么去使用,既保SpringBoot留所有的自动配置,也能用用户自己扩展的配置!

全面接管SpringMVC

全面接管即SpringBoot对SpringMVC的自动配置不需要了,所有都是用户自己去配置,只需在配置类加上@EnableWebMvc

原理分析:

1、点进注解@EnableWebMvc。

@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

2、点进DelegatingWebMvcConfiguration类查看。

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  // ......
}

DelegatingWebMvcConfiguration:从容器中获取所有的WebMvcConfig。

它继承了一个父类 WebMvcConfigurationSupport。

WebMvc自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 这个注解的意思就是:容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
}

@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了。

而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能

十二、页面国际化

部分网站涉及中英文等多语言的切换,这时需要学习国际化。

1、首先在IDEA中统一设置properties的编码问题

setting->file encodings。

2、编写国际化配置文件,抽取页面需要显示的国际化页面消息。

12-1、配置文件编写

1、在resources资源文件下新建目录i18n存放国际化配置文件。

2、新建文件login.properties、login_zh_CN.properties。

此时IDEA会自动识别到要做国际化操作,刚创建的两个文件变成存放于Resource Bundle 'login’目录下。

3、可以在Resource Bundle 'login’目录上新建一个文件(Add Property Files to Resource Bundle)。

在这里插入图片描述

在这里插入图片描述

点击”+“,弹出界面,添加英文

在这里插入图片描述

点击OK即可。

4、进入国际化配置文件,点击左下角的Resource Bundle。

在这里插入图片描述

点击后,该视图下点击+号可以直接添加属性。

例如,添加一个login.tip,在右边有三个文件框可以输入。添加内容即可。

login.properties:默认

login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名

英文:

login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please Sign in
login.username=Username

中文:

login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名

怎样使配置文件生效?

SpringBoot对国际化的自动配置。这里涉及到一个类:MessageSourceAutoConfiguration

在该类里有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource。

// 获取 properties 传递过来的值进行判断
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    if (StringUtils.hasText(properties.getBasename())) {
        // 设置国际化文件的基础名(去掉语言国家代码的)
        messageSource.setBasenames(
            StringUtils.commaDelimitedListToStringArray(
                                       StringUtils.trimAllWhitespace(properties.getBasename())));
    }
    if (properties.getEncoding() != null) {
        messageSource.setDefaultEncoding(properties.getEncoding().name());
    }
    messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
    Duration cacheDuration = properties.getCacheDuration();
    if (cacheDuration != null) {
        messageSource.setCacheMillis(cacheDuration.toMillis());
    }
    messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
    messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
    return messageSource;
}

因为配置文件存放在i18n目录下,所以需要在application.properties中配置这个messages的路径。

spring.messages.basename=i18n.login

12-2、配置页面国际化值

去页面获取国际化的值,查看Thymeleaf的文档,message取值操作为:#{...}

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<meta name="description" content="">
		<meta name="author" content="">
		<title>Signin Template for Bootstrap</title>
		<!-- Bootstrap core CSS -->
		<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
		<!-- Custom styles for this template -->
		<link th:href="@{/css/signin.css}" rel="stylesheet">
	</head>

	<body class="text-center">
		<form class="form-signin" action="dashboard.html">
			<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
			<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
			<input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
			<input type="password" class="form-control" th:placeholder="#{login.password}" required="">
			<div class="checkbox mb-3">
				<label>
          <input type="checkbox" value="remember-me" th:text="#{login.remember}">
        </label>
			</div>
			<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
			<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
			<a class="btn btn-sm">中文</a>
			<a class="btn btn-sm">English</a>
		</form>

	</body>

</html>

12-3、配置国际化解析

在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器。

WebMvc自动文件中,SpringBoot默认配置:

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
    // 容器中没有就自己配,有的话就用用户配置的
    if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    }
    // 接收头国际化分解
    AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
    localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
    return localeResolver;
}

AcceptHeaderLocaleResolver 这个类中有一个方法:

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // 默认的就是根据请求头带来的区域信息获取Locale进行国际化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    } else {
        Locale requestLocale = request.getLocale();
        List<Locale> supportedLocales = this.getSupportedLocales();
        if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
            Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
            if (supportedLocale != null) {
                return supportedLocale;
            } else {
                return defaultLocale != null ? defaultLocale : requestLocale;
            }
        } else {
            return requestLocale;
        }
    }
}

若想点击中英文字样链接使国际化资源生效,就需要使我们自己的Locale生效。

自定义LocaleResolver,在链接上携带区域信息。

修改一下页面跳转链接:

<!-- 这里传入参数不需要使用 ?使用 (key=value)-->
<a class="btn btn-sm" th:href="@{/index.html(language=zh_CN)}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(language=en_US)}">English</a>

写一个处理请求的类:

package com.lskj.config;

import org.springframework.web.servlet.LocaleResolver;
import org.thymeleaf.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocaleResolver implements LocaleResolver {
    //解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        //获取请求中的语言参数
        String language = httpServletRequest.getParameter("language");

        Locale locale = Locale.getDefault();  //如果没有就使用默认的
        //如果请求链接携带了国际化的参数
        if(!StringUtils.isEmpty(language)){
            //zh_CN  en_US
            String[] split = language.split("_");
            //国家  地区
            locale = new Locale(split[0],split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

为了使区域化信息能够生效,需要再配置一下。在MvcConofig下添加bean。

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

重启项目,访问测试即可。

首页配置:所有页面的静态资源都要使用thymeleaf接管。url使用@{}
页面国际化:
	1、需要配置i18n文件
	2、如果需要在项目中进行按钮自动切换,需要自定义一个逐渐LocaleResolver
	3、将写的组件配置到Spring容器@Bean

十三、整合JDBC

13-1、SpringData

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

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

Sping Data 官网:https://spring.io/projects/spring-data

数据库相关的启动器官方文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

13-2、JDBC

创建测试项目测试数据源

1、新建一个项目,引入相应的模块以及基础模块。

Web->Spring Web

SQL->JDBC API 、MySQL Driver

2、编写yml配置文件连接数据库。

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver

3、测试类中测试一下。(配置完后,SpringBoot默认进行了自动配置)

package com.lskj;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class Springboot04DataApplicationTests {
    //DI注入数据源
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //查看默认的数据源 class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

        //获得数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        //关闭
        connection.close();
    }

}

application.yml配置文件中,CTRL+鼠标点击datasource下的属性,可以查看到可配置的属性。

全局搜索DataSourceAutoConfiguration,在DataSourceAutoConfiguration文件中可查看数据源所有的自动配置。

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
         DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
         DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {

}

这里导入的类都在 DataSourceConfiguration 配置类下,Spring Boot 2.3.4 默认使用HikariDataSource 数据源。而Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;

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

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

xxxTemplate:SpringBoot已经配置好模板bean,拿来即用

JDBCTemplate

1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;

2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。

3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用

5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

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

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

测试

编写一个Controller,注入JDBCTemplate。

package com.lskj.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class JDBCController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询数据库的所有信息
    //没有实体类,数据库中的东西,通过万能的 Map 获取
    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
        return list_maps;
    }

    @GetMapping("/addUser")
    public String addUser(){
        String sql = "insert into mybatis.user(id,name,pwd) values(5,'test','123456')";
        jdbcTemplate.update(sql);
        return "insert success!";
    }

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id){
        String sql = "update mybatis.user set name=?,pwd=? where id="+id;

        //封装
        Object[] objects = new Object[2];
        objects[0] = "test-02";
        objects[1] = "654321";

        jdbcTemplate.update(sql,objects);
        return "update success!";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id){
        String sql = "delete from mybatis.user where id=?";
        jdbcTemplate.update(sql,id);
        return "delete success!";
    }
}

运行测试时,会出现以下的报红,但不影响测试。

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'.The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

问题原因:com.mysql.jdbc.Driver已经被弃用了。新的驱动程序类是com.mysql.cj.jdbc.Driver。驱动程序是通过SPI自动注册的,手动加载驱动程序类通常是不必要的。
**解决方案:**将数据配置文件里com.mysql.jdbc.Driver修改为com.mysql.cj.jdbc.Driver

十四、整合Druid

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

14-1、Druid

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

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

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

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

14-2、配置数据源

1、添加Druid数据源依赖。

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.1</version>
</dependency>

2、切换数据源。

Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type指定数据源。

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

3、在测试类总注入DataSource,测试获取。

@SpringBootTest
class Springboot04DataApplicationTests {
    //DI注入数据源
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //查看默认的数据源 class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

        //获得数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        //关闭
        connection.close();
    }

}

4、切换成功,可设置数据源连接初始化大小、最大连接数、等待时间、最小连接数等设置项。

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

5、导入log4j依赖。

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

6、为DruidDataSource绑定全局配置文件中的参数,在添加到容器中,而不再使用SpringBoot自动生成的。需要自己添加DruidDataSource组件到容器中,并绑定属性。

package com.lskj.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class DruidConfig {
    /*
       将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
       绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
       @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
       前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }
}

7、测试类中进行测试是否成功。

package com.lskj;

import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class Springboot04DataApplicationTests {
    //DI注入数据源
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //查看默认的数据源 class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

        //获得数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        DruidDataSource druidDataSource = (DruidDataSource) dataSource;
        System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
        System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());

        //关闭
        connection.close();
    }

}

输出结果如下:

druidDataSource 数据源最大连接数:8
druidDataSource 数据源初始化连接数:0

则配置参数已经生效。

14-3、配置Druid数据源监控

Druid 数据源具有监控的功能,并提供了一个web界面方便用户查看。

1、设置Druid后台管理页面(登录账号、密码等),配置后台管理。

package com.lskj.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DruidConfig {
    /*
       将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
       绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
       @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
       前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }


    //配置 Druid 监控管理后台的Servlet;
    //内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");

        // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
        // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
        Map<String, String> initParams = new HashMap<>();
        //loginUsername loginPassword 是固定的,不可更改
        initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
        initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

        //后台允许谁可以访问
        //initParams.put("allow", "localhost"):表示只有本机可以访问
        //initParams.put("allow", ""):为空或者为null时,表示允许所有访问
        initParams.put("allow", "");
        //deny:Druid 后台拒绝谁访问
        //initParams.put("test", "192.168.1.1");  //表示禁止此ip访问

        //设置初始化参数
        bean.setInitParameters(initParams);
        return bean;
    }
}

因为SpringBoot内置了Servlet容器,所以没有web.xml。
替代方法:ServletRegistrationBean。

2、启动项目,访问http://localhost:8080/druid,进行测试

3、配置Druid web监控的filter过滤器

//配置 Druid 监控 之  web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());

    //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
    Map<String, String> initParams = new HashMap<>();
    initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
    bean.setInitParameters(initParams);

    //"/*" 表示过滤所有请求
    bean.setUrlPatterns(Arrays.asList("/*"));
    return bean;
}

十五、整合Mybatis

官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/

SpringBoot继承Mybatis,最主要的是两个注解@Mapper,和@MapperScan

@Mapper:需要在每一个Mapper接口类上添加,作用是扫描mapper接口;

@MapperScan:是在SpringBoot启动入口类上添加的,它是扫描所有的包。

关于Mapper映射文件存放的位置的写法有两种

  • 将Mapper接口和Mapper映射文件存放到src/main/java同一目录下,还需要在pom文件中手动指定资源文件夹路径resources;
  • 将Mapper接口和Mapper映射文件分开存放。Mapper接口类存放到src/main/java目录下,Mapper映射文件存放到resources(类路径),在springboot核心配置文件中指定mapper映射文件存放的位置。

1、新建一个springboot项目,(Spring Web、JDBC API、MySQL Driver)。

2、导入Mybatis依赖。

<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

3、application.properties中配置数据库连接信息。

spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

4、测试数据库连接是否成功。

package com.lskj;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.SQLException;

@SpringBootTest
class Springboot05MybatisApplicationTests {
    @Autowired
    DataSource dataSource;
    @Test
    void contextLoads() throws SQLException {
        System.out.println(dataSource.getClass());
        System.out.println(dataSource.getConnection());
    }

}

SpringBoot工程下使用Mybatis反向工程

1、编写Mybatis反向工程配置文件(在项目的根目录下)

GeneratorMapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE generatorConfiguration
       PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
       "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

   <!-- 指定连接数据库的JDBC驱动包所在位置,指定到你本机的完整路径 -->
   <classPathEntry location="...\mysql-connector-java-xxx.jar"/>

   <!-- 配置table表信息内容体,targetRuntime指定采用MyBatis3的版本 -->
   <context id="tables" targetRuntime="MyBatis3">

       <!-- 抑制生成注释,由于生成的注释都是英文的,可以不让它生成 -->
       <commentGenerator>
           <property name="suppressAllComments" value="true" />
       </commentGenerator>

       <!-- 配置数据库连接信息 -->
       <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                       connectionURL="jdbc:mysql://localhost:3306/xxx"
                       userId="root"
                       password="root">
       </jdbcConnection>
       
       <!--
       <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                       connectionURL="jdbc:mysql://localhost:3306/xxx?useSSL=false&amp;serverTimezone=Hongkong&amp;characterEncoding=utf-8&amp;autoReconnect=true"
                       userId="root"
                       password="root">
           <property name="nullCatalogMeansCurrent" value="true"/>
       </jdbcConnection>
		-->

       <!-- 生成pojo(实体)类,targetPackage指定pojo类的包名, targetProject指定生成的pojo放在哪个工程(包)下面-->
       <javaModelGenerator targetPackage="com.lskj.pojo" targetProject="src/main/java">
           <property name="enableSubPackages" value="false" />
           <property name="trimStrings" value="false" />
       </javaModelGenerator>

       <!-- 生成MyBatis的Mapper.xml文件,targetPackage指定mapper.xml文件的包名, targetProject指定生成的mapper.xml放在哪个工程下面 -->
       <sqlMapGenerator targetPackage="com.lskj.mapper" targetProject="src/main/java">
           <property name="enableSubPackages" value="false" />
       </sqlMapGenerator>

       <!-- 生成MyBatis的Mapper接口类文件,targetPackage指定Mapper接口类的包名, targetProject指定生成的Mapper接口放在哪个工程下面 -->
       <javaClientGenerator type="XMLMAPPER" targetPackage="com.lskj.mapper" targetProject="src/main/java">
           <property name="enableSubPackages" value="false" />
       </javaClientGenerator>

       <!-- 数据库表名及对应的Java模型类名 -->
       <table tableName="t_xxx" domainObjectName="Xxx"
              enableCountByExample="false"
              enableUpdateByExample="false"
              enableDeleteByExample="false"
              enableSelectByExample="false"
              selectByExampleQueryId="false"/>
   </context>

</generatorConfiguration>
  • 如果使用的MySQL是8版本的,驱动类变为com.mysql.cj.jdbc.Driver
  • url后应该加属性nullCatalogMeansCurrent=true,否则生成有问题

2、在pom.xml文件的build中指定.xml文件夹为resources,并添加mysql反向工程依赖。

<build>
	<!--手动指定文件夹为resources-->
   <resources>
   	<resource>
       	<directory>src/main/java</directory>
           <include>**/*.xml</include>
       </resource>
   </resources>
   
   <plugins>
   	!--mybatis代码自动生成插件-->
       <plugin>
           <groupId>org.mybatis.generator</groupId>
           <artifactId>mybatis-generator-maven-plugin</artifactId>
           <version>1.3.6</version>
           <configuration>
               <!--配置文件的位置-->
               <configurationFile>GeneratorMapper.xml</configurationFile>
               <verbose>true</verbose>
               <overwrite>true</overwrite>
           </configuration>
       </plugin>
   </plugins>
</build>

3、点击IDEA工具中右侧的Maven,点击展开对应项目->Plugins->mybatis-generator->mybatis-generator:generate即可生成相关文件。

5、连接成功后,创建实体类。(导入Lombok)

导入lombok依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

User.java

package com.lskj.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

UserMapper.java

//@Mapper表示本类是一个Mybatis的Mapper
@Mapper
@Repository
public interface UserMapper{
    //获取所有用户信息
    List<User> queryUserList();
    
    //通过id查询用户
    User queryUserById(Integer id);
    
    int addUser(User user);
    
    int updateUser(User user);
    
    int deleteUser(Integer id);
}

6、在resources下创建mybatis/mapper目录,写对应的Mapper映射文件。

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lskj.mapper.UserMapper">
    <select id="queryUserList" resultType="User">
        select * from user;
    </select>

    <select id="queryUserById" resultType="User">
        select * from user where id = #{id}
    </select>

    <insert id="addUser" parameterType="User">
        insert into user (id,name,pwd) values (#{id},#{name},#{pwd})
    </insert>

    <update id="updateUser" parameterType="User">
        update user set name = #{name},pwd = #{pwd} where id = #{id}
    </update>

    <delete id="deleteUser" parameterType="int">
        delete from user where id = #{id}
    </delete>
</mapper>

7、application.properties配置文件中整合Mybatis。

spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#整合Mybatis
mybatis.type-aliases-package=con.lskj.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

8、编写UserController进行测试。

package com.lskj.controller;

import com.lskj.mapper.UserMapper;
import com.lskj.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/queryUserList")
    public List<User> queryUserList(){
        List<User> userList = userMapper.queryUserList();
        for (User user : userList) {
            System.out.println(user);
        }
        return userList;
    }

    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id") int id){
        User user = userMapper.queryUserById(id);
        System.out.println(user);
        return user;
    }

    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(5,"test","test"));
        return "insert success!";
    }

    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(5,"test","123456"));
        return "update success!";
    }

    @GetMapping("/deleteUser")
    public String deleteUser(){
        userMapper.deleteUser(5);
        return "delete success!";
    }
}

9、启动项目,进行访问测试。

十六、SpringSecurity

SpringSecurity是基于Spring应用提供的声明式的安全保护性的框架,它可以在web请求级别的和方法调用级别处理身份和授权。(Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。)

官网:https://spring.io/projects/spring-security

Spring Security的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。

对于安全控制,仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理。

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证用户身份后,最终会授予用户访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

16-1、测试

1、新建一个springboot项目。(勾选上Spring Web和thymeleaf模块,若未勾选,后续导入相关依赖也可)

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

2、准备静态资源。(使用到semantic-ui框架)

semantic-ui:https://semantic-ui.com/

中文semantic-ui:https://zijieke.com/semantic-ui/

static目录

在 https://github.com/Semantic-Org/Semantic-UI/tree/master/dist 中下载semantic.min.js 存放在static/js目录下。

static/css/style.css

body{
    min-width: 1150px;
    overflow: auto;
}
#index-header-nav{
    font-size: 0px;
}

templates目录

templates/index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>首页</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/css/style.css}" rel="stylesheet">
    <script src="http://libs.baidu.com/jquery/2.1.1/jquery.min.js"></script>
    <script th:src="@{/js/semantic.min.js}"></script>
</head>
<body>
    <!--主容器-->
    <div class="ui container">
        <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
            <div class="ui secondary menu">
                <a class="item"  th:href="@{/index}">首页</a>

                <!--登录注销-->
                <div class="right menu">
                    <!--未登录-->
                    <a class="item" th:href="@{/toLogin}">
                        <i class="sign-in icon"></i> 登录
                    </a>
                </div>
            </div>
        </div>

        <div>
            <br>
            <div class="ui three column stackable grid">
                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 1</h5>
                                <hr>
                                <div><a th:href="@{/level/Lv1}"><i class="arrow alternate circle up icon"></i> Lv1</a></div>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 2</h5>
                                <hr>
                                <div><a th:href="@{/level/Lv2}"><i class="arrow alternate circle up icon"></i> Lv2</a></div>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 3</h5>
                                <hr>
                                <div><a th:href="@{/level/Lv3}"><i class="arrow alternate circle up icon"></i> Lv3</a></div>
                            </div>
                        </div>
                    </div>
                </div>

            </div>
        </div>
    </div>
</body>
</html>

templates/views/login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>登录</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <script src="http://libs.baidu.com/jquery/2.1.1/jquery.min.js"></script>
    <script th:src="@{/js/semantic.min.js}"></script>
</head>
<body>
    <!--主容器-->
    <div class="ui container">
        <div class="ui segment" style="margin-top: 100px">
            <div style="text-align: center">
                <h1 class="header">登录</h1>
            </div>

            <div class="ui placeholder segment">
                <div class="ui column very relaxed stackable grid">
                    <div class="column">
                        <div class="ui form">
                            <form th:action="@{/login}" method="post">
                                <div class="field">
                                    <label>用户名:</label>
                                    <div class="ui left icon input">
                                        <input type="text" placeholder="用户名" name="username">
                                        <i class="user icon"></i>
                                    </div>
                                </div>
                                <div class="field">
                                    <label>密码:</label>
                                    <div class="ui left icon input">
                                        <input type="password" placeholder="密码" name="password">
                                        <i class="lock icon"></i>
                                    </div>
                                </div>
                                <input type="submit" value="登录" class="ui blue submit button"/>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>
</html>

templates/views/level1/Lv1.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Lv1</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/static/css/style.css}" rel="stylesheet">
    <script src="http://libs.baidu.com/jquery/2.1.1/jquery.min.js"></script>
    <script th:src="@{/js/semantic.min.js}"></script>
</head>
<body>
    <!--主容器-->
    <div class="ui container">
        <div th:replace="~{index::nav-menu}"></div>

        <div class="ui segment" style="text-align: center">
            <h3>Lv1</h3>
        </div>
    </div>
</body>
</html>

templates/views/level/Lv2.htmltemplates/views/level/Lv3.html类似于templates/views/level/Lv1.html

3、配置文件中,关闭模板引擎的缓存。

application.properties

spring.thymeleaf.cache=false

4、controller路由跳转。

package com.lskj.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RouterController {
    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    @RequestMapping("/level/{level}")
    public String level(@PathVariable("level") String level){
        return "views/level/"+level;
    }
}

5、启动项目,进行访问测试。

16-2、认证和授权

1、引入Spring Security。

<!--security-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、Spring Security配置类。

帮助文档:https://docs.spring.io/spring-security/site/docs/

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

编写基础配置类SecurityConfig,继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)方法。

package com.lskj.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity  //开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
    }
}

3、定义请求的授权规则。

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人可以访问,对应等级只有对应权限的用户可以访问
    //请求授权规则
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level/Lv1").hasRole("Lv1")
        .antMatchers("/level/Lv2").hasRole("Lv2")
        .antMatchers("/level/Lv3").hasRole("Lv3");
}

4、启动项目,进行测试。此时除了首页和登录页面可以访问,等级页面无法访问。

5、在上述方法中加入没有权限默认跳转到登录页面配置,开启自动配置的登录功能。

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人可以访问,对应等级只有对应权限的用户可以访问
    //请求授权规则
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level/Lv1").hasRole("Lv1")
        .antMatchers("/level/Lv2").hasRole("Lv2")
        .antMatchers("/level/Lv3").hasRole("Lv3");

    //没有权限默认跳转到登录页面,需开启登录的页面
    // 开启自动配置的登录功能
    // /login 请求来到登录页
    // /login?error 重定向到这里表示登录失败
    http.formLogin();
}

启动项目,进行访问。没有无权限时,自动跳转到登录的页面。(此时无权限跳转的登录页并非测试写的login页面)

6、定义认证规则。

重写configure(AuthenticationManagerBuilder auth)方法。

//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //super.configure(auth);
    //以下数据正常情况下应从数据库中读取
    //从内存中读(也可以在jdbc中去拿 jdbcAuthentication())
    auth.inMemoryAuthentication()
        .withUser("admin").password("admin").roles("Lv1","Lv2","Lv3")
        .and()
        .withUser("lskj").password("lskj").roles("Lv1","Lv2")
        .and()
        .withUser("guest").password("guest").roles("Lv1");
}

启动项目,使用以上账号信息进行访问测试。(springboot2.1.x中可直接使用)

登录失败,会报错 There is no PasswordEncoder mapped for the id "null"

PasswordEncoder:密码编码。

需要对密码进行某种方式加密。

7、对密码进行编码。

//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //super.configure(auth);
    //以下数据正常情况下应从数据库中读取
    //从内存中读
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
        .withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("Lv1","Lv2","Lv3")
        .and()
        .withUser("lskj").password(new BCryptPasswordEncoder().encode("lskj")).roles("Lv1","Lv2")
        .and()
        .withUser("guest").password(new BCryptPasswordEncoder().encode("guest")).roles("Lv1");
}

启动项目,进行访问测试。此时,每个用户只能访问对应权限的等级页面。

16-3、权限控制和注销

1、开启自动配置的注销功能。

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //...

    //开启注销功能,注销后跳转到首页
    http.logout().logoutSuccessUrl("/");
}

2、在首页index.html的导航栏中增加注销按钮。

<!-- ... -->
<!--登录注销-->
<div class="right menu">
    <!--未登录-->
    <a class="item" th:href="@{/toLogin}">
        <i class="sign-in icon"></i> 登录
    </a>
    <!--注销-->
    <a class="item" th:href="@{/logout}">
        <i class="sign-out icon"></i> 注销
    </a>
</div>
<!-- ... -->

启动测试,注销后,成功跳转到首页。

实现:在用户未登录时,导航栏只显示登录按钮;登录成功后,导航栏显示登录的用户信息以及注销按钮。同时,用户登录成功后,只显示其权限对应的页面。

3、导入thymeleaf和security的整合包。

<!-- thymeleaf-security -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

4、修改前端页面,index.html。

导入命名空间

xmlns:sec="http://www.thymeleaf.org/extras/spring-security"

修改导航栏,增加认证判断(sec:authorize=“isAuthenticated()”:是否认证登录)

<!--登录注销-->
<div class="right menu">
    <!--未登录-->
    <div sec:authorize="!isAuthenticated()">
        <a class="item" th:href="@{/toLogin}">
            <i class="sign-in icon"></i> 登录
        </a>
    </div>

    <!--已登录,才能注销-->
    <div sec:authorize="isAuthenticated()">
        <a class="item">
            用户名:<span sec:authentication="principal.username"></span>&nbsp;
            <!--等级:<span sec:authentication="principal.authorities"></span>-->
        </a>
    </div>
    <div sec:authorize="isAuthenticated()">
        <a class="item" th:href="@{/logout}">
            <i class="sign-out icon"></i> 注销
        </a>
    </div>

</div>

重启测试。

如果注销,404,是因为它默认防止csrf跨站请求伪造,因为会产生安全问题。

可以将请求改为post表单提交,或在spring security中关闭csrf功能。

spring security中关闭csrf功能:(SecurityConfig配置类中)

http.csrf().disable();  //关闭csrf功能

(CSRF一般指跨站请求伪造。)

5、完成用户功能块认证。

<!--等级显示:根据用户的角色动态实现-->
<div class="column" sec:authorize="hasRole('Lv1')">
    <!-- ... -->
</div>

<div class="column" sec:authorize="hasRole('Lv2')">
    <!-- ... -->
</div>

<div class="column" sec:authorize="hasRole('Lv3')">
    <!-- ... -->
</div>

Remember me功能

现在登录之后,关闭浏览器,在进入首页,需要重新登录。

开启记住我功能之后,登录后关闭浏览器,重新打开浏览器进行访问,用户依旧存在。

开启记住我功能:

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
	//...
   http.rememberMe();  //开启记住我功能
}

16-4、定制登录页

上述所用到的登录页面是spring security默认的。

使用自定义的login界面:

1、在SecurityConfig配置类中的登录页配置后面指定loginPage。

//定制登录页
http.formLogin().loginPage("/toLogin");

2、首页需要指向自定义的login请求。

<a class="item" th:href="@{/toLogin}">
    <i class="sign-in icon"></i> 登录
</a>

登录需要将相关信息发送到哪,需要配置。

login.html配置提交方式必须为post。

<form th:action="@{/login}" method="post">
    <div class="field">
        <label>用户名:</label>
        <div class="ui left icon input">
            <input type="text" placeholder="用户名" name="username">
            <i class="user icon"></i>
        </div>
    </div>
    <div class="field">
        <label>密码:</label>
        <div class="ui left icon input">
            <input type="password" placeholder="密码" name="pwd">
            <i class="lock icon"></i>
        </div>
    </div>
    <input type="submit" value="登录" class="ui blue submit button"/>
</form>

请求提交后,还需要验证处理。需配置接收登录的用户名和密码的参数,同时应注意表单提交的地址。

源码:

/**
	* 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.usernameParameter(&quot;username&quot;) // default is username
	 * 				.passwordParameter(&quot;password&quot;) // default is password
	 * 				.loginPage(&quot;/authentication/login&quot;) // default is /login with an HTTP get
	 * 				.failureUrl(&quot;/authentication/login?failed&quot;) // default is /login?error
	 * 				.loginProcessingUrl(&quot;/authentication/login/process&quot;); // default is /login
	 * 																		// with an HTTP
	 * 																		// post
	 * 	}
*/

默认接收的参数是username、password。若表单中提交的name不是默认的username、password,则需要配置其参数。

//定制登录页
http.formLogin().loginPage("/toLogin")
    .usernameParameter("username")
    .passwordParameter("pwd")
    .loginProcessingUrl("/login");

3、在登录页面增加记住我多选框。

<div class="field">
    <input type="checkbox" name="remember"/>记住我
</div>

4、后端验证处理。

//开启记住我 cookie默认保存两周 自定义接收前端的参数
http.rememberMe().rememberMeParameter("remember");

5、启动项目,访问测试。

十七、Shiro

Apache Shiro是Java的一个安全(权限)框架。

  • Shiro可以容易地开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境中。
  • Shiro可以完成认证、授权、加密、会话管理、web集成、缓存等。

官网地址:http://shiro.apache.org/

GitHub:https://github.com/apache/shiro.git

17-1、Shiro功能

在这里插入图片描述

  • Authentication:身份认证、登录,验证用户是否拥有相应的身份。
  • Authorization: 授权,即权限校验,验证某个已认证的用户是否拥有某个权限。确定“谁”可以访问“什么”。
  • Session Management:会话管理,管理用户登录后的会话。即用户登录后就是一次会话,在没有退出之前,它的所有信息都会在会话中。会话可以是普通的JavaSE环境,也可以是Web环境。
  • Cryptography:加密,保护数据的安全性。如密码加密存储到数据库中,而不是明文存储。
  • Web Support:Web支持,能够比较轻易地整合到Web环境中。
  • Caching:缓存,对用户的数据进行缓存。(用户信息、角色、权限等缓存到如redis等缓存中)
  • Concurrency:并发,Shiro支持具有并发功能的多线程应用程序,也就是说支持在多线程应用中并发验证。例如在一个线程中开启另一个线程,能把权限自动的传过去。
  • Testing:测试,提供了测试的支持。
  • Run as :允许用户以其他用户的身份来登录。(允许一个用户假装为另一个用户(如果允许)的身份进行访问)
  • Remember me :记住我。

17-2、Shiro架构

从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:

在这里插入图片描述

上图可看出:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。

Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

从Shiro内部来看:

在这里插入图片描述

  • Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”。

  • SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。

  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能。

  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以一般在应用中都需要实现自己的 Realm。

  • SessionManager:SessionManager知道如何创建会话、管理用户回话的声明周期以便在所有运行环境下都可以给用户提供一个健壮的回话管理体验。Shiro在任何运行环境下都可以在本地管理用户会话(即便没有Web或者EJB容器也可以)——这在安全管理的框架中算是独门绝技了。当然,如果当前环境中有会话管理机制(比如Servlet容器),则Shiro默认会使用该环境的会话管理机制。而如果像控制台程序这种独立的应用程序,本身没有会话管理机制,此时Shiro就会使用内部的会话管理器来给应用的开发提供一直的编程体验。SessionDAO允许用户使用任何类型的数据源来存储Session数据。

  • SessionDAO:用于代替SessionManager执行Session相关的增删改查。这个接口允许我们将任意种类的数据存储方式引入到Session管理的基础框架中。DAO(数据访问对象),用于会话的 CRUD,比如想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。

  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。

  • Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密等。

17-3、Hello Shiro

官网文档:http://shiro.apache.org/tutorial.html

quickstart:https://github.com/apache/shiro/tree/master/samples/quickstart

1、创建一个普通maven项目,删除src目录。

2、在父项目上新建一个普通Module子项目。

3、根据官方文档,导入shiro依赖。

<dependencies>
    <!-- shiro-core -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.1</version>
    </dependency>

    <!-- configure logging -->
    <!-- jcl-over-slf4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.21</version>
    </dependency>

    <!-- slf4j-log4j12 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
    </dependency>

    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

</dependencies>

4、编写配置文件log4j.properties。(resources/log4j.properties)

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

5、编写初始化文件shiro.ini。(resources/shiro.ini)

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

6、编写Quickstart类。(从GitHub上或官网文档复制过来时应注意导包问题)

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        //获取当前的用户对象 subject
        Subject currentUser = SecurityUtils.getSubject();

        // 通过当前用户拿到Session
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            //log.info("Retrieved the correct value! [" + value + "]");
            log.info("Subject=>session[" + value + "]");
        }

        //判断当前用户是否被认证
        if (!currentUser.isAuthenticated()) {
            //Token:令牌
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true); //设置记住我
            try {
                currentUser.login(token);  //执行登录操作
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //注销
        currentUser.logout();
        //结束
        System.exit(0);
    }
}

官网文档中并未使用log4j,而GitHub上导入依赖时并未指定相关依赖对应的版本,同时应注明其作用域。

5、运行Quickstart类,运行结果如下:

INFO [Quickstart] - Subject=>session[aValue] 
INFO [Quickstart] - User [lonestarr] logged in successfully. 
INFO [Quickstart] - May the Schwartz be with you! 
INFO [Quickstart] - You may use a lightsaber ring.  Use it wisely. 

17-4、shiro-springboot

1、新建一个Module(Spring Initializr),勾选上Spring Web模块。

2、导入thymeleaf依赖。

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

3、编写首页页面 templates/index.html。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <span th:text="${msg}"></span>
</body>
</html>

4、编写Controller。

package com.lskj.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {
    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","Hello Shiro!");
        return "index";
    }
}

5、启动项目,访问测试。

6、测试成功后,导入shiro整合spring的包。

<!-- shiro-spring -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.0</version>
</dependency>

7、编写配置类。

需要先自定Realm对象。

package com.lskj.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行 授权");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行 认证");
        return null;
    }
}

编写配置类ShiroConfig。

package com.lskj.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    //3、ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }

    //2、DefaultWebSecurityManager
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //1、创建 realm 对象 ,需自定义
    //@Bean(name="userRea")
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
}

Shiro实现登录拦截

8、编写几个页面。

templates/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <hr>
    <form action="">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

templates/user/add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>增加</title>
</head>
<body>
    <h1>add</h1>
</body>
</html>

templates/update.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改</title>
</head>
<body>
    <h1>update</h1>
</body>
</html>

controll中完成跳转

@RequestMapping("/toLogin")
public String toLogin(){
    return "login";
}

@RequestMapping("/user/add")
public String add(){
    return "user/add";
}

@RequestMapping("/user/update")
public String update(){
    return "user/update";
}

首页中增加跳转链接

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <span th:text="${msg}"></span>
    <hr>
    <a th:href="@{/user/add}">add</a>
    <a th:href="@{/user/update}">update</a>
</body>
</html>

9、配置类中进行相关配置。

@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    //添加shiro的内置过滤器
    /**
         * anon:无需认证就可以访问
         * authc:必须认证才能访问
         * user:必须拥有 记住我 功能才能使用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         */
    //拦截
    Map<String, String> filterMap = new LinkedHashMap<>();
    /*filterMap.put("/user/add","authc");
        filterMap.put("/user/update","authc");*/
    filterMap.put("/user/*","authc");
    bean.setFilterChainDefinitionMap(filterMap);

    //设置登录的请求
    bean.setLoginUrl("/toLogin");


    return bean;
}

Shiro实现用户认证

10、获取用户登录的信息。

controller中获取登录信息

@RequestMapping("/login")
public String login(String username,String password,Model model){
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    try{
        subject.login(token);  //执行登录方法,如果没有异常就说明登录成功
        return "index";
    }catch (UnknownAccountException e){ //用户名不存在
        model.addAttribute("msg","用户名错误");
        return "login";
    }catch (IncorrectCredentialsException e){  //密码不存在
        model.addAttribute("msg","密码错误");
        return "login";
    }
}

登录页面,表单提交过来

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <hr>
    <span th:text="${msg}" style="color:red;"></span>
    <form th:action="@{/login}">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

启动项目,访问测试登录,执行了认证。

11、认证。

UserRealm.java

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行 认证");

    //用户名、密码   数据库中取
    String username = "admin";
    String password = "admin";

    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

    if (!token.getUsername().equals(username)){
        return null;  //抛出异常 UnknownAccountException
    }

    //密码认证 shiro做
    return new SimpleAuthenticationInfo("",password,"");
}

Shiro整合Mybatis

12、导入连接数据库相关的依赖。

<!-- mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.1</version>
</dependency>
<!-- log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!-- mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

13、编写配置文件。

resources/application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

14、编写实体类。

package com.lskj.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

15、编写mapper。

UserMapper.java

package com.lskj.mapper;

import com.lskj.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;


@Repository
@Mapper
public interface UserMapper {
    public User queryUserByName(String name);
}

resources/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lskj.mapper.UserMapper">
    <select id="queryUserByName" parameterType="String" resultType="User">
        select * from mybatis.user where name = #{name};
    </select>
</mapper>

配置application.properties

mybatis.type-aliases-package=com.lskj.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

16、编写service层。

UserService.java

package com.lskj.service;

import com.lskj.pojo.User;

public interface UserService {
    public User queryUserByName(String name);
}

UserServiceImpl.java

package com.lskj.service;

import com.lskj.mapper.UserMapper;
import com.lskj.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userMapper;
    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}

17、测试。

package com.lskj;

import com.lskj.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ShiroSpringbootApplicationTests {
    @Autowired
    UserServiceImpl userService;

    @Test
    void contextLoads() {
        userService.queryUserByName("admin");
    }

}

18、修改Realm中的认证。

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行 授权");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行 认证");

        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //连接数据库,获取正尝试登录用户的信息
        User user = userService.queryUserByName(token.getUsername());

        if(user == null){ //查无此人
            return null;  //抛出异常 UnknownAccountException
        }

        //密码认证 shiro做
        return new SimpleAuthenticationInfo("",user.getPwd(),"");
    }
}

Shiro实现请求授权

19、数据库用户表增加权限字段perms,同时给对应用户添加相应权限。【user:add、user:update】

admin->user:add lskj->user:update

同时,实体类中加入对应属性。

20、授权。

ShiroConfig.java

@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    //添加shiro的内置过滤器
    //拦截
    Map<String, String> filterMap = new LinkedHashMap<>();

    //授权
    filterMap.put("/user/add","perms[user:add");
    filterMap.put("/user/update","perms[user:update");

    filterMap.put("/user/*","authc");
    bean.setFilterChainDefinitionMap(filterMap);

    //未授权,未授权跳转到未授权页面
    bean.setUnauthorizedUrl("/unauth");

    //设置登录的请求
    bean.setLoginUrl("/toLogin");

    return bean;
}

MyController.java

@RequestMapping("/unauth")
@ResponseBody
public String unauthorized(){
    return "未经授权,无法访问此页面!";
}

UserRealm.java

//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行 授权");
        //SimpleAuthorizationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission("user:add");

        //拿到当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal();  //拿到User对象
        //设置当前用户的权限
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //...

        //密码认证 shiro做
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

Shiro整合thymeleaf

21、导入shiro-thymeleaf整合包。

<!-- thymeleaf-extras-shiro -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

22、整合ShiroDialect:用来整合shiro thymeleaf。

ShiroConfig.java

//整合ShiroDialect
@Bean
public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
}

23、获取已登录当前用户的信息

UserRealm.java

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行 认证");

    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    //连接数据库,获取正尝试登录用户的信息
    User user = userService.queryUserByName(token.getUsername());

    if(user == null){ //查无此人
        return null;  //抛出异常 UnknownAccountException
    }

    Subject currentSubject = SecurityUtils.getSubject();
    Session session = currentSubject.getSession();
    session.setAttribute("loginUser",user);

    //密码认证 shiro做
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}

24、修改首页templates/user/index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <div th:if="${session.loginUser == null}">
        <a th:href="@{/toLogin}">登录</a>
    </div>
    <span th:text="${msg}"></span>
    <hr>

    <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">add</a>
    </div>

    <div shiro:hasPermission="user:update">
        <a th:href="@{/user/update}">update</a>
    </div>
</body>
</html> 

十八、Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

官网:https://swagger.io/

  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
  • 直接运行,在线测试API
  • 支持多种语言 (如:Java,PHP等)

18-1、SpringBoot集成Swagger

SpringBoot集成Swagger => springfox,两个jar包

  • springfox-swagger2
  • springfox-swagger-ui

1、新建一个SpringBoot-web项目。

2、导入依赖。

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

导入不同的ui包,可以实现不同的皮肤。

上述的ui包,访问 http://localhost:8080/swagger-ui.html

bootstrap-ui,访问 http://localhost:8080/doc.html

<!-- 引入swagger-bootstrap-ui包 /doc.html-->
<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>swagger-bootstrap-ui</artifactId>
   <version>1.9.1</version>
</dependency>

Layui-ui,访问 http://localhost:8080/docs.html

<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
   <groupId>com.github.caspar-chen</groupId>
   <artifactId>swagger-ui-layer</artifactId>
   <version>1.1.3</version>
</dependency>

mg-ui,访问 http://localhost:8080/document.html

<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
   <groupId>com.zyplayer</groupId>
   <artifactId>swagger-mg-ui</artifactId>
   <version>1.0.6</version>
</dependency>

3、编写controller,启动项目测试运行。

package com.lskj.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping(value = "/hello")
    public String hello(){
        return "Hello Swagger!";
    }
}

4、编写配置SwaggerConfig配置类。

package com.lskj.config;

import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2  //开启Swagger-2
public class SwaggerConfig {

}

5、启动项目,访问http://localhost:8080/swagger-ui.html。

(swagger-ui3.0不能访问swagger-ui界面,因为在swagger-ui3.0.0的jar包目录下找不到swagger-ui.html这个页面)

18-2、配置Swagger

Swagger的bean实例是Docket。

@Configuration
@EnableSwagger2  //开启Swagger2
public class SwaggerConfig {
    //配置Swagger的Docket的bean实例
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo());
    }

    //配置Swagger信息 apiInfo
    private ApiInfo apiInfo(){
        Contact DEFAULT_CONTACT = new Contact("", "", "");
        return new ApiInfo("API文档",
                "Api Documentation",
                "v1.0",
                "http://localhost:8080",
                DEFAULT_CONTACT,
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList());
    }
}

重启项目,访问测试 http://localhost:8080/swagger-ui.html。

18-3、配置扫描接口

SwaggerConfig.java

//配置Swagger的Docket的bean实例
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //RequestHandlerSelectors 配置要扫描接口的方式
        /*
                    basePackage() 指定要扫描的包
                    any() 扫描全部
                    none() 不扫描
                    withClassAnnotation() 扫描类上的注解,参数是一个注解的反射对象
                    withMethodAnnotation() 扫描方法上的注解
                 */
        .apis(RequestHandlerSelectors.basePackage("com.lskj.controller"))
        //paths() 过滤什么路径
        .paths(PathSelectors.ant("/lskj/**"))
        .build();
}

18-4、配置Swagger开关

//配置Swagger的Docket的bean实例
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(false)  //配置是否启用Swagger,如果是false,在浏览器将无法访问
        .select()
        .apis(RequestHandlerSelectors.basePackage("com.lskj.controller"))
        .build();
}

动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示。

(Swagger在生产环境中使用,在发布时不使用。)

  • 判断是否是生产环境
  • enable注入

application.properties,激活dev环境

spring.profiles.active=dev

设置dev环境的端口号,application-dev.properties

server.port=8081

设置prod环境的端口号,application-prod.properties

server.port=8082

判断是否是生产环境,同时注入enable,SwaggerConfig.java

//配置Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
    //设置要显示Swagger的环境
    Profiles profiles = Profiles.of("dev","test");
    //通过environment.acceptsProfiles判断是否处在设定的环境中
    boolean flag = environment.acceptsProfiles(profiles);

    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(flag)  //配置是否启用Swagger,如果是false,在浏览器将无法访问
        .select()
        .apis(RequestHandlerSelectors.basePackage("com.lskj.controller"))
        .build();
}

18-5、配置API文档的分组

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

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

配置多个分组只需要配置多个docket即可。

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

18-6、实体类配置

1、新建一个实体类。

package com.lskj.pojo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel("用户实体类")
public class User {
    @ApiModelProperty("用户名")
    public String username;
    @ApiModelProperty("密码")
    public String password;
}

2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中。

@RestController
public class HelloController {
    @GetMapping(value = "/hello")
    public String hello(){
        return "Hello Swagger!";
    }

    //只要接口中,返回值存在实体类,就会被扫描到Swagger中
    @PostMapping("/user")
    public User user(){
        return new User();
    }
}

并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。

@ApiModel为类添加注释

@ApiModelProperty为类属性添加注释

3、启动测试,访问http://localhost:8081/swagger-ui.html。

在这里插入图片描述

18-7、常用注解

Swagger注解说明
@Api(tags = “xxx模块说明”)作用在模块类上
@ApiOperation(“xxx接口说明”)作用在接口方法上
@ApiModel(“xxxPOJO说明”)作用在模型类上:如VO、BO
@ApiModelProperty(value = “xxx属性说明”,hidden = true)作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam(“xxx参数说明”)作用在参数、方法和字段上,类似@ApiModelProperty

Swagger的所有注解定义在io.swagger.annotations包下。

例如给请求的接口配置一些注释。

//Operation接口,不是放在类上,而是放在方法上
@ApiOperation("接口")
@GetMapping(value = "/test")
public String test(@ApiParam("用户名") String username){
    return  "test" + username;
}

@ApiOperation("Post测试类")
@PostMapping(value = "/post")
public User test_02(@ApiParam("用户名") User user){
    return user;
}

在这里插入图片描述

给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读、理解。

使用swagger不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。

项目正式发布时,关闭Swagger。(出于安全问题考虑,同时 也可以节省运行时内存)

十九、异步任务

1、新将一个spring web项目。

2、创建service包,编写AsyncService.java。

编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况

package com.lskj.service;

import org.springframework.stereotype.Service;

@Service
public class AsyncService {
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("数据正在处理...");
    }
}

3、创建controller包,编写AsyncController.java。

package com.lskj.controller;

import com.lskj.service.AsyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {
    @Autowired
    AsyncService asyncService;

    @RequestMapping("/hello")
    public String hello(){
        asyncService.hello(); //停止3秒
        return "OK";
    }
}

4、访问http://localhost:8080/hello进行测试,3秒后出现OK,这是同步等待的情况。

如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要手动去编写多线程的实现的话,过于繁琐。

现只需要用一个简单的办法,在方法上加一个简单的注解即可。

5、给hello方法添加@Async注解。

@Async //告诉Spring这是一个异步的方法
public void hello(){
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("数据正在处理...");
}

添加@Async注解后,SpringBoot就会自己开一个线程池,进行调用。

但是要让这个注解生效,还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能。

@EnableAsync  //开启异步注解功能
@SpringBootApplication
public class SpringTaskApplication {

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

}

重启项目测试,网页瞬间响应,后台代码依旧执行。

二十、邮件任务

Springboot支持邮件发送:

  • 邮件发送需要引入spring-boot-start-mail
  • SpringBoot 自动配置MailSenderAutoConfiguration
  • 定义MailProperties内容,配置在application.yml中
  • 自动装配JavaMailSender
  • 测试邮件发送

1、导入依赖。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2、查看自动配置类:MailSenderAutoConfiguration。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({MimeMessage.class, MimeType.class, MailSender.class})
@ConditionalOnMissingBean({MailSender.class})
@Conditional({MailSenderAutoConfiguration.MailSenderCondition.class})
@EnableConfigurationProperties({MailProperties.class})
@Import({MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class})
public class MailSenderAutoConfiguration {
    //...
}

点击导入的类进行查看:

MailSenderJndiConfiguration.class

class MailSenderJndiConfiguration {
    private final MailProperties properties;

    MailSenderJndiConfiguration(MailProperties properties) {
        this.properties = properties;
    }

    @Bean
    JavaMailSenderImpl mailSender(Session session) {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();
        sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
        sender.setSession(session);
        return sender;
    }

    @Bean
    @ConditionalOnMissingBean
    Session session() {
        String jndiName = this.properties.getJndiName();

        try {
            return (Session)JndiLocatorDelegate.createDefaultResourceRefLocator().lookup(jndiName, Session.class);
        } catch (NamingException var3) {
            throw new IllegalStateException(String.format("Unable to find Session in JNDI location %s", jndiName), var3);
        }
    }
}

查看配置文件:

MailProperties.class

@ConfigurationProperties(
    prefix = "spring.mail"
)
public class MailProperties {
    private static final Charset DEFAULT_CHARSET;
    private String host;
    private Integer port;
    private String username;
    private String password;
    private String protocol = "smtp";
    private Charset defaultEncoding;
    private Map<String, String> properties;
    private String jndiName;

    //...
}

3、配置文件。

获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务。

application.properties

spring.mail.username=qq邮箱
spring.mail.password=qq授权码
spring.mail.host=smtp.qq.com
# 开启加密验证:qq需要配置ssl 
spring.mail.properties.mail.smtp.ssl.enable=true

4、测试。

@SpringBootTest
class SpringTaskApplicationTests {
    @Autowired
    JavaMailSenderImpl mailSender;

    @Test
    void contextLoads() {
        //简单的邮件
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setSubject("Hello,你好呀!");
        mailMessage.setText("test!");

        mailMessage.setTo("2292807998@qq.com");
        mailMessage.setFrom("2292807998@qq.com");

        mailSender.send(mailMessage);
    }
    
    @Test
    void contextLoads_test() throws MessagingException {
        //复杂的邮件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);

        helper.setSubject("Test");
        helper.setText("<p style='color:red'>This is a test mail.</p>",true);

        //附件
        //helper.addAttachment("test.jpg",new File("文件路径"));

        helper.setTo("2292807998@qq.com");
        helper.setFrom("2292807998@qq.com");

        mailSender.send(mimeMessage);
    }
}

查看邮箱,邮件接收成功!

使用Thymeleaf进行前后端结合即可实现网站邮件收发功能。

二十一、定时任务

Spring提供了异步执行任务调度的方式,提供了两个接口。

  • TaskExecutor接口(任务执行)
  • TaskScheduler接口(任务调度)

两个注解:

  • @EnableScheduling(开启定时功能的注解)
  • @Scheduled(何时执行)

Cron表达式的格式:秒 分 时 日 月 周

字段允许值允许的特殊字符
秒(Seconds)0~59的整数, - * /
分(Minutes)0~59的整数, - * /
小时(Hours)0~23的整数, - * /
日期(DayofMonth)1~31的整数(但是需要考虑月的天数),- * ? / L W C
月份(Month)1~12的整数或者 JAN-DEC, - * /
星期(DayofWeek)0~7的整数或者 SUN-SAT (0、7=SUN), - * ? / L C #
  • *:表示匹配该域的任意值

  • ?:表示不确定的值。只能用在DayofMonth和DayofWeek两个域

  • ,:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次

  • -:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

  • /:表示起始时间开始触发,然后每隔固定时间触发一次。在Minutes域使用0/15表示从第0分开始,没隔15分钟触发一次

  • L:表示最后,只能出现在DayofWeek和DayofMonth域。用在日表示一个月中的最后一天,用在周表示该月最后一个星期X

  • W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。

  • LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

  • C:代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天

  • #:用于确定每个月第几个星期几,只能出现在DayofMonth域。6#3表示某月第3个周五

1、编写ScheduledService

ScheduledService.java

package com.lskj.service;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class ScheduledService {

    //特定的时间执行这个方法
    //cron表达式 秒 分 时 日 月 周几
    @Scheduled(cron = "0 * * * * 0-7")
    public void test(){
        System.out.println("test被执行了");
    }
}

2、在主程序上增加@EnableScheduling开启定时任务功能。

@EnableAsync  //开启异步注解功能
@EnableScheduling  //开启定时功能的注解
@SpringBootApplication
public class SpringTaskApplication {

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

}

二十二、分布式理论

22-1、分布式系统

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。

分布式系统(distributed system)是建立在网络之上的软件系统。

22-2、Dubbo文档

Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC 分布式服务框架,现已成为 Apache 基金会孵化项目。

官网文档:http://dubbo.apache.org/zh-cn/docs/user/quick-start.html

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。

发展演变

在这里插入图片描述

22-3、单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

22-4、垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

缺点:公用模块无法重复利用,开发性的浪费.

22-5、分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

22-6、流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

二十三、RPC

RPC(Remote Procedure Call)是指远程过程调用,是一种进程间通信方式,它是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

RPC调用过程:

在这里插入图片描述

在这里插入图片描述

RPC两个核心模块:通讯,序列化。

在这里插入图片描述

二十四、Dubbo

Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

官网:http://dubbo.apache.org

中文官网:http://dubbo.apache.org/zh-cn/

在这里插入图片描述

  • 服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

  • 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  • 注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  • 监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

节点角色说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器

过程:

  • 启动容器,加载,运行服务提供者
  • 服务提供者在启动时,在注册中心发布注册自己提供的服务
  • 服务消费者在启动时,在注册中心订阅自己所需的服务

如果考虑失败或变更的情况,就需要考虑下面的过程。

  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

在这里插入图片描述

24-1、环境搭建

官方推荐使用Zookeeper注册中心。

Zookeeper 是 Apache Hadoop 的子项目,是一个树型的目录服务,支持变更推送,适合作为 Dubbo 服务的注册中心,工业强度较高,可用于生产环境。

window下安装zookeeper

1、下载zookeeper,并解压。(下载bin那个)

下载地址:https://mirror.bit.edu.cn/apache/zookeeper/

2、解压后,运次/bin/zkServer.cmd

初次运次会报错:没有zoo.cfg配置文件。

如果遇到闪退,在zkServer.cmd文件末尾添加pause

重新运行出错不会退出,会提示错误信息,方便找到错误原因。

3、修改zoo.cfg配置文件。

将conf文件夹下的zoo_sample.cfg复制一份改名为zoo.cfg。

dataDir=./   临时数据存储的目录(可写相对路径)
clientPort=2181   zookeeper的端口号

修改完成后,再次启动zookeeper。

4、运行zkCli.cmd测试。

ls /:列出zookeeper根下保存的所有节点。

[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]

create –e /lskj test:创建一个lskj节点,值为test

[zk: localhost:2181(CONNECTED) 1]  create -e /lskj test
Created /lskj

get /lskj:获取/lskj节点的值

[zk: localhost:2181(CONNECTED) 2] get /lskj
test
[zk: localhost:2181(CONNECTED) 3] ls /
[lskj, zookeeper]

window下安装dubbo-admin

dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮助java程序连接到zookeeper,并利用zookeeper消费、提供服务。

为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。

1、下载dubbo-admin,并解压。

地址:https://github.com/apache/dubbo-admin/tree/master

2、修改dubbo-admin/src/main/resources/application.properties指定zookeeper。

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3、在项目目录下打包dubbo-admin。

项目目录即dubbo-admin-master目录。(打包时间可能会有点长)

mvn clean package -Dmaven.test.skip=true

4、打包成功后,执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar。

【注意:zookeeper的服务一定要打开!】

执行成功后,访问 http://localhost:7001/ ,需要输入登录账户和密码,默认都是root。

dubbo-admin是一个监控管理后台,可查看注册了哪些服务,哪些服务被消费了。(可不要)

24-2、SpringBoot+Zookeeper+Dubbo

测试环境搭建

1、新建一个空项目。

2、创建一个Spring Web模块provider-server。

3、编写一个卖票服务。

编写接口

package com.lskj.service;

public interface TicketService {
    public String getTicket();
}

编写实现类

package com.lskj.service;

public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "test";
    }
}

4、创建一个Spring Web模块consumer-server。

创建时,最后一步注意路径位置。

5、编写一个用户服务。

编写service

package com.lskj.service;

public class UserService {
    //想拿到provider-server提供的票
}

服务提供者

1、将服务提供者注册到注册中心,需整合Dubbo和zookeeper,所以需要导包。

<!--导入Dubbo + zookeeper 依赖-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.3</version>
</dependency>
<!--zkclient-->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>
<!-- zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.2</version>
    <!--排除这个slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2、在springboot配置文件中配置Dubbo相关属性。

application.properties

#设置内嵌Tomcat端口号
server.port=8001
#设置上下文根
server.servlet.context-path=/

#服务应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://localhost:2181
#哪些服务要被注册,扫描指定包下的服务
dubbo.scan.base-packages=com.lskj.service

3、在service的实现类中配置服务注解,发布服务。

TicketServiceImpl.java,注意导包。

package com.lskj.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

//zookeeper服务注册与发现
@Service //将服务发布出去
@Component   //放到容器中
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "test";
    }
}

应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中。

服务消费者

1、导入依赖,与服务提供者那里导的包一样。

2、配置参数。

application.properties

server.port=8002

# 消费者去哪里拿服务,需要暴露自己的名字
dubbo.application.name=consumer-server
# 注册中心的地址
dubbo.registry.address=zookeeper://localhost:2181

3、将服务提供者的接口打包,然后用pom文件导入。

这里直接将服务的接口拿过来(将服务提供者的接口复制到service包下)。消费者的路径必须和服务提供者的相同。

4、完善消费者的服务类。

package com.lskj.service;

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service  //放到容器中
public class UserService {
    //想拿到provider-server提供的票,要去注册中心拿到服务
    @Reference //引用, pom坐标 可定义路径和相同的接口
    TicketService ticketService;

    public void buyTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心拿到=>"+ticket);
    }
}

5、编写测试类。

@SpringBootTest
class ConsumerServerApplicationTests {
    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        userService.buyTicket();
    }

}

启动测试

1、开启zookeeper。

2、打开dubbo-admin实现监控(可不需要)。

3、开启服务者。

4、消费者消费测试。

在注册中心拿到=>test
  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值